diff options
218 files changed, 3325 insertions, 2753 deletions
@@ -12,6 +12,9 @@ gem "rack", :git => "git://github.com/rack/rack.git" gem "rack-test", :git => "git://github.com/brynary/rack-test.git" gem "sprockets", :git => "git://github.com/sstephenson/sprockets.git" +gem "coffee-script" +gem "sass" +gem "uglifier" gem "rake", ">= 0.8.7" gem "mocha", ">= 0.9.8" @@ -24,12 +27,12 @@ end # AS gem "memcache-client", ">= 1.8.5" -gem "fssm", "~> 0.2.5" platforms :mri_18 do gem "system_timer" gem "ruby-debug", ">= 0.10.3" gem 'ruby-prof' + gem "json" end platforms :mri_19 do @@ -49,7 +52,7 @@ platforms :ruby do gem "sqlite3", "~> 1.3.3" group :db do - gem "pg", ">= 0.9.0" + gem "pg", ">= 0.11.0" gem "mysql", ">= 2.8.1" gem "mysql2", :git => "git://github.com/brianmario/mysql2.git" end @@ -57,7 +60,7 @@ end platforms :jruby do gem "ruby-debug", ">= 0.10.3" - + gem "json" gem "activerecord-jdbcsqlite3-adapter" # This is needed by now to let tests work on JRuby diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index ee02cf6945..447e25ca8a 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -18,5 +18,5 @@ Gem::Specification.new do |s| s.requirements << 'none' s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.2.15') + s.add_dependency('mail', '~> 2.3.0') end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 1bde73563e..1e91d62e24 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -297,6 +297,10 @@ module ActionMailer #:nodoc: # information and a cryptographic Message Digest 5 algorithm to hash important information) # * <tt>:enable_starttls_auto</tt> - When set to true, detects if STARTTLS is enabled in your SMTP server # and starts to use it. + # * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is + # really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name + # of an OpenSSL verify constant ('none', 'peer', 'client_once','fail_if_no_peer_cert') or directly the + # constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER,...). # # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method. # * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>. @@ -682,6 +686,9 @@ module ActionMailer #:nodoc: end end + # Translates the +subject+ using Rails I18n class under <tt>[:actionmailer, mailer_scope, action_name]</tt> scope. + # If it does not find a translation for the +subject+ under the specified scope it will default to a + # humanized version of the <tt>action_name</tt>. def default_i18n_subject #:nodoc: mailer_scope = self.class.mailer_name.gsub('/', '.') I18n.t(:subject, :scope => [mailer_scope, action_name], :default => action_name.humanize) diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index ce664bf301..0b076e1ff9 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -23,11 +23,6 @@ if "ruby".encoding_aware? end end -silence_warnings do - # These external dependencies have warnings :/ - require 'mail' -end - lib = File.expand_path("#{File.dirname(__FILE__)}/../lib") $:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) @@ -35,6 +30,11 @@ require 'test/unit' require 'action_mailer' require 'action_mailer/test_case' +silence_warnings do + # These external dependencies have warnings :/ + require 'mail' +end + # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index d3c66800d9..f771737779 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.add_dependency('activemodel', version) s.add_dependency('rack-cache', '~> 1.0.0') s.add_dependency('builder', '~> 3.0.0') - s.add_dependency('i18n', '~> 0.5.0') + s.add_dependency('i18n', '~> 0.6.0beta1') s.add_dependency('rack', '~> 1.2.1') s.add_dependency('rack-test', '~> 0.5.7') s.add_dependency('rack-mount', '~> 0.7.1') diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 20f8601a8e..0ff1c0491a 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -112,17 +112,6 @@ module AbstractController default_helper_module! unless anonymous? end - private - # Makes all the (instance) methods in the helper module available to templates - # rendered through this controller. - # - # ==== Parameters - # * <tt>module</tt> - The module to include into the current helper module - # for the class - def add_template_helper(mod) - _helpers.module_eval { include mod } - end - # Returns a list of modules, normalized from the acceptable kinds of # helpers with the following behavior: # @@ -155,6 +144,17 @@ module AbstractController end end + private + # Makes all the (instance) methods in the helper module available to templates + # rendered through this controller. + # + # ==== Parameters + # * <tt>module</tt> - The module to include into the current helper module + # for the class + def add_template_helper(mod) + _helpers.module_eval { include mod } + end + def default_helper_module! module_name = name.sub(/Controller$/, '') module_path = module_name.underscore diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index ecd0c4fb73..306bd41e2d 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -105,16 +105,16 @@ module AbstractController # Normalize arguments, options and then delegates render_to_body and # sticks the result in self.response_body. def render(*args, &block) - self.response_body = render_to_string(*args, &block) + options = _normalize_render(*args, &block) + self.response_body = render_to_body(options) end # Raw rendering of a template to a string. Just convert the results of - # render_to_body into a String. + # render_response into a String. # :api: plugin def render_to_string(*args, &block) - options = _normalize_args(*args, &block) - _normalize_options(options) - render_to_body(options).tap { self.response_body = nil } + options = _normalize_render(*args, &block) + render_to_body(options) end # Raw rendering of a template to a Rack-compatible body. @@ -151,8 +151,17 @@ module AbstractController hash end - # Normalize options by converting render "foo" to render :action => "foo" and + # Normalize args and options. + # :api: private + def _normalize_render(*args, &block) + options = _normalize_args(*args, &block) + _normalize_options(options) + options + end + + # Normalize args by converting render "foo" to render :action => "foo" and # render "foo/bar" to render :file => "foo/bar". + # :api: plugin def _normalize_args(action=nil, options={}) case action when NilClass @@ -169,12 +178,14 @@ module AbstractController options end + # Normalize options. + # :api: plugin def _normalize_options(options) if options[:partial] == true options[:partial] = action_name end - if (options.keys & [:partial, :file, :template, :once]).empty? + if (options.keys & [:partial, :file, :template]).empty? options[:prefixes] ||= _prefixes end @@ -182,6 +193,8 @@ module AbstractController options end + # Process extra options. + # :api: plugin def _process_options(options) end end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 62cc18b253..aab2b9dc25 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -13,6 +13,7 @@ module ActionController autoload :Compatibility autoload :ConditionalGet autoload :Cookies + autoload :DataStreaming autoload :Flash autoload :ForceSSL autoload :Head diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5f9e082cd3..ca0dccf575 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -200,6 +200,7 @@ module ActionController RequestForgeryProtection, ForceSSL, Streaming, + DataStreaming, RecordIdentifier, HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 585bd5e5ab..0133b2ecbc 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -131,7 +131,7 @@ module ActionController attr_internal :headers, :response, :request delegate :session, :to => "@_request" - def initialize(*) + def initialize @_headers = {"Content-Type" => "text/html"} @_status = 200 @_request = nil diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb new file mode 100644 index 0000000000..997bc6e958 --- /dev/null +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -0,0 +1,145 @@ +require 'active_support/core_ext/file/path' + +module ActionController #:nodoc: + # Methods for sending arbitrary data and for streaming files to the browser, + # instead of rendering. + module DataStreaming + extend ActiveSupport::Concern + + include ActionController::Rendering + + DEFAULT_SEND_FILE_OPTIONS = { + :type => 'application/octet-stream'.freeze, + :disposition => 'attachment'.freeze, + }.freeze + + protected + # Sends the file. This uses a server-appropriate method (such as X-Sendfile) + # via the Rack::Sendfile middleware. The header to use is set via + # config.action_dispatch.x_sendfile_header, and defaults to "X-Sendfile". + # Your server can also configure this for you by setting the X-Sendfile-Type header. + # + # Be careful to sanitize the path parameter if it is coming from a web + # page. <tt>send_file(params[:path])</tt> allows a malicious user to + # download any file on your server. + # + # Options: + # * <tt>:filename</tt> - suggests a filename for the browser to use. + # Defaults to <tt>File.basename(path)</tt>. + # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify + # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json + # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. + # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from + # the URL, which is necessary for i18n filenames on certain browsers + # (setting <tt>:filename</tt> overrides this option). + # + # The default Content-Type and Content-Disposition headers are + # set to download arbitrary binary files in as many browsers as + # possible. IE versions 4, 5, 5.5, and 6 are all known to have + # a variety of quirks (especially when downloading over SSL). + # + # Simple download: + # + # send_file '/path/to.zip' + # + # Show a JPEG in the browser: + # + # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' + # + # Show a 404 page in the browser: + # + # send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404 + # + # Read about the other Content-* HTTP headers if you'd like to + # provide the user with more information (such as Content-Description) in + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11. + # + # Also be aware that the document may be cached by proxies and browsers. + # The Pragma and Cache-Control headers declare how the file may be cached + # by intermediaries. They default to require clients to validate with + # the server before releasing cached responses. See + # http://www.mnot.net/cache_docs/ for an overview of web caching and + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + # for the Cache-Control header spec. + def send_file(path, options = {}) #:doc: + raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) + + options[:filename] ||= File.basename(path) unless options[:url_based_filename] + send_file_headers! options + + self.status = options[:status] || 200 + self.content_type = options[:content_type] if options.key?(:content_type) + self.response_body = File.open(path, "rb") + end + + # Sends the given binary data to the browser. This method is similar to + # <tt>render :text => data</tt>, but also allows you to specify whether + # the browser should display the response as a file attachment (i.e. in a + # download dialog) or as inline data. You may also set the content type, + # the apparent file name, and other things. + # + # Options: + # * <tt>:filename</tt> - suggests a filename for the browser to use. + # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify + # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json + # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. + # + # Generic data download: + # + # send_data buffer + # + # Download a dynamically-generated tarball: + # + # send_data generate_tgz('dir'), :filename => 'dir.tgz' + # + # Display an image Active Record in the browser: + # + # send_data image.data, :type => image.content_type, :disposition => 'inline' + # + # See +send_file+ for more information on HTTP Content-* headers and caching. + def send_data(data, options = {}) #:doc: + send_file_headers! options.dup + render options.slice(:status, :content_type).merge(:text => data) + end + + private + def send_file_headers!(options) + options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) + [:type, :disposition].each do |arg| + raise ArgumentError, ":#{arg} option required" if options[arg].nil? + end + + disposition = options[:disposition] + disposition += %(; filename="#{options[:filename]}") if options[:filename] + + content_type = options[:type] + + if content_type.is_a?(Symbol) + extension = Mime[content_type] + raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension + self.content_type = extension + else + self.content_type = content_type + end + + headers.merge!( + 'Content-Disposition' => disposition, + 'Content-Transfer-Encoding' => 'binary' + ) + + response.sending_file = true + + # Fix a problem with IE 6.0 on opening downloaded files: + # If Cache-Control: no-cache is set (which Rails does by default), + # IE removes the file it just downloaded from its cache immediately + # 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 + response.cache_control[:public] ||= false + end + end +end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 91a88ab68a..75757db564 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -76,35 +76,35 @@ module ActionController @helper_proxy ||= ActionView::Base.new.extend(_helpers) end - private - # Overwrite modules_for_helpers to accept :all as argument, which loads - # all helpers in helpers_path. - # - # ==== Parameters - # * <tt>args</tt> - A list of helpers - # - # ==== Returns - # * <tt>array</tt> - A normalized list of modules for the list of helpers provided. - def modules_for_helpers(args) - args += all_application_helpers if args.delete(:all) - super(args) - end + # Overwrite modules_for_helpers to accept :all as argument, which loads + # all helpers in helpers_path. + # + # ==== Parameters + # * <tt>args</tt> - A list of helpers + # + # ==== Returns + # * <tt>array</tt> - A normalized list of modules for the list of helpers provided. + def modules_for_helpers(args) + args += all_application_helpers if args.delete(:all) + super(args) + end - # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt> - def all_application_helpers - all_helpers_from_path(helpers_path) + def all_helpers_from_path(path) + helpers = [] + Array.wrap(path).each do |_path| + extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ + helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } end + helpers.sort! + helpers.uniq! + helpers + end - def all_helpers_from_path(path) - helpers = [] - Array.wrap(path).each do |_path| - extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } - end - helpers.sort! - helpers.uniq! - helpers - end + private + # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt> + def all_application_helpers + all_helpers_from_path(helpers_path) + end end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 16d48e4677..f10287afb4 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -6,6 +6,8 @@ module ActionController #:nodoc: module MimeResponds extend ActiveSupport::Concern + include ActionController::ImplicitRender + included do class_attribute :responder, :mimes_for_respond_to self.responder = ActionController::Responder diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index dfda6618e7..0ad9dbeda9 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -95,17 +95,17 @@ module ActionController json = json.to_json(options) unless json.kind_of?(String) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? self.content_type ||= Mime::JSON - self.response_body = json + json end add :js do |js, options| self.content_type ||= Mime::JS - self.response_body = js.respond_to?(:to_js) ? js.to_js(options) : js + js.respond_to?(:to_js) ? js.to_js(options) : js end add :xml do |xml, options| self.content_type ||= Mime::XML - self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml + xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml end end end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 32d52c84c4..70fd79bb8b 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -18,6 +18,17 @@ module ActionController response_body end + # Overwrite render_to_string because body can now be set to a rack body. + def render_to_string(*) + if self.response_body = super + string = "" + response_body.each { |r| string << r } + string + end + ensure + self.response_body = nil + end + private # Normalize arguments by catching blocks and setting them on :update. diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 312dc8eb3e..b9bd49f670 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -1,145 +1,61 @@ require 'active_support/core_ext/file/path' +require 'rack/chunked' module ActionController #:nodoc: - # Methods for sending arbitrary data and for streaming files to the browser, - # instead of rendering. + # Methods for sending streaming templates back to the client. module Streaming extend ActiveSupport::Concern - include ActionController::Rendering + include AbstractController::Rendering + attr_internal :stream - DEFAULT_SEND_FILE_OPTIONS = { - :type => 'application/octet-stream'.freeze, - :disposition => 'attachment'.freeze, - }.freeze - - protected - # Sends the file. This uses a server-appropriate method (such as X-Sendfile) - # via the Rack::Sendfile middleware. The header to use is set via - # config.action_dispatch.x_sendfile_header, and defaults to "X-Sendfile". - # Your server can also configure this for you by setting the X-Sendfile-Type header. - # - # Be careful to sanitize the path parameter if it is coming from a web - # page. <tt>send_file(params[:path])</tt> allows a malicious user to - # download any file on your server. - # - # Options: - # * <tt>:filename</tt> - suggests a filename for the browser to use. - # Defaults to <tt>File.basename(path)</tt>. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify - # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json - # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. - # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from - # the URL, which is necessary for i18n filenames on certain browsers - # (setting <tt>:filename</tt> overrides this option). - # - # The default Content-Type and Content-Disposition headers are - # set to download arbitrary binary files in as many browsers as - # possible. IE versions 4, 5, 5.5, and 6 are all known to have - # a variety of quirks (especially when downloading over SSL). - # - # Simple download: - # - # send_file '/path/to.zip' - # - # Show a JPEG in the browser: - # - # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' - # - # Show a 404 page in the browser: - # - # send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404 - # - # Read about the other Content-* HTTP headers if you'd like to - # provide the user with more information (such as Content-Description) in - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11. - # - # Also be aware that the document may be cached by proxies and browsers. - # The Pragma and Cache-Control headers declare how the file may be cached - # by intermediaries. They default to require clients to validate with - # the server before releasing cached responses. See - # http://www.mnot.net/cache_docs/ for an overview of web caching and - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 - # for the Cache-Control header spec. - def send_file(path, options = {}) #:doc: - raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) - - options[:filename] ||= File.basename(path) unless options[:url_based_filename] - send_file_headers! options - - self.status = options[:status] || 200 - self.content_type = options[:content_type] if options.key?(:content_type) - self.response_body = File.open(path, "rb") - end - - # Sends the given binary data to the browser. This method is similar to - # <tt>render :text => data</tt>, but also allows you to specify whether - # the browser should display the response as a file attachment (i.e. in a - # download dialog) or as inline data. You may also set the content type, - # the apparent file name, and other things. - # - # Options: - # * <tt>:filename</tt> - suggests a filename for the browser to use. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify - # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json - # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. - # - # Generic data download: - # - # send_data buffer - # - # Download a dynamically-generated tarball: - # - # send_data generate_tgz('dir'), :filename => 'dir.tgz' - # - # Display an image Active Record in the browser: - # - # send_data image.data, :type => image.content_type, :disposition => 'inline' - # - # See +send_file+ for more information on HTTP Content-* headers and caching. - def send_data(data, options = {}) #:doc: - send_file_headers! options.dup - render options.slice(:status, :content_type).merge(:text => data) - end - - private - def send_file_headers!(options) - options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) - [:type, :disposition].each do |arg| - raise ArgumentError, ":#{arg} option required" if options[arg].nil? + module ClassMethods + # Render streaming templates. It accepts :only, :except, :if and :unless as options + # to specify when to stream, as in ActionController filters. + def stream(options={}) + if defined?(Fiber) + before_filter :_stream_filter, options + else + raise "You cannot use streaming if Fiber is not available." end + end + end - disposition = options[:disposition] - disposition += %(; filename="#{options[:filename]}") if options[:filename] - - content_type = options[:type] + protected - if content_type.is_a?(Symbol) - extension = Mime[content_type] - raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension - self.content_type = extension + # Mark following render calls as streaming. + def _stream_filter #:nodoc: + self.stream = true + end + + # Consider the stream option when normalazing options. + def _normalize_options(options) #:nodoc: + super + options[:stream] = self.stream unless options.key?(:stream) + end + + # Set proper cache control and transfer encoding when streaming + def _process_options(options) #:nodoc: + super + if options[:stream] + if env["HTTP_VERSION"] == "HTTP/1.0" + options.delete(:stream) else - self.content_type = content_type + headers["Cache-Control"] ||= "no-cache" + headers["Transfer-Encoding"] = "chunked" + headers.delete("Content-Length") end - - headers.merge!( - 'Content-Disposition' => disposition, - 'Content-Transfer-Encoding' => 'binary' - ) - - response.sending_file = true - - # Fix a problem with IE 6.0 on opening downloaded files: - # If Cache-Control: no-cache is set (which Rails does by default), - # IE removes the file it just downloaded from its cache immediately - # 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 - response.cache_control[:public] ||= false end + end + + # Call render_to_body if we are streaming instead of usual +render+. + def _render_template(options) #:nodoc: + if options.delete(:stream) + Rack::Chunked::Body.new view_context.render_body(options) + else + super + end + end end end +
\ No newline at end of file diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 0dd13fa9bf..d2ba052c8d 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -4,6 +4,7 @@ require "action_dispatch/railtie" require "action_view/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/paths" +require "sprockets/railtie" module ActionController class Railtie < Rails::Railtie @@ -21,7 +22,6 @@ module ActionController paths = app.config.paths options = app.config.action_controller - options.use_sprockets ||= app.config.assets.enabled options.assets_dir ||= paths["public"].first options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first diff --git a/actionpack/lib/action_controller/railties/paths.rb b/actionpack/lib/action_controller/railties/paths.rb index dce3c2fe88..699c44c62c 100644 --- a/actionpack/lib/action_controller/railties/paths.rb +++ b/actionpack/lib/action_controller/railties/paths.rb @@ -16,14 +16,6 @@ module ActionController if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers klass.helper :all end - - if app.config.serve_static_assets && namespace - paths = namespace._railtie.config.paths - - klass.config.assets_dir = paths["public"].first - klass.config.javascripts_dir = paths["public/javascripts"].first - klass.config.stylesheets_dir = paths["public/stylesheets"].first - end end end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 8e03a7879f..1f4f3ac0da 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -32,24 +32,35 @@ module ActionDispatch # :nodoc: # puts @response.body # end # end - class Response < Rack::Response - attr_accessor :request, :blank + class Response + attr_accessor :request, :header, :status + attr_writer :sending_file - attr_writer :header, :sending_file alias_method :headers=, :header= + alias_method :headers, :header + + delegate :[], :[]=, :to => :@header + delegate :each, :to => :@body + + # Sets the HTTP response's content MIME type. For example, in the controller + # you could write this: + # + # response.content_type = "text/plain" + # + # If a character set has been defined for this response (see charset=) then + # the character set information will also be included in the content type + # information. + attr_accessor :charset, :content_type + + CONTENT_TYPE = "Content-Type" + + cattr_accessor(:default_charset) { "utf-8" } module Setup def initialize(status = 200, header = {}, body = []) - @writer = lambda { |x| @body << x } - @block = nil - @length = 0 + self.body, self.header, self.status = body, header, status - @header = header - self.body, self.status = body, status - - @cookie = [] @sending_file = false - @blank = false if content_type = self["Content-Type"] @@ -62,6 +73,7 @@ module ActionDispatch # :nodoc: end end + include Rack::Response::Helpers include Setup include ActionDispatch::Http::Cache::Response @@ -106,13 +118,29 @@ module ActionDispatch # :nodoc: def body=(body) @blank = true if body == EMPTY - @body = body.respond_to?(:to_str) ? [body] : body + + # Explicitly check for strings. This is *wrong* theoretically + # but if we don't check this, the performance on string bodies + # is bad on Ruby 1.8 (because strings responds to each then). + @body = if body.respond_to?(:to_str) || !body.respond_to?(:each) + [body] + else + body + end end def body_parts @body end + def set_cookie(key, value) + ::Rack::Utils.set_cookie_header!(header, key, value) + end + + def delete_cookie(key, value={}) + ::Rack::Utils.delete_cookie_header!(header, key, value) + end + def location headers['Location'] end @@ -122,46 +150,21 @@ module ActionDispatch # :nodoc: headers['Location'] = url end - # Sets the HTTP response's content MIME type. For example, in the controller - # you could write this: - # - # response.content_type = "text/plain" - # - # If a character set has been defined for this response (see charset=) then - # the character set information will also be included in the content type - # information. - attr_accessor :charset, :content_type - - CONTENT_TYPE = "Content-Type" - - cattr_accessor(:default_charset) { "utf-8" } - def to_a assign_default_content_type_and_charset! handle_conditional_get! - self["Set-Cookie"] = self["Set-Cookie"].join("\n") if self["Set-Cookie"].respond_to?(:join) - super - end - alias prepare! to_a + @header["Set-Cookie"] = @header["Set-Cookie"].join("\n") if @header["Set-Cookie"].respond_to?(:join) - def each(&callback) - if @body.respond_to?(:call) - @writer = lambda { |x| callback.call(x) } - @body.call(self, self) + if [204, 304].include?(@status) + @header.delete "Content-Type" + [@status, @header, []] else - @body.each { |part| callback.call(part.to_s) } + [@status, @header, self] end - - @writer = callback - @block.call(self) if @block - end - - def write(str) - str = str.to_s - @writer.call str - str end + alias prepare! to_a + alias to_ary to_a # For implicit splat on 1.9.2 # Returns the response cookies, converted to a Hash of (name => value) pairs # @@ -180,18 +183,18 @@ module ActionDispatch # :nodoc: cookies end - private - def assign_default_content_type_and_charset! - return if headers[CONTENT_TYPE].present? + private - @content_type ||= Mime::HTML - @charset ||= self.class.default_charset + def assign_default_content_type_and_charset! + return if headers[CONTENT_TYPE].present? - type = @content_type.to_s.dup - type << "; charset=#{@charset}" unless @sending_file + @content_type ||= Mime::HTML + @charset ||= self.class.default_charset - headers[CONTENT_TYPE] = type - end + type = @content_type.to_s.dup + type << "; charset=#{@charset}" unless @sending_file + headers[CONTENT_TYPE] = type + end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 027ff7f8ac..2adbce031b 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -4,7 +4,7 @@ module ActionDispatch # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash - @env['action_dispatch.request.flash_hash'] ||= (session["flash"] || Flash::FlashHash.new) + @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new) end end @@ -40,18 +40,16 @@ module ActionDispatch # # See docs on the FlashHash class for more details about the flash. class Flash + KEY = 'action_dispatch.request.flash_hash'.freeze + class FlashNow #:nodoc: + attr_accessor :flash + def initialize(flash) @flash = flash - @closed = false end - attr_reader :closed - alias :closed? :closed - def close!; @closed = true end - def []=(k, v) - raise ClosedError, :flash if closed? @flash[k] = v @flash.discard(k) v @@ -79,11 +77,16 @@ module ActionDispatch @used = Set.new @closed = false @flashes = {} + @now = nil end - attr_reader :closed - alias :closed? :closed - def close!; @closed = true end + def initialize_copy(other) + if other.now_is_loaded? + @now = other.now.dup + @now.flash = self + end + super + end def []=(k, v) #:nodoc: raise ClosedError, :flash if closed? @@ -152,6 +155,10 @@ module ActionDispatch @now ||= FlashNow.new(self) end + attr_reader :closed + alias :closed? :closed + def close!; @closed = true; end + # Keeps either the entire current flash or a specific flash entry available for the next action: # # flash.keep # keeps the entire flash @@ -205,7 +212,12 @@ module ActionDispatch self[:notice] = message end - private + protected + + def now_is_loaded? + !!@now + end + # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods # use() # marks the entire flash as used # use('msg') # marks the "msg" entry as used @@ -231,13 +243,18 @@ module ActionDispatch @app.call(env) ensure session = env['rack.session'] || {} - flash_hash = env['action_dispatch.request.flash_hash'] + flash_hash = env[KEY] if flash_hash - if !flash_hash.empty? || session.key?('flash') - session["flash"] = flash_hash - end - flash_hash.close! + if !flash_hash.empty? || session.key?('flash') + session["flash"] = flash_hash + new_hash = flash_hash.dup + else + new_hash = flash_hash + end + + env[KEY] = new_hash + new_hash.close! end if session.key?('flash') && session['flash'].empty? diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index dbe3206808..b1adf3d2d1 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -50,7 +50,7 @@ module ActionDispatch # Only this middleware cares about RoutingError. So, let's just raise # it here. if headers['X-Cascade'] == 'pass' - raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" end rescue Exception => exception raise exception if env['action_dispatch.show_exceptions'] == false diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index c57f694c4d..348f7b86b8 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -2,25 +2,23 @@ require 'rack/utils' module ActionDispatch class FileHandler - def initialize(at, root) - @at, @root = at.chomp('/'), root.chomp('/') - @compiled_at = @at.blank? ? nil : /^#{Regexp.escape(at)}/ + def initialize(root) + @root = root.chomp('/') @compiled_root = /^#{Regexp.escape(root)}/ @file_server = ::Rack::File.new(@root) end def match?(path) path = path.dup - if !@compiled_at || path.sub!(@compiled_at, '') - full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path)) - paths = "#{full_path}#{ext}" - matches = Dir[paths] - match = matches.detect { |m| File.file?(m) } - if match - match.sub!(@compiled_root, '') - match - end + full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path)) + paths = "#{full_path}#{ext}" + + matches = Dir[paths] + match = matches.detect { |m| File.file?(m) } + if match + match.sub!(@compiled_root, '') + match end end @@ -39,9 +37,9 @@ module ActionDispatch class Static FILE_METHODS = %w(GET HEAD).freeze - def initialize(app, roots) + def initialize(app, path) @app = app - @file_handlers = create_file_handlers(roots) + @file_handler = FileHandler.new(path) end def call(env) @@ -49,24 +47,13 @@ module ActionDispatch method = env['REQUEST_METHOD'] if FILE_METHODS.include?(method) - @file_handlers.each do |file_handler| - if match = file_handler.match?(path) - env["PATH_INFO"] = match - return file_handler.call(env) - end + if match = @file_handler.match?(path) + env["PATH_INFO"] = match + return @file_handler.call(env) end end @app.call(env) end - - private - def create_file_handlers(roots) - roots = { '' => roots } unless roots.is_a?(Hash) - - roots.map do |at, root| - FileHandler.new(at, root) if File.exist?(root) - end.compact - end end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index b28f6c2297..1d09091dc7 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -275,8 +275,7 @@ module ActionDispatch module MountedHelpers end - def mounted_helpers(name = :main_app) - define_mounted_helper(name) if name + def mounted_helpers MountedHelpers end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 8a04cfa886..e209978fb7 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -42,7 +42,7 @@ module ActionDispatch elsif type.is_a?(Symbol) && @response.response_code == Rack::Utils::SYMBOL_TO_STATUS_CODE[type] assert_block("") { true } # to count the assertion else - assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false } + assert(false, build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) end end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 60665387b6..4547aceb28 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -44,6 +44,7 @@ module ActionView autoload :AbstractRenderer autoload :PartialRenderer autoload :TemplateRenderer + autoload :StreamingTemplateRenderer end autoload_at "action_view/template/resolver" do @@ -53,6 +54,16 @@ module ActionView autoload :FallbackFileSystemResolver end + autoload_at "action_view/buffers" do + autoload :OutputBuffer + autoload :StreamingBuffer + end + + autoload_at "action_view/flows" do + autoload :OutputFlow + autoload :StreamingFlow + end + autoload_at "action_view/template/error" do autoload :MissingTemplate autoload :ActionViewError diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 7488127e35..87501d5b88 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -137,6 +137,12 @@ module ActionView #:nodoc: cattr_accessor :field_error_proc @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } + # How to complete the streaming when an exception occurs. + # This is our best guess: first try to close the attribute, then the tag. + # Currently this is private API and may be changed at *any* time. + cattr_accessor :streaming_completion_on_exception + @@streaming_completion_on_exception = %("><script type="text/javascript">window.location = "/500.html"</script></html>) + class_attribute :helpers class_attribute :_routes @@ -153,7 +159,7 @@ module ActionView #:nodoc: end end - attr_accessor :_template + attr_accessor :_template, :_view_flow attr_internal :request, :controller, :config, :assigns, :lookup_context delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context @@ -181,8 +187,8 @@ module ActionView #:nodoc: self.helpers = Module.new unless self.class.helpers @_config = {} - @_content_for = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } @_virtual_path = nil + @_view_flow = OutputFlow.new @output_buffer = nil if @_controller = controller @@ -195,10 +201,6 @@ module ActionView #:nodoc: @_lookup_context.formats = formats if formats end - def store_content_for(key, value) - @_content_for[key] = value - end - def controller_path @controller_path ||= controller && controller.controller_path end diff --git a/actionpack/lib/action_view/buffers.rb b/actionpack/lib/action_view/buffers.rb new file mode 100644 index 0000000000..089fc68706 --- /dev/null +++ b/actionpack/lib/action_view/buffers.rb @@ -0,0 +1,43 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: + def initialize(*) + super + encode! if encoding_aware? + end + + def <<(value) + super(value.to_s) + end + alias :append= :<< + alias :safe_append= :safe_concat + end + + class StreamingBuffer #:nodoc: + def initialize(block) + @block = block + end + + def <<(value) + value = value.to_s + value = ERB::Util.h(value) unless value.html_safe? + @block.call(value) + end + alias :concat :<< + alias :append= :<< + + def safe_concat(value) + @block.call(value.to_s) + end + alias :safe_append= :safe_concat + + def html_safe? + true + end + + def html_safe + self + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/context.rb b/actionpack/lib/action_view/context.rb index 39d88333e8..a2a64de206 100644 --- a/actionpack/lib/action_view/context.rb +++ b/actionpack/lib/action_view/context.rb @@ -2,6 +2,7 @@ module ActionView module CompiledTemplates #:nodoc: # holds compiled template code end + # = Action View Context # # Action View contexts are supplied to Action Controller to render template. diff --git a/actionpack/lib/action_view/flows.rb b/actionpack/lib/action_view/flows.rb new file mode 100644 index 0000000000..386a06511f --- /dev/null +++ b/actionpack/lib/action_view/flows.rb @@ -0,0 +1,79 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputFlow #:nodoc: + attr_reader :content + + def initialize + @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } + end + + # Called by _layout_for to read stored values. + def get(key) + @content[key] + end + + # Called by each renderer object to set the layout contents. + def set(key, value) + @content[key] = value + end + + # Called by content_for + def append(key, value) + @content[key] << value + end + + # Called by provide + def append!(key, value) + @content[key] << value + end + end + + class StreamingFlow < OutputFlow #:nodoc: + def initialize(view, fiber) + @view = view + @parent = nil + @child = view.output_buffer + @content = view._view_flow.content + @fiber = fiber + @root = Fiber.current.object_id + end + + # Try to get an stored content. If the content + # is not available and we are inside the layout + # fiber, we set that we are waiting for the given + # key and yield. + def get(key) + return super if @content.key?(key) + + if inside_fiber? + view = @view + + begin + @waiting_for = key + view.output_buffer, @parent = @child, view.output_buffer + Fiber.yield + ensure + @waiting_for = nil + view.output_buffer, @child = @parent, view.output_buffer + end + end + + super + end + + # Appends the contents for the given key. This is called + # by provides and resumes back to the fiber if it is + # the key it is waiting for. + def append!(key, value) + super + @fiber.resume if @waiting_for == key + end + + private + + def inside_fiber? + Fiber.current.object_id != @root + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_paths.rb new file mode 100644 index 0000000000..55a4c442fd --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_paths.rb @@ -0,0 +1,80 @@ +require 'active_support/core_ext/file' +require 'action_view/helpers/asset_paths' + +module ActionView + module Helpers + + class AssetPaths #:nodoc: + attr_reader :config, :controller + + def initialize(config, controller) + @config = config + @controller = controller + end + + # Add the extension +ext+ if not present. Return full URLs otherwise untouched. + # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL + # roots. Rewrite the asset path for cache-busting asset ids. Include + # asset host, if configured, with the correct request protocol. + def compute_public_path(source, dir, ext = nil, include_host = true) + source = source.to_s + return source if is_uri?(source) + + source = rewrite_extension(source, dir, ext) if ext + source = "/#{dir}/#{source}" unless source[0] == ?/ + source = rewrite_asset_path(source, dir) + + if controller && include_host + has_request = controller.respond_to?(:request) + source = rewrite_host_and_protocol(source, has_request) + end + + source + end + + def is_uri?(path) + path =~ %r{^[-a-z]+://|^cid:} + end + + private + + def rewrite_extension(source, dir, ext) + raise NotImplementedError + end + + def rewrite_asset_path(source, path = nil) + raise NotImplementedError + end + + def rewrite_host_and_protocol(source, has_request) + host = compute_asset_host(source) + if has_request && host && !is_uri?(host) + host = "#{controller.request.protocol}#{host}" + end + "#{host}#{source}" + 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), + # or the value returned from invoking the proc if it's a proc or the value from + # invoking call if it's an object responding to call. + def compute_asset_host(source) + if host = config.asset_host + if host.is_a?(Proc) || host.respond_to?(:call) + case host.is_a?(Proc) ? host.arity : host.method(:call).arity + when 2 + request = controller.respond_to?(:request) && controller.request + host.call(source, request) + else + host.call(source) + end + else + (host =~ /%d/) ? host % (source.hash % 4) : host + end + end + end + end + + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index f6b2d4f3f4..9bc847a1ab 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -57,7 +57,7 @@ module ActionView # +asset_host+ to a proc like this: # # ActionController::Base.asset_host = Proc.new { |source| - # "http://assets#{source.hash % 2 + 1}.example.com" + # "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com" # } # image_tag("rails.png") # # => <img alt="Rails" src="http://assets1.example.com/images/rails.png?1230601161" /> @@ -268,13 +268,17 @@ module ActionView # image_path("edit.png") # => "/images/edit.png" # image_path("icons/edit.png") # => "/images/icons/edit.png" # image_path("/icons/edit.png") # => "/icons/edit.png" - # image_path("http://www.railsapplication.com/img/edit.png") # => "http://www.railsapplication.com/img/edit.png" + # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png" # # If you have images as application resources this method may conflict with their named routes. # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and # plugin authors are encouraged to do so. def image_path(source) - asset_paths.compute_public_path(source, 'images') + if config.use_sprockets + asset_path(source) + else + asset_paths.compute_public_path(source, 'images') + end end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route @@ -287,9 +291,13 @@ module ActionView # video_path("hd.avi") # => /videos/hd.avi # video_path("trailers/hd.avi") # => /videos/trailers/hd.avi # video_path("/trailers/hd.avi") # => /trailers/hd.avi - # video_path("http://www.railsapplication.com/vid/hd.avi") # => http://www.railsapplication.com/vid/hd.avi + # video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi def video_path(source) - asset_paths.compute_public_path(source, 'videos') + if config.use_sprockets + asset_path(source) + else + asset_paths.compute_public_path(source, 'videos') + end end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route @@ -302,9 +310,13 @@ module ActionView # audio_path("horse.wav") # => /audios/horse.wav # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav # audio_path("/sounds/horse.wav") # => /sounds/horse.wav - # audio_path("http://www.railsapplication.com/sounds/horse.wav") # => http://www.railsapplication.com/sounds/horse.wav + # audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav def audio_path(source) - asset_paths.compute_public_path(source, 'audios') + if config.use_sprockets + asset_path(source) + else + asset_paths.compute_public_path(source, 'audios') + end end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route @@ -434,7 +446,7 @@ module ActionView private def asset_paths - @asset_paths ||= AssetPaths.new(config, controller) + @asset_paths ||= AssetTagHelper::AssetPaths.new(config, controller) end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb index 52eb43a1cd..e4662a2919 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb @@ -8,9 +8,11 @@ module ActionView module AssetTagHelper class AssetIncludeTag - attr_reader :config, :asset_paths + include TagHelper + attr_reader :config, :asset_paths class_attribute :expansions + def self.inherited(base) base.expansions = { } end @@ -56,7 +58,6 @@ module ActionView end end - private def path_to_asset(source, include_host = true) diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb index 014a03c54d..38860431b4 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -1,10 +1,11 @@ require 'active_support/core_ext/file' +require 'action_view/helpers/asset_paths' module ActionView module Helpers module AssetTagHelper - class AssetPaths + class AssetPaths < ActionView::Helpers::AssetPaths #:nodoc: # You can enable or disable the asset tag ids cache. # With the cache enabled, the asset tag helper methods will make fewer # expensive file system calls (the default implementation checks the file @@ -14,34 +15,6 @@ module ActionView # ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false mattr_accessor :cache_asset_ids - attr_reader :config, :controller - - def initialize(config, controller) - @config = config - @controller = controller - end - - # Add the extension +ext+ if not present. Return full URLs otherwise untouched. - # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL - # roots. Rewrite the asset path for cache-busting asset ids. Include - # asset host, if configured, with the correct request protocol. - def compute_public_path(source, dir, ext = nil, include_host = true) - return source if is_uri?(source) - - source = rewrite_extension(source, dir, ext) if ext - source = "/#{dir}/#{source}" unless source[0] == ?/ - if controller.respond_to?(:env) && controller.env["action_dispatch.asset_path"] - source = rewrite_asset_path(source, controller.env["action_dispatch.asset_path"]) - end - source = rewrite_asset_path(source, config.asset_path) - - has_request = controller.respond_to?(:request) - source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request && include_host - source = rewrite_host_and_protocol(source, has_request) if include_host - - source - end - # Add or change an asset id in the asset id cache. This can be used # for SASS on Heroku. # :api: public @@ -51,101 +24,75 @@ module ActionView end end - def is_uri?(path) - path =~ %r{^[-a-z]+://|^cid:} - end + private - private + def rewrite_extension(source, dir, ext) + source_ext = File.extname(source) - def rewrite_extension(source, dir, ext) - source_ext = File.extname(source) + source_with_ext = if source_ext.empty? + "#{source}.#{ext}" + elsif ext != source_ext[1..-1] + with_ext = "#{source}.#{ext}" + with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext)) + end - source_with_ext = if source_ext.empty? - "#{source}.#{ext}" - elsif ext != source_ext[1..-1] - with_ext = "#{source}.#{ext}" - with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext)) - end + source_with_ext || source + end - source_with_ext || source - end + # Break out the asset path rewrite in case plugins wish to put the asset id + # someplace other than the query string. + def rewrite_asset_path(source, path = nil) + path = config.asset_path - # Break out the asset path rewrite in case plugins wish to put the asset id - # someplace other than the query string. - def rewrite_asset_path(source, path = nil) - if path && path.respond_to?(:call) - return path.call(source) - elsif path && path.is_a?(String) - return path % [source] - end + if path && path.respond_to?(:call) + return path.call(source) + elsif path && path.is_a?(String) + return path % [source] + end - asset_id = rails_asset_id(source) - if asset_id.empty? - source - else - "#{source}?#{asset_id}" - end + asset_id = rails_asset_id(source) + if asset_id.empty? + source + else + "#{source}?#{asset_id}" end + end - mattr_accessor :asset_ids_cache - self.asset_ids_cache = {} + mattr_accessor :asset_ids_cache + self.asset_ids_cache = {} - mattr_accessor :asset_ids_cache_guard - self.asset_ids_cache_guard = Mutex.new + mattr_accessor :asset_ids_cache_guard + self.asset_ids_cache_guard = Mutex.new - # Use the RAILS_ASSET_ID environment variable or the source's - # modification time as its cache-busting asset id. - def rails_asset_id(source) - if asset_id = ENV["RAILS_ASSET_ID"] + # Use the RAILS_ASSET_ID environment variable or the source's + # modification time as its cache-busting asset id. + def rails_asset_id(source) + if asset_id = ENV["RAILS_ASSET_ID"] + asset_id + else + if self.cache_asset_ids && (asset_id = self.asset_ids_cache[source]) asset_id else - if self.cache_asset_ids && (asset_id = self.asset_ids_cache[source]) - asset_id - else - path = File.join(config.assets_dir, source) - asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' + path = File.join(config.assets_dir, source) + asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' - if self.cache_asset_ids - add_to_asset_ids_cache(source, asset_id) - end - - asset_id + if self.cache_asset_ids + add_to_asset_ids_cache(source, asset_id) end - end - end - - def rewrite_relative_url_root(source, relative_url_root) - relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source - end - def rewrite_host_and_protocol(source, has_request) - host = compute_asset_host(source) - if has_request && host && !is_uri?(host) - host = "#{controller.request.protocol}#{host}" + asset_id end - "#{host}#{source}" end + 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), - # or the value returned from invoking the proc if it's a proc or the value from - # invoking call if it's an object responding to call. - def compute_asset_host(source) - if host = config.asset_host - if host.is_a?(Proc) || host.respond_to?(:call) - case host.is_a?(Proc) ? host.arity : host.method(:call).arity - when 2 - request = controller.respond_to?(:request) && controller.request - host.call(source, request) - else - host.call(source) - end - else - (host =~ /%d/) ? host % (source.hash % 4) : host - end - end - end + def rewrite_relative_url_root(source, relative_url_root) + relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source + end + + def rewrite_host_and_protocol(source, has_request) + source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request + super(source, has_request) + end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb index ce5a7dc2e5..3d815b5e1f 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -1,6 +1,5 @@ require 'active_support/concern' require 'active_support/core_ext/file' -require 'action_view/helpers/tag_helper' require 'action_view/helpers/asset_tag_helpers/asset_include_tag' module ActionView @@ -8,8 +7,6 @@ module ActionView module AssetTagHelper class JavascriptIncludeTag < AssetIncludeTag - include TagHelper - def asset_name 'javascript' end @@ -80,14 +77,14 @@ module ActionView # Used internally by javascript_include_tag to build the script path. # # ==== Examples - # 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 - # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js + # 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.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr + # javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def javascript_path(source) if config.use_sprockets - sprockets_javascript_path(source) + asset_path(source, 'js') else asset_paths.compute_public_path(source, 'javascripts', 'js') end @@ -126,11 +123,11 @@ module ActionView # # => <script type="text/javascript" src="/javascripts/common.javascript?1284139606"></script> # # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script> # - # javascript_include_tag "http://www.railsapplication.com/xmlhr" - # # => <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # javascript_include_tag "http://www.example.com/xmlhr" + # # => <script type="text/javascript" src="http://www.example.com/xmlhr.js?1284139606"></script> # - # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" - # # => <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # javascript_include_tag "http://www.example.com/xmlhr.js" + # # => <script type="text/javascript" src="http://www.example.com/xmlhr.js?1284139606"></script> # # javascript_include_tag :defaults # # => <script type="text/javascript" src="/javascripts/jquery.js?1284139606"></script> diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb index a994afb65e..a95eb221be 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -1,6 +1,5 @@ require 'active_support/concern' require 'active_support/core_ext/file' -require 'action_view/helpers/tag_helper' require 'action_view/helpers/asset_tag_helpers/asset_include_tag' module ActionView @@ -8,8 +7,6 @@ module ActionView module AssetTagHelper class StylesheetIncludeTag < AssetIncludeTag - include TagHelper - def asset_name 'stylesheet' end @@ -57,14 +54,14 @@ module ActionView # Used internally by +stylesheet_link_tag+ to build the stylesheet path. # # ==== Examples - # 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 - # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css + # 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.example.com/css/style" # => http://www.example.com/css/style + # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css def stylesheet_path(source) if config.use_sprockets - sprockets_stylesheet_path(source) + asset_path(source, 'css') else asset_paths.compute_public_path(source, 'stylesheets', 'css') end @@ -82,8 +79,8 @@ module ActionView # stylesheet_link_tag "style.css" # => # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> # - # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => - # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" /> + # stylesheet_link_tag "http://www.example.com/style.css" # => + # <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" type="text/css" /> # # stylesheet_link_tag "style", :media => "all" # => # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" /> diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 3808e231f1..0139714240 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -135,8 +135,19 @@ module ActionView # for elements that will be fragment cached. def content_for(name, content = nil, &block) content = capture(&block) if block_given? - @_content_for[name] << content if content - @_content_for[name] unless content + result = @_view_flow.append(name, content) if content + result unless content + end + + # The same as +content_for+ but when used with streaming flushes + # straight back to the layout. In other words, if you want to + # concatenate several times to the same buffer when rendering a given + # template, you should use +content_for+, if not, use +provide+ to tell + # the layout to stop looking for more contents. + def provide(name, content = nil, &block) + content = capture(&block) if block_given? + result = @_view_flow.append!(name, content) if content + result unless content end # content_for? simply checks whether any content has been captured yet using content_for @@ -158,7 +169,7 @@ module ActionView # </body> # </html> def content_for?(name) - @_content_for[name].present? + @_view_flow.get(name).present? end # Use an alternate output buffer for the duration of the block. diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 313a2591bf..9277359d5c 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -51,7 +51,7 @@ module ActionView # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years - # distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days + # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute @@ -112,10 +112,12 @@ module ActionView # # ==== Examples # time_ago_in_words(3.minutes.from_now) # => 3 minutes - # time_ago_in_words(Time.now - 15.hours) # => 15 hours + # time_ago_in_words(Time.now - 15.hours) # => about 15 hours # time_ago_in_words(Time.now) # => less than a minute # - # from_time = Time.now - 3.days - 14.minutes - 25.seconds # => 3 days + # from_time = Time.now - 3.days - 14.minutes - 25.seconds + # time_ago_in_words(from_time) # => 3 days + # def time_ago_in_words(from_time, include_seconds = false) distance_of_time_in_words(from_time, Time.now, include_seconds) end @@ -464,7 +466,7 @@ module ActionView # # # Generates a select field for hours with a custom prompt. Use :prompt => true for a # # generic prompt. - # select_hour(13, :prompt =>'Choose hour') + # select_hour(13, :prompt => 'Choose hour') # def select_hour(datetime, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_hour diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 9025d9e24c..440acafa88 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -610,10 +610,11 @@ module ActionView # label(:post, :body) # # => <label for="post_body">Write your entire text here</label> # - # Localization can also be based purely on the translation of the attribute-name like this: + # Localization can also be based purely on the translation of the attribute-name + # (if you are using ActiveRecord): # - # activemodel: - # attribute: + # activerecord: + # attributes: # post: # cost: "Total cost" # diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 05a9c5b4f1..b545031fcc 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -100,7 +100,7 @@ module ActionView # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,506 € # - # number_to_currency(1234567890.50, :negative_format => "(%u%n)") + # number_to_currency(-1234567890.50, :negative_format => "(%u%n)") # # => ($1,234,567,890.51) # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") # # => £1234567890,50 @@ -238,7 +238,7 @@ module ActionView # number_with_precision(111.2345, :precision => 1, :significant => true) # => 100 # number_with_precision(13, :precision => 5, :significant => true) # => 13.000 # number_with_precision(111.234, :locale => :fr) # => 111,234 - # number_with_precision(13, :precision => 5, :significant => true, strip_insignificant_zeros => true) + # number_with_precision(13, :precision => 5, :significant => true, :strip_insignificant_zeros => true) # # => 13 # number_with_precision(389.32314, :precision => 4, :significant => true) # => 389.3 # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') @@ -318,7 +318,7 @@ module ActionView # Non-significant zeros after the fractional separator are stripped out by default (set # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" - # number_to_human_size(524288000, :precision=>5) # => "500 MB" + # number_to_human_size(524288000, :precision => 5) # => "500 MB" def number_to_human_size(number, options = {}) options.symbolize_keys! @@ -407,7 +407,7 @@ module ActionView # Unsignificant zeros after the decimal separator are stripped out by default (set # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): # number_to_human(12345012345, :significant_digits => 6) # => "12.345 Billion" - # number_to_human(500000000, :precision=>5) # => "500 Million" + # number_to_human(500000000, :precision => 5) # => "500 Million" # # ==== Custom Unit Quantifiers # diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index 4d300a1469..142a25f118 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -10,7 +10,7 @@ module ActionView # relate to the specified Active Record object. Usage example: # # <%= div_for(@person, :class => "foo") do %> - # <%=h @person.name %> + # <%= @person.name %> # <% end %> # # produces: @@ -25,8 +25,8 @@ module ActionView # that relate to the specified Active Record object. For example: # # <%= content_tag_for(:tr, @person) do %> - # <td><%=h @person.first_name %></td> - # <td><%=h @person.last_name %></td> + # <td><%= @person.first_name %></td> + # <td><%= @person.last_name %></td> # <% end %> # # would produce the following HTML (assuming @person is an instance of diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index 0fee34f8a4..841be0a567 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -94,7 +94,7 @@ module ActionView # # => Please e-mail me at me@email.com. # # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.') - # # => Blog: Visit + # # => Blog: Visit. def strip_links(html) self.class.link_sanitizer.sanitize(html) end diff --git a/actionpack/lib/action_view/helpers/sprockets_helper.rb b/actionpack/lib/action_view/helpers/sprockets_helper.rb index 408a2030ab..b43b91178c 100644 --- a/actionpack/lib/action_view/helpers/sprockets_helper.rb +++ b/actionpack/lib/action_view/helpers/sprockets_helper.rb @@ -1,85 +1,69 @@ require 'uri' +require 'action_view/helpers/asset_paths' module ActionView module Helpers module SprocketsHelper - def sprockets_javascript_path(source) - compute_sprockets_path source, 'assets', 'js' + def asset_path(source, default_ext = nil) + sprockets_asset_paths.compute_public_path(source, 'assets', default_ext, true) end def sprockets_javascript_include_tag(source, options = {}) options = { 'type' => "text/javascript", - 'src' => sprockets_javascript_path(source) + 'src' => asset_path(source, 'js') }.merge(options.stringify_keys) content_tag 'script', "", options end - def sprockets_stylesheet_path(source) - compute_sprockets_path source, 'assets', 'css' - end - def sprockets_stylesheet_link_tag(source, options = {}) options = { 'rel' => "stylesheet", 'type' => "text/css", 'media' => "screen", - 'href' => sprockets_stylesheet_path(source) + 'href' => asset_path(source, 'css') }.merge(options.stringify_keys) tag 'link', options end private - def compute_sprockets_path(source, dir, default_ext) - source = source.to_s - - return source if URI.parse(source).host - - # Add /javscripts to relative paths - if source[0] != ?/ - source = "/#{dir}/#{source}" - end - # Add default extension if there isn't one - if default_ext && File.extname(source).empty? - source = "#{source}.#{default_ext}" - end + def sprockets_asset_paths + @sprockets_asset_paths ||= begin + config = self.config if respond_to?(:config) + controller = self.controller if respond_to?(:controller) + SprocketsHelper::AssetPaths.new(config, controller) + end + end - # Fingerprint url + class AssetPaths < ActionView::Helpers::AssetPaths #:nodoc: + def rewrite_asset_path(source, dir) if source =~ /^\/#{dir}\/(.+)/ - source = assets.path($1, config.perform_caching, dir) + assets.path($1, performing_caching?, dir) + else + source end - - host = compute_asset_host(source) - - if controller.respond_to?(:request) && host && URI.parse(host).host - source = "#{controller.request.protocol}#{host}#{source}" - end - - source end - def compute_asset_host(source) - if host = config.asset_host - if host.is_a?(Proc) || host.respond_to?(:call) - case host.is_a?(Proc) ? host.arity : host.method(:call).arity - when 2 - request = controller.respond_to?(:request) && controller.request - host.call(source, request) - else - host.call(source) - end - else - (host =~ /%d/) ? host % (source.hash % 4) : host - end + def rewrite_extension(source, dir, ext) + if ext && File.extname(source).empty? + "#{source}.#{ext}" + else + source end end def assets Rails.application.assets end + + # When included in Sprockets::Context, we need to ask the top-level config as the controller is not available + def performing_caching? + @config ? @config.perform_caching : Rails.application.config.action_controller.perform_caching + end + end end end -end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index bdda1df437..06e2b027da 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -19,7 +19,7 @@ module ActionView # simple_format('<a href="http://example.com/">Example</a>') # # => "<p><a href=\"http://example.com/\">Example</a></p>" # - # simple_format('<a href="javascript:alert('no!')">Example</a>') + # simple_format('<a href="javascript:alert(\'no!\')">Example</a>') # # => "<p><a>Example</a></p>" # # If you want to escape all content, you should invoke the +h+ method before @@ -295,11 +295,11 @@ module ActionView # +link+ as its optional second parameter and the +html_options+ hash # as its optional third parameter: # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com." - # auto_link(post_body, :urls) # => Once upon\na time + # auto_link(post_body, :urls) # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\">http://www.myblog.com</a>. # Please e-mail me at me@email.com." # - # auto_link(post_body, :all, :target => "_blank") # => Once upon\na time + # auto_link(post_body, :all, :target => "_blank") # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>. # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>." def auto_link(text, *args, &block)#link = :all, html = {}, &block) diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 59e6ce878f..26ebae6546 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -5,7 +5,7 @@ module I18n class ExceptionHandler include Module.new { def call(exception, locale, key, options) - exception.is_a?(MissingTranslationData) ? super.html_safe : super + exception.is_a?(MissingTranslation) ? super.html_safe : super end } end @@ -17,8 +17,8 @@ module ActionView module TranslationHelper # Delegates to I18n#translate but also performs three additional functions. # - # First, it'll pass the :rescue_format => :html option to I18n so that any caught - # MissingTranslationData exceptions will be turned into inline spans that + # First, it'll pass the :rescue_format => :html option to I18n so that any + # thrown MissingTranslation messages will be turned into inline spans that # # * have a "translation-missing" class set, # * contain the missing key as a title attribute and diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index de75488e72..ffa9a5bb0b 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -68,7 +68,7 @@ module ActionView # # => /books/find # # <%= url_for(:action => 'login', :controller => 'members', :only_path => false, :protocol => 'https') %> - # # => https://www.railsapplication.com/members/login/ + # # => https://www.example.com/members/login/ # # <%= url_for(:action => 'play', :anchor => 'player') %> # # => /messages/play/#player @@ -555,10 +555,10 @@ module ActionView # current_page?(:controller => 'shop', :action => 'checkout') # # => true # - # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page=>'1') + # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page => '1') # # => true # - # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page=>'2') + # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page => '2') # # => false # # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc') diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 94c0a8a8fb..10cd37d56f 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -1,5 +1,3 @@ -require 'action_view/renderer/abstract_renderer' - module ActionView class PartialRenderer < AbstractRenderer #:nodoc: PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } @@ -79,7 +77,7 @@ module ActionView locals[as] = object content = @template.render(view, locals) do |*name| - view._layout_for(*name, &block) + view._block_layout_for(*name, &block) end content = layout.render(view, locals){ content } if layout diff --git a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb new file mode 100644 index 0000000000..03aab444f8 --- /dev/null +++ b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb @@ -0,0 +1,130 @@ +# 1.9 ships with Fibers but we need to require the extra +# methods explicitly. We only load those extra methods if +# Fiber is available in the first place. +require 'fiber' if defined?(Fiber) + +module ActionView + # Consider the following layout: + # + # <%= yield :header %> + # 2 + # <%= yield %> + # 5 + # <%= yield :footer %> + # + # And template: + # + # <%= provide :header, "1" %> + # 3 + # 4 + # <%= provide :footer, "6" %> + # + # It will stream: + # + # "1\n", "2\n", "3\n4\n", "5\n", "6\n" + # + # Notice that once you <%= yield %>, it will render the whole template + # before streaming again. In the future, we can also support streaming + # from the template and not only the layout. + # + # Also, notice we use +provide+ instead of +content_for+, as +provide+ + # gives the control back to the layout as soon as it is called. + # With +content_for+, it would render all the template to find all + # +content_for+ calls. For instance, consider this layout: + # + # <%= yield :header %> + # + # With this template: + # + # <%= content_for :header, "1" %> + # <%= provide :header, "2" %> + # <%= provide :header, "3" %> + # + # It will return "12\n" because +content_for+ continues rendering the + # template but it is returns back to the layout as soon as it sees the + # first +provide+. + # + # == TODO + # + # * Support streaming from child templates, partials and so on. + # * Integrate exceptions with exceptron + class StreamingTemplateRenderer < TemplateRenderer #:nodoc: + # A valid Rack::Body (i.e. it responds to each). + # It is initialized with a block that, when called, starts + # rendering the template. + class Body #:nodoc: + def initialize(&start) + @start = start + end + + def each(&block) + begin + @start.call(block) + rescue + block.call ActionView::Base.streaming_completion_on_exception + end + self + end + end + + # For streaming, instead of rendering a given a template, we return a Body + # object that responds to each. This object is initialized with a block + # that knows how to render the template. + def render_template(template, layout_name = nil, locals = {}) #:nodoc: + return [super] unless layout_name && template.supports_streaming? + + locals ||= {} + layout = layout_name && find_layout(layout_name, locals.keys) + + Body.new do |buffer| + delayed_render(buffer, template, layout, @view, locals) + end + end + + private + + def delayed_render(buffer, template, layout, view, locals) + # Wrap the given buffer in the StreamingBuffer and pass it to the + # underlying template handler. Now, everytime something is concatenated + # to the buffer, it is not appended to an array, but streamed straight + # to the client. + output = ActionView::StreamingBuffer.new(buffer) + yielder = lambda { |*name| view._layout_for(*name) } + + instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do + fiber = Fiber.new do + if layout + layout.render(view, locals, output, &yielder) + else + # If you don't have a layout, just render the thing + # and concatenate the final result. This is the same + # as a layout with just <%= yield %> + output.safe_concat view._layout_for + end + end + + # Set the view flow to support streaming. It will be aware + # when to stop rendering the layout because it needs to search + # something in the template and vice-versa. + view._view_flow = StreamingFlow.new(view, fiber) + + # Yo! Start the fiber! + fiber.resume + + # If the fiber is still alive, it means we need something + # from the template, so start rendering it. If not, it means + # the layout exited without requiring anything from the template. + if fiber.alive? + content = template.render(view, locals, &yielder) + + # Once rendering the template is done, sets its content in the :layout key. + view._view_flow.set(:layout, content) + + # In case the layout continues yielding, we need to resume + # the fiber until all yields are handled. + fiber.resume while fiber.alive? + end + end + end + end +end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index 9ae1636131..6b5ead463f 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -1,41 +1,16 @@ -require 'set' require 'active_support/core_ext/object/try' require 'active_support/core_ext/array/wrap' -require 'action_view/renderer/abstract_renderer' module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: - attr_reader :rendered - - def initialize(view) - super - @rendered = Set.new - end - def render(options) wrap_formats(options[:template] || options[:file]) do template = determine_template(options) + freeze_formats(template.formats, true) render_template(template, options[:layout], options[:locals]) end end - def render_once(options) - paths, locals = options[:once], options[:locals] || {} - layout, keys = options[:layout], locals.keys - prefixes = options.fetch(:prefixes, @view.controller_prefixes) - - raise "render :once expects a String or an Array to be given" unless paths - - render_with_layout(layout, locals) do - contents = [] - Array.wrap(paths).each do |path| - template = find_template(path, prefixes, false, keys) - contents << render_template(template, nil, locals) if @rendered.add?(template) - end - contents.join("\n") - end - end - # Determine the template to be rendered using the given options. def determine_template(options) #:nodoc: keys = options[:locals].try(:keys) || [] @@ -56,7 +31,6 @@ module ActionView # Renders the given template. An string representing the layout can be # supplied as well. def render_template(template, layout_name = nil, locals = {}) #:nodoc: - freeze_formats(template.formats, true) view, locals = @view, locals || {} render_with_layout(layout_name, locals) do |layout| @@ -72,7 +46,7 @@ module ActionView if layout view = @view - view.store_content_for(:layout, content) + view._view_flow.set(:layout, content) layout.render(view, locals){ |*name| view._layout_for(*name) } else content diff --git a/actionpack/lib/action_view/rendering.rb b/actionpack/lib/action_view/rendering.rb index 2b488fa845..2bce2fb045 100644 --- a/actionpack/lib/action_view/rendering.rb +++ b/actionpack/lib/action_view/rendering.rb @@ -9,7 +9,6 @@ module ActionView # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:text</tt> - Renders the text passed in out. - # * <tt>:once</tt> - Accepts a string or an array of strings and Rails will ensure they each of them are rendered just once. # # 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. @@ -20,8 +19,6 @@ module ActionView _render_partial(options.merge(:partial => options[:layout]), &block) elsif options.key?(:partial) _render_partial(options) - elsif options.key?(:once) - _render_once(options) else _render_template(options) end @@ -30,6 +27,19 @@ module ActionView end end + # Render but returns a valid Rack body. If fibers are defined, we return + # a streaming body that renders the template piece by piece. + # + # Note that partials are not supported to be rendered with streaming, + # so in such cases, we just wrap them in an array. + def render_body(options) + if options.key?(:partial) + [_render_partial(options)] + else + StreamingTemplateRenderer.new(self).render(options) + end + end + # Returns the contents that are yielded to a layout, given a name or a block. # # You can think of a layout as a method that is called with a block. If the user calls @@ -76,22 +86,23 @@ module ActionView # Hello David # </html> # - def _layout_for(*args, &block) + def _layout_for(*args) + name = args.first + name = :layout unless name.is_a?(Symbol) + @_view_flow.get(name).html_safe + end + + # Handle layout for calls from partials that supports blocks. + def _block_layout_for(*args, &block) name = args.first - if name.is_a?(Symbol) - @_content_for[name].html_safe - elsif block + if !name.is_a?(Symbol) && block capture(*args, &block) else - @_content_for[:layout].html_safe + _layout_for(*args) end end - def _render_once(options) #:nodoc: - _template_renderer.render_once(options) - end - def _render_template(options) #:nodoc: _template_renderer.render(options) end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 96d506fac5..6dfc4f68ae 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -126,17 +126,23 @@ module ActionView @formats = Array.wrap(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f } end + # Returns if the underlying handler supports streaming. If so, + # a streaming buffer *may* be passed when it start rendering. + def supports_streaming? + handler.respond_to?(:supports_streaming?) && handler.supports_streaming? + end + # Render a template. If the template was not compiled yet, it is done # exactly before rendering. # # This method is instrumented as "!render_template.action_view". Notice that # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. - def render(view, locals, &block) + def render(view, locals, buffer=nil, &block) old_template, view._template = view._template, self ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do compile!(view) - view.send(method_name, locals, &block) + view.send(method_name, locals, buffer, &block) end rescue Exception => e handle_render_error(view, e) @@ -167,28 +173,6 @@ module ActionView end end - # Expires this template by setting his updated_at date to Jan 1st, 1970. - def expire! - @updated_at = Time.utc(1970) - end - - # Receives a view context and renders a template exactly like self by using - # the @virtual_path. It raises an error if no @virtual_path was given. - def rerender(view) - raise "A template needs to have a virtual path in order to be rerendered" unless @virtual_path - name = @virtual_path.dup - if name.sub!(/(^|\/)_([^\/]*)$/, '\1\2') - view.render :partial => name - else - view.render :template => @virtual_path - end - end - - # Used to store template data by template handlers. - def data - @data ||= {} - end - def inspect @inspect ||= if defined?(Rails.root) @@ -274,13 +258,12 @@ module ActionView end end - arity = @handler.respond_to?(:arity) ? @handler.arity : @handler.method(:call).arity - code = arity.abs == 1 ? @handler.call(self) : @handler.call(self, view) + code = @handler.call(self) # Make sure that the resulting String to be evalled is in the # encoding of the code source = <<-end_src - def #{method_name}(local_assigns) + def #{method_name}(local_assigns, output_buffer) _old_output_buffer = @output_buffer;#{locals_code};#{code} ensure @output_buffer = _old_output_buffer diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 0443b1b0f5..7e9e4e518a 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,28 +1,14 @@ require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/string/output_safety' require 'action_view/template' require 'action_view/template/handler' require 'erubis' module ActionView - class OutputBuffer < ActiveSupport::SafeBuffer - def initialize(*) - super - encode! if encoding_aware? - end - - def <<(value) - super(value.to_s) - end - alias :append= :<< - alias :safe_append= :safe_concat - end - class Template module Handlers class Erubis < ::Erubis::Eruby def add_preamble(src) - src << "@output_buffer = ActionView::OutputBuffer.new;" + src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" end def add_text(src, text) @@ -73,6 +59,10 @@ module ActionView new.call(template) end + def supports_streaming? + true + end + def handles_encoding? true end diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb new file mode 100644 index 0000000000..9c10decd60 --- /dev/null +++ b/actionpack/lib/sprockets/railtie.rb @@ -0,0 +1,100 @@ +module Sprockets + class Railtie < Rails::Railtie + def self.using_coffee? + require 'coffee-script' + defined?(CoffeeScript) + rescue LoadError + false + end + + def self.using_scss? + require 'sass' + defined?(Sass) + rescue LoadError + false + end + + config.app_generators.javascript_engine :coffee if using_coffee? + config.app_generators.stylesheet_engine :scss if using_scss? + + # Configure ActionController to use sprockets. + initializer "sprockets.set_configs", :after => "action_controller.set_configs" do |app| + ActiveSupport.on_load(:action_controller) do + self.use_sprockets = app.config.assets.enabled + end + end + + # We need to configure this after initialization to ensure we collect + # paths from all engines. This hook is invoked exactly before routes + # are compiled. + config.after_initialize do |app| + assets = app.config.assets + next unless assets.enabled + + app.assets = asset_environment(app) + + ActiveSupport.on_load(:action_view) do + app.assets.context.instance_eval do + include ::ActionView::Helpers::SprocketsHelper + end + end + + app.routes.append do + mount app.assets => assets.prefix + end + + if config.action_controller.perform_caching + app.assets = app.assets.index + end + end + + protected + + def asset_environment(app) + require "sprockets" + assets = app.config.assets + env = Sprockets::Environment.new(app.root.to_s) + env.static_root = File.join(app.root.join("public"), assets.prefix) + env.paths.concat assets.paths + env.logger = Rails.logger + env.js_compressor = expand_js_compressor(assets.js_compressor) + env.css_compressor = expand_css_compressor(assets.css_compressor) + env + end + + def expand_js_compressor(sym) + case sym + when :closure + require 'closure-compiler' + Closure::Compiler.new + when :uglifier + require 'uglifier' + Uglifier.new + when :yui + require 'yui/compressor' + YUI::JavaScriptCompressor.new + else + sym + end + end + + def expand_css_compressor(sym) + case sym + when :scss + require 'sass' + compressor = Object.new + def compressor.compress(source) + Sass::Engine.new(source, + :syntax => :scss, :style => :compressed + ).render + end + compressor + when :yui + require 'yui/compressor' + YUI::JavaScriptCompressor.new(:munge => true) + else + sym + end + end + end +end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 9c89f1334d..e19612eace 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -264,6 +264,22 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest end end + def test_setting_flash_does_not_raise_in_following_requests + with_test_route_set do + env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } + get '/set_flash', nil, env + get '/set_flash', nil, env + end + end + + def test_setting_flash_now_does_not_raise_in_following_requests + with_test_route_set do + env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } + get '/set_flash_now', nil, env + get '/set_flash_now', nil, env + end + end + def test_setting_flash_raises_after_stream_back_to_client_even_with_an_empty_flash with_test_route_set do env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } @@ -294,7 +310,6 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest end end - private # Overwrite get to send SessionSecret in env hash diff --git a/actionpack/test/controller/new_base/render_once_test.rb b/actionpack/test/controller/new_base/render_once_test.rb deleted file mode 100644 index 175abf8a7e..0000000000 --- a/actionpack/test/controller/new_base/render_once_test.rb +++ /dev/null @@ -1,86 +0,0 @@ -require 'abstract_unit' - -module RenderTemplate - class RenderOnceController < ActionController::Base - layout false - - RESOLVER = ActionView::FixtureResolver.new( - "test/a.html.erb" => "a", - "test/b.html.erb" => "<>", - "test/c.html.erb" => "c", - "test/one.html.erb" => "<%= render :once => 'result' %>", - "test/two.html.erb" => "<%= render :once => 'result' %>", - "test/three.html.erb" => "<%= render :once => 'result' %>", - "test/result.html.erb" => "YES!", - "other/result.html.erb" => "NO!", - "layouts/test.html.erb" => "l<%= yield %>l" - ) - - self.view_paths = [RESOLVER] - - def _prefixes - %w(test) - end - - def multiple - render :once => %w(a b c) - end - - def once - render :once => %w(one two three) - end - - def duplicate - render :once => %w(a a a) - end - - def with_layout - render :once => %w(a b c), :layout => "test" - end - - def with_prefix - render :once => "result", :prefixes => %w(other) - end - - def with_nil_prefix - render :once => "test/result", :prefixes => [] - end - end - - module Tests - def test_mutliple_arguments_get_all_rendered - get :multiple - assert_response "a\n<>\nc" - end - - def test_referenced_templates_get_rendered_once - get :once - assert_response "YES!\n\n" - end - - def test_duplicated_templates_get_rendered_once - get :duplicate - assert_response "a" - end - - def test_layout_wraps_all_rendered_templates - get :with_layout - assert_response "la\n<>\ncl" - end - - def test_with_prefix_option - get :with_prefix - assert_response "NO!" - end - - def test_with_nil_prefix_option - get :with_nil_prefix - assert_response "YES!" - end - end - - class TestRenderOnce < Rack::TestCase - testing RenderTemplate::RenderOnceController - include Tests - end -end diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb new file mode 100644 index 0000000000..fed8d40b47 --- /dev/null +++ b/actionpack/test/controller/new_base/render_streaming_test.rb @@ -0,0 +1,101 @@ +require 'abstract_unit' + +module RenderStreaming + class BasicController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new( + "render_streaming/basic/hello_world.html.erb" => "Hello world", + "render_streaming/basic/boom.html.erb" => "<%= nil.invalid! %>", + "layouts/application.html.erb" => "<%= yield %>, I'm here!", + "layouts/boom.html.erb" => "<body class=\"<%= nil.invalid! %>\"<%= yield %></body>" + )] + + layout "application" + stream :only => [:hello_world, :skip] + + def hello_world + end + + def layout_exception + render :action => "hello_world", :stream => true, :layout => "boom" + end + + def template_exception + render :action => "boom", :stream => true + end + + def skip + render :action => "hello_world", :stream => false + end + + def explicit + render :action => "hello_world", :stream => true + end + + def no_layout + render :action => "hello_world", :stream => true, :layout => false + end + + def explicit_cache + headers["Cache-Control"] = "private" + render :action => "hello_world", :stream => true + end + end + + class StreamingTest < Rack::TestCase + test "rendering with streaming enabled at the class level" do + get "/render_streaming/basic/hello_world" + assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n" + assert_streaming! + end + + test "rendering with streaming given to render" do + get "/render_streaming/basic/explicit" + assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n" + assert_streaming! + end + + test "rendering with streaming do not override explicit cache control given to render" do + get "/render_streaming/basic/explicit_cache" + assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n" + assert_streaming! "private" + end + + test "rendering with streaming no layout" do + get "/render_streaming/basic/no_layout" + assert_body "b\r\nHello world\r\n0\r\n\r\n" + assert_streaming! + end + + test "skip rendering with streaming at render level" do + get "/render_streaming/basic/skip" + assert_body "Hello world, I'm here!" + end + + test "rendering with layout exception" do + get "/render_streaming/basic/layout_exception" + assert_body "d\r\n<body class=\"\r\n4e\r\n\"><script type=\"text/javascript\">window.location = \"/500.html\"</script></html>\r\n0\r\n\r\n" + assert_streaming! + end + + test "rendering with template exception" do + get "/render_streaming/basic/template_exception" + assert_body "4e\r\n\"><script type=\"text/javascript\">window.location = \"/500.html\"</script></html>\r\n0\r\n\r\n" + assert_streaming! + end + + test "do not stream on HTTP/1.0" do + get "/render_streaming/basic/hello_world", nil, "HTTP_VERSION" => "HTTP/1.0" + assert_body "Hello world, I'm here!" + assert_status 200 + assert_equal "22", headers["Content-Length"] + assert_equal nil, headers["Transfer-Encoding"] + end + + def assert_streaming!(cache="no-cache") + assert_status 200 + assert_equal nil, headers["Content-Length"] + assert_equal "chunked", headers["Transfer-Encoding"] + assert_equal cache, headers["Cache-Control"] + end + end +end if defined?(Fiber) diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb index d6062bfa8c..60468bf5c7 100644 --- a/actionpack/test/controller/new_base/render_test.rb +++ b/actionpack/test/controller/new_base/render_test.rb @@ -81,8 +81,7 @@ module Render end class TestOnlyRenderPublicActions < Rack::TestCase - describe "Only public methods on actual controllers are callable actions" - + # Only public methods on actual controllers are callable actions test "raises an exception when a method of Object is called" do assert_raises(AbstractController::ActionNotFound) do get "/render/blank_render/clone", {}, "action_dispatch.show_exceptions" => false diff --git a/actionpack/test/controller/view_paths_test.rb b/actionpack/test/controller/view_paths_test.rb index 9280a1c2d3..42356be1ea 100644 --- a/actionpack/test/controller/view_paths_test.rb +++ b/actionpack/test/controller/view_paths_test.rb @@ -131,8 +131,8 @@ class ViewLoadPathsTest < ActionController::TestCase assert_equal "Hello overridden world!", @response.body end - def test_override_view_paths_with_custom_resolver - resolver_class = Class.new(ActionView::PathResolver) do + def test_decorate_view_paths_with_custom_resolver + decorator_class = Class.new(ActionView::PathResolver) do def initialize(path_set) @path_set = path_set end @@ -140,7 +140,7 @@ class ViewLoadPathsTest < ActionController::TestCase def find_all(*args) @path_set.find_all(*args).collect do |template| ::ActionView::Template.new( - "Customized body", + "Decorated body", template.identifier, template.handler, { @@ -152,12 +152,12 @@ class ViewLoadPathsTest < ActionController::TestCase end end - resolver = resolver_class.new(TestController.view_paths) - TestController.view_paths = ActionView::PathSet.new.push(resolver) + decorator = decorator_class.new(TestController.view_paths) + TestController.view_paths = ActionView::PathSet.new.push(decorator) get :hello_world assert_response :success - assert_equal "Customized body", @response.body + assert_equal "Decorated body", @response.body end def test_inheritance diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 18f28deee4..b28a058250 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -69,6 +69,7 @@ module TestGenerationPrefix # force draw RailsApplication.routes + RailsApplication.routes.define_mounted_helper(:main_app) class ::InsideEngineGeneratingController < ActionController::Base include BlogEngine.routes.url_helpers diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 6f38714b2e..5abbaf74fe 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -33,22 +33,6 @@ class ResponseTest < ActiveSupport::TestCase }, headers) end - test "streaming block" do - @response.body = Proc.new do |response, output| - 5.times { |n| output.write(n) } - end - - status, headers, body = @response.to_a - assert_equal 200, status - assert_equal({ - "Content-Type" => "text/html; charset=utf-8" - }, headers) - - parts = [] - body.each { |part| parts << part.to_s } - assert_equal ["0", "1", "2", "3", "4"], parts - end - test "content type" do [204, 304].each do |c| @response.status = c.to_s diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 655745a848..2ebbed4414 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -47,35 +47,4 @@ class StaticTest < ActiveSupport::TestCase end include StaticTests -end - -class MultipleDirectorisStaticTest < ActiveSupport::TestCase - DummyApp = lambda { |env| - [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] - } - App = ActionDispatch::Static.new(DummyApp, - { "/" => "#{FIXTURE_LOAD_PATH}/public", - "/blog" => "#{FIXTURE_LOAD_PATH}/blog_public", - "/foo" => "#{FIXTURE_LOAD_PATH}/non_existing_dir" - }) - - def setup - @app = App - end - - include StaticTests - - test "serves files from other mounted directories" do - assert_html "/blog/index.html", get("/blog/index.html") - assert_html "/blog/index.html", get("/blog/index") - assert_html "/blog/index.html", get("/blog/") - - assert_html "/blog/blog.html", get("/blog/blog/") - assert_html "/blog/blog.html", get("/blog/blog.html") - assert_html "/blog/blog.html", get("/blog/blog") - - assert_html "/blog/subdir/index.html", get("/blog/subdir/index.html") - assert_html "/blog/subdir/index.html", get("/blog/subdir/") - assert_html "/blog/subdir/index.html", get("/blog/subdir") - end -end +end
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/streaming.erb b/actionpack/test/fixtures/layouts/streaming.erb new file mode 100644 index 0000000000..d3f896a6ca --- /dev/null +++ b/actionpack/test/fixtures/layouts/streaming.erb @@ -0,0 +1,4 @@ +<%= yield :header -%> +<%= yield -%> +<%= yield :footer -%> +<%= yield(:unknown).presence || "." -%>
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/public/images/rails.png b/actionpack/test/fixtures/sprockets/app/images/logo.png Binary files differindex d5edc04e65..d5edc04e65 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/images/rails.png +++ b/actionpack/test/fixtures/sprockets/app/images/logo.png diff --git a/actionpack/test/fixtures/test/nested_streaming.erb b/actionpack/test/fixtures/test/nested_streaming.erb new file mode 100644 index 0000000000..55525e0c92 --- /dev/null +++ b/actionpack/test/fixtures/test/nested_streaming.erb @@ -0,0 +1,3 @@ +<%- content_for :header do -%>?<%- end -%> +<%= render :template => "test/streaming" %> +?
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/streaming.erb b/actionpack/test/fixtures/test/streaming.erb new file mode 100644 index 0000000000..fb9b8b1ade --- /dev/null +++ b/actionpack/test/fixtures/test/streaming.erb @@ -0,0 +1,3 @@ +<%- provide :header do -%>Yes, <%- end -%> +this works +<%- content_for :footer, " like a charm" -%> diff --git a/actionpack/test/fixtures/test/streaming_buster.erb b/actionpack/test/fixtures/test/streaming_buster.erb new file mode 100644 index 0000000000..4221d56dcc --- /dev/null +++ b/actionpack/test/fixtures/test/streaming_buster.erb @@ -0,0 +1,3 @@ +<%= yield :foo -%> +This won't look +<% provide :unknown, " good." -%> diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 1bf748af14..4a93def5a8 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -477,15 +477,6 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") end - def test_env_asset_path - @controller.config.asset_path = "/assets%s" - def @controller.env; @_env ||= {} end - @controller.env["action_dispatch.asset_path"] = "/omg%s" - - expected_path = "/assets/omg/images/rails.png" - assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") - end - def test_proc_asset_id @controller.config.asset_path = Proc.new do |asset_path| "/assets.v12345#{asset_path}" @@ -495,20 +486,6 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") end - def test_env_proc_asset_path - @controller.config.asset_path = Proc.new do |asset_path| - "/assets.v12345#{asset_path}" - end - - def @controller.env; @_env ||= {} end - @controller.env["action_dispatch.asset_path"] = Proc.new do |asset_path| - "/omg#{asset_path}" - end - - expected_path = "/assets.v12345/omg/images/rails.png" - assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") - end - def test_image_tag_interpreting_email_cid_correctly # An inline image has no need for an alt tag to be automatically generated from the cid: assert_equal '<img src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid") diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb index 03050485fa..a9a36e6e6b 100644 --- a/actionpack/test/template/capture_helper_test.rb +++ b/actionpack/test/template/capture_helper_test.rb @@ -4,7 +4,7 @@ class CaptureHelperTest < ActionView::TestCase def setup super @av = ActionView::Base.new - @_content_for = Hash.new {|h,k| h[k] = "" } + @_view_flow = ActionView::OutputFlow.new end def test_capture_captures_the_temporary_output_buffer_in_its_block @@ -45,6 +45,20 @@ class CaptureHelperTest < ActionView::TestCase assert ! content_for?(:something_else) end + def test_provide + assert !content_for?(:title) + provide :title, "hi" + assert content_for?(:title) + assert_equal "hi", @_view_flow.get(:title) + provide :title, "<p>title</p>" + assert_equal "hi<p>title</p>", @_view_flow.get(:title) + + @_view_flow = ActionView::OutputFlow.new + provide :title, "hi" + provide :title, "<p>title</p>".html_safe + assert_equal "hi<p>title</p>", @_view_flow.get(:title) + end + def test_with_output_buffer_swaps_the_output_buffer_given_no_argument assert_nil @av.output_buffer buffer = @av.with_output_buffer do diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index 8d063e66b0..ff94cba59f 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -180,16 +180,6 @@ class LookupContextTest < ActiveSupport::TestCase assert_not_equal template, old_template end - - test "data can be stored in cached templates" do - template = @lookup_context.find("hello_world", %w(test)) - template.data["cached"] = "data" - assert_equal "Hello world!", template.source - - template = @lookup_context.find("hello_world", %w(test)) - assert_equal "data", template.data["cached"] - assert_equal "Hello world!", template.source - end end class LookupContextWithFalseCaching < ActiveSupport::TestCase @@ -235,21 +225,6 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source end - - test "data can be stored as long as template was not updated" do - template = @lookup_context.find("foo", %w(test), true) - template.data["cached"] = "data" - assert_equal "Foo", template.source - - template = @lookup_context.find("foo", %w(test), true) - assert_equal "data", template.data["cached"] - assert_equal "Foo", template.source - - @resolver.hash["test/_foo.erb"][1] = Time.now.utc - template = @lookup_context.find("foo", %w(test), true) - assert_nil template.data["cached"] - assert_equal "Foo", template.source - end end class TestMissingTemplate < ActiveSupport::TestCase diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index c8d50ebf75..23a7e17e65 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -32,6 +32,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal("555-1234", number_to_phone(5551234)) assert_equal("800-555-1212", number_to_phone(8005551212)) assert_equal("(800) 555-1212", number_to_phone(8005551212, {:area_code => true})) + assert_equal("", number_to_phone("", {:area_code => true})) assert_equal("800 555 1212", number_to_phone(8005551212, {:delimiter => " "})) assert_equal("(800) 555-1212 x 123", number_to_phone(8005551212, {:area_code => true, :extension => 123})) assert_equal("800-555-1212", number_to_phone(8005551212, :extension => " ")) diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index bce11c4dd3..d4e912c410 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -227,20 +227,11 @@ module RenderTestCases "@output_buffer << 'source: #{template.source.inspect}'\n" end - WithViewHandler = lambda do |template, view| - %'"#{template.class} #{view.class}"' - end - def test_render_inline_with_render_from_to_proc ActionView::Template.register_template_handler :ruby_handler, :source.to_proc assert_equal '3', @view.render(:inline => "(1 + 2).to_s", :type => :ruby_handler) end - def test_render_inline_with_template_handler_with_view - ActionView::Template.register_template_handler :with_view, WithViewHandler - assert_equal 'ActionView::Template ActionView::Base', @view.render(:inline => "Hello, World!", :type => :with_view) - end - def test_render_inline_with_compilable_custom_type ActionView::Template.register_template_handler :foo, CustomHandler assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo) diff --git a/actionpack/test/template/sprockets_helper_test.rb b/actionpack/test/template/sprockets_helper_test.rb index 67aee86d02..8d3be09a4f 100644 --- a/actionpack/test/template/sprockets_helper_test.rb +++ b/actionpack/test/template/sprockets_helper_test.rb @@ -1,5 +1,8 @@ require 'abstract_unit' require 'sprockets' +require 'mocha' + +module Rails; end class SprocketsHelperTest < ActionView::TestCase tests ActionView::Helpers::SprocketsHelper @@ -22,6 +25,12 @@ class SprocketsHelperTest < ActionView::TestCase @assets = Sprockets::Environment.new @assets.paths << FIXTURES.join("sprockets/app/javascripts") @assets.paths << FIXTURES.join("sprockets/app/stylesheets") + @assets.paths << FIXTURES.join("sprockets/app/images") + + application = Object.new + Rails.stubs(:application).returns(application) + application.stubs(:config).returns(config) + application.stubs(:assets).returns(@assets) config.perform_caching = true end @@ -30,22 +39,40 @@ class SprocketsHelperTest < ActionView::TestCase "http://www.example.com" end + test "asset path" do + assert_equal "/assets/logo-9c0a079bdd7701d7e729bd956823d153.png", + asset_path("logo.png") + + assert_equal "/images/logo", + asset_path("/images/logo") + assert_equal "/images/logo.gif", + asset_path("/images/logo.gif") + + assert_equal "/dir/audio", + asset_path("/dir/audio") + + assert_equal "http://www.example.com/video/play", + asset_path("http://www.example.com/video/play") + assert_equal "http://www.example.com/video/play.mp4", + asset_path("http://www.example.com/video/play.mp4") + end + test "javascript path" do assert_equal "/assets/application-d41d8cd98f00b204e9800998ecf8427e.js", - sprockets_javascript_path(:application) + asset_path(:application, "js") assert_equal "/assets/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js", - sprockets_javascript_path("xmlhr") + asset_path("xmlhr", "js") assert_equal "/assets/dir/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js", - sprockets_javascript_path("dir/xmlhr.js") + asset_path("dir/xmlhr.js", "js") assert_equal "/dir/xmlhr.js", - sprockets_javascript_path("/dir/xmlhr") + asset_path("/dir/xmlhr", "js") - assert_equal "http://www.railsapplication.com/js/xmlhr", - sprockets_javascript_path("http://www.railsapplication.com/js/xmlhr") - assert_equal "http://www.railsapplication.com/js/xmlhr.js", - sprockets_javascript_path("http://www.railsapplication.com/js/xmlhr.js") + assert_equal "http://www.example.com/js/xmlhr", + asset_path("http://www.example.com/js/xmlhr", "js") + assert_equal "http://www.example.com/js/xmlhr.js", + asset_path("http://www.example.com/js/xmlhr.js", "js") end test "javascript include tag" do @@ -56,25 +83,21 @@ class SprocketsHelperTest < ActionView::TestCase sprockets_javascript_include_tag("xmlhr") assert_equal '<script src="/assets/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js" type="text/javascript"></script>', sprockets_javascript_include_tag("xmlhr.js") - assert_equal '<script src="http://www.railsapplication.com/xmlhr" type="text/javascript"></script>', - sprockets_javascript_include_tag("http://www.railsapplication.com/xmlhr") + assert_equal '<script src="http://www.example.com/xmlhr" type="text/javascript"></script>', + sprockets_javascript_include_tag("http://www.example.com/xmlhr") end test "stylesheet path" do - assert_equal "/assets/application-d41d8cd98f00b204e9800998ecf8427e.css", - sprockets_stylesheet_path(:application) - - assert_equal "/assets/style-d41d8cd98f00b204e9800998ecf8427e.css", - sprockets_stylesheet_path("style") - assert_equal "/assets/dir/style-d41d8cd98f00b204e9800998ecf8427e.css", - sprockets_stylesheet_path("dir/style.css") - assert_equal "/dir/style.css", - sprockets_stylesheet_path("/dir/style.css") - - assert_equal "http://www.railsapplication.com/css/style", - sprockets_stylesheet_path("http://www.railsapplication.com/css/style") - assert_equal "http://www.railsapplication.com/css/style.css", - sprockets_stylesheet_path("http://www.railsapplication.com/css/style.css") + assert_equal "/assets/application-d41d8cd98f00b204e9800998ecf8427e.css", asset_path(:application, "css") + + assert_equal "/assets/style-d41d8cd98f00b204e9800998ecf8427e.css", asset_path("style", "css") + assert_equal "/assets/dir/style-d41d8cd98f00b204e9800998ecf8427e.css", asset_path("dir/style.css", "css") + assert_equal "/dir/style.css", asset_path("/dir/style.css", "css") + + assert_equal "http://www.example.com/css/style", + asset_path("http://www.example.com/css/style", "css") + assert_equal "http://www.example.com/css/style.css", + asset_path("http://www.example.com/css/style.css", "css") end test "stylesheet link tag" do @@ -86,8 +109,8 @@ class SprocketsHelperTest < ActionView::TestCase assert_equal '<link href="/assets/style-d41d8cd98f00b204e9800998ecf8427e.css" media="screen" rel="stylesheet" type="text/css" />', sprockets_stylesheet_link_tag("style.css") - assert_equal '<link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" />', - sprockets_stylesheet_link_tag("http://www.railsapplication.com/style.css") + assert_equal '<link href="http://www.example.com/style.css" media="screen" rel="stylesheet" type="text/css" />', + sprockets_stylesheet_link_tag("http://www.example.com/style.css") assert_equal '<link href="/assets/style-d41d8cd98f00b204e9800998ecf8427e.css" media="all" rel="stylesheet" type="text/css" />', sprockets_stylesheet_link_tag("style", :media => "all") assert_equal '<link href="/assets/style-d41d8cd98f00b204e9800998ecf8427e.css" media="print" rel="stylesheet" type="text/css" />', diff --git a/actionpack/test/template/streaming_render_test.rb b/actionpack/test/template/streaming_render_test.rb new file mode 100644 index 0000000000..4d69081570 --- /dev/null +++ b/actionpack/test/template/streaming_render_test.rb @@ -0,0 +1,105 @@ +# encoding: utf-8 +require 'abstract_unit' +require 'controller/fake_models' + +class TestController < ActionController::Base +end + +class FiberedTest < ActiveSupport::TestCase + def setup + view_paths = ActionController::Base.view_paths + @assigns = { :secret => 'in the sauce' } + @view = ActionView::Base.new(view_paths, @assigns) + @controller_view = TestController.new.view_context + end + + def buffered_render(options) + body = @view.render_body(options) + string = "" + body.each do |piece| + string << piece + end + string + end + + def test_streaming_works + content = [] + body = @view.render_body(:template => "test/hello_world.erb", :layout => "layouts/yield") + + body.each do |piece| + content << piece + end + + assert_equal "<title>", content[0] + assert_equal "", content[1] + assert_equal "</title>\n", content[2] + assert_equal "Hello world!", content[3] + assert_equal "\n", content[4] + end + + def test_render_file + assert_equal "Hello world!", buffered_render(:file => "test/hello_world.erb") + end + + def test_render_file_with_locals + locals = { :secret => 'in the sauce' } + assert_equal "The secret is in the sauce\n", buffered_render(:file => "test/render_file_with_locals.erb", :locals => locals) + end + + def test_render_partial + assert_equal "only partial", buffered_render(:partial => "test/partial_only") + end + + def test_render_inline + assert_equal "Hello, World!", buffered_render(:inline => "Hello, World!") + end + + def test_render_without_layout + assert_equal "Hello world!", buffered_render(:template => "test/hello_world") + end + + def test_render_with_layout + assert_equal %(<title></title>\nHello world!\n), + buffered_render(:template => "test/hello_world.erb", :layout => "layouts/yield") + end + + def test_render_with_layout_which_has_render_inline + assert_equal %(welcome\nHello world!\n), + buffered_render(:template => "test/hello_world.erb", :layout => "layouts/yield_with_render_inline_inside") + end + + def test_render_with_layout_which_renders_another_partial + assert_equal %(partial html\nHello world!\n), + buffered_render(:template => "test/hello_world.erb", :layout => "layouts/yield_with_render_partial_inside") + end + + def test_render_with_nested_layout + assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n), + buffered_render(:template => "test/nested_layout.erb", :layout => "layouts/yield") + end + + def test_render_with_file_in_layout + assert_equal %(\n<title>title</title>\n\n), + buffered_render(:template => "test/layout_render_file.erb") + end + + def test_render_with_handler_without_streaming_support + assert_match "<p>This is grand!</p>", buffered_render(:template => "test/hello") + end + + def test_render_with_streaming_multiple_yields_provide_and_content_for + assert_equal "Yes, \nthis works\n like a charm.", + buffered_render(:template => "test/streaming", :layout => "layouts/streaming") + end + + def test_render_with_streaming_with_fake_yields_and_streaming_buster + assert_equal "This won't look\n good.", + buffered_render(:template => "test/streaming_buster", :layout => "layouts/streaming") + end + + def test_render_with_nested_streaming_multiple_yields_provide_and_content_for + assert_equal "?Yes, \n\nthis works\n\n? like a charm.", + buffered_render(:template => "test/nested_streaming", :layout => "layouts/streaming") + end + +end if defined?(Fiber)
\ No newline at end of file diff --git a/actionpack/test/template/template_test.rb b/actionpack/test/template/template_test.rb index 3432a02c3c..5c655d5b69 100644 --- a/actionpack/test/template/template_test.rb +++ b/actionpack/test/template/template_test.rb @@ -113,44 +113,6 @@ class TestERBTemplate < ActiveSupport::TestCase end end - def test_template_expire_sets_the_timestamp_to_1970 - @template = new_template("Hello", :updated_at => Time.utc(2010)) - assert_equal Time.utc(2010), @template.updated_at - @template.expire! - assert_equal Time.utc(1970), @template.updated_at - end - - def test_template_rerender_renders_a_template_like_self - @template = new_template("Hello", :virtual_path => "test/foo_bar") - @context.expects(:render).with(:template => "test/foo_bar").returns("template") - assert_equal "template", @template.rerender(@context) - end - - def test_template_rerender_renders_a_root_template_like_self - @template = new_template("Hello", :virtual_path => "foo_bar") - @context.expects(:render).with(:template => "foo_bar").returns("template") - assert_equal "template", @template.rerender(@context) - end - - def test_template_rerender_renders_a_partial_like_self - @template = new_template("Hello", :virtual_path => "test/_foo_bar") - @context.expects(:render).with(:partial => "test/foo_bar").returns("partial") - assert_equal "partial", @template.rerender(@context) - end - - def test_template_rerender_renders_a_root_partial_like_self - @template = new_template("Hello", :virtual_path => "_foo_bar") - @context.expects(:render).with(:partial => "foo_bar").returns("partial") - assert_equal "partial", @template.rerender(@context) - end - - def test_rerender_raises_an_error_without_virtual_path - @template = new_template("Hello", :virtual_path => nil) - assert_raise RuntimeError do - @template.rerender(@context) - end - end - if "ruby".encoding_aware? def test_resulting_string_is_utf8 @template = new_template diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 9f80673bb8..ce69c4a201 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -19,6 +19,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 3.0.0') - s.add_dependency('i18n', '~> 0.5.0') + s.add_dependency('i18n', '~> 0.6.0beta1') s.add_dependency('bcrypt-ruby', '~> 2.1.4') end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index be55581c66..6ee5e04267 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -56,6 +56,8 @@ module ActiveModel module AttributeMethods extend ActiveSupport::Concern + COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/ + included do class_attribute :attribute_method_matchers, :instance_writer => false self.attribute_method_matchers = [] @@ -106,10 +108,13 @@ module ActiveModel if block_given? sing.send :define_method, name, &block else - if name =~ /^[a-zA-Z_]\w*[!?=]?$/ - sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end - eorb + # If we can compile the method name, do it. Otherwise use define_method. + # This is an important *optimization*, please don't change it. define_method + # has slower dispatch and consumes more memory. + if name =~ COMPILABLE_REGEXP + sing.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end + RUBY else value = value.to_s if value sing.send(:define_method, name) { value } @@ -232,8 +237,19 @@ module ActiveModel def alias_attribute(new_name, old_name) attribute_method_matchers.each do |matcher| - define_method(matcher.method_name(new_name)) do |*args| - send(matcher.method_name(old_name), *args) + matcher_new = matcher.method_name(new_name).to_s + matcher_old = matcher.method_name(old_name).to_s + + if matcher_new =~ COMPILABLE_REGEXP && matcher_old =~ COMPILABLE_REGEXP + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{matcher_new}(*args) + send(:#{matcher_old}, *args) + end + RUBY + else + define_method(matcher_new) do |*args| + send(matcher_old, *args) + end end end end @@ -276,14 +292,25 @@ module ActiveModel else method_name = matcher.method_name(attr_name) - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 if method_defined?('#{method_name}') undef :'#{method_name}' end - define_method('#{method_name}') do |*args| - send('#{matcher.method_missing_target}', '#{attr_name}', *args) - end - STR + RUBY + + if method_name.to_s =~ COMPILABLE_REGEXP + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method_name}(*args) + send(:#{matcher.method_missing_target}, '#{attr_name}', *args) + end + RUBY + else + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + define_method('#{method_name}') do |*args| + send('#{matcher.method_missing_target}', '#{attr_name}', *args) + end + RUBY + end end end end diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index be48415739..01eef762fd 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -24,10 +24,7 @@ module ActiveModel # include ActiveModel::MassAssignmentSecurity # # attr_accessible :first_name, :last_name - # - # def self.admin_accessible_attributes - # accessible_attributes + [ :plan_id ] - # end + # attr_accessible :first_name, :last_name, :plan_id, :as => :admin # # def update # ... @@ -38,18 +35,17 @@ module ActiveModel # protected # # def account_params - # sanitize_for_mass_assignment(params[:account]) - # end - # - # def mass_assignment_authorizer - # admin ? admin_accessible_attributes : super + # scope = admin ? :admin : :default + # sanitize_for_mass_assignment(params[:account], scope) # end # # end # module ClassMethods # Attributes named in this macro are protected from mass-assignment - # whenever attributes are sanitized before assignment. + # whenever attributes are sanitized before assignment. A scope for the + # attributes is optional, if no scope is provided then :default is used. + # A scope can be defined by using the :as option. # # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect @@ -60,36 +56,58 @@ module ActiveModel # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :credit_rating - # attr_protected :credit_rating # - # def attributes=(values) - # sanitize_for_mass_assignment(values).each do |k, v| + # attr_protected :credit_rating, :last_login + # attr_protected :last_login, :as => :admin + # + # def assign_attributes(values, options = {}) + # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| # send("#{k}=", v) # end # end # end # + # When using a :default scope : + # # customer = Customer.new - # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" } + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) # customer.name # => "David" # customer.credit_rating # => nil + # customer.last_login # => nil # # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # + # And using the :admin scope : + # + # customer = Customer.new + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.name # => "David" + # customer.credit_rating # => "Excellent" + # customer.last_login # => nil + # # To start from an all-closed default and enable attributes as needed, # have a look at +attr_accessible+. # # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ # to sanitize attributes won't provide sufficient protection. - def attr_protected(*names) - self._protected_attributes = self.protected_attributes + names + def attr_protected(*args) + options = args.extract_options! + scope = options[:as] || :default + + self._protected_attributes = protected_attributes_configs.dup + self._protected_attributes[scope] = self.protected_attributes(scope) + args + self._active_authorizer = self._protected_attributes end # Specifies a white list of model attributes that can be set via # mass-assignment. # + # Like +attr_protected+, a scope for the attributes is optional, + # if no scope is provided then :default is used. A scope can be defined by + # using the :as option. + # # This is the opposite of the +attr_protected+ macro: Mass-assignment # will only set attributes in this list, to assign to the rest of # attributes you can use direct writer methods. This is meant to protect @@ -102,57 +120,90 @@ module ActiveModel # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :credit_rating + # # attr_accessible :name + # attr_accessible :name, :credit_rating, :as => :admin # - # def attributes=(values) - # sanitize_for_mass_assignment(values).each do |k, v| + # def assign_attributes(values, options = {}) + # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| # send("#{k}=", v) # end # end # end # + # When using a :default scope : + # # customer = Customer.new - # customer.attributes = { :name => "David", :credit_rating => "Excellent" } + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) # customer.name # => "David" # customer.credit_rating # => nil # # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # + # And using the :admin scope : + # + # customer = Customer.new + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.name # => "David" + # customer.credit_rating # => "Excellent" + # # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ # to sanitize attributes won't provide sufficient protection. - def attr_accessible(*names) - self._accessible_attributes = self.accessible_attributes + names + def attr_accessible(*args) + options = args.extract_options! + scope = options[:as] || :default + + self._accessible_attributes = accessible_attributes_configs.dup + self._accessible_attributes[scope] = self.accessible_attributes(scope) + args + self._active_authorizer = self._accessible_attributes end - def protected_attributes - self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w| - w.logger = self.logger if self.respond_to?(:logger) - end + def protected_attributes(scope = :default) + protected_attributes_configs[scope] end - def accessible_attributes - self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + def accessible_attributes(scope = :default) + accessible_attributes_configs[scope] end - def active_authorizer - self._active_authorizer ||= protected_attributes + def active_authorizers + self._active_authorizer ||= protected_attributes_configs end + alias active_authorizer active_authorizers def attributes_protected_by_default [] end + + private + + def protected_attributes_configs + self._protected_attributes ||= begin + default_black_list = BlackList.new(attributes_protected_by_default).tap do |w| + w.logger = self.logger if self.respond_to?(:logger) + end + Hash.new(default_black_list) + end + end + + def accessible_attributes_configs + self._accessible_attributes ||= begin + default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + Hash.new(default_white_list) + end + end end protected - def sanitize_for_mass_assignment(attributes) - mass_assignment_authorizer.sanitize(attributes) + def sanitize_for_mass_assignment(attributes, scope = :default) + mass_assignment_authorizer(scope).sanitize(attributes) end - def mass_assignment_authorizer - self.class.active_authorizer + def mass_assignment_authorizer(scope = :default) + self.class.active_authorizer[scope] end end end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index ef36f80bec..3c80d584fe 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -43,7 +43,7 @@ module ActiveModel @observer_instances ||= [] end - # Instantiate the global Active Record observers. + # Instantiate the global observers. def instantiate_observers observers.each { |o| instantiate_observer(o) } end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index f84e55e8d9..b22ce874ea 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -10,10 +10,27 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal expected, sanitized end + def test_only_moderator_scope_attribute_accessible + user = SpecialUser.new + expected = { "name" => "John Smith", "email" => "john@smith.com" } + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator) + assert_equal expected, sanitized + + sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true }) + assert_equal({}, sanitized) + end + def test_attributes_accessible user = Person.new expected = { "name" => "John Smith", "email" => "john@smith.com" } - sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true)) + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) + assert_equal expected, sanitized + end + + def test_admin_scoped_attributes_accessible + user = Person.new + expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true } + sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin) assert_equal expected, sanitized end @@ -26,20 +43,30 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase def test_mass_assignment_protection_inheritance assert_blank LoosePerson.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes + assert_equal Set.new(['credit_rating', 'administrator']), LoosePerson.protected_attributes + + assert_blank LoosePerson.accessible_attributes + assert_equal Set.new(['credit_rating']), LoosePerson.protected_attributes(:admin) assert_blank LooseDescendant.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes + assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes assert_blank LooseDescendantSecond.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, + assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections' assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default - assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes + assert_equal Set.new(['name', 'address']), TightPerson.accessible_attributes + + assert_blank TightPerson.protected_attributes(:admin) - TightPerson.attributes_protected_by_default + assert_equal Set.new(['name', 'address', 'admin']), TightPerson.accessible_attributes(:admin) assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default - assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes + assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes + + assert_blank TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default + assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin) + end def test_mass_assignment_multiparameter_protector diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index c455cf57b3..6950c3be1f 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -45,13 +45,14 @@ class SecurePasswordTest < ActiveModel::TestCase end test "visitor#password_digest should be protected against mass assignment" do - assert Visitor.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::BlackList) - assert Visitor.active_authorizer.include?(:password_digest) + assert Visitor.active_authorizers[:default].kind_of?(ActiveModel::MassAssignmentSecurity::BlackList) + assert Visitor.active_authorizers[:default].include?(:password_digest) end test "Administrator's mass_assignment_authorizer should be WhiteList" do - assert Administrator.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList) - assert !Administrator.active_authorizer.include?(:password_digest) - assert Administrator.active_authorizer.include?(:name) + active_authorizer = Administrator.active_authorizers[:default] + assert active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList) + assert !active_authorizer.include?(:password_digest) + assert active_authorizer.include?(:name) end end diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb index 2a8fe170c2..53b37369ff 100644 --- a/activemodel/test/models/mass_assignment_specific.rb +++ b/activemodel/test/models/mass_assignment_specific.rb @@ -5,9 +5,17 @@ class User public :sanitize_for_mass_assignment end +class SpecialUser + include ActiveModel::MassAssignmentSecurity + attr_accessible :name, :email, :as => :moderator + + public :sanitize_for_mass_assignment +end + class Person include ActiveModel::MassAssignmentSecurity attr_accessible :name, :email + attr_accessible :name, :email, :admin, :as => :admin public :sanitize_for_mass_assignment end @@ -32,6 +40,7 @@ end class LoosePerson include ActiveModel::MassAssignmentSecurity attr_protected :credit_rating, :administrator + attr_protected :credit_rating, :as => :admin end class LooseDescendant < LoosePerson @@ -46,6 +55,7 @@ end class TightPerson include ActiveModel::MassAssignmentSecurity attr_accessible :name, :address + attr_accessible :name, :address, :admin, :as => :admin def self.attributes_protected_by_default ["mobile_number"] @@ -54,4 +64,5 @@ end class TightDescendant < TightPerson attr_accessible :phone_number + attr_accessible :super_powers, :as => :admin end
\ No newline at end of file diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 93eb42a52c..9ff29f1155 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,22 +1,15 @@ *Rails 3.1.0 (unreleased)* -* Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your - scope to be lazily evaluated, or takes parameters, please define it as a normal class method - instead. For example, change this: +* default_scope can take a block, lambda, or any other object which responds to `call` for lazy + evaluation: - class Post < ActiveRecord::Base - scope :unpublished, lambda { where('published_at > ?', Time.now) } - end - - To this: - - class Post < ActiveRecord::Base - def self.unpublished - where('published_at > ?', Time.now) - end - end + default_scope { ... } + default_scope lambda { ... } + default_scope method(:foo) - [Jon Leighton] + This feature was originally implemented by Tim Morgan, but was then removed in favour of + defining a 'default_scope' class method, but has now been added back in by Jon Leighton. + The relevant lighthouse ticket is #1812. * Default scopes are now evaluated at the latest possible moment, to avoid problems where scopes would be created which would implicitly contain the default scope, which would then @@ -29,25 +22,34 @@ [Jon Leighton] -* Deprecated support for passing hashes and relations to 'default_scope'. Please create a class - method for your scope instead. For example, change this: +* Calling 'default_scope' multiple times in a class (including when a superclass calls + 'default_scope') is deprecated. The current behavior is that this will merge the default + scopes together: - class Post < ActiveRecord::Base + class Post < ActiveRecord::Base # Rails 3.1 default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:published => true, :hidden => false) end - To this: + In Rails 3.2, the behavior will be changed to overwrite previous scopes: + + class Post < ActiveRecord::Base # Rails 3.2 + default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:hidden => false) + end + + If you wish to merge default scopes in special ways, it is recommended to define your default + scope as a class method and use the standard techniques for sharing code (inheritance, mixins, + etc.): class Post < ActiveRecord::Base def self.default_scope - where(:published => true) + where(:published => true).where(:hidden => false) end end - Rationale: It will make the implementation simpler because we can simply use inheritance to - handle inheritance scenarios, rather than trying to make up our own rules about what should - happen when you call default_scope multiple times or in subclasses. - [Jon Leighton] * PostgreSQL adapter only supports PostgreSQL version 8.2 and higher. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 43f736c89c..aef99e3129 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -43,7 +43,7 @@ module ActiveRecord end if attr_name == primary_key && attr_name != "id" - define_read_method(:id, attr_name, columns_hash[attr_name]) + define_read_method('id', attr_name, columns_hash[attr_name]) end end @@ -59,7 +59,9 @@ module ActiveRecord end # Define an attribute reader method. Cope with nil column. - def define_read_method(symbol, attr_name, column) + # method_name is the same as attr_name except when a non-standard primary key is used, + # we still define #id as an accessor for the key + def define_read_method(method_name, attr_name, column) cast_code = column.type_cast_code('v') access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" @@ -70,12 +72,25 @@ module ActiveRecord if cache_attribute?(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" end - if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/ - generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__) + + # Where possible, generate the method by evalling a string, as this will result in + # faster accesses because it avoids the block eval and then string eval incurred + # by the second branch. + # + # The second, slower, branch is necessary to support instances where the database + # returns columns with extra stuff in (like 'my_column(omg)'). + if method_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + def _#{method_name} + #{access_code} + end + + alias #{method_name} _#{method_name} + STR else generated_attribute_methods.module_eval do - define_method("_#{symbol}") { eval(access_code) } - alias_method(symbol, "_#{symbol}") + define_method("_#{method_name}") { eval(access_code) } + alias_method(method_name, "_#{method_name}") end end end @@ -85,7 +100,7 @@ module ActiveRecord # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) if respond_to? "_#{attr_name}" - send "_#{attr_name}" + send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s) else _read_attribute attr_name end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 7661676f8c..c77a3ac145 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -10,7 +10,7 @@ module ActiveRecord module ClassMethods protected def define_method_attribute=(attr_name) - if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/ + if attr_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) else generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 08a41e2d8b..04c12f86b6 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1177,13 +1177,11 @@ MSG Thread.current[:"#{self}_current_scope"] = scope end - # Implement this method in your model to set a default scope for all operations on + # Use this macro in your model to set a default scope for all operations on # the model. # # class Person < ActiveRecord::Base - # def self.default_scope - # order('last_name, first_name') - # end + # default_scope order('last_name, first_name') # end # # Person.all # => SELECT * FROM people ORDER BY last_name, first_name @@ -1192,40 +1190,59 @@ MSG # applied while updating a record. # # class Article < ActiveRecord::Base - # def self.default_scope - # where(:published => true) - # end + # default_scope where(:published => true) # end # # Article.new.published # => true # Article.create.published # => true # - # === Deprecation warning - # - # There is an alternative syntax as follows: + # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: # - # class Person < ActiveRecord::Base - # default_scope order('last_name, first_name') + # class Article < ActiveRecord::Base + # default_scope { where(:published_at => Time.now - 1.week) } # end # - # This is now deprecated and will be removed in Rails 3.2. + # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> + # macro, and it will be called when building the default scope.) + # + # If you need to do more complex things with a default scope, you can alternatively + # define it as a class method: + # + # class Article < ActiveRecord::Base + # def self.default_scope + # # Should return a scope, you can call 'super' here etc. + # end + # end def default_scope(scope = {}) - ActiveSupport::Deprecation.warn <<-WARN -Passing a hash or scope to default_scope is deprecated and will be removed in Rails 3.2. You should create a class method for your scope instead. For example, change this: + if default_scopes.length != 0 + ActiveSupport::Deprecation.warn <<-WARN +Calling 'default_scope' multiple times in a class (including when a superclass calls 'default_scope') is deprecated. The current behavior is that this will merge the default scopes together: -class Post < ActiveRecord::Base +class Post < ActiveRecord::Base # Rails 3.1 default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:published => true, :hidden => false) end -To this: +In Rails 3.2, the behavior will be changed to overwrite previous scopes: + +class Post < ActiveRecord::Base # Rails 3.2 + default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:hidden => false) +end + +If you wish to merge default scopes in special ways, it is recommended to define your default scope as a class method and use the standard techniques for sharing code (inheritance, mixins, etc.): class Post < ActiveRecord::Base def self.default_scope - where(:published => true) + where(:published => true).where(:hidden => false) end end -WARN + WARN + end + scope = Proc.new if block_given? self.default_scopes = default_scopes.dup << scope end @@ -1238,6 +1255,8 @@ WARN default_scopes.inject(relation) do |default_scope, scope| if scope.is_a?(Hash) default_scope.apply_finder_options(scope) + elsif !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(scope.call) else default_scope.merge(scope) end @@ -1602,11 +1621,11 @@ WARN # Allows you to set all the attributes at once by passing in a hash with keys # matching the attribute names (which again matches the column names). # - # If +guard_protected_attributes+ is true (the default), then sensitive - # attributes can be protected from this form of mass-assignment by using - # the +attr_protected+ macro. Or you can alternatively specify which - # attributes *can* be accessed with the +attr_accessible+ macro. Then all the - # attributes not included in that won't be allowed to be mass-assigned. + # If any attributes are protected by either +attr_protected+ or + # +attr_accessible+ then only settable attributes will be assigned. + # + # The +guard_protected_attributes+ argument is now deprecated, use + # the +assign_attributes+ method if you want to bypass mass-assignment security. # # class User < ActiveRecord::Base # attr_protected :is_admin @@ -1616,15 +1635,59 @@ WARN # user.attributes = { :username => 'Phusion', :is_admin => true } # user.username # => "Phusion" # user.is_admin? # => false + def attributes=(new_attributes, guard_protected_attributes = nil) + unless guard_protected_attributes.nil? + message = "the use of 'guard_protected_attributes' will be removed from the next major release of rails, " + + "if you want to bypass mass-assignment security then look into using assign_attributes" + ActiveSupport::Deprecation.warn(message) + end + + return unless new_attributes.is_a?(Hash) + + guard_protected_attributes ||= true + if guard_protected_attributes + assign_attributes(new_attributes) + else + assign_attributes(new_attributes, :without_protection => true) + end + end + + # Allows you to set all the attributes for a particular mass-assignment + # security scope by passing in a hash of attributes with keys matching + # the attribute names (which again matches the column names) and the scope + # name using the :as option. + # + # To bypass mass-assignment security you can use the :without_protection => true + # option. + # + # class User < ActiveRecord::Base + # attr_accessible :name + # attr_accessible :name, :is_admin, :as => :admin + # end + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }) + # user.name # => "Josh" + # user.is_admin? # => false # - # user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false) + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) + # user.name # => "Josh" # user.is_admin? # => true - def attributes=(new_attributes, guard_protected_attributes = true) - return unless new_attributes.is_a?(Hash) + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) + # user.name # => "Josh" + # user.is_admin? # => true + def assign_attributes(new_attributes, options = {}) attributes = new_attributes.stringify_keys + scope = options[:as] || :default multi_parameter_attributes = [] - attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes + + unless options[:without_protection] + attributes = sanitize_for_mass_assignment(attributes, scope) + end attributes.each do |k, v| if k.include?("(") diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 6d9b5c7b32..70da9d5f1e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -55,6 +55,13 @@ module ActiveRecord def exec_query(sql, name = 'SQL', binds = []) end + # Executes insert +sql+ statement in the context of this connection using + # +binds+ as the bind substitutes. +name+ is the logged along with + # the executed +sql+ statement. + def exec_insert(sql, name, binds) + exec_query(sql, name, binds) + end + # Returns the last auto-generated ID from the affected table. # # +id_value+ will be returned unless the value is nil, in @@ -280,10 +287,6 @@ module ActiveRecord execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' end - def null_insert_value - Arel.sql 'DEFAULT' - end - def empty_insert_statement_value "VALUES(DEFAULT)" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d24cce0a3c..468a2b106b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -223,7 +223,9 @@ module ActiveRecord rescue Exception => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.debug message if @logger - raise translate_exception(e, message) + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + raise exception end def translate_exception(e, message) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index c2e75acb9a..2c05ff21f9 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -208,16 +208,18 @@ module ActiveRecord true end - # Returns +true+ when the connection adapter supports prepared statement - # caching, otherwise returns +false+ + # Returns +true+, since this connection adapter supports prepared statement + # caching. def supports_statement_cache? true end + # Returns true. def supports_migrations? #:nodoc: true end + # Returns true. def supports_primary_key? #:nodoc: true end @@ -308,6 +310,8 @@ module ActiveRecord connect end + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. def disconnect! @connection.close rescue nil end @@ -330,6 +334,7 @@ module ActiveRecord rows end + # Clears the prepared statements cache. def clear_cache! @statements.values.each do |cache| cache[:stmt].close @@ -427,10 +432,6 @@ module ActiveRecord end end - def exec_insert(sql, name, binds) - exec_query(sql, name, binds) - end - def last_inserted_id(result) @connection.insert_id end @@ -558,6 +559,10 @@ module ActiveRecord end end + # Drops a MySQL database. + # + # Example: + # drop_database 'sebastian_development' def drop_database(name) #:nodoc: execute "DROP DATABASE IF EXISTS `#{name}`" end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e74ec84e81..0c2afc180b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,6 +1,9 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' + +# Make sure we're using pg high enough for PGResult#values +gem 'pg', '~> 0.11' require 'pg' module ActiveRecord @@ -95,6 +98,9 @@ module ActiveRecord # XML type when 'xml' :xml + # tsvector type + when 'tsvector' + :tsvector # Arrays when /^\D+\[\]$/ :string @@ -186,6 +192,11 @@ module ActiveRecord options = args.extract_options! column(args[0], 'xml', options) end + + def tsvector(*args) + options = args.extract_options! + column(args[0], 'tsvector', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -203,7 +214,8 @@ module ActiveRecord :date => { :name => "date" }, :binary => { :name => "bytea" }, :boolean => { :name => "boolean" }, - :xml => { :name => "xml" } + :xml => { :name => "xml" }, + :tsvector => { :name => "tsvector" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -211,8 +223,8 @@ module ActiveRecord ADAPTER_NAME end - # Returns +true+ when the connection adapter supports prepared statement - # caching, otherwise returns +false+ + # Returns +true+, since this connection adapter supports prepared statement + # caching. def supports_statement_cache? true end @@ -237,6 +249,7 @@ module ActiveRecord @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] end + # Clears the prepared statements cache. def clear_cache! @statements.each_value do |value| @connection.query "DEALLOCATE #{value}" @@ -275,7 +288,8 @@ module ActiveRecord super end - # Close the connection. + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. def disconnect! clear_cache! @connection.close rescue nil @@ -366,7 +380,7 @@ module ActiveRecord case value when String return super unless 'bytea' == column.sql_type - escape_bytea(value) + { :value => value, :format => 1 } else super end @@ -460,42 +474,43 @@ module ActiveRecord # create a 2D array representing the result set def result_as_array(res) #:nodoc: # check if we have any binary column and if they need escaping - unescape_col = [] - res.nfields.times do |j| - unescape_col << res.ftype(j) + ftypes = Array.new(res.nfields) do |i| + [i, res.ftype(i)] end - ary = [] - res.ntuples.times do |i| - ary << [] - res.nfields.times do |j| - data = res.getvalue(i,j) - case unescape_col[j] - - # unescape string passed BYTEA field (OID == 17) - when BYTEA_COLUMN_TYPE_OID - data = unescape_bytea(data) if String === data - - # If this is a money type column and there are any currency symbols, - # then strip them off. Indeed it would be prettier to do this in - # PostgreSQLColumn.string_to_decimal but would break form input - # fields that call value_before_type_cast. - when MONEY_COLUMN_TYPE_OID - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - case data - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - data.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end + rows = res.values + return rows unless ftypes.any? { |_, x| + x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID + } + + typehash = ftypes.group_by { |_, type| type } + binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] + monies = typehash[MONEY_COLUMN_TYPE_OID] || [] + + rows.each do |row| + # unescape string passed BYTEA field (OID == 17) + binaries.each do |index, _| + row[index] = unescape_bytea(row[index]) + end + + # If this is a money type column and there are any currency symbols, + # then strip them off. Indeed it would be prettier to do this in + # PostgreSQLColumn.string_to_decimal but would break form input + # fields that call value_before_type_cast. + monies.each do |index, _| + data = row[index] + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + case data + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + data.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') end - ary[i] << data end end - return ary end @@ -552,10 +567,6 @@ module ActiveRecord end end - def exec_insert(sql, name, binds) - exec_query(sql, name, binds) - end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) unless pk _, table = extract_schema_and_table(sql.split(" ", 4)[2]) @@ -641,7 +652,7 @@ module ActiveRecord execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" end - # Drops a PostgreSQL database + # Drops a PostgreSQL database. # # Example: # drop_database 'matt_development' @@ -933,10 +944,7 @@ module ActiveRecord order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } - # Return a DISTINCT ON() clause that's distinct on the columns we want but includes - # all the required columns for the ORDER BY to work properly. - sql = "DISTINCT ON (#{columns}) #{columns}, " - sql << order_columns * ', ' + "DISTINCT #{columns}, #{order_columns * ', '}" end protected @@ -1093,4 +1101,3 @@ module ActiveRecord end end end - diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 9e7f874f4b..ed5006dcec 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -66,16 +66,18 @@ module ActiveRecord sqlite_version >= '3.6.8' end - # Returns +true+ when the connection adapter supports prepared statement - # caching, otherwise returns +false+ + # Returns true, since this connection adapter supports prepared statement + # caching. def supports_statement_cache? true end + # Returns true. def supports_migrations? #:nodoc: true end + # Returns true. def supports_primary_key? #:nodoc: true end @@ -88,12 +90,15 @@ module ActiveRecord sqlite_version >= '3.1.6' end + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. def disconnect! super clear_cache! @connection.close rescue nil end + # Clears the prepared statements cache. def clear_cache! @statements.clear end @@ -173,10 +178,6 @@ module ActiveRecord end end - def exec_insert(sql, name, binds) - exec_query(sql, name, binds) - end - def last_inserted_id(result) @connection.last_insert_row_id end @@ -345,10 +346,6 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end - def null_insert_value - Arel.sql 'NULL' - end - def empty_insert_statement_value "VALUES(NULL)" end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index f1df04950b..588f52be44 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -82,16 +82,18 @@ module ActiveRecord # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean # only shirts. # - # If you need to pass parameters to a scope, define it as a normal method: + # Named \scopes can also be procedural: # # class Shirt < ActiveRecord::Base - # def self.colored(color) - # where(:color => color) - # end + # scope :colored, lambda { |color| where(:color => color) } # end # # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. # + # On Ruby 1.9 you can use the 'stabby lambda' syntax: + # + # scope :colored, ->(color) { where(:color => color) } + # # Note that scopes defined with \scope will be evaluated when they are defined, rather than # when they are used. For example, the following would be incorrect: # @@ -101,13 +103,11 @@ module ActiveRecord # # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt> # class was defined, and so the resultant SQL query would always be the same. The correct - # way to do this would be via a class method, which will re-evaluate the scope each time + # way to do this would be via a lambda, which will re-evaluate the scope each time # it is called: # # class Post < ActiveRecord::Base - # def self.recent - # where('published_at >= ?', Time.now - 1.week) - # end + # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) } # end # # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: @@ -120,18 +120,6 @@ module ActiveRecord # end # end # - # The above could also be written as a class method like so: - # - # class Shirt < ActiveRecord::Base - # def self.red - # where(:color => 'red').extending do - # def dom_id - # 'red_shirts' - # end - # end - # end - # end - # # Scopes can also be used while creating/building a record. # # class Article < ActiveRecord::Base @@ -168,24 +156,6 @@ module ActiveRecord valid_scope_name?(name) extension = Module.new(&Proc.new) if block_given? - if !scope_options.is_a?(Relation) && scope_options.respond_to?(:call) - ActiveSupport::Deprecation.warn <<-WARN -Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your scope to be lazily evaluated, or takes parameters, please define it as a normal class method instead. For example, change this: - -class Post < ActiveRecord::Base - scope :unpublished, lambda { where('published_at > ?', Time.now) } -end - -To this: - -class Post < ActiveRecord::Base - def self.unpublished - where('published_at > ?', Time.now) - end -end - WARN - end - scope_proc = lambda do |*args| options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options options = scoped.apply_finder_options(options) if options.is_a?(Hash) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a916c88348..787ac977e0 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -136,22 +136,27 @@ module ActiveRecord # Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. - def update_attributes(attributes) + # + # When updating model attributes, mass-assignment security protection is respected. + # If no +:as+ option is supplied then the +:default+ scope will be used. + # If you want to bypass the protection given by +attr_protected+ and + # +attr_accessible+ then you can do so using the +:without_protection+ option. + def update_attributes(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.attributes = attributes + self.assign_attributes(attributes, options) save end end # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. - def update_attributes!(attributes) + def update_attributes!(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.attributes = attributes + self.assign_attributes(attributes, options) save! end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index cace6f0cc0..d38588519b 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -50,6 +50,9 @@ module ActiveRecord initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do + if app.config.active_record.delete(:whitelist_attributes) + attr_accessible(nil) + end app.config.active_record.each do |k,v| send "#{k}=", v end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6b3c38cb58..7d76d7a19f 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -70,7 +70,13 @@ db_namespace = namespace :db do @charset = ENV['CHARSET'] || 'utf8' @collation = ENV['COLLATION'] || 'utf8_unicode_ci' creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} - error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error + if config['adapter'] =~ /jdbc/ + #FIXME After Jdbcmysql gives this class + require 'active_record/railties/jdbcmysql_error' + error_class = ArJdbcMySQL::Error + else + error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error + end access_denied_error = 1045 begin ActiveRecord::Base.establish_connection(config.merge('database' => nil)) @@ -94,7 +100,7 @@ db_namespace = namespace :db do $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] end end - when 'postgresql' + when /postgresql/ @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8' begin ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb new file mode 100644 index 0000000000..6b9af2a0cb --- /dev/null +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -0,0 +1,16 @@ +#FIXME Remove if ArJdbcMysql will give. +module ArJdbcMySQL + class Error < StandardError + attr_accessor :error_number, :sql_state + + def initialize msg + super + @error_number = nil + @sql_state = nil + end + + # Mysql gem compatibility + alias_method :errno, :error_number + alias_method :error, :message + end +end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 359f9d8a66..8e5f66ec1d 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -56,11 +56,11 @@ module ActiveRecord end substitutes.each_with_index do |tuple, i| - tuple[1] = conn.substitute_at(tuple.first, i) + tuple[1] = conn.substitute_at(binds[i][0], i) end if values.empty? # empty insert - im.values = im.create_values [connection.null_insert_value], [] + im.values = Arel.sql(connection.empty_insert_statement_value) else im.insert substitutes end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index aae257a0e7..57c9921ea8 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -246,6 +246,8 @@ module ActiveRecord orders = relation.order_values values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) + relation = relation.dup + ids_array = relation.select(values).collect {|row| row[primary_key]} ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end @@ -277,8 +279,8 @@ module ActiveRecord unless record record = @klass.new do |r| - r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty? - r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty? + r.assign_attributes(protected_attributes_for_create) + r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) end yield(record) if block_given? record.save if match.instantiator == :create diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index d1225a9ed9..4db4105389 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -56,8 +56,9 @@ module ActiveRecord column = klass.columns_hash[attribute.to_s] value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? - if !options[:case_sensitive] && column.text? - relation = table[attribute].matches(value) + if !options[:case_sensitive] && value && column.text? + # will use SQL LOWER function before comparison + relation = table[attribute].lower.eq(table.lower(value)) else value = klass.connection.case_sensitive_modifier(value) relation = table[attribute].eq(value) diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 5bb8fa2f93..ce08e4c6a7 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -3,6 +3,9 @@ require "cases/helper" class PostgresqlArray < ActiveRecord::Base end +class PostgresqlTsvector < ActiveRecord::Base +end + class PostgresqlMoney < ActiveRecord::Base end @@ -34,6 +37,9 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @connection.execute("INSERT INTO postgresql_arrays (commission_by_quarter, nicknames) VALUES ( '{35000,21000,18000,17000}', '{foo,bar,baz}' )") @first_array = PostgresqlArray.find(1) + @connection.execute("INSERT INTO postgresql_tsvectors (text_vector) VALUES (' ''text'' ''vector'' ')") + @first_tsvector = PostgresqlTsvector.find(1) + @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('567.89'::money)") @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('-567.89'::money)") @first_money = PostgresqlMoney.find(1) @@ -62,6 +68,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :string, @first_array.column_for_attribute(:nicknames).type end + def test_data_type_of_tsvector_types + assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type + end + def test_data_type_of_money_types assert_equal :decimal, @first_money.column_for_attribute(:wealth).type end @@ -95,11 +105,26 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '{foo,bar,baz}', @first_array.nicknames end + def test_tsvector_values + assert_equal "'text' 'vector'", @first_tsvector.text_vector + end + def test_money_values assert_equal 567.89, @first_money.wealth assert_equal(-567.89, @second_money.wealth) end + def test_update_tsvector + new_text_vector = "'new' 'text' 'vector'" + assert @first_tsvector.text_vector = new_text_vector + assert @first_tsvector.save + assert @first_tsvector.reload + assert @first_tsvector.text_vector = new_text_vector + assert @first_tsvector.save + assert @first_tsvector.reload + assert_equal @first_tsvector.text_vector, new_text_vector + end + def test_number_values assert_equal 123.456, @first_number.single assert_equal 123456.789, @first_number.double diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 40c82f2fb8..9bc7910fc6 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -170,6 +170,16 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal [comment], category.posts[0].comments end end + + def test_associations_loaded_for_all_records + post = Post.create!(:title => 'foo', :body => "I like cars!") + comment = SpecialComment.create!(:body => 'Come on!', :post => post) + first_category = Category.create! :name => 'First!', :posts => [post] + second_category = Category.create! :name => 'Second!', :posts => [post] + + categories = Category.where(:id => [first_category.id, second_category.id]).includes(:posts => :special_comments) + assert_equal categories.map { |category| category.posts.first.special_comments.loaded? }, [true, true] + end def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once author_id = authors(:david).id diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 1efe3420a0..70a4e06dbe 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -44,7 +44,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_associate_existing - assert_queries(2) { posts(:thinking); people(:david) } + posts(:thinking); people(:david) # Warm cache assert_queries(1) do posts(:thinking).people << people(:david) diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index afc830cae9..49a1c117bc 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -516,10 +516,10 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_collection_size_uses_counter_cache_if_it_exists - author = authors(:david) - author.stubs(:_read_attribute).with('comments_count').returns(100) - assert_equal 100, author.comments.size - assert !author.comments.loaded? + c = categories(:general) + c.categorizations_count = 100 + assert_equal 100, c.categorizations.size + assert !c.categorizations.loaded? end def test_adding_junk_to_has_many_through_should_raise_type_mismatch diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index b898c003bd..5074ae50ab 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -10,6 +10,7 @@ require 'models/company' require 'models/category' require 'models/reply' require 'models/contact' +require 'models/keyboard' class AttributeMethodsTest < ActiveRecord::TestCase fixtures :topics, :developers, :companies, :computers @@ -77,6 +78,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_respond_to? topic = Topic.find(1) assert_respond_to topic, "title" + assert_respond_to topic, "_title" assert_respond_to topic, "title?" assert_respond_to topic, "title=" assert_respond_to topic, :title @@ -88,6 +90,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert !topic.respond_to?(:nothingness) end + def test_respond_to_with_custom_primary_key + keyboard = Keyboard.create + assert_not_nil keyboard.key_number + assert_equal keyboard.key_number, keyboard.id + assert keyboard.respond_to?('key_number') + assert keyboard.respond_to?('_key_number') + assert keyboard.respond_to?('id') + assert keyboard.respond_to?('_id') + end + # Syck calls respond_to? before actually calling initialize def test_respond_to_with_allocated_object topic = Topic.allocate diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index e57c5b3b87..5ee3b2d776 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -18,7 +18,7 @@ require 'models/comment' require 'models/minimalistic' require 'models/warehouse_thing' require 'models/parrot' -require 'models/loose_person' +require 'models/person' require 'models/edge' require 'models/joke' require 'rexml/document' @@ -489,6 +489,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 'value2', weird.send('a$b') end + def test_attributes_guard_protected_attributes_is_deprecated + attributes = { "title" => "An amazing title" } + topic = Topic.new + assert_deprecated { topic.send(:attributes=, attributes, false) } + end + def test_multiparameter_attributes_on_date attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) @@ -895,7 +901,7 @@ class BasicsTest < ActiveRecord::TestCase assert g.save # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) + h = ActiveRecord::IdentityMap.without { Geometric.find(g.id) } assert_equal '(5,6.1)', h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment @@ -923,7 +929,7 @@ class BasicsTest < ActiveRecord::TestCase assert g.save # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) + h = ActiveRecord::IdentityMap.without { Geometric.find(g.id) } assert_equal '(5,6.1)', h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 3c242667eb..655437318f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -247,9 +247,10 @@ class FinderTest < ActiveRecord::TestCase def test_find_only_some_columns topic = Topic.find(1, :select => "author_name") assert_raise(ActiveModel::MissingAttributeError) {topic.title} + assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name assert !topic.attribute_present?("title") - #assert !topic.respond_to?("title") + assert !topic.attribute_present?(:title) assert topic.attribute_present?("author_name") assert_respond_to topic, "author_name" end diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 025ec1d3fa..43016df479 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -3,6 +3,7 @@ require 'models/company' require 'models/subscriber' require 'models/keyboard' require 'models/task' +require 'models/person' class MassAssignmentSecurityTest < ActiveRecord::TestCase @@ -30,6 +31,66 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end end + def test_assign_attributes_uses_default_scope_when_no_scope_is_provided + p = LoosePerson.new + p.assign_attributes(attributes_hash) + + assert_equal nil, p.id + assert_equal 'Josh', p.first_name + assert_equal 'male', p.gender + assert_equal nil, p.comments + end + + def test_assign_attributes_skips_mass_assignment_security_protection_when_without_protection_is_used + p = LoosePerson.new + p.assign_attributes(attributes_hash, :without_protection => true) + + assert_equal 5, p.id + assert_equal 'Josh', p.first_name + assert_equal 'male', p.gender + assert_equal 'rides a sweet bike', p.comments + end + + def test_assign_attributes_with_default_scope_and_attr_protected_attributes + p = LoosePerson.new + p.assign_attributes(attributes_hash, :as => :default) + + assert_equal nil, p.id + assert_equal 'Josh', p.first_name + assert_equal 'male', p.gender + assert_equal nil, p.comments + end + + def test_assign_attributes_with_admin_scope_and_attr_protected_attributes + p = LoosePerson.new + p.assign_attributes(attributes_hash, :as => :admin) + + assert_equal nil, p.id + assert_equal 'Josh', p.first_name + assert_equal 'male', p.gender + assert_equal 'rides a sweet bike', p.comments + end + + def test_assign_attributes_with_default_scope_and_attr_accessible_attributes + p = TightPerson.new + p.assign_attributes(attributes_hash, :as => :default) + + assert_equal nil, p.id + assert_equal 'Josh', p.first_name + assert_equal 'male', p.gender + assert_equal nil, p.comments + end + + def test_assign_attributes_with_admin_scope_and_attr_accessible_attributes + p = TightPerson.new + p.assign_attributes(attributes_hash, :as => :admin) + + assert_equal nil, p.id + assert_equal 'Josh', p.first_name + assert_equal 'male', p.gender + assert_equal 'rides a sweet bike', p.comments + end + def test_protection_against_class_attribute_writers [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method| @@ -40,4 +101,14 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end end + private + + def attributes_hash + { + :id => 5, + :first_name => 'Josh', + :gender => 'male', + :comments => 'rides a sweet bike' + } + end end
\ No newline at end of file diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 2880fdc651..8fd1fc2577 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -471,12 +471,6 @@ class NamedScopeTest < ActiveRecord::TestCase require "models/without_table" end end - - def test_scopes_with_callables_are_deprecated - assert_deprecated do - Post.scope :WE_SO_EXCITED, lambda { |partyingpartyingpartying, yeah| fun!.fun!.fun! } - end - end end class DynamicScopeMatchTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 9aa13f04cd..2044bc6e3f 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -12,7 +12,7 @@ require 'models/minimalistic' require 'models/warehouse_thing' require 'models/parrot' require 'models/minivan' -require 'models/loose_person' +require 'models/person' require 'rexml/document' require 'active_support/core_ext/exception' @@ -491,6 +491,26 @@ class PersistencesTest < ActiveRecord::TestCase assert_equal "The First Topic", topic.title end + def test_update_attributes_as_admin + person = TightPerson.create + person.update_attributes({ "first_name" => 'Josh', "gender" => 'male', "comments" => 'from NZ' }, :as => :admin) + person.reload + + assert_equal 'Josh', person.first_name + assert_equal 'male', person.gender + assert_equal 'from NZ', person.comments + end + + def test_update_attributes_as_without_protection + person = TightPerson.create + person.update_attributes({ "first_name" => 'Josh', "gender" => 'male', "comments" => 'from NZ' }, :without_protection => true) + person.reload + + assert_equal 'Josh', person.first_name + assert_equal 'male', person.gender + assert_equal 'from NZ', person.comments + end + def test_update_attributes! Reply.validates_presence_of(:title) reply = Reply.find(2) @@ -512,6 +532,26 @@ class PersistencesTest < ActiveRecord::TestCase Reply.reset_callbacks(:validate) end + def test_update_attributes_as_admin + person = TightPerson.create + person.update_attributes!({ "first_name" => 'Josh', "gender" => 'male', "comments" => 'from NZ' }, :as => :admin) + person.reload + + assert_equal 'Josh', person.first_name + assert_equal 'male', person.gender + assert_equal 'from NZ', person.comments + end + + def test_update_attributes_as_without_protection + person = TightPerson.create + person.update_attributes!({ "first_name" => 'Josh', "gender" => 'male', "comments" => 'from NZ' }, :without_protection => true) + person.reload + + assert_equal 'Josh', person.first_name + assert_equal 'male', person.gender + assert_equal 'from NZ', person.comments + end + def test_destroyed_returns_boolean developer = Developer.first assert_equal false, developer.destroyed? diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 5079aec9ba..2ed676fe69 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -308,6 +308,22 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_default_scope_as_class_method + assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all + end + + def test_default_scope_with_lambda + assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all + end + + def test_default_scope_with_block + assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.all + end + + def test_default_scope_with_callable + assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.all + end + def test_default_scope_is_unscoped_on_find assert_equal 1, DeveloperCalledDavid.count assert_equal 11, DeveloperCalledDavid.unscoped.count @@ -339,6 +355,12 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 50000, wheres[:salary] end + def test_default_scope_with_multiple_calls + wheres = MultiplePoorDeveloperCalledJamis.scoped.where_values_hash + assert_equal "Jamis", wheres[:name] + assert_equal 50000, wheres[:salary] + end + def test_method_scope expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } @@ -434,175 +456,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis)) assert_equal 10, DeveloperCalledJamis.unscoped.poor.length end -end - -class DeprecatedDefaultScopingTest < ActiveRecord::TestCase - fixtures :developers, :posts - - def test_default_scope - expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_default_scope_is_unscoped_on_find - assert_equal 1, DeprecatedDeveloperCalledDavid.count - assert_equal 11, DeprecatedDeveloperCalledDavid.unscoped.count - end - - def test_default_scope_is_unscoped_on_create - assert_nil DeprecatedDeveloperCalledJamis.unscoped.create!.name - end - - def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeprecatedDeveloperCalledDavid.find(:all).map(&:id).sort - assert_equal nil, DeprecatedDeveloperCalledDavid.create!.name - end - def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeprecatedDeveloperCalledJamis.find(:all).map(&:id).sort - assert_equal 'Jamis', DeprecatedDeveloperCalledJamis.create!.name - end + def test_multiple_default_scope_calls_are_deprecated + klass = Class.new(ActiveRecord::Base) - def test_default_scoping_with_threads - 2.times do - Thread.new { assert DeprecatedDeveloperOrderedBySalary.scoped.to_sql.include?('salary DESC') }.join + assert_not_deprecated do + klass.send(:default_scope, :foo => :bar) end - end - def test_default_scoping_with_inheritance - # Inherit a class having a default scope and define a new default scope - klass = Class.new(DeprecatedDeveloperOrderedBySalary) - ActiveSupport::Deprecation.silence { klass.send :default_scope, :limit => 1 } - - # Scopes added on children should append to parent scope - assert_equal [developers(:jamis).id], klass.all.map(&:id) - - # Parent should still have the original scope - assert_equal Developer.order('salary DESC').map(&:id), DeprecatedDeveloperOrderedBySalary.all.map(&:id) - end - - def test_default_scope_called_twice_merges_conditions - Developer.destroy_all - Developer.create!(:name => "David", :salary => 80000) - Developer.create!(:name => "David", :salary => 100000) - Developer.create!(:name => "Brian", :salary => 100000) - - klass = Class.new(Developer) - ActiveSupport::Deprecation.silence do - klass.__send__ :default_scope, :conditions => { :name => "David" } - klass.__send__ :default_scope, :conditions => { :salary => 100000 } - end - assert_equal 1, klass.count - assert_equal "David", klass.first.name - assert_equal 100000, klass.first.salary - end - - def test_default_scope_called_twice_in_different_place_merges_where_clause - Developer.destroy_all - Developer.create!(:name => "David", :salary => 80000) - Developer.create!(:name => "David", :salary => 100000) - Developer.create!(:name => "Brian", :salary => 100000) - - klass = Class.new(Developer) - ActiveSupport::Deprecation.silence do - klass.class_eval do - default_scope where("name = 'David'") - default_scope where("salary = 100000") - end + assert_deprecated do + klass.send(:default_scope, :foo => :bar) end - assert_equal 1, klass.count - assert_equal "David", klass.first.name - assert_equal 100000, klass.first.salary - end - - def test_method_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_nested_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do - DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - - def test_scope_overwrites_default - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } - received = DeprecatedDeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } - assert_equal expected, received - end - - def test_reorder_overrides_default_scope_order - expected = Developer.order('name DESC').collect { |dev| dev.name } - received = DeprecatedDeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } - assert_equal expected, received - end - - def test_nested_exclusive_scope - expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do - DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - - def test_order_in_default_scope_should_prevail - expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_default_scope_using_relation - posts = DeprecatedPostWithComment.scoped - assert_equal 2, posts.to_a.length - assert_equal posts(:thinking), posts.first - end - - def test_create_attribute_overwrites_default_scoping - assert_equal 'David', DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').name - assert_equal 200000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary - end - - def test_create_attribute_overwrites_default_values - assert_equal nil, DeprecatedPoorDeveloperCalledJamis.create!(:salary => nil).salary - assert_equal 50000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').salary - end - - def test_default_scope_attribute - jamis = DeprecatedPoorDeveloperCalledJamis.new(:name => 'David') - assert_equal 50000, jamis.salary - end - - def test_where_attribute - aaron = DeprecatedPoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') - assert_equal 20, aaron.salary - assert_equal 'Aaron', aaron.name - end - - def test_where_attribute_merge - aaron = DeprecatedPoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') - assert_equal 'Aaron', aaron.name - end - - def test_create_with_merge - aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( - DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new - assert_equal 20, aaron.salary - assert_equal 'Aaron', aaron.name - - aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). - create_with(:name => 'Aaron').new - assert_equal 20, aaron.salary - assert_equal 'Aaron', aaron.name - end - - def test_create_with_reset - jamis = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new - assert_equal 'Jamis', jamis.name + assert_equal 2, klass.default_scopes.length end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 9b2c7c00df..e8f2f44189 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -203,6 +203,13 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t.xml "data"}, output end end + + def test_schema_dump_includes_tsvector_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_tsvectors"} =~ output + assert_match %r{t.tsvector "text_vector"}, output + end + end end def test_schema_dump_keeps_large_precision_integer_columns_as_decimal diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index b4f3dd034c..0f1b3667cc 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -162,6 +162,32 @@ class UniquenessValidationTest < ActiveRecord::TestCase end end + def test_validate_case_sensitive_uniqueness_with_special_sql_like_chars + Topic.validates_uniqueness_of(:title, :case_sensitive => true) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t2 = Topic.new("title" => "I'm %") + assert t2.save, "Should save t2 as unique" + + t3 = Topic.new("title" => "I'm uniqu_!") + assert t3.save, "Should save t3 as unique" + end + + def test_validate_case_insensitive_uniqueness_with_special_sql_like_chars + Topic.validates_uniqueness_of(:title, :case_sensitive => false) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t2 = Topic.new("title" => "I'm %") + assert t2.save, "Should save t2 as unique" + + t3 = Topic.new("title" => "I'm uniqu_!") + assert t3.save, "Should save t3 as unique" + end + def test_validate_case_sensitive_uniqueness Topic.validates_uniqueness_of(:title, :case_sensitive => true, :allow_nil => true) diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 89ee5416bf..c68d008c26 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,8 +1,5 @@ class Bulb < ActiveRecord::Base - def self.default_scope - where :name => 'defaulty' - end - + default_scope where(:name => 'defaulty') belongs_to :car attr_reader :scope_after_initialize diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index a978debb58..b036f0f5c9 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -15,13 +15,9 @@ class Car < ActiveRecord::Base end class CoolCar < Car - def self.default_scope - order 'name desc' - end + default_scope :order => 'name desc' end class FastCar < Car - def self.default_scope - order 'name desc' - end + default_scope :order => 'name desc' end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 39441e8610..4bd980e606 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -12,10 +12,7 @@ end class SpecialCategorization < ActiveRecord::Base self.table_name = 'categorizations' - - def self.default_scope - where(:special => true) - end + default_scope where(:special => true) belongs_to :author belongs_to :category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 3bd7db7834..2a4c37089a 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,8 +1,5 @@ class Comment < ActiveRecord::Base - def self.limit_by(l) - limit(l) - end - + scope :limit_by, lambda {|l| limit(l) } scope :containing_the_letter_e, :conditions => "comments.body LIKE '%e%'" scope :not_again, where("comments.body NOT LIKE '%again%'") scope :for_first_post, :conditions => { :post_id => 1 } diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 10385ba899..10701dd6fd 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -1,3 +1,5 @@ +require 'ostruct' + module DeveloperProjectsAssociationExtension def find_most_recent find(:first, :order => "id DESC") @@ -86,10 +88,7 @@ end class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' - - def self.default_scope - order('salary DESC') - end + default_scope :order => 'salary DESC' scope :by_name, order('name DESC') @@ -102,74 +101,56 @@ end class DeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - - def self.default_scope - where "name = 'David'" - end + default_scope where("name = 'David'") end -class DeveloperCalledJamis < ActiveRecord::Base +class LazyLambdaDeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' + default_scope lambda { where(:name => 'David') } +end - def self.default_scope - where :name => 'Jamis' - end +class LazyBlockDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + default_scope { where(:name => 'David') } +end - scope :poor, where('salary < 150000') +class CallableDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + default_scope OpenStruct.new(:call => where(:name => 'David')) end -class AbstractDeveloperCalledJamis < ActiveRecord::Base - self.abstract_class = true +class ClassMethodDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' def self.default_scope - where :name => 'Jamis' + where(:name => 'David') end end -class PoorDeveloperCalledJamis < ActiveRecord::Base +class DeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' + default_scope where(:name => 'Jamis') + scope :poor, where('salary < 150000') +end - def self.default_scope - where :name => 'Jamis', :salary => 50000 - end +class PoorDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope where(:name => 'Jamis', :salary => 50000) end class InheritedPoorDeveloperCalledJamis < DeveloperCalledJamis self.table_name = 'developers' - def self.default_scope - super.where :salary => 50000 + ActiveSupport::Deprecation.silence do + default_scope where(:salary => 50000) end end -ActiveSupport::Deprecation.silence do - class DeprecatedDeveloperOrderedBySalary < ActiveRecord::Base - self.table_name = 'developers' - default_scope :order => 'salary DESC' - - def self.by_name - order('name DESC') - end - - def self.all_ordered_by_name - with_scope(:find => { :order => 'name DESC' }) do - find(:all) - end - end - end - - class DeprecatedDeveloperCalledDavid < ActiveRecord::Base - self.table_name = 'developers' - default_scope :conditions => "name = 'David'" - end - - class DeprecatedDeveloperCalledJamis < ActiveRecord::Base - self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis' } - end +class MultiplePoorDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope where(:name => 'Jamis') - class DeprecatedPoorDeveloperCalledJamis < ActiveRecord::Base - self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis', :salary => 50000 } + ActiveSupport::Deprecation.silence do + default_scope where(:salary => 50000) end end diff --git a/activerecord/test/models/loose_person.rb b/activerecord/test/models/loose_person.rb deleted file mode 100644 index 256c281d0d..0000000000 --- a/activerecord/test/models/loose_person.rb +++ /dev/null @@ -1,24 +0,0 @@ -class LoosePerson < ActiveRecord::Base - self.table_name = 'people' - self.abstract_class = true - - attr_protected :credit_rating, :administrator -end - -class LooseDescendant < LoosePerson - attr_protected :phone_number -end - -class LooseDescendantSecond< LoosePerson - attr_protected :phone_number - attr_protected :name -end - -class TightPerson < ActiveRecord::Base - self.table_name = 'people' - attr_accessible :name, :address -end - -class TightDescendant < TightPerson - attr_accessible :phone_number -end
\ No newline at end of file diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index ad59d12672..9c4794902d 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -48,3 +48,22 @@ class PersonWithDependentNullifyJobs < ActiveRecord::Base has_many :references, :foreign_key => :person_id has_many :jobs, :source => :job, :through => :references, :dependent => :nullify end + + +class LoosePerson < ActiveRecord::Base + self.table_name = 'people' + self.abstract_class = true + + attr_protected :comments + attr_protected :as => :admin +end + +class LooseDescendant < LoosePerson; end + +class TightPerson < ActiveRecord::Base + self.table_name = 'people' + attr_accessible :first_name, :gender + attr_accessible :first_name, :gender, :comments, :as => :admin +end + +class TightDescendant < TightPerson; end
\ No newline at end of file diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index a91c10276b..80296032bb 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -8,13 +8,12 @@ class Post < ActiveRecord::Base scope :containing_the_letter_a, where("body LIKE '%a%'") scope :ranked_by_comments, order("comments_count DESC") - def self.limit_by(l) - limit(l) - end - - def self.with_authors_at_address(address) - where('authors.author_address_id = ?', address.id).joins('JOIN authors ON authors.id = posts.author_id') - end + scope :limit_by, lambda {|l| limit(l) } + scope :with_authors_at_address, lambda { |address| { + :conditions => [ 'authors.author_address_id = ?', address.id ], + :joins => 'JOIN authors ON authors.id = posts.author_id' + } + } belongs_to :author do def greeting @@ -29,10 +28,9 @@ class Post < ActiveRecord::Base scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} } scope :with_very_special_comments, joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) - - def self.with_post(post_id) - joins(:comments).where(:comments => { :post_id => post_id }) - end + scope :with_post, lambda {|post_id| + { :joins => :comments, :conditions => {:comments => {:post_id => post_id} } } + } has_many :comments do def find_most_recent @@ -159,10 +157,7 @@ end class FirstPost < ActiveRecord::Base self.table_name = 'posts' - - def self.default_scope - where(:id => 1) - end + default_scope where(:id => 1) has_many :comments, :foreign_key => :post_id has_one :comment, :foreign_key => :post_id diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 76c0a1a32e..c5af0b5d5f 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -19,8 +19,5 @@ end class BadReference < ActiveRecord::Base self.table_name = 'references' - - def self.default_scope - where :favourite => false - end + default_scope where(:favourite => false) end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 60e750e6c4..6440dbe8ab 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -1,20 +1,10 @@ class Topic < ActiveRecord::Base scope :base - - ActiveSupport::Deprecation.silence do - scope :written_before, lambda { |time| - if time - { :conditions => ['written_on < ?', time] } - end - } - - scope :with_object, Class.new(Struct.new(:klass)) { - def call - klass.where(:approved => true) - end - }.new(self) - end - + scope :written_before, lambda { |time| + if time + { :conditions => ['written_on < ?', time] } + end + } scope :approved, :conditions => {:approved => true} scope :rejected, :conditions => {:approved => false} @@ -29,6 +19,12 @@ class Topic < ActiveRecord::Base end end + scope :with_object, Class.new(Struct.new(:klass)) { + def call + klass.where(:approved => true) + end + }.new(self) + module NamedExtension def two 2 diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb index 1a63d6ceb6..184ab1649e 100644 --- a/activerecord/test/models/without_table.rb +++ b/activerecord/test/models/without_table.rb @@ -1,5 +1,3 @@ class WithoutTable < ActiveRecord::Base - def self.default_scope - where(:published => true) - end + default_scope where(:published => true) end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f38f4f3b44..5cf9a207f3 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,6 @@ ActiveRecord::Schema.define do - %w(postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings + %w(postgresql_tsvectors postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -55,6 +55,14 @@ _SQL nicknames TEXT[] ); _SQL + + execute <<_SQL + CREATE TABLE postgresql_tsvectors ( + id SERIAL PRIMARY KEY, + text_vector tsvector + ); +_SQL + execute <<_SQL CREATE TABLE postgresql_moneys ( id SERIAL PRIMARY KEY, diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 163f1c932c..69e9cbfd42 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.1.0 (unreleased)* +* Add String#inquiry as a convenience method for turning a string into a StringInquirer object [DHH] + * Add Object#in? to test if an object is included in another object [Prem Sichanugrist, Brian Morearty, John Reitano] * LocalCache strategy is now a real middleware class, not an anonymous class diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index eaecb30090..37a74a9e62 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -16,4 +16,6 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' + + s.add_dependency('multi_json', '~> 1.0.0') end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 6b662ac660..6b87774978 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -42,7 +42,6 @@ module ActiveSupport autoload :DescendantsTracker autoload :FileUpdateChecker - autoload :FileWatcher autoload :LogSubscriber autoload :Notifications diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index e41731f3e7..88b50fc506 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -41,7 +41,7 @@ module ActiveSupport def initialize(log, level = DEBUG) @level = level - @buffer = {} + @buffer = Hash.new { |h,k| h[k] = [] } @auto_flushing = 1 @guard = Mutex.new @@ -100,13 +100,8 @@ module ActiveSupport def flush @guard.synchronize do - unless buffer.empty? - old_buffer = buffer - all_content = StringIO.new - old_buffer.each do |content| - all_content << content - end - @log.write(all_content.string) + buffer.each do |content| + @log.write(content) end # Important to do this even if buffer was empty or else @buffer will @@ -127,7 +122,7 @@ module ActiveSupport end def buffer - @buffer[Thread.current] ||= [] + @buffer[Thread.current] end def clear_buffer diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 9936b33e22..a1376ae52a 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -121,7 +121,7 @@ module ActiveSupport # Lock a file for a block so only one process can modify it at a time. def lock_file(file_name, &block) # :nodoc: if File.exist?(file_name) - File.open(file_name, 'r') do |f| + File.open(file_name, 'r+') do |f| begin f.flock File::LOCK_EX yield diff --git a/activesupport/lib/active_support/core_ext/array/random_access.rb b/activesupport/lib/active_support/core_ext/array/random_access.rb index ab1fa7cd5b..9eba4642b8 100644 --- a/activesupport/lib/active_support/core_ext/array/random_access.rb +++ b/activesupport/lib/active_support/core_ext/array/random_access.rb @@ -1,12 +1,15 @@ class Array # Backport of Array#sample based on Marc-Andre Lafortune's https://github.com/marcandre/backports/ # Returns a random element or +n+ random elements from the array. - # If the array is empty and +n+ is nil, returns <tt>nil</tt>. if +n+ is passed, returns <tt>[]</tt>. + # If the array is empty and +n+ is nil, returns <tt>nil</tt>. + # If +n+ is passed and its value is less than 0, it raises an +ArgumentError+ exception. + # If the value of +n+ is equal or greater than 0 it returns <tt>[]</tt>. # - # [1,2,3,4,5,6].sample # => 4 - # [1,2,3,4,5,6].sample(3) # => [2, 4, 5] - # [].sample # => nil - # [].sample(3) # => [] + # [1,2,3,4,5,6].sample # => 4 + # [1,2,3,4,5,6].sample(3) # => [2, 4, 5] + # [1,2,3,4,5,6].sample(-3) # => ArgumentError: negative sample number + # [].sample # => nil + # [].sample(3) # => [] def sample(n=nil) return self[Kernel.rand(size)] if n.nil? n = n.to_int diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index d0c1ea8326..20085c4fb3 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -18,8 +18,8 @@ class Object !blank? end - # Returns object if it's #present? otherwise returns nil. - # object.presence is equivalent to object.present? ? object : nil. + # Returns object if it's <tt>present?</tt> otherwise returns +nil+. + # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>. # # This is handy for any representation of objects where blank is the same # as not present at all. For example, this simplifies a common check for @@ -37,39 +37,72 @@ class Object end end -class NilClass #:nodoc: +class NilClass + # +nil+ is blank: + # + # nil.blank? # => true + # def blank? true end end -class FalseClass #:nodoc: +class FalseClass + # +false+ is blank: + # + # false.blank? # => true + # def blank? true end end -class TrueClass #:nodoc: +class TrueClass + # +true+ is not blank: + # + # true.blank? # => false + # def blank? false end end -class Array #:nodoc: +class Array + # An array is blank if it's empty: + # + # [].blank? # => true + # [1,2,3].blank? # => false + # alias_method :blank?, :empty? end -class Hash #:nodoc: +class Hash + # A hash is blank if it's empty: + # + # {}.blank? # => true + # {:key => 'value'}.blank? # => false + # alias_method :blank?, :empty? end -class String #:nodoc: +class String + # A string is blank if it's empty or contains whitespaces only: + # + # "".blank? # => true + # " ".blank? # => true + # " something here ".blank? # => false + # def blank? self !~ /\S/ end end class Numeric #:nodoc: + # No number is blank: + # + # 1.blank? # => false + # 0.blank? # => false + # def blank? false end diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index b05325790c..02cb5dfee7 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -15,50 +15,89 @@ # That's why we hardcode the following cases and check duplicable? instead of # using that rescue idiom. class Object - # Can you safely .dup this object? - # False for nil, false, true, symbols, numbers, class and module objects; true otherwise. + # Can you safely dup this object? + # + # False for +nil+, +false+, +true+, symbols, numbers, class and module objects; + # true otherwise. def duplicable? true end end -class NilClass #:nodoc: +class NilClass + # +nil+ is not duplicable: + # + # nil.duplicable? # => false + # nil.dup # => TypeError: can't dup NilClass + # def duplicable? false end end -class FalseClass #:nodoc: +class FalseClass + # +false+ is not duplicable: + # + # false.duplicable? # => false + # false.dup # => TypeError: can't dup FalseClass + # def duplicable? false end end -class TrueClass #:nodoc: +class TrueClass + # +true+ is not duplicable: + # + # true.duplicable? # => false + # true.dup # => TypeError: can't dup TrueClass + # def duplicable? false end end -class Symbol #:nodoc: +class Symbol + # Symbols are not duplicable: + # + # :my_symbol.duplicable? # => false + # :my_symbol.dup # => TypeError: can't dup Symbol + # def duplicable? false end end -class Numeric #:nodoc: +class Numeric + # Numbers are not duplicable: + # + # 3.duplicable? # => false + # 3.dup # => TypeError: can't dup Fixnum + # def duplicable? false end end -class Class #:nodoc: +class Class + # Classes are not duplicable: + # + # c = Class.new # => #<Class:0x10328fd80> + # c.dup # => #<Class:0x10328fd80> + # + # Note +dup+ returned the same class object. def duplicable? false end end -class Module #:nodoc: +class Module + # Modules are not duplicable: + # + # m = Module.new # => #<Module:0x10328b6e0> + # m.dup # => #<Module:0x10328b6e0> + # + # Note +dup+ returned the same module object. def duplicable? false end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index 51cfc62f2b..b5671f66d0 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -1,11 +1,11 @@ class Object # Returns true if this object is included in the argument. Argument must be - # any object which respond to +#include?+. Usage: + # any object which responds to +#include?+. Usage: # # characters = ["Konata", "Kagami", "Tsukasa"] # "Konata".in?(characters) # => true # - # This will throw an ArgumentError if the supplied argument doesnt not respond + # This will throw an ArgumentError if the argument doesn't respond # to +#include?+. def in?(another_object) another_object.include?(self) diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 04619124a1..e77a9da0ec 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -9,12 +9,12 @@ class Object # # ==== Examples # - # Without try + # Without +try+ # @person && @person.name # or # @person ? @person.name : nil # - # With try + # With +try+ # @person.try(:name) # # +try+ also accepts arguments and/or a block, for the method it is trying @@ -34,7 +34,19 @@ class Object end end -class NilClass #:nodoc: +class NilClass + # Calling +try+ on +nil+ always returns +nil+. + # It becomes specially helpful when navigating through associations that may return +nil+. + # + # === Examples + # + # nil.try(:name) # => nil + # + # Without +try+ + # @person && !@person.children.blank? && @person.children.first.name + # + # With +try+ + # @person.try(:children).try(:first).try(:name) def try(*args) nil end diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb index 8fb8c31ade..72522d395c 100644 --- a/activesupport/lib/active_support/core_ext/string.rb +++ b/activesupport/lib/active_support/core_ext/string.rb @@ -11,3 +11,4 @@ require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/exclude' require 'active_support/core_ext/string/encoding' require 'active_support/core_ext/string/strip' +require 'active_support/core_ext/string/inquiry' diff --git a/activesupport/lib/active_support/core_ext/string/inquiry.rb b/activesupport/lib/active_support/core_ext/string/inquiry.rb new file mode 100644 index 0000000000..604f3bf4dc --- /dev/null +++ b/activesupport/lib/active_support/core_ext/string/inquiry.rb @@ -0,0 +1,13 @@ +require 'active_support/string_inquirer' + +class String + # Wraps the current string in the ActiveSupport::StringInquirer class, + # which gives you a prettier way to test for equality. Example: + # + # env = "production".inquiry + # env.production? # => true + # env.development? # => false + def inquiry + ActiveSupport::StringInquirer.new(self) + end +end diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index ce0775a690..45b9dda5ca 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -9,7 +9,7 @@ module ActiveSupport # The version the deprecated behavior will be removed, by default. attr_accessor :deprecation_horizon end - self.deprecation_horizon = '3.1' + self.deprecation_horizon = '3.2' # By default, warnings are not silenced and debugging is off. self.silenced = false diff --git a/activesupport/lib/active_support/json/backends/jsongem.rb b/activesupport/lib/active_support/json/backends/jsongem.rb deleted file mode 100644 index 533ba25da3..0000000000 --- a/activesupport/lib/active_support/json/backends/jsongem.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'json' unless defined?(JSON) - -module ActiveSupport - module JSON - module Backends - module JSONGem - ParseError = ::JSON::ParserError - extend self - - # Parses a JSON string or IO and convert it into an object - def decode(json) - if json.respond_to?(:read) - json = json.read - end - data = ::JSON.parse(json) - if ActiveSupport.parse_json_times - convert_dates_from(data) - else - data - end - end - - private - def convert_dates_from(data) - case data - when nil - nil - when DATE_REGEX - begin - DateTime.parse(data) - rescue ArgumentError - data - end - when Array - data.map! { |d| convert_dates_from(d) } - when Hash - data.each do |key, value| - data[key] = convert_dates_from(value) - end - else - data - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/json/backends/yajl.rb b/activesupport/lib/active_support/json/backends/yajl.rb deleted file mode 100644 index 58818658c7..0000000000 --- a/activesupport/lib/active_support/json/backends/yajl.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'yajl' unless defined?(Yajl) - -module ActiveSupport - module JSON - module Backends - module Yajl - ParseError = ::Yajl::ParseError - extend self - - # Parses a JSON string or IO and convert it into an object - def decode(json) - data = ::Yajl::Parser.new.parse(json) - if ActiveSupport.parse_json_times - convert_dates_from(data) - else - data - end - end - - private - def convert_dates_from(data) - case data - when nil - nil - when DATE_REGEX - begin - DateTime.parse(data) - rescue ArgumentError - data - end - when Array - data.map! { |d| convert_dates_from(d) } - when Hash - data.each do |key, value| - data[key] = convert_dates_from(value) - end - else - data - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/json/backends/yaml.rb b/activesupport/lib/active_support/json/backends/yaml.rb deleted file mode 100644 index e25e29d36b..0000000000 --- a/activesupport/lib/active_support/json/backends/yaml.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'active_support/core_ext/string/starts_ends_with' - -module ActiveSupport - module JSON - module Backends - module Yaml - ParseError = ::StandardError - extend self - - EXCEPTIONS = [::ArgumentError] # :nodoc: - begin - require 'psych' - EXCEPTIONS << Psych::SyntaxError - rescue LoadError - end - - # Parses a JSON string or IO and converts it into an object - def decode(json) - if json.respond_to?(:read) - json = json.read - end - YAML.load(convert_json_to_yaml(json)) - rescue *EXCEPTIONS => e - raise ParseError, "Invalid JSON string: '%s'" % json - end - - protected - # Ensure that ":" and "," are always followed by a space - def convert_json_to_yaml(json) #:nodoc: - require 'strscan' unless defined? ::StringScanner - scanner, quoting, marks, pos, times = ::StringScanner.new(json), false, [], nil, [] - while scanner.scan_until(/(\\['"]|['":,\\]|\\.|[\]])/) - case char = scanner[1] - when '"', "'" - if !quoting - quoting = char - pos = scanner.pos - elsif quoting == char - if valid_date?(json[pos..scanner.pos-2]) - # found a date, track the exact positions of the quotes so we can - # overwrite them with spaces later. - times << pos - end - quoting = false - end - when ":",",", "]" - marks << scanner.pos - 1 unless quoting - when "\\" - scanner.skip(/\\/) - end - end - - if marks.empty? - json.gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do - ustr = $1 - if ustr.start_with?('u') - char = [ustr[1..-1].to_i(16)].pack("U") - # "\n" needs extra escaping due to yaml formatting - char == "\n" ? "\\n" : char - elsif ustr == '\\' - '\\\\' - else - ustr - end - end - else - left_pos = [-1].push(*marks) - right_pos = marks << scanner.pos + scanner.rest_size - output = [] - left_pos.each_with_index do |left, i| - scanner.pos = left.succ - chunk = scanner.peek(right_pos[i] - scanner.pos + 1) - if ActiveSupport.parse_json_times - # overwrite the quotes found around the dates with spaces - while times.size > 0 && times[0] <= right_pos[i] - chunk.insert(times.shift - scanner.pos - 1, '! ') - end - end - chunk.gsub!(/\\([\\\/]|u[[:xdigit:]]{4})/) do - ustr = $1 - if ustr.start_with?('u') - char = [ustr[1..-1].to_i(16)].pack("U") - # "\n" needs extra escaping due to yaml formatting - char == "\n" ? "\\n" : char - elsif ustr == '\\' - '\\\\' - else - ustr - end - end - output << chunk - end - output = output * " " - - output.gsub!(/\\\//, '/') - output - end - end - - private - def valid_date?(date_string) - begin - date_string =~ DATE_REGEX && DateTime.parse(date_string) - rescue ArgumentError - false - end - end - - end - end - end -end - diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index c1f6330c6c..cbeb6c0a28 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -1,32 +1,31 @@ require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/delegation' +require 'multi_json' module ActiveSupport # Look for and parse json strings that look like ISO 8601 times. mattr_accessor :parse_json_times module JSON - # Listed in order of preference. - DECODERS = %w(Yajl Yaml) - class << self - attr_reader :parse_error - delegate :decode, :to => :backend + def decode(json, options ={}) + data = MultiJson.decode(json, options) + if ActiveSupport.parse_json_times + convert_dates_from(data) + else + data + end + end - def backend - set_default_backend unless defined?(@backend) - @backend + def engine + MultiJson.engine end + alias :backend :engine - def backend=(name) - if name.is_a?(Module) - @backend = name - else - require "active_support/json/backends/#{name.to_s.downcase}" - @backend = ActiveSupport::JSON::Backends::const_get(name) - end - @parse_error = @backend::ParseError + def engine=(name) + MultiJson.engine = name end + alias :backend= :engine= def with_backend(name) old_backend, self.backend = backend, name @@ -35,15 +34,30 @@ module ActiveSupport self.backend = old_backend end - def set_default_backend - DECODERS.find do |name| + def parse_error + MultiJson::DecodeError + end + + private + + def convert_dates_from(data) + case data + when nil + nil + when DATE_REGEX begin - self.backend = name - true - rescue LoadError - # Try next decoder. - false + DateTime.parse(data) + rescue ArgumentError + data end + when Array + data.map! { |d| convert_dates_from(d) } + when Hash + data.each do |key, value| + data[key] = convert_dates_from(value) + end + else + data end end end diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index 10675edac5..1c4dd24227 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -81,7 +81,7 @@ module ActiveSupport # Flush all log_subscribers' logger. def flush_all! - flushable_loggers.each(&:flush) + flushable_loggers.each { |log| log.flush } end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index bb865cae91..f0c289a418 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -250,6 +250,11 @@ class StringInflectionsTest < Test::Unit::TestCase # And changes the original string: assert_equal original, expected end + + def test_string_inquiry + assert "production".inquiry.production? + assert !"production".inquiry.development? + end def test_truncate assert_equal "Hello World!", "Hello World!".truncate(12) diff --git a/activesupport/test/file_watcher_test.rb b/activesupport/test/file_watcher_test.rb deleted file mode 100644 index 7b4d4be24f..0000000000 --- a/activesupport/test/file_watcher_test.rb +++ /dev/null @@ -1,233 +0,0 @@ -require 'abstract_unit' -require 'fssm' -require "fileutils" -require "timeout" - - -class FileWatcherTest < ActiveSupport::TestCase - class DumbBackend < ActiveSupport::FileWatcher::Backend - end - - def setup - @watcher = ActiveSupport::FileWatcher.new - - # In real life, the backend would take the path and use it to observe the file - # system. In our case, we will manually trigger the events for unit testing, - # so we can pass any path. - @backend = DumbBackend.new("RAILS_WOOT", @watcher) - - @payload = [] - @watcher.watch %r{^app/assets/.*\.scss$} do |pay| - pay.each do |status, files| - files.sort! - end - @payload << pay - end - end - - def test_use_triple_equals - fw = ActiveSupport::FileWatcher.new - called = [] - fw.watch("some_arbitrary_file.rb") do |file| - called << "omg" - end - fw.trigger(%w{ some_arbitrary_file.rb }) - assert_equal ['omg'], called - end - - def test_one_change - @backend.trigger("app/assets/main.scss" => :changed) - assert_equal({:changed => ["app/assets/main.scss"]}, @payload.first) - end - - def test_multiple_changes - @backend.trigger("app/assets/main.scss" => :changed, "app/assets/javascripts/foo.coffee" => :changed) - assert_equal([{:changed => ["app/assets/main.scss"]}], @payload) - end - - def test_multiple_changes_match - @backend.trigger("app/assets/main.scss" => :changed, "app/assets/print.scss" => :changed, "app/assets/javascripts/foo.coffee" => :changed) - assert_equal([{:changed => ["app/assets/main.scss", "app/assets/print.scss"]}], @payload) - end - - def test_multiple_state_changes - @backend.trigger("app/assets/main.scss" => :created, "app/assets/print.scss" => :changed) - assert_equal([{:changed => ["app/assets/print.scss"], :created => ["app/assets/main.scss"]}], @payload) - end - - def test_more_blocks - payload = [] - @watcher.watch %r{^config/routes\.rb$} do |pay| - payload << pay - end - - @backend.trigger "config/routes.rb" => :changed - assert_equal [:changed => ["config/routes.rb"]], payload - assert_equal [], @payload - end - - def test_overlapping_watchers - payload = [] - @watcher.watch %r{^app/assets/main\.scss$} do |pay| - payload << pay - end - - @backend.trigger "app/assets/print.scss" => :changed, "app/assets/main.scss" => :changed - assert_equal [:changed => ["app/assets/main.scss"]], payload - assert_equal [:changed => ["app/assets/main.scss", "app/assets/print.scss"]], @payload - end -end - -module FSSM::Backends - class Polling - def initialize_with_low_latency(options={}) - initialize_without_low_latency(options.merge(:latency => 0.1)) - end - alias_method_chain :initialize, :low_latency - end -end - -class FSSMFileWatcherTest < ActiveSupport::TestCase - class FSSMBackend < ActiveSupport::FileWatcher::Backend - def initialize(path, watcher) - super - - monitor = FSSM::Monitor.new - monitor.path(path, '**/*') do |p| - p.update { |base, relative| trigger relative => :changed } - p.delete { |base, relative| trigger relative => :deleted } - p.create { |base, relative| trigger relative => :created } - end - - @thread = Thread.new do - monitor.run - end - end - - def stop - @thread.kill - end - end - - def setup - Thread.abort_on_exception = true - - @payload = [] - @triggered = false - - @watcher = ActiveSupport::FileWatcher.new - - @path = path = File.expand_path("../tmp", __FILE__) - FileUtils.rm_rf path - - create "app/assets/main.scss", true - create "app/assets/javascripts/foo.coffee", true - create "app/assets/print.scss", true - create "app/assets/videos.scss", true - - @backend = FSSMBackend.new(path, @watcher) - - @watcher.watch %r{^app/assets/.*\.scss$} do |pay| - pay.each do |status, files| - files.sort! - end - @payload << pay - trigger - end - end - - def teardown - @backend.stop - Thread.abort_on_exception = false - end - - def create(path, past = false) - wait(past) do - path = File.join(@path, path) - FileUtils.mkdir_p(File.dirname(path)) - - FileUtils.touch(path) - File.utime(Time.now - 100, Time.now - 100, path) if past - end - end - - def change(path) - wait do - FileUtils.touch(File.join(@path, path)) - end - end - - def delete(path) - wait do - FileUtils.rm(File.join(@path, path)) - end - end - - def wait(past = false) - yield - return if past - - begin - Timeout.timeout(1) do - sleep 0.05 until @triggered - end - rescue Timeout::Error - end - - @triggered = false - end - - def trigger - @triggered = true - end - - def test_one_change - change "app/assets/main.scss" - assert_equal({:changed => ["app/assets/main.scss"]}, @payload.first) - end - - def test_multiple_changes - change "app/assets/main.scss" - change "app/assets/javascripts/foo.coffee" - assert_equal([{:changed => ["app/assets/main.scss"]}], @payload) - end - - def test_multiple_changes_match - change "app/assets/main.scss" - change "app/assets/print.scss" - change "app/assets/javascripts/foo.coffee" - assert_equal([{:changed => ["app/assets/main.scss"]}, {:changed => ["app/assets/print.scss"]}], @payload) - end - - def test_multiple_state_changes - create "app/assets/new.scss" - change "app/assets/print.scss" - delete "app/assets/videos.scss" - assert_equal([{:created => ["app/assets/new.scss"]}, {:changed => ["app/assets/print.scss"]}, {:deleted => ["app/assets/videos.scss"]}], @payload) - end - - def test_more_blocks - payload = [] - @watcher.watch %r{^config/routes\.rb$} do |pay| - payload << pay - trigger - end - - create "config/routes.rb" - assert_equal [{:created => ["config/routes.rb"]}], payload - assert_equal [], @payload - end - - def test_overlapping_watchers - payload = [] - @watcher.watch %r{^app/assets/main\.scss$} do |pay| - payload << pay - trigger - end - - change "app/assets/main.scss" - change "app/assets/print.scss" - assert_equal [{:changed => ["app/assets/main.scss"]}], payload - assert_equal [{:changed => ["app/assets/main.scss"]}, {:changed => ["app/assets/print.scss"]}], @payload - end -end diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 88cf97de7e..6ccffa59b1 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -56,12 +56,9 @@ class TestJSONDecoding < ActiveSupport::TestCase %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"} } - # load the default JSON backend - ActiveSupport::JSON.backend = 'Yaml' - - backends = %w(Yaml) - backends << "JSONGem" if defined?(::JSON) - backends << "Yajl" if defined?(::Yajl) + backends = [:ok_json] + backends << :json_gem if defined?(::JSON) + backends << :yajl if defined?(::Yajl) backends.each do |backend| TESTS.each do |json, expected| diff --git a/load_paths.rb b/load_paths.rb index 8f37364629..15345e31e3 100644 --- a/load_paths.rb +++ b/load_paths.rb @@ -1,32 +1,9 @@ -begin - require File.expand_path('../.bundle/environment', __FILE__) -rescue LoadError - begin - # bust gem prelude - if defined? Gem - Gem.source_index - gem 'bundler' - else - require 'rubygems' - end - require 'bundler' - Bundler.setup - rescue LoadError - module Bundler - def self.require(*args, &block); end - def self.method_missing(*args, &block); end - end - - %w( - actionmailer - actionpack - activemodel - activerecord - activeresource - activesupport - railties - ).each do |framework| - $:.unshift File.expand_path("../#{framework}/lib", __FILE__) - end - end +# bust gem prelude +if defined? Gem + Gem.source_index + gem 'bundler' +else + require 'rubygems' end +require 'bundler' +Bundler.setup
\ No newline at end of file diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index 496dc7224b..f8b586c151 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -368,7 +368,7 @@ class UsersController < ApplicationController respond_to do |format| format.html # index.html.erb format.xml { render :xml => @users} - format.json { render :json => @users} + format.json { render :json => @users} end end end diff --git a/railties/guides/source/action_view_overview.textile b/railties/guides/source/action_view_overview.textile index d0b3ee6bfc..172932fdab 100644 --- a/railties/guides/source/action_view_overview.textile +++ b/railties/guides/source/action_view_overview.textile @@ -20,7 +20,28 @@ Note: Some features of Action View are tied to Active Record, but that doesn't m h3. Using Action View with Rails -TODO... +For each controller there is an associated directory in the <tt>app/views</tt> directory which holds the template files that make up the views associated with that controller. These files are used to display the view that results from each controller action. + +Let's take a look at what Rails does by default when creating a new resource using the scaffold generator: + +<shell> +$ rails generate scaffold post + [...] + invoke scaffold_controller + create app/controllers/posts_controller.rb + invoke erb + create app/views/posts + create app/views/posts/index.html.erb + create app/views/posts/edit.html.erb + create app/views/posts/show.html.erb + create app/views/posts/new.html.erb + create app/views/posts/_form.html.erb + [...] +</shell> + +There is a naming convention for views in Rails. Typically, the views share their name with the associated controller action, as you can see above. +For example, the index controller action of the <tt>posts_controller.rb</tt> will use the <tt>index.html.erb</tt> view file in the <tt>app/views/posts</tt> directory. +The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Later on this guide you can find a more detailed documentation of each one of this three components. h3. Using Action View outside of Rails @@ -94,9 +115,213 @@ TODO needs a screenshot? I have one - not sure where to put it. h3. Templates, Partials and Layouts -TODO... +As mentioned before, the final HTML output is a composition of three Rails elements: +Templates+, +Partials+ and +Layouts+. +Find below a brief overview of each one of them. + +h4. Templates + +Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> extension then it uses a mixture of ERB (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> extension then a fresh instance of <tt>Builder::XmlMarkup</tt> library is used. + +Rails supports multiple template systems and uses a file extension to distinguish amongst them. For example, an HTML file using the ERB template system will have <tt>.html.erb</tt> as a file extension. + +h5. ERB + +Within an ERB template Ruby code can be included using both +<% %>+ and +<%= %>+ tags. The +<% %>+ are used to execute Ruby code that does not return anything, such as conditions, loops or blocks, and the +<%= %>+ tags are used when you want output. + +Consider the following loop for names: + +<erb> +<b>Names of all the people</b> +<% @people.each do |person| %> + Name: <%= person.name %><br/> +<% end %> +</erb> + +The loop is setup in regular embedding tags +<% %>+ and the name is written using the output embedding tag +<%= %>+. Note that this is not just a usage suggestion, for Regular output functions like print or puts won't work with ERB templates. So this would be wrong: + +<erb> +<%# WRONG %> +Hi, Mr. <% puts "Frodo" %> +</erb> + +To suppress leading and trailing whitespaces, you can use +<%-+ +-%>+ interchangeably with +<%+ and +%>+. + +h5. Builder + +Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension. + +Here are some basic examples: + +<ruby> +xml.em("emphasized") +xml.em { xml.b("emph & bold") } +xml.a("A Link", "href"=>"http://rubyonrails.org") +xml.target("name"=>"compile", "option"=>"fast") +</ruby> + +will produce + +<html> +<em>emphasized</em> +<em><b>emph & bold</b></em> +<a href="http://rubyonrails.org">A link</a> +<target option="fast" name="compile" \> +</html> + +Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: + +<ruby> +xml.div { + xml.h1(@person.name) + xml.p(@person.bio) +} +</ruby> + +would produce something like: + +<html> +<div> + <h1>David Heinemeier Hansson</h1> + <p>A product of Danish Design during the Winter of '79...</p> +</div> +</html> + +A full-length RSS example actually used on Basecamp: + +<ruby> +xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do + xml.channel do + xml.title(@feed_title) + xml.link(@url) + xml.description "Basecamp: Recent items" + xml.language "en-us" + xml.ttl "40" + + for item in @recent_items + xml.item do + xml.title(item_title(item)) + xml.description(item_description(item)) if item_description(item) + xml.pubDate(item_pubDate(item)) + xml.guid(@person.firm.account.url + @recent_items.url(item)) + xml.link(@person.firm.account.url + @recent_items.url(item)) + xml.tag!("dc:creator", item.author_name) if item_has_creator?(item) + end + end + end +end +</ruby> + +h5. Template caching + +By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will check the file's modification time and recompile it in development mode. + +h4. Partials -TODO see http://guides.rubyonrails.org/layouts_and_rendering.html +Partial templates – usually just called "partials" – are another device for breaking the rendering process into more manageable chunks. With a partial, you can move the code for rendering a particular piece of a response to its own file. + +h5. Naming Partials + +To render a partial as part of a view, you use the +render+ method within the view: + +<ruby> +<%= render "menu" %> +</ruby> + +This will render a file named +_menu.html.erb+ at that point within the view is being rendered. Note the leading underscore character: partials are named with a leading underscore to distinguish them from regular views, even though they are referred to without the underscore. This holds true even when you're pulling in a partial from another folder: + +<ruby> +<%= render "shared/menu" %> +</ruby> + +That code will pull in the partial from +app/views/shared/_menu.html.erb+. + +h5. Using Partials to Simplify Views + +One way to use partials is to treat them as the equivalent of subroutines: as a way to move details out of a view so that you can grasp what's going on more easily. For example, you might have a view that looked like this: + +<erb> +<%= render "shared/ad_banner" %> + +<h1>Products</h1> + +<p>Here are a few of our fine products:</p> +<% @products.each do |product| %> + <%= render :partial => "product", :locals => { :product => product } %> +<% end %> + +<%= render "shared/footer" %> +</erb> + +Here, the +_ad_banner.html.erb+ and +_footer.html.erb+ partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page. + +h5. The :as and :object options + +By default <tt>ActionView::Partials::PartialRenderer</tt> has its object in a local variable with the same name as the template. So, given + +<erb> +<%= render :partial => "product" %> +</erb> + +within product we'll get <tt>@product</tt> in the local variable +product+, as if we had written: + +<erb> +<%= render :partial => "product", :locals => { :product => @product } %> +</erb> + +With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we wanted it to be +item+ instead of product+ we'd do: + +<erb> +<%= render :partial => "product", :as => 'item' %> +</erb> + +The <tt>:object</tt> option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere, in a different ivar or in a local variable for instance. + +For example, instead of: + +<erb> +<%= render :partial => "product", :locals => { :product => @item } %> +</erb> + +you'd do: + +<erb> +<%= render :partial => "product", :object => @item %> +</erb> + +The <tt>:object</tt> and <tt>:as</tt> options can be used together. + +h5. Rendering Collections + +The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders a partial by the same name as the elements contained within. +So the three-lined example for rendering all the products can be rewritten with a single line: + +<erb> +<%= render :partial => "product", :collection => @products %> +</erb> + +When a partial is called with a pluralized collection, then the individual instances of the partial have access to the member of the collection being rendered via a variable named after the partial. In this case, the partial is +_product+ , and within the +_product+ partial, you can refer to +product+ to get the instance that is being rendered. + +You can use a shorthand syntax for rendering collections. Assuming @products is a collection of +Product+ instances, you can simply write the following to produce the same result: + +<erb> +<%= render @products %> +</erb> + +Rails determines the name of the partial to use by looking at the model name in the collection. In fact, you can even create a heterogeneous collection and render it this way, and Rails will choose the proper partial for each member of the collection. + +h5. Spacer Templates + +You can also specify a second partial to be rendered between instances of the main partial by using the +:spacer_template+ option: + +<erb> +<%= render @products, :spacer_template => "product_ruler" %> +</erb> + +Rails will render the +_product_ruler+ partial (with no data passed in to it) between each pair of +_product+ partials. + +h4. Layouts + +TODO... h3. Using Templates, Partials and Layouts in "The Rails Way" @@ -262,9 +487,9 @@ Register one or more stylesheet files to be included when symbol is passed to +s ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"] stylesheet_link_tag :monkey # => - <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" /> - <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" /> - <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" /> + <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" /> + <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" /> + <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" /> </ruby> h5. auto_discovery_link_tag @@ -1472,5 +1697,6 @@ You can read more about the Rails Internationalization (I18n) API "here":i18n.ht h3. Changelog +* April 16, 2011: Added 'Using Action View with Rails', 'Templates' and 'Partials' sections. "Sebastian Martinez":http://wyeworks.com * September 3, 2009: Continuing work by Trevor Turk, leveraging the Action Pack docs and "What's new in Edge Rails":http://ryandaigle.com/articles/2007/8/3/what-s-new-in-edge-rails-partials-get-layouts * April 5, 2009: Starting work by Trevor Turk, leveraging Mike Gunderloy's docs diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index df8e35ed33..2f0a51e868 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -418,7 +418,7 @@ SELECT viewable_by, locked FROM clients Be careful because this also means you're initializing a model object with only the fields that you've selected. If you attempt to access a field that is not in the initialized record you'll receive: <shell> -ActiveRecord::MissingAttributeError: missing attribute: <attribute> +ActiveModel::MissingAttributeError: missing attribute: <attribute> </shell> Where +<attribute>+ is the attribute you asked for. The +id+ method will not raise the +ActiveRecord::MissingAttributeError+, so just be careful when working with associations because they need the +id+ method to function properly. @@ -546,7 +546,7 @@ Active Record provides two locking mechanisms: h4. Optimistic Locking -Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened. An +ActiveRecord::StaleObjectError+ exception is thrown if that has occurred and the update is ignored. +Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened. An +ActiveRecord::StaleObjectError+ exception is thrown if that has occurred and the update is ignored. <strong>Optimistic locking column</strong> @@ -993,7 +993,7 @@ Client.count # SELECT count(*) AS count_all FROM clients </ruby> -Or on a relation : +Or on a relation: <ruby> Client.where(:first_name => 'Ryan').count @@ -1060,7 +1060,7 @@ If you want to find the sum of a field for all records in your table you can cal Client.sum("orders_count") </ruby> -For options, please see the parent section, "Calculations":#calculations. +For options, please see the parent section, "Calculations":#calculations. h3. Changelog diff --git a/railties/guides/source/active_record_validations_callbacks.textile b/railties/guides/source/active_record_validations_callbacks.textile index c65dd52e48..19bd4ad0f1 100644 --- a/railties/guides/source/active_record_validations_callbacks.textile +++ b/railties/guides/source/active_record_validations_callbacks.textile @@ -552,6 +552,21 @@ class Account < ActiveRecord::Base end </ruby> +h4. Grouping conditional validations + +Sometimes it is useful to have multiple validations use one condition, it can be easily achieved using +with_options+. + +<ruby> +class User < ActiveRecord::Base + with_options :if => :is_admin? do |admin| + admin.validates_length_of :password, :minimum => 10 + admin.validates_presence_of :email + end +end +</ruby> + +All validations inside of +with_options+ block will have automatically passed the condition +:if => :is_admin?+ + h3. Creating Custom Validation Methods When the built-in validation helpers are not enough for your needs, you can write your own validation methods. @@ -882,8 +897,9 @@ The macro-style class methods can also receive a block. Consider using this styl class User < ActiveRecord::Base validates_presence_of :login, :email - before_create {|user| user.name = user.login.capitalize - if user.name.blank?} + before_create do |user| + user.name = user.login.capitalize if user.name.blank? + end end </ruby> diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 3ba840c044..f89c83e4cd 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -442,9 +442,9 @@ require_library_or_gem('mysql') NOTE: Defined in +active_support/core_ext/kernel/requires.rb+. -h4. +in?+ and +either?+ +h4. +in?+ -The predicate +in?+ tests if an object is included in another object, and the predicate +either?+ tests if an object is included in a list of objects which will be passed as arguments. +The predicate +in?+ tests if an object is included in another object. An +ArgumentError+ exception will be raised if the argument passed does not respond to +include?+. Examples of +in?+: @@ -454,14 +454,6 @@ Examples of +in?+: 25.in?(30..50) # => false </ruby> -Examples of +either?+: - -<ruby> - 1.either?(1,2,3) # => true - 5.either?(1,2,3) # => false - [1,2,3].either?([1,2,3], 2, [3,4,5]) # => true -</ruby> - NOTE: Defined in +active_support/core_ext/object/inclusion.rb+. h3. Extensions to +Module+ @@ -1274,6 +1266,15 @@ WARNING: The option +:separator+ can't be a regexp. NOTE: Defined in +active_support/core_ext/string/filters.rb+. +h4. +inquiry+ + +The <tt>inquiry</tt> method converts a string into a +StringInquirer+ object making equality checks prettier. + +<ruby> +"production".inquiry.production? # => true +"active".inquiry.inactive? # => false +</ruby> + h4. Key-based Interpolation In Ruby 1.9 the <tt>%</tt> string operator supports key-based interpolation, both formatted and unformatted: @@ -2005,6 +2006,11 @@ Similarly, +from+ returns the tail from the element at the passed index on: The methods +second+, +third+, +fourth+, and +fifth+ return the corresponding element (+first+ is built-in). Thanks to social wisdom and positive constructiveness all around, +forty_two+ is also available. +<ruby> +%w(a b c d).third # => c +%w(a b c d).fifth # => nil +</ruby> + NOTE: Defined in +active_support/core_ext/array/access.rb+. h4. Random Access @@ -2100,7 +2106,7 @@ h5. +to_xml+ The method +to_xml+ returns a string containing an XML representation of its receiver: <ruby> -Contributor.all(:limit => 2, :order => 'rank ASC').to_xml +Contributor.limit(2).order(:rank).to_xml # => # <?xml version="1.0" encoding="UTF-8"?> # <contributors type="array"> @@ -2175,7 +2181,7 @@ The name of children nodes is by default the name of the root node singularized. The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can configure your own builder via the <tt>:builder</tt> option. The method also accepts options like <tt>:dasherize</tt> and friends, they are forwarded to the builder: <ruby> -Contributor.all(:limit => 2, :order => 'rank ASC').to_xml(:skip_types => true) +Contributor.limit(2).order(:rank).to_xml(:skip_types => true) # => # <?xml version="1.0" encoding="UTF-8"?> # <contributors> @@ -3415,11 +3421,11 @@ h4. +silence+ Silences every log level lesser to the specified one for the duration of the given block. Log level orders are: debug, info, error and fatal. <ruby> - logger = Logger.new("log/development.log") - logger.silence(Logger::INFO) do - logger.debug("In space, no one can hear you scream.") - logger.info("Scream all you want, small mailman!") - end +logger = Logger.new("log/development.log") +logger.silence(Logger::INFO) do + logger.debug("In space, no one can hear you scream.") + logger.info("Scream all you want, small mailman!") +end </ruby> h4. +datetime_format=+ @@ -3427,17 +3433,17 @@ h4. +datetime_format=+ Modifies the datetime format output by the formatter class associated with this logger. If the formatter class does not have a +datetime_format+ method then this is ignored. <ruby> - class Logger::FormatWithTime < Logger::Formatter - cattr_accessor(:datetime_format) { "%Y%m%d%H%m%S" } +class Logger::FormatWithTime < Logger::Formatter + cattr_accessor(:datetime_format) { "%Y%m%d%H%m%S" } - def self.call(severity, timestamp, progname, msg) - "#{timestamp.strftime(datetime_format)} -- #{String === msg ? msg : msg.inspect}\n" - end + def self.call(severity, timestamp, progname, msg) + "#{timestamp.strftime(datetime_format)} -- #{String === msg ? msg : msg.inspect}\n" end +end - logger = Logger.new("log/development.log") - logger.formatter = Logger::FormatWithTime - logger.info("<- is the current time") +logger = Logger.new("log/development.log") +logger.formatter = Logger::FormatWithTime +logger.info("<- is the current time") </ruby> NOTE: Defined in +active_support/core_ext/logger.rb+. diff --git a/railties/guides/source/ajax_on_rails.textile b/railties/guides/source/ajax_on_rails.textile index 38a63ea483..1566c23414 100644 --- a/railties/guides/source/ajax_on_rails.textile +++ b/railties/guides/source/ajax_on_rails.textile @@ -42,7 +42,7 @@ You are ready to add some AJAX love to your Rails app! h4. The Quintessential AJAX Rails Helper: link_to_remote -Let's start with what is probably the most often used helper: +link_to_remote+. It has an interesting feature from the documentation point of view: the options supplied to +link_to_remote+ are shared by all other AJAX helpers, so learning the mechanics and options of +link_to_remote+ is a great help when using other helpers. +Let's start with what is probably the most often used helper: +link_to_remote+. It has an interesting feature from the documentation point of view: the options supplied to +link_to_remote+ are shared by all other AJAX helpers, so learning the mechanics and options of +link_to_remote+ is a great help when using other helpers. The signature of +link_to_remote+ function is the same as that of the standard +link_to+ helper: @@ -72,7 +72,7 @@ link_to_remote "Add to cart", If the server returns 200, the output of the above example is equivalent to our first, simple one. However, in case of error, the element with the DOM id +error+ is updated rather than the +cart+ element. -** *position* By default (i.e. when not specifying this option, like in the examples before) the repsonse is injected into the element with the specified DOM id, replacing the original content of the element (if there was any). You might want to alter this behavior by keeping the original content - the only question is where to place the new content? This can specified by the +position+ parameter, with four possibilities: +** *position* By default (i.e. when not specifying this option, like in the examples before) the repsonse is injected into the element with the specified DOM id, replacing the original content of the element (if there was any). You might want to alter this behavior by keeping the original content - the only question is where to place the new content? This can specified by the +position+ parameter, with four possibilities: *** +:before+ Inserts the response text just before the target element. More precisely, it creates a text node from the response and inserts it as the left sibling of the target element. *** +:after+ Similar behavior to +:before+, but in this case the response is inserted after the target element. *** +:top+ Inserts the text into the target element, before it's original content. If the target element was empty, this is equivalent with not specifying +:position+ at all. diff --git a/railties/guides/source/caching_with_rails.textile b/railties/guides/source/caching_with_rails.textile index 297ba2d661..799339e674 100644 --- a/railties/guides/source/caching_with_rails.textile +++ b/railties/guides/source/caching_with_rails.textile @@ -25,7 +25,7 @@ h4. Page Caching Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. apache or nginx), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. -So, how do you enable this super-fast cache behavior? Simple, let's say you have a controller called +ProductsController+ and an +index+ action that lists all the products +So, how do you enable this super-fast cache behavior? Simple, let's say you have a controller called +ProductsController+ and an +index+ action that lists all the products <ruby> class ProductsController < ActionController @@ -152,7 +152,7 @@ expire_fragment('all_available_products') h4. Sweepers -Cache sweeping is a mechanism which allows you to get around having a ton of +expire_{page,action,fragment}+ calls in your code. It does this by moving all the work required to expire cached content into an +ActionController::Caching::Sweeper+ subclass. This class is an observer and looks for changes to an object via callbacks, and when a change occurs it expires the caches associated with that object in an around or after filter. +Cache sweeping is a mechanism which allows you to get around having a ton of +expire_{page,action,fragment}+ calls in your code. It does this by moving all the work required to expire cached content into an +ActionController::Caching::Sweeper+ subclass. This class is an observer and looks for changes to an object via callbacks, and when a change occurs it expires the caches associated with that object in an around or after filter. Continuing with our Product controller example, we could rewrite it with a sweeper like this: @@ -230,9 +230,9 @@ class ProductsController < ActionController end </ruby> -The second time the same query is run against the database, it's not actually going to hit the database. The first time the result is returned from the query it is stored in the query cache (in memory) and the second time it's pulled from memory. +The second time the same query is run against the database, it's not actually going to hit the database. The first time the result is returned from the query it is stored in the query cache (in memory) and the second time it's pulled from memory. -However, it's important to note that query caches are created at the start of an action and destroyed at the end of that action and thus persist only for the duration of the action. If you'd like to store query results in a more persistent fashion, you can in Rails by using low level caching. +However, it's important to note that query caches are created at the start of an action and destroyed at the end of that action and thus persist only for the duration of the action. If you'd like to store query results in a more persistent fashion, you can in Rails by using low level caching. h3. Cache Stores @@ -322,7 +322,7 @@ You can use Hashes and Arrays of values as cache keys. <ruby> # This is a legal cache key -Rails.cache.read(:site => "mysite", :owners => [owner_1, owner2]) +Rails.cache.read(:site => "mysite", :owners => [owner_1, owner_2]) </ruby> The keys you use on +Rails.cache+ will not be the same as those actually used with the storage engine. They may be modified with a namespace or altered to fit technology backend constraints. This means, for instance, that you can't save values with +Rails.cache+ and then try to pull them out with the +memcache-client+ gem. However, you also don't need to worry about exceeding the memcached size limit or violating syntax rules. @@ -352,7 +352,7 @@ class ProductsController < ApplicationController # If the request is fresh (i.e. it's not modified) then you don't need to do # anything. The default render checks for this using the parameters # used in the previous call to stale? and will automatically send a - # :not_modified. So that's it, you're done. + # :not_modified. So that's it, you're done. end </ruby> diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile index 9d8a9caf08..ad36c6532e 100644 --- a/railties/guides/source/command_line.textile +++ b/railties/guides/source/command_line.textile @@ -8,10 +8,10 @@ Rails comes with every command line tool you'll need to * Mess with objects through an interactive shell * Profile and benchmark your new creation -NOTE: This tutorial assumes you have basic Rails knowledge from reading the "Getting Started with Rails Guide":getting_started.html. - endprologue. +NOTE: This tutorial assumes you have basic Rails knowledge from reading the "Getting Started with Rails Guide":getting_started.html. + WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in earlier versions of Rails. h3. Command Line Basics @@ -144,10 +144,13 @@ $ rails generate controller Greetings hello create app/helpers/greetings_helper.rb invoke test_unit create test/unit/helpers/greetings_helper_test.rb + invoke assets + create app/assets/javascripts/greetings.js + create app/assets/stylesheets/greetings.css </shell> -What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a functional test file, a helper for the view, and a view file. +What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a javascript file and a stylesheet file. Check out the controller and modify it a little (in +app/controllers/greetings_controller.rb+): diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 53460b8c36..d7069b31fc 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -229,6 +229,8 @@ h4. Configuring Active Record * +config.active_record.lock_optimistically+ controls whether ActiveRecord will use optimistic locking. By default this is +true+. +* +config.active_record.whitelist_attributes+ will create an empty whitelist of attributes available for mass-assignment security for all models in your app. + The MySQL adapter adds one additional configuration option: * +ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans+ controls whether ActiveRecord will consider all +tinyint(1)+ columns in a MySQL database to be booleans. By default this is +true+. diff --git a/railties/guides/source/contributing_to_ruby_on_rails.textile b/railties/guides/source/contributing_to_ruby_on_rails.textile index 1fcc4fd7e3..cbc4acfeca 100644 --- a/railties/guides/source/contributing_to_ruby_on_rails.textile +++ b/railties/guides/source/contributing_to_ruby_on_rails.textile @@ -314,14 +314,20 @@ You should not be the only person who looks at the code before you submit it. Yo You might also want to check out the "RailsBridge BugMash":http://wiki.railsbridge.org/projects/railsbridge/wiki/BugMash as a way to get involved in a group effort to improve Rails. This can help you get started and help check your code when you're writing your first patches. +h4. Create a Lighthouse Ticket + +Now create a ticket for your patch. Go to the "new ticket":http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/new page at Lighthouse. Fill in a reasonable title and description, as well as tag the ticket with the ‘patch’ tag and whatever other subject area tags make sense. Write down your ticket number, for you will need it in the following step. + h4. Commit Your Changes When you're happy with the code on your computer, you need to commit the changes to git: <shell> -$ git commit -a -m "Here is a commit message" +$ git commit -a -m "Here is a commit message [#ticket_number state:committed]" </shell> +NOTE: By adding '[#ticket_number state:committed]' at the end of your commit message, the ticket will automatically change its status to commited once your patch is pushed to the repository. + h4. Update master It’s pretty likely that other changes to master have happened while you were working. Go get them: @@ -361,13 +367,12 @@ $ git apply --check my_new_patch.diff Please make sure the patch does not introduce whitespace errors: <shell> -$ git apply --whitespace=error-all mynew_patch.diff +$ git apply --whitespace=error-all my_new_patch.diff </shell> +h4. Attach your Patch to the Lighthouse Ticket -h4. Create a Lighthouse Ticket - -Now create a ticket with your patch. Go to the "new ticket":http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/new page at Lighthouse. Fill in a reasonable title and description, remember to attach your patch file, and tag the ticket with the ‘patch’ tag and whatever other subject area tags make sense. +Now you need to update the ticket by attaching the patch file you just created. h4. Get Some Feedback @@ -385,6 +390,7 @@ All contributions, either via master or docrails, get credit in "Rails Contribut h3. Changelog +* April 14, 2001: Modified Contributing to the Rails Code section to add '[#ticket_number state:commited]' on patches commit messages by "Sebastian Martinez":http://wyeworks.com * December 28, 2010: Complete revision by "Xavier Noria":credits.html#fxn * April 6, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * August 1, 2009: Updates/amplifications by "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/debugging_rails_applications.textile b/railties/guides/source/debugging_rails_applications.textile index fb17dccfb8..477ff29dbd 100644 --- a/railties/guides/source/debugging_rails_applications.textile +++ b/railties/guides/source/debugging_rails_applications.textile @@ -308,6 +308,41 @@ If you repeat the +list+ command, this time using just +l+, the next ten lines o And so on until the end of the current file. When the end of file is reached, the +list+ command will start again from the beginning of the file and continue again up to the end, treating the file as a circular buffer. +On the other hand, to see the previous ten lines you should type +list-+ (or +l-+) + +<shell> +(rdb:7) l- +[1, 10] in /PathToProject/posts_controller.rb + 1 class PostsController < ApplicationController + 2 # GET /posts + 3 # GET /posts.xml + 4 def index + 5 debugger + 6 @posts = Post.all + 7 + 8 respond_to do |format| + 9 format.html # index.html.erb + 10 format.xml { render :xml => @posts } +</shell> + +This way you can move inside the file, being able to see the code above and over the line you added the +debugger+. +Finally, to see where you are in the code again you can type +list=+ + +<shell> +(rdb:7) list= +[1, 10] in /PathToProject/posts_controller.rb + 1 class PostsController < ApplicationController + 2 # GET /posts + 3 # GET /posts.xml + 4 def index + 5 debugger +=> 6 @posts = Post.all + 7 + 8 respond_to do |format| + 9 format.html # index.html.erb + 10 format.xml { render :xml => @posts } +</shell> + h4. The Context When you start debugging your application, you will be placed in different contexts as you go through the different parts of the stack. diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile index 1f21c27ae6..a63245acec 100644 --- a/railties/guides/source/form_helpers.textile +++ b/railties/guides/source/form_helpers.textile @@ -258,7 +258,7 @@ The name passed to +form_for+ controls the key used in +params+ to access the fo The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. -You can create a similar binding without actually creating +<form>+ tags with the +fields_for+ helper. This is useful for editing additional model objects with the same form. For example if you had a Person model with an associated ContactDetail model you could create a form for creating both like so: +You can create a similar binding without actually creating +<form>+ tags with the +fields_for+ helper. This is useful for editing additional model objects with the same form. For example if you had a Person model with an associated ContactDetail model you could create a form for creating both like so: <erb> <%= form_for @person, :url => { :action => "create" } do |person_form| %> @@ -438,7 +438,7 @@ As with other helpers, if you were to use the +select+ helper on a form builder <%= f.select(:city_id, ...) %> </erb> -WARNING: If you are using +select+ (or similar helpers such as +collection_select+, +select_tag+) to set a +belongs_to+ association you must pass the name of the foreign key (in the example above +city_id+), not the name of association itself. If you specify +city+ instead of +city_id+ Active Record will raise an error along the lines of <tt> ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) </tt> when you pass the +params+ hash to +Person.new+ or +update_attributes+. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. You may wish to consider the use of +attr_protected+ and +attr_accessible+. For further details on this, see the "Ruby On Rails Security Guide":security.html#_mass_assignment. +WARNING: If you are using +select+ (or similar helpers such as +collection_select+, +select_tag+) to set a +belongs_to+ association you must pass the name of the foreign key (in the example above +city_id+), not the name of association itself. If you specify +city+ instead of +city_id+ Active Record will raise an error along the lines of <tt> ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) </tt> when you pass the +params+ hash to +Person.new+ or +update_attributes+. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. You may wish to consider the use of +attr_protected+ and +attr_accessible+. For further details on this, see the "Ruby On Rails Security Guide":security.html#_mass_assignment. h4. Option Tags from a Collection of Arbitrary Objects @@ -513,7 +513,7 @@ The +:prefix+ option is the key used to retrieve the hash of date components fro h4(#select-model-object-helpers). Model Object Helpers +select_date+ does not work well with forms that update or create Active Record objects as Active Record expects each element of the +params+ hash to correspond to one attribute. -The model object helpers for dates and times submit parameters with special names, when Active Record sees parameters with such names it knows they must be combined with the other parameters and given to a constructor appropriate to the column type. For example: +The model object helpers for dates and times submit parameters with special names, when Active Record sees parameters with such names it knows they must be combined with the other parameters and given to a constructor appropriate to the column type. For example: <erb> <%= date_select :person, :birth_date %> diff --git a/railties/guides/source/generators.textile b/railties/guides/source/generators.textile index cd8ac3d6fd..ac709968d9 100644 --- a/railties/guides/source/generators.textile +++ b/railties/guides/source/generators.textile @@ -190,7 +190,7 @@ $ rails generate scaffold User name:string invoke test_unit create test/unit/helpers/users_helper_test.rb invoke stylesheets - create app/assets/stylesheets/scaffold.css.scss + create app/assets/stylesheets/scaffold.css </shell> Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. @@ -383,7 +383,7 @@ if yes?("Would you like to install Devise?") end </ruby> -In the above template we specify that the application relies on the +rspec-rails+ and +cucumber-rails+ gem so these two will be added to the +test+ group in the +Gemfile+. Then we pose a question to the user about whether or not they would like to install Devise. If the user replies "y" or "yes" to this question, then the template will add Devise to the +Gemfile+ outside of any group and then runs the +devise:install+ generator. This template then takes the users input and runs the +devise+ generator, with the user's answer from the last question being passed to this generator. +In the above template we specify that the application relies on the +rspec-rails+ and +cucumber-rails+ gem so these two will be added to the +test+ group in the +Gemfile+. Then we pose a question to the user about whether or not they would like to install Devise. If the user replies "y" or "yes" to this question, then the template will add Devise to the +Gemfile+ outside of any group and then runs the +devise:install+ generator. This template then takes the users input and runs the +devise+ generator, with the user's answer from the last question being passed to this generator. Imagine that this template was in a file called +template.rb+. We can use it to modify the outcome of the +rails new+ command by using the +-m+ option and passing in the filename: diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index 3f9bd2b6da..366df9cd84 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -22,7 +22,7 @@ TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails 3. * The "RubyGems":http://rubyforge.org/frs/?group_id=126 packaging system * A working installation of the "SQLite3 Database":http://www.sqlite.org -Rails is a web application framework running on the Ruby programming language. If you have no prior experience with Ruby, you will find a very steep learning curve diving straight into Rails. There are some good free resources on the internet for learning Ruby, including: +Rails is a web application framework running on the Ruby programming language. If you have no prior experience with Ruby, you will find a very steep learning curve diving straight into Rails. There are some good free resources on the internet for learning Ruby, including: * "Mr. Neighborly's Humble Little Ruby Book":http://www.humblelittlerubybook.com * "Programming Ruby":http://www.ruby-doc.org/docs/ProgrammingRuby/ @@ -78,7 +78,7 @@ Rails ships as many individual components. h5. Action Pack -Action Pack is a single gem that contains Action Controller, Action View and Action Dispatch. The "VC" part of "MVC". +Action Pack is a single gem that contains Action Controller, Action View and Action Dispatch. The "VC" part of "MVC". h5. Action Controller @@ -98,7 +98,7 @@ Action Mailer is a framework for building e-mail services. You can use Action Ma h5. Active Model -Active Model provides a defined interface between the Action Pack gem services and Object Relationship Mapping gems such as Active Record. Active Model allows Rails to utilize other ORM frameworks in place of Active Record if your application needs this. +Active Model provides a defined interface between the Action Pack gem services and Object Relationship Mapping gems such as Active Record. Active Model allows Rails to utilize other ORM frameworks in place of Active Record if your application needs this. h5. Active Record @@ -153,7 +153,7 @@ TIP. If you're working on Windows, you can quickly install Ruby and Rails with " h4. Creating the Blog Application -The best way to use this guide is to follow each step as it happens, no code or step needed to make this example application has been left out, so you can literally follow along step by step. If you need to see the completed code, you can download it from "Getting Started Code":https://github.com/mikel/getting-started-code. +The best way to use this guide is to follow each step as it happens, no code or step needed to make this example application has been left out, so you can literally follow along step by step. If you need to see the completed code, you can download it from "Getting Started Code":https://github.com/mikel/getting-started-code. To begin, open a terminal, navigate to a folder where you have rights to create files, and type: @@ -184,7 +184,7 @@ In any case, Rails will create a folder in your working directory called <tt>blo |doc/|In-depth documentation for your application.| |lib/|Extended modules for your application (not covered in this guide).| |log/|Application log files.| -|public/|The only folder seen to the world as-is. This is where your images, JavaScript files, stylesheets (CSS), and other static files go.| +|public/|The only folder seen to the world as-is. This is where your images, JavaScript files, stylesheets (CSS), and other static files go.| |script/|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.| |test/|Unit tests, fixtures, and other test apparatus. These are covered in "Testing Rails Applications":testing.html| |tmp/|Temporary files| @@ -290,7 +290,7 @@ This will fire up an instance of the WEBrick web server by default (Rails can al TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. In development mode, Rails does not generally require you to stop the server; changes you make in files will be automatically picked up by the server. -The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a page. You can also click on the _About your application’s environment_ link to see a summary of your Application's environment. +The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a page. You can also click on the _About your application’s environment_ link to see a summary of your Application's environment. h4. Say "Hello", Rails @@ -310,7 +310,7 @@ Rails will create several files for you, including +app/views/home/index.html.er h4. Setting the Application Home Page -Now that we have made the controller and view, we need to tell Rails when we want "Hello Rails" to show up. In our case, we want it to show up when we navigate to the root URL of our site, "http://localhost:3000":http://localhost:3000, instead of the "Welcome Aboard" smoke test. +Now that we have made the controller and view, we need to tell Rails when we want "Hello Rails" to show up. In our case, we want it to show up when we navigate to the root URL of our site, "http://localhost:3000":http://localhost:3000, instead of the "Welcome Aboard" smoke test. The first step to doing this is to delete the default page from your application: @@ -394,7 +394,7 @@ class CreatePosts < ActiveRecord::Migration end </ruby> -The above migration creates two methods, +up+, called when you run this migration into the database, and +down+ in case you need to reverse the changes made by this migration at a later date. The +up+ command in this case creates a +posts+ table with two string columns and a text column. It also creates two timestamp fields to track record creation and updating. More information about Rails migrations can be found in the "Rails Database Migrations":migrations.html guide. +The above migration creates two methods, +up+, called when you run this migration into the database, and +down+ in case you need to reverse the changes made by this migration at a later date. The +up+ command in this case creates a +posts+ table with two string columns and a text column. It also creates two timestamp fields to track record creation and updating. More information about Rails migrations can be found in the "Rails Database Migrations":migrations.html guide. At this point, you can use a rake command to run the migration: @@ -548,7 +548,7 @@ This view iterates over the contents of the +@posts+ array to display content an * +link_to+ builds a hyperlink to a particular destination * +edit_post_path+ and +new_post_path+ are helpers that Rails provides as part of RESTful routing. You'll see a variety of these helpers for the different actions that the controller includes. -NOTE. In previous versions of Rails, you had to use +<%=h post.name %>+ so that any HTML would be escaped before being inserted into the page. In Rails 3.0, this is now the default. To get unescaped HTML, you now use +<%= raw post.name %>+. +NOTE. In previous versions of Rails, you had to use +<%=h post.name %>+ so that any HTML would be escaped before being inserted into the page. In Rails 3.0, this is now the default. To get unescaped HTML, you now use +<%= raw post.name %>+. TIP: For more details on the rendering process, see "Layouts and Rendering in Rails":layouts_and_rendering.html. @@ -600,7 +600,7 @@ The +new.html.erb+ view displays this empty Post to the user: <%= link_to 'Back', posts_path %> </erb> -The +<%= render 'form' %>+ line is our first introduction to _partials_ in Rails. A partial is a snippet of HTML and Ruby code that can be reused in multiple locations. In this case, the form used to make a new post, is basically identical to a form used to edit a post, both have text fields for the name and title and a text area for the content with a button to make a new post or update the existing post. +The +<%= render 'form' %>+ line is our first introduction to _partials_ in Rails. A partial is a snippet of HTML and Ruby code that can be reused in multiple locations. In this case, the form used to make a new post, is basically identical to a form used to edit a post, both have text fields for the name and title and a text area for the content with a button to make a new post or update the existing post. If you take a look at +views/posts/_form.html.erb+ file, you will see the following: @@ -873,7 +873,7 @@ TIP: For more information on Active Record associations, see the "Active Record h4. Adding a Route for Comments -As with the +home+ controller, we will need to add a route so that Rails knows where we would like to navigate to see +comments+. Open up the +config/routes.rb+ file again, you will see an entry that was added automatically for +posts+ near the top by the scaffold generator, +resources :posts+, edit it as follows: +As with the +home+ controller, we will need to add a route so that Rails knows where we would like to navigate to see +comments+. Open up the +config/routes.rb+ file again, you will see an entry that was added automatically for +posts+ near the top by the scaffold generator, +resources :posts+, edit it as follows: <ruby> resources :posts do @@ -901,7 +901,7 @@ This creates four files and one empty directory: * +test/unit/helpers/comments_helper_test.rb+ - The unit tests for the helper * +app/views/comments/+ - Views of the controller are stored here -Like with any blog, our readers will create their comments directly after reading the post, and once they have added their comment, will be sent back to the post show page to see their comment now listed. Due to this, our +CommentsController+ is there to provide a method to create comments and delete SPAM comments when they arrive. +Like with any blog, our readers will create their comments directly after reading the post, and once they have added their comment, will be sent back to the post show page to see their comment now listed. Due to this, our +CommentsController+ is there to provide a method to create comments and delete SPAM comments when they arrive. So first, we'll wire up the Post show template (+/app/views/posts/show.html.erb+) to let us make a new comment: @@ -1078,7 +1078,7 @@ Then in the +app/views/posts/show.html.erb+ you can change it to look like the f <%= link_to 'Back to Posts', posts_path %> | </erb> -This will now render the partial in +app/views/comments/_comment.html.erb+ once for each comment that is in the +@post.comments+ collection. As the +render+ method iterates over the <tt>@post.comments</tt> collection, it assigns each comment to a local variable named the same as the partial, in this case +comment+ which is then available in the partial for us to show. +This will now render the partial in +app/views/comments/_comment.html.erb+ once for each comment that is in the +@post.comments+ collection. As the +render+ method iterates over the <tt>@post.comments</tt> collection, it assigns each comment to a local variable named the same as the partial, in this case +comment+ which is then available in the partial for us to show. h4. Rendering a Partial Form @@ -1138,7 +1138,7 @@ The +@post+ object is available to any partials rendered in the view because we h3. Deleting Comments -Another important feature on a blog is being able to delete SPAM comments. To do this, we need to implement a link of some sort in the view and a +DELETE+ action in the +CommentsController+. +Another important feature on a blog is being able to delete SPAM comments. To do this, we need to implement a link of some sort in the view and a +DELETE+ action in the +CommentsController+. So first, let's add the delete link in the +app/views/comments/_comment.html.erb+ partial: @@ -1312,7 +1312,7 @@ Note that we have changed the +f+ in +form_for(@post) do |f|+ to +post_form+ to This example shows another option of the render helper, being able to pass in local variables, in this case, we want the local variable +form+ in the partial to refer to the +post_form+ object. -We also add a <tt>@post.tags.build</tt> at the top of this form, this is to make sure there is a new tag ready to have it's name filled in by the user. If you do not build the new tag, then the form will not appear as there is no new Tag object ready to create. +We also add a <tt>@post.tags.build</tt> at the top of this form, this is to make sure there is a new tag ready to have it's name filled in by the user. If you do not build the new tag, then the form will not appear as there is no new Tag object ready to create. Now create the folder <tt>app/views/tags</tt> and make a file in there called <tt>_form.html.erb</tt> which contains the form for the tag: @@ -1373,7 +1373,7 @@ However, that method call <tt>@post.tags.map { |t| t.name }.join(", ")</tt> is a h3. View Helpers -View Helpers live in <tt>app/helpers</tt> and provide small snippets of reusable code for views. In our case, we want a method that strings a bunch of objects together using their name attribute and joining them with a comma. As this is for the Post show template, we put it in the PostsHelper. +View Helpers live in <tt>app/helpers</tt> and provide small snippets of reusable code for views. In our case, we want a method that strings a bunch of objects together using their name attribute and joining them with a comma. As this is for the Post show template, we put it in the PostsHelper. Open up <tt>app/helpers/posts_helper.rb</tt> and add the following: diff --git a/railties/guides/source/i18n.textile b/railties/guides/source/i18n.textile index 3e7e396e8d..608643b3d3 100644 --- a/railties/guides/source/i18n.textile +++ b/railties/guides/source/i18n.textile @@ -91,7 +91,7 @@ This means, that in the +:en+ locale, the key _hello_ will map to the _Hello wor The I18n library will use *English* as a *default locale*, i.e. if you don't set a different locale, +:en+ will be used for looking up translations. -NOTE: The i18n library takes a *pragmatic approach* to locale keys (after "some discussion":http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like +:en+, +:pl+, not the _region_ part, like +:en-US+ or +:en-UK+, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as +:cz+, +:th+ or +:es+ (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the +:en-US+ locale you would have $ as a currency symbol, while in +:en-UK+, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a +:en-UK+ dictionary. Various "Rails I18n plugins":http://rails-i18n.org/wiki such as "Globalize2":https://github.com/joshmh/globalize2/tree/master may help you implement it. +NOTE: The i18n library takes a *pragmatic approach* to locale keys (after "some discussion":http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like +:en+, +:pl+, not the _region_ part, like +:en-US+ or +:en-UK+, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as +:cz+, +:th+ or +:es+ (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the +:en-US+ locale you would have $ as a currency symbol, while in +:en-UK+, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a +:en-UK+ dictionary. Various "Rails I18n plugins":http://rails-i18n.org/wiki such as "Globalize2":https://github.com/joshmh/globalize2/tree/master may help you implement it. The *translations load path* (+I18n.load_path+) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. @@ -133,9 +133,9 @@ The _setting part_ is easy. You can set the locale in a +before_filter+ in the + <ruby> before_filter :set_locale + def set_locale - # if params[:locale] is nil then I18n.default_locale will be used - I18n.locale = params[:locale] + I18n.locale = params[:locale] || I18n.default_locale end </ruby> @@ -158,7 +158,7 @@ You can implement it like this in your +ApplicationController+: before_filter :set_locale def set_locale - I18n.locale = extract_locale_from_tld + I18n.locale = extract_locale_from_tld || I18n.default_locale end # Get locale from top-level domain or return nil if such locale is not available @@ -182,7 +182,7 @@ We can also set the locale from the _subdomain_ in a very similar way: # in your /etc/hosts file to try this out locally def extract_locale_from_subdomain parsed_locale = request.subdomains.first - I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil + I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil end </ruby> @@ -441,7 +441,7 @@ Do check the "Rails i18n Wiki":http://rails-i18n.org/wiki for list of tools avai h3. Overview of the I18n API Features -You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. +You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. Covered are features like these: @@ -520,7 +520,7 @@ I18n.t 'activerecord.errors.messages' h5. "Lazy" Lookup -Rails 2.3 implements a convenient way to look up the locale inside _views_. When you have the following dictionary: +Rails implements a convenient way to look up the locale inside _views_. When you have the following dictionary: <yaml> es: @@ -863,7 +863,7 @@ If you find anything missing or wrong in this guide, please file a ticket on our h3. Contributing to Rails I18n -I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first, and only then cherry-picking the best-of-bread of most widely useful features for inclusion in the core. +I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core. Thus we encourage everybody to experiment with new ideas and features in plugins or other libraries and make them available to the community. (Don't forget to announce your work on our "mailing list":http://groups.google.com/group/rails-i18n!) diff --git a/railties/guides/source/index.html.erb b/railties/guides/source/index.html.erb index af46beee56..b48488d8a2 100644 --- a/railties/guides/source/index.html.erb +++ b/railties/guides/source/index.html.erb @@ -17,7 +17,7 @@ Ruby on Rails Guides <% else %> <p> These are the new guides for Rails 3. The guides for Rails 2.3 are still available - at <a href="http://guides.rubyonrails.org/v2.3.8/">http://guides.rubyonrails.org/v2.3.8/</a>. + at <a href="http://guides.rubyonrails.org/v2.3.11/">http://guides.rubyonrails.org/v2.3.11/</a>. </p> <% end %> <p> diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 7c01f01b24..638830cd83 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -468,7 +468,7 @@ h4. +config/application.rb+ This file requires +config/boot.rb+, but only if it hasn't been required before, which would be the case in +rails server+ but *wouldn't* be the case with Passenger. -Then the fun begins! +Then the fun begins! h3. Loading Rails @@ -592,7 +592,11 @@ This file defines the behavior of the +ActiveSupport::Deprecation+ module, setti h4. +activesupport/lib/active_support/notifications.rb+ -TODO: document +ActiveSupport::Notifications+. +This file defines the +ActiveSupport::Notifications+ module. Notifications provides an instrumentation API for Ruby, shipping with a queue implementation that consumes and publish events to log subscribers in a thread. + +The "API documentation":http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html for +ActiveSupport::Notifications+ explains the usage of this module, including the methods that it defines. + +The file required in +active_support/notifications.rb+ is +active_support/core_ext/module/delegation+ which is documented in the "Active Support Core Extensions Guide":http://guides.rubyonrails.org/active_support_core_extensions.html#method-delegation. h4. +activesupport/core_ext/array/wrap+ @@ -660,7 +664,7 @@ The +active_support/inflector/methods+ file has already been required by +active h4. +activesupport/lib/active_support/inflector/inflections.rb+ -This file references the +ActiveSupport::Inflector+ constant which isn't loaded by this point. But there were autoloads set up in +activesupport/lib/active_support.rb+ which will load the file which loads this constant and so then it will be defined. Then this file defines pluralization and singularization rules for words in Rails. This is how Rails knows how to pluralize "tomato" to "tomatoes". +This file references the +ActiveSupport::Inflector+ constant which isn't loaded by this point. But there were autoloads set up in +activesupport/lib/active_support.rb+ which will load the file which loads this constant and so then it will be defined. Then this file defines pluralization and singularization rules for words in Rails. This is how Rails knows how to pluralize "tomato" to "tomatoes". h4. +activesupport/lib/active_support/inflector/transliterate.rb+ @@ -694,7 +698,7 @@ h4. Back to +railties/lib/rails/plugin.rb+ The next file required in this is a core extension from Active Support called +array/conversions+ which is covered in "this section":http://guides.rubyonrails.org/active_support_core_extensions.html#array-conversions of the Active Support Core Extensions Guide. -Once that file has finished loading, the +Rails::Plugin+ class is defined. +Once that file has finished loading, the +Rails::Plugin+ class is defined. h4. Back to +railties/lib/rails/application.rb+ @@ -704,7 +708,7 @@ Once this file's done then we go back to the +railties/lib/rails.rb+ file, which h4. +railties/lib/rails/version.rb+ -Much like +active_support/version+, this file defines the +VERSION+ constant which has a +STRING+ constant on it which returns the current version of Rails. +Much like +active_support/version+, this file defines the +VERSION+ constant which has a +STRING+ constant on it which returns the current version of Rails. Once this file has finished loading we go back to +railties/lib/rails.rb+ which then requires +active_support/railtie.rb+. @@ -922,7 +926,7 @@ This file defines the +ActionDispatch::Railtie+ class, but not before requiring h4. +activesupport/lib/action_dispatch.rb+ -This file attempts to locate the +active_support+ and +active_model+ libraries by looking a couple of directories back from the current file and then adds the +active_support+ and +active_model+ +lib+ directories to the load path, but only if they aren't already, which they are. +This file attempts to locate the +active_support+ and +active_model+ libraries by looking a couple of directories back from the current file and then adds the +active_support+ and +active_model+ +lib+ directories to the load path, but only if they aren't already, which they are. <ruby> activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) @@ -1005,7 +1009,7 @@ The loading of this file finishes the loading of +active_model+ and so we go bac h4. Back to +activesupport/lib/action_dispatch.rb+ -The remainder of this file requires the +rack+ file from the Rack gem which defines the +Rack+ module. After +rack+, there's autoloads defined for the +Rack+, +ActionDispatch+, +ActionDispatch::Http+, +ActionDispatch::Session+. A new method called +autoload_under+ is used here, and this simply prefixes the files where the modules are autoloaded from with the path specified. For example here: +The remainder of this file requires the +rack+ file from the Rack gem which defines the +Rack+ module. After +rack+, there's autoloads defined for the +Rack+, +ActionDispatch+, +ActionDispatch::Http+, +ActionDispatch::Session+. A new method called +autoload_under+ is used here, and this simply prefixes the files where the modules are autoloaded from with the path specified. For example here: <ruby> autoload_under 'testing' do @@ -1108,4 +1112,4 @@ However the require after these is to a file that hasn't yet been loaded, +actio h4. +actionpack/lib/action_view.rb+ -+action_view.rb+ ++action_view.rb+ diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile index 8dab578e6b..620df970dc 100644 --- a/railties/guides/source/layouts_and_rendering.textile +++ b/railties/guides/source/layouts_and_rendering.textile @@ -394,7 +394,7 @@ end Now, if the current user is a special user, they'll get a special layout when viewing a product. You can even use an inline method to determine the layout: -You can also decide the layout by passing a Proc object, the block you give the Proc will be given the +controller+ instance, so you can make decisions based on the current request. For example: +You can also decide the layout by passing a Proc object, the block you give the Proc will be given the +controller+ instance, so you can make decisions based on the current request. For example: <ruby> class ProductsController < ApplicationController @@ -572,7 +572,7 @@ With this code, the browser will make a fresh request for the index page, the co The only downside to this code, is that it requires a round trip to the browser, the browser requested the show action with +/books/1+ and the controller finds that there are no books, so the controller sends out a 302 redirect response to the browser telling it to go to +/books/+, the browser complies and sends a new request back to the controller asking now for the +index+ action, the controller then gets all the books in the database and renders the index template, sending it back down to the browser which then shows it on your screen. -While in a small app, this added latency might not be a problem, it is something to think about when speed of response is of the essence. One way to handle this double request (though a contrived example) could be: +While in a small app, this added latency might not be a problem, it is something to think about when speed of response is of the essence. One way to handle this double request (though a contrived example) could be: <ruby> def index @@ -860,7 +860,7 @@ Produces <video src="/videos/movie.ogg" /> </erb> -Like an +image_tag+ you can supply a path, either absolute, or relative to the +public/videos+ directory. Additionally you can specify the +:size => "#{width}x#{height}"+ option just like an +image_tag+. Video tags can also have any of the HTML options specified at the end (+id+, +class+ et al). +Like an +image_tag+ you can supply a path, either absolute, or relative to the +public/videos+ directory. Additionally you can specify the +:size => "#{width}x#{height}"+ option just like an +image_tag+. Video tags can also have any of the HTML options specified at the end (+id+, +class+ et al). The video tag also supports all of the +<video>+ HTML options through the HTML options hash, including: @@ -1133,7 +1133,7 @@ You can also pass in arbitrary local variables to any partial you are rendering Would render a partial +_products.html.erb+ once for each instance of +product+ in the +@products+ instance variable passing the instance to the partial as a local variable called +item+ and to each partial, make the local variable +title+ available with the value +Products Page+. -TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by +_counter+. For example, if you're rendering +@products+, within the partial you can refer to +product_counter+ to tell you how many times the partial has been rendered. This does not work in conjunction with the +:as => :value+ option. +TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by +_counter+. For example, if you're rendering +@products+, within the partial you can refer to +product_counter+ to tell you how many times the partial has been rendered. This does not work in conjunction with the +:as => :value+ option. You can also specify a second partial to be rendered between instances of the main partial by using the +:spacer_template+ option: diff --git a/railties/guides/source/migrations.textile b/railties/guides/source/migrations.textile index 57e6bcd123..bf5d4d3e4d 100644 --- a/railties/guides/source/migrations.textile +++ b/railties/guides/source/migrations.textile @@ -284,7 +284,7 @@ change_table :products do |t| t.rename :upccode, :upc_code end </ruby> -removes the +description+ and +name+ columns, creates a +part_number+ column and adds an index on it. Finally it renames the +upccode+ column. This is the same as doing +removes the +description+ and +name+ columns, creates a +part_number+ column and adds an index on it. Finally it renames the +upccode+ column. This is the same as doing <ruby> remove_column :products, :description @@ -335,7 +335,7 @@ NOTE: The +references+ helper does not actually create foreign key constraints f If the helpers provided by Active Record aren't enough you can use the +execute+ function to execute arbitrary SQL. -For more details and examples of individual methods check the API documentation, in particular the documentation for "<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html (which provides the methods available in the +up+ and +down+ methods), "<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html (which provides the methods available on the object yielded by +create_table+) and "<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html (which provides the methods available on the object yielded by +change_table+). +For more details and examples of individual methods check the API documentation, in particular the documentation for "<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html (which provides the methods available in the +up+ and +down+ methods), "<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html (which provides the methods available on the object yielded by +create_table+) and "<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html (which provides the methods available on the object yielded by +change_table+). h4. Writing Your +down+ Method diff --git a/railties/guides/source/plugins.textile b/railties/guides/source/plugins.textile index 2d9821e627..d486e8ade3 100644 --- a/railties/guides/source/plugins.textile +++ b/railties/guides/source/plugins.textile @@ -17,8 +17,8 @@ This guide describes how to build a test-driven plugin that will: * Add methods to ActiveRecord::Base in the tradition of the 'acts_as' plugins * Give you information about where to put generators in your plugin. -For the purpose of this guide pretend for a moment that you are an avid bird watcher. -Your favorite bird is the Yaffle, and you want to create a plugin that allows other developers to share in the Yaffle +For the purpose of this guide pretend for a moment that you are an avid bird watcher. +Your favorite bird is the Yaffle, and you want to create a plugin that allows other developers to share in the Yaffle goodness. endprologue. @@ -27,21 +27,21 @@ h3. Setup h4. Generating the Plugin Skeleton -Rails currently ships with a generator to generate a plugin within a Rails application. Help text is available that will explain +Rails currently ships with a generator to generate a plugin within a Rails application. Help text is available that will explain how this generator works. <shell> $ rails generate plugin --help </shell> -This generator places the plugin into the vendor/plugins directory. +This generator places the plugin into the vendor/plugins directory. -Vendored plugins are useful for quickly prototyping your plugin but current thinking in the Rails community is shifting towards +Vendored plugins are useful for quickly prototyping your plugin but current thinking in the Rails community is shifting towards packaging plugins as gems, especially with the inclusion of Bundler as the Rails dependency manager. Packaging a plugin as a gem may be overkill for any plugins that will not be shared across projects but doing so from the start makes it easier to share the plugin going forward without adding too much additional overhead during development. Rails 3.1 will ship with a plugin generator that will default to setting up a plugin -as a gem. This tutorial will begin to bridge that gap by demonstrating how to create a gem based plugin using the +as a gem. This tutorial will begin to bridge that gap by demonstrating how to create a gem based plugin using the "Enginex gem":http://www.github.com/josevalim/enginex. <shell> @@ -69,7 +69,7 @@ h3. Extending Core Classes This section will explain how to add a method to String that will be available anywhere in your rails application. -In this example you will add a method to String named +to_squawk+. To begin, create a new test file with a few assertions: +In this example you will add a method to String named +to_squawk+. To begin, create a new test file with a few assertions: <ruby> # yaffle/test/core_ext_test.rb @@ -83,7 +83,7 @@ class CoreExtTest < Test::Unit::TestCase end </ruby> -Run +rake+ to run the test. This test should fail because we haven't implemented the +to_squak+ method: +Run +rake+ to run the test. This test should fail because we haven't implemented the +to_squak+ method: <shell> 1) Error: @@ -133,7 +133,7 @@ $ rails console h3. Add an "acts_as" Method to Active Record -A common pattern in plugins is to add a method called 'acts_as_something' to models. In this case, you +A common pattern in plugins is to add a method called 'acts_as_something' to models. In this case, you want to write a method called 'acts_as_yaffle' that adds a 'squawk' method to your Active Record models. To begin, set up your files so that you have: @@ -169,9 +169,9 @@ end h4. Add a Class Method -This plugin will expect that you've added a method to your model named 'last_squawk'. However, the -plugin users might have already defined a method on their model named 'last_squawk' that they use -for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'. +This plugin will expect that you've added a method to your model named 'last_squawk'. However, the +plugin users might have already defined a method on their model named 'last_squawk' that they use +for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'. To start out, write a failing test that shows the behavior you'd like: @@ -210,7 +210,7 @@ When you run +rake+, you should see the following: </shell> This tells us that we don't have the necessary models (Hickwall and Wickwall) that we are trying to test. -We can easily generate these models in our "dummy" Rails application by running the following commands from the +We can easily generate these models in our "dummy" Rails application by running the following commands from the test/dummy directory: <shell> @@ -220,7 +220,7 @@ $ rails generate model Wickwall last_squak:string last_tweet:string </shell> Now you can create the necessary database tables in your testing database by navigating to your dummy app -and migrating the database. First +and migrating the database. First <shell> $ cd test/dummy @@ -319,7 +319,7 @@ When you run +rake+ you should see the tests all pass: h4. Add an Instance Method -This plugin will add a method named 'squawk' to any Active Record objects that call 'acts_as_yaffle'. The 'squawk' +This plugin will add a method named 'squawk' to any Active Record objects that call 'acts_as_yaffle'. The 'squawk' method will simply set the value of one of the fields in the database. To start out, write a failing test that shows the behavior you'd like: @@ -352,7 +352,7 @@ class ActsAsYaffleTest < Test::Unit::TestCase end </ruby> -Run the test to make sure the last two tests fail the an error that contains "NoMethodError: undefined method `squawk'", +Run the test to make sure the last two tests fail the an error that contains "NoMethodError: undefined method `squawk'", then update 'acts_as_yaffle.rb' to look like this: <ruby> @@ -387,18 +387,18 @@ Run +rake+ one final time and you should see: 7 tests, 7 assertions, 0 failures, 0 errors, 0 skips </shell> -NOTE: The use of +write_attribute+ to write to the field in model is just one example of how a plugin can -interact with the model, and will not always be the right method to use. For example, you could also +NOTE: The use of +write_attribute+ to write to the field in model is just one example of how a plugin can +interact with the model, and will not always be the right method to use. For example, you could also use +send("#{self.class.yaffle_text_field}=", string.to_squawk)+. h3. Generators -Generators can be included in your gem simply by creating them in a lib/generators directory of your plugin. More information about +Generators can be included in your gem simply by creating them in a lib/generators directory of your plugin. More information about the creation of generators can be found in the "Generators Guide":generators.html h3. Publishing your Gem -Gem plugins in progress can be easily be shared from any Git repository. To share the Yaffle gem with others, simply +Gem plugins in progress can be easily be shared from any Git repository. To share the Yaffle gem with others, simply commit the code to a Git repository (like Github) and add a line to the Gemfile of the any application: <ruby> @@ -412,7 +412,7 @@ For more information about publishing gems to RubyGems, see: "http://blog.thepet h3. Non-Gem Plugins -Non-gem plugins are useful for functionality that won't be shared with another project. Keeping your custom functionality in the +Non-gem plugins are useful for functionality that won't be shared with another project. Keeping your custom functionality in the vendor/plugins directory un-clutters the rest of the application. Move the directory that you created for the gem based plugin into the vendor/plugins directory of a generated Rails application, create a vendor/plugins/yaffle/init.rb file that contains "require 'yaffle'" and everything will still work. @@ -423,7 +423,7 @@ Move the directory that you created for the gem based plugin into the vendor/plu require 'yaffle' </ruby> -You can test this by changing to the Rails application that you added the plugin to and starting a rails console. Once in the +You can test this by changing to the Rails application that you added the plugin to and starting a rails console. Once in the console we can check to see if the String has an instance method of to_squawk. <shell> $ cd my_app @@ -435,7 +435,7 @@ You can also remove the .gemspec, Gemfile and Gemfile.lock files as they will no h3. RDoc Documentation -Once your plugin is stable and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy. +Once your plugin is stable and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy. The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are: diff --git a/railties/guides/source/rails_application_templates.textile b/railties/guides/source/rails_application_templates.textile index 8e51f9e23b..388d8eea3e 100644 --- a/railties/guides/source/rails_application_templates.textile +++ b/railties/guides/source/rails_application_templates.textile @@ -11,19 +11,19 @@ endprologue. h3. Usage -To apply a template, you need to provide the Rails generator with the location of the template you wish to apply, using -m option : +To apply a template, you need to provide the Rails generator with the location of the template you wish to apply, using -m option: <shell> $ rails new blog -m ~/template.rb </shell> -It's also possible to apply a template using a URL : +It's also possible to apply a template using a URL: <shell> $ rails new blog -m https://gist.github.com/755496.txt </shell> -Alternatively, you can use the rake task +rails:template+ to apply a template to an existing Rails application : +Alternatively, you can use the rake task +rails:template+ to apply a template to an existing Rails application: <shell> $ rake rails:template LOCATION=~/template.rb @@ -31,7 +31,7 @@ $ rake rails:template LOCATION=~/template.rb h3. Template API -Rails templates API is very self explanatory and easy to understand. Here's an example of a typical Rails template : +Rails templates API is very self explanatory and easy to understand. Here's an example of a typical Rails template: <ruby> # template.rb @@ -45,20 +45,20 @@ git :add => "." git :commit => "-a -m 'Initial commit'" </ruby> -The following sections outlines the primary methods provided by the API : +The following sections outlines the primary methods provided by the API: h4. gem(name, options = {}) Adds a +gem+ entry for the supplied gem to the generated application’s +Gemfile+. -For example, if your application depends on the gems +bj+ and +nokogiri+ : +For example, if your application depends on the gems +bj+ and +nokogiri+: <ruby> gem "bj" gem "nokogiri" </ruby> -Please note that this will NOT install the gems for you. So you may want to run the +rake gems:install+ task too : +Please note that this will NOT install the gems for you. So you may want to run the +rake gems:install+ task too: <ruby> rake "gems:install" @@ -80,13 +80,13 @@ h4. plugin(name, options = {}) Installs a plugin to the generated application. -Plugin can be installed from Git : +Plugin can be installed from Git: <ruby> plugin 'authentication', :git => 'git://github.com/foor/bar.git' </ruby> -You can even install plugins as git submodules : +You can even install plugins as git submodules: <ruby> plugin 'authentication', :git => 'git://github.com/foor/bar.git', @@ -95,7 +95,7 @@ plugin 'authentication', :git => 'git://github.com/foor/bar.git', Please note that you need to +git :init+ before you can install a plugin as a submodule. -Or use plain old SVN : +Or use plain old SVN: <ruby> plugin 'usingsvn', :svn => 'svn://example.com/usingsvn/trunk' @@ -105,7 +105,7 @@ h4. vendor/lib/file/initializer(filename, data = nil, &block) Adds an initializer to the generated application’s +config/initializers+ directory. -Lets say you like using +Object#not_nil?+ and +Object#not_blank?+ : +Lets say you like using +Object#not_nil?+ and +Object#not_blank?+: <ruby> initializer 'bloatlol.rb', <<-CODE @@ -123,7 +123,7 @@ CODE Similarly +lib()+ creates a file in the +lib/+ directory and +vendor()+ creates a file in the +vendor/+ directory. -There is even +file()+, which accepts a relative path from +Rails.root+ and creates all the directories/file needed : +There is even +file()+, which accepts a relative path from +Rails.root+ and creates all the directories/file needed: <ruby> file 'app/components/foo.rb', <<-CODE @@ -136,7 +136,7 @@ That’ll create +app/components+ directory and put +foo.rb+ in there. h4. rakefile(filename, data = nil, &block) -Creates a new rake file under +lib/tasks+ with the supplied tasks : +Creates a new rake file under +lib/tasks+ with the supplied tasks: <ruby> rakefile("bootstrap.rake") do @@ -154,7 +154,7 @@ The above creates +lib/tasks/bootstrap.rake+ with a +boot:strap+ rake task. h4. generate(what, args) -Runs the supplied rails generator with given arguments. For example, I love to scaffold some whenever I’m playing with Rails : +Runs the supplied rails generator with given arguments. For example, I love to scaffold some whenever I’m playing with Rails: <ruby> generate(:scaffold, "person", "name:string", "address:text", "age:number") @@ -162,7 +162,7 @@ generate(:scaffold, "person", "name:string", "address:text", "age:number") h4. run(command) -Executes an arbitrary command. Just like the backticks. Let's say you want to remove the +public/index.html+ file : +Executes an arbitrary command. Just like the backticks. Let's say you want to remove the +public/index.html+ file: <ruby> run "rm public/index.html" @@ -170,19 +170,19 @@ run "rm public/index.html" h4. rake(command, options = {}) -Runs the supplied rake tasks in the Rails application. Let's say you want to migrate the database : +Runs the supplied rake tasks in the Rails application. Let's say you want to migrate the database: <ruby> rake "db:migrate" </ruby> -You can also run rake tasks with a different Rails environment : +You can also run rake tasks with a different Rails environment: <ruby> rake "db:migrate", :env => 'production' </ruby> -Or even use sudo : +Or even use sudo: <ruby> rake "gems:install", :sudo => true @@ -190,7 +190,7 @@ rake "gems:install", :sudo => true h4. route(routing_code) -This adds a routing entry to the +config/routes.rb+ file. In above steps, we generated a person scaffold and also removed +public/index.html+. Now to make +PeopleController#index+ as the default page for the application : +This adds a routing entry to the +config/routes.rb+ file. In above steps, we generated a person scaffold and also removed +public/index.html+. Now to make +PeopleController#index+ as the default page for the application: <ruby> route "root :to => 'person#index'" @@ -208,7 +208,7 @@ end h4. ask(question) -+ask()+ gives you a chance to get some feedback from the user and use it in your templates. Lets say you want your user to name the new shiny library you’re adding : ++ask()+ gives you a chance to get some feedback from the user and use it in your templates. Lets say you want your user to name the new shiny library you’re adding: <ruby> lib_name = ask("What do you want to call the shiny library ?") @@ -222,7 +222,7 @@ CODE h4. yes?(question) or no?(question) -These methods let you ask questions from templates and decide the flow based on the user’s answer. Lets say you want to freeze rails only if the user want to : +These methods let you ask questions from templates and decide the flow based on the user’s answer. Lets say you want to freeze rails only if the user want to: <ruby> rake("rails:freeze:gems") if yes?("Freeze rails gems ?") @@ -231,7 +231,7 @@ no?(question) acts just the opposite. h4. git(:must => "-a love") -Rails templates let you run any git command : +Rails templates let you run any git command: <ruby> git :init diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 58b75b9a1d..43c08165dc 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -321,7 +321,7 @@ resources :photos do end </ruby> -This will recognize +/photos/1/preview+ with GET, and route to the +preview+ action of +PhotosController+. It will also create the +preview_photo_url+ and +preview_photo_path+ helpers. +This will recognize +/photos/1/preview+ with GET, and route to the +preview+ action of +PhotosController+. It will also create the +preview_photo_url+ and +preview_photo_path+ helpers. Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use +get+, +put+, +post+, or +delete+ here. If you don't have multiple +member+ routes, you can also pass +:on+ to a route, eliminating the block: @@ -421,7 +421,7 @@ You do not need to explicitly use the +:controller+ and +:action+ symbols within match 'photos/:id' => 'photos#show' </ruby> -With this route, Rails will match an incoming path of +/photos/12+ to the +show+ action of +PhotosController+. +With this route, Rails will match an incoming path of +/photos/12+ to the +show+ action of +PhotosController+. You can also define other defaults in a route by supplying a hash for the +:defaults+ option. This even applies to parameters that you do not specify as dynamic segments. For example: @@ -557,7 +557,7 @@ match '*a/foo/*b' => 'test#index' would match +zoo/woo/foo/bar/baz+ with +params[:a]+ equals +"zoo/woo"+, and +params[:b]+ equals +"bar/baz"+. -NOTE: Starting from Rails 3.1, wildcard route will always matching the optional format segment by default. For example if you have this route: +NOTE: Starting from Rails 3.1, wildcard routes will always match the optional format segment by default. For example if you have this route: <ruby> map '*pages' => 'pages#show' diff --git a/railties/guides/source/ruby_on_rails_guides_guidelines.textile b/railties/guides/source/ruby_on_rails_guides_guidelines.textile index 8e55780dca..26a5a4c3c9 100644 --- a/railties/guides/source/ruby_on_rails_guides_guidelines.textile +++ b/railties/guides/source/ruby_on_rails_guides_guidelines.textile @@ -13,7 +13,7 @@ h3. Prologue Each guide should start with motivational text at the top (that's the little introduction in the blue area.) The prologue should tell the reader what the guide is about, and what they will learn. See for example the "Routing Guide":routing.html. h3. Titles - + The title of every guide uses +h2+, guide sections use +h3+, subsections +h4+, etc. Capitalize all words except for internal articles, prepositions, conjunctions, and forms of the verb to be: diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile index c9dc1c2d7c..f87ffdb20d 100644 --- a/railties/guides/source/security.textile +++ b/railties/guides/source/security.textile @@ -147,7 +147,7 @@ reset_session If you use the popular RestfulAuthentication plugin for user management, add reset_session to the SessionsController#create action. Note that this removes any value from the session, _(highlight)you have to transfer them to the new session_. -Another countermeasure is to _(highlight)save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _(highlight)These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way. +Another countermeasure is to _(highlight)save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _(highlight)These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way. h4. Session Expiry @@ -238,7 +238,7 @@ Or the attacker places the code into the onmouseover event handler of an image: <img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." /> </html> -There are many other possibilities, including Ajax to attack the victim in the background.
The _(highlight)solution to this is including a security token in non-GET requests_ which check on the server-side. In Rails 2 or higher, this is a one-liner in the application controller: +There are many other possibilities, including Ajax to attack the victim in the background.
The _(highlight)solution to this is including a security token in non-GET requests_ which check on the server-side. In Rails 2 or higher, this is a one-liner in the application controller: <ruby> protect_from_forgery :secret => "123456789012345678901234567890..." @@ -394,7 +394,7 @@ params[:user] # => {:name => “ow3ned”, :admin => true} So if you create a new user using mass-assignment, it may be too easy to become an administrator. -Note that this vulnerability is not restricted to database columns. Any setter method, unless explicitly protected, is accessible via the <tt>attributes=</tt> method. In fact, this vulnerability is extended even further with the introduction of nested mass assignment (and nested object forms) in Rails 2.3. The +accepts_nested_attributes_for+ declaration provides us the ability to extend mass assignment to model associations (+has_many+, +has_one+, +has_and_belongs_to_many+). For example: +Note that this vulnerability is not restricted to database columns. Any setter method, unless explicitly protected, is accessible via the <tt>attributes=</tt> method. In fact, this vulnerability is extended even further with the introduction of nested mass assignment (and nested object forms) in Rails 2.3. The +accepts_nested_attributes_for+ declaration provides us the ability to extend mass assignment to model associations (+has_many+, +has_one+, +has_and_belongs_to_many+). For example: <ruby> class Person < ActiveRecord::Base @@ -418,10 +418,17 @@ To avoid this, Rails provides two class methods in your Active Record class to c attr_protected :admin </ruby> ++attr_protected+ also optionally takes a scope option using :as which allows you to define multiple mass-assignment groupings. If no scope is defined then attributes will be added to the default group. + +<ruby> +attr_protected :last_login, :as => :admin +</ruby> + A much better way, because it follows the whitelist-principle, is the +attr_accessible+ method. It is the exact opposite of +attr_protected+, because _(highlight)it takes a list of attributes that will be accessible_. All other attributes will be protected. This way you won't forget to protect attributes when adding new ones in the course of development. Here is an example: <ruby> attr_accessible :name +attr_accessible :name, :is_admin, :as => :admin </ruby> If you want to set a protected attribute, you will to have to assign it individually: @@ -434,13 +441,31 @@ params[:user] # => {:name => "ow3ned", :admin => true} @user.admin # => true </ruby> -A more paranoid technique to protect your whole project would be to enforce that all models whitelist their accessible attributes. This can be easily achieved with a very simple initializer: +When assigning attributes in Active Record using +new+, +attributes=+, or +update_attributes+ the :default scope will be used. To assign attributes using different scopes you should use +assign_attributes+ which accepts an optional :as options parameter. If no :as option is provided then the :default scope will be used. You can also bypass mass-assignment security by using the +:without_protection+ option. Here is an example: + +<ruby> +@user = User.new + +@user.assign_attributes({ :name => 'Josh', :is_admin => true }) +@user.name # => Josh +@user.is_admin # => false + +@user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) +@user.name # => Josh +@user.is_admin # => true + +@user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) +@user.name # => Josh +@user.is_admin # => true +</ruby> + +A more paranoid technique to protect your whole project would be to enforce that all models define their accessible attributes. This can be easily achieved with a very simple application config option of: <ruby> -ActiveRecord::Base.send(:attr_accessible, nil) +config.active_record.whitelist_attributes = true </ruby> -This will create an empty whitelist of attributes available for mass assignment for all models in your app. As such, your models will need to explicitly whitelist accessible parameters by using an +attr_accessible+ declaration. This technique is best applied at the start of a new project. However, for an existing project with a thorough set of functional tests, it should be straightforward and relatively quick to insert this initializer, run your tests, and expose each attribute (via +attr_accessible+) as dictated by your failing tests. +This will create an empty whitelist of attributes available for mass-assignment for all models in your app. As such, your models will need to explicitly whitelist or blacklist accessible parameters by using an +attr_accessible+ or +attr_protected+ declaration. This technique is best applied at the start of a new project. However, for an existing project with a thorough set of functional tests, it should be straightforward and relatively quick to use this application config option; run your tests, and expose each attribute (via +attr_accessible+ or +attr_protected+) as dictated by your failing tests. h3. User Management diff --git a/railties/guides/source/testing.textile b/railties/guides/source/testing.textile index e2317661ea..9b1ef22b97 100644 --- a/railties/guides/source/testing.textile +++ b/railties/guides/source/testing.textile @@ -415,8 +415,8 @@ NOTE: +assert_valid(record)+ has been deprecated. Please use +assert(record.vali |_.Assertion |_.Purpose| |+assert_valid(record)+ |Ensures that the passed record is valid by Active Record standards and returns any error messages if it is not.| -|+assert_difference(expressions, difference = 1, message = nil) {...}+ |Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| -|+assert_no_difference(expressions, message = nil, &block)+ |Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| +|+assert_difference(expressions, difference = 1, message = nil) {...}+ |Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| +|+assert_no_difference(expressions, message = nil, &block)+ |Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| |+assert_recognizes(expected_options, path, extras={}, message=nil)+ |Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| |+assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)+ |Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.| |+assert_response(type, message = nil)+ |Asserts that the response comes with a specific status code. You can specify +:success+ to indicate 200, +:redirect+ to indicate 300-399, +:missing+ to indicate 404, or +:error+ to match the 500-599 range| diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index e3942f0f1f..0c3c7737ea 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -50,6 +50,7 @@ module Rails end end + attr_accessor :assets delegate :default_url_options, :default_url_options=, :to => :routes # This method is called just after an application inherits from Rails::Application, @@ -116,13 +117,10 @@ module Rails self end - alias :build_middleware_stack :app - def env_config @env_config ||= super.merge({ "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.secret_token" => config.secret_token, - "action_dispatch.asset_path" => nil, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions }) end @@ -137,45 +135,9 @@ module Rails @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd)) end - def assets - @assets ||= build_asset_environment - end - attr_writer :assets - - def build_asset_environment - return unless config.assets.enabled - require 'sprockets' - env = Sprockets::Environment.new(root.to_s) - env.static_root = File.join(root.join("public"), config.assets.prefix) - env - end - - initializer :add_sprockets_paths do |app| - if config.assets.enabled - paths = [ - "app/assets/javascripts", - "app/assets/stylesheets", - "vendor/assets/javascripts", - "vendor/assets/stylesheets", - "vendor/plugins/*/app/javascripts", - "vendor/plugins/*/app/stylesheets", - "vendor/plugins/*/javascripts", - "vendor/plugins/*/stylesheets" - ] + config.assets.paths - - paths.each do |pattern| - Dir[app.root.join(pattern)].each do |dir| - app.assets.paths << dir - end - end - end - end - protected - def default_asset_path - nil - end + alias :build_middleware_stack :app def default_middleware_stack ActionDispatch::MiddlewareStack.new.tap do |middleware| @@ -190,8 +152,7 @@ module Rails end if config.serve_static_assets - asset_paths = ActiveSupport::OrderedHash[config.static_asset_paths.to_a.reverse] - middleware.use ::ActionDispatch::Static, asset_paths + middleware.use ::ActionDispatch::Static, paths["public"].first end middleware.use ::Rack::Lock unless config.allow_concurrency @@ -239,4 +200,4 @@ module Rails require "rails/console/helpers" end end -end +end
\ No newline at end of file diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4a042e0033..f818313955 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -4,13 +4,12 @@ require 'rails/engine/configuration' module Rails class Application class Configuration < ::Rails::Engine::Configuration - attr_accessor :allow_concurrency, :asset_host, :cache_classes, :cache_store, - :encoding, :consider_all_requests_local, :dependency_loading, - :filter_parameters, :helpers_paths, :logger, - :preload_frameworks, :reload_plugins, - :secret_token, :serve_static_assets, :session_options, - :time_zone, :whiny_nils, :force_ssl, - :assets + attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets, + :cache_classes, :cache_store, :consider_all_requests_local, + :dependency_loading, :encoding, :filter_parameters, + :force_ssl, :helpers_paths, :logger, :preload_frameworks, + :reload_plugins, :secret_token, :serve_static_assets, + :session_options, :time_zone, :whiny_nils attr_writer :log_level @@ -34,8 +33,11 @@ module Rails @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false @assets.paths = [] - @assets.precompile = [] + @assets.precompile = [ /\w+\.(?!js|css)$/, "application.js", "application.css" ] @assets.prefix = "/assets" + + @assets.js_compressor = nil + @assets.css_compressor = nil end def compiled_asset_path @@ -63,6 +65,9 @@ module Rails paths.add "config/environment", :with => "config/environment.rb" paths.add "lib/templates" paths.add "log", :with => "log/#{Rails.env}.log" + paths.add "public" + paths.add "public/javascripts" + paths.add "public/stylesheets" paths.add "tmp" paths.add "tmp/cache" paths diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index bae464a840..028c8814c4 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -33,27 +33,6 @@ module Rails end end - initializer :add_sprockets_route do |app| - assets = config.assets - if assets.enabled - app.routes.append do - mount app.assets => assets.prefix - end - end - end - - initializer :set_sprockets_logger do |app| - if config.assets.enabled - app.assets.logger = Rails.logger - end - end - - initializer :index_sprockets_environment do |app| - if config.assets.enabled && config.action_controller.perform_caching - app.assets = app.assets.index - end - end - initializer :build_middleware_stack do build_middleware_stack end @@ -62,6 +41,10 @@ module Rails ActionDispatch::Reloader.prepare! end + initializer :define_main_app_helper do |app| + app.routes.define_mounted_helper(:main_app) + end + initializer :eager_load! do if config.cache_classes && !$rails_rake_task ActiveSupport.run_load_hooks(:before_eager_load, self) @@ -74,6 +57,8 @@ module Rails end # Force routes to be loaded just at the end and add it to to_prepare callbacks + # This needs to be after the finisher hook to ensure routes added in the hook + # are still loaded. initializer :set_routes_reloader do |app| reloader = lambda { app.routes_reloader.execute_if_updated } reloader.call diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index ee265366ff..6c1064c609 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -171,32 +171,6 @@ module Rails # # Now, +Engine+ will get only requests that were not handled by +Application+. # - # == Asset path - # - # When you use +Engine+ with its own public directory, you will probably want to copy or symlink it - # to application's public directory. To simplify generating paths for assets, you can set <tt>asset_path</tt> - # for an engine: - # - # module MyEngine - # class Engine < Rails::Engine - # config.asset_path = "/my_engine/%s" - # end - # end - # - # With such a config, asset paths will be automatically modified inside +Engine+: - # - # image_path("foo.jpg") #=> "/my_engine/images/foo.jpg" - # - # == Serving static files - # - # By default, Rails uses <tt>ActionDispatch::Static</tt> to serve static files in development mode. This is ok - # while you develop your application, but when you want to deploy it, assets from an engine will not be - # served by default. You should choose one of the two following strategies: - # - # * enable serving static files by setting config.serve_static_assets to true - # * copy engine's public files to application's public folder with <tt>rake ENGINE_NAME:install:assets</tt>, for example - # <tt>rake my_engine:install:assets</tt> - # # == Engine name # # There are some places where an Engine's name is used: @@ -312,6 +286,27 @@ module Rails # # This code will use <tt>my_engine.user_path(@user)</tt> to generate the proper route. # + # == Isolated engine's helpers + # + # Sometimes you may want to isolate engine, but use helpers that are defined for it. + # If you want to share just a few specific helpers you can add them to application's + # helpers in ApplicationController: + # + # class ApplicationController < ActionController::Base + # helper MyEngine::SharedEngineHelper + # end + # + # If you want to include all of the engine's helpers, you can use #helpers method on egine's + # instance: + # + # class ApplicationController < ActionController::Base + # helper MyEngine::Engine.helpers + # end + # + # It will include all of the helpers from engine's directory. Take into account that this does + # not include helpers defined in controllers with helper_method or other similar solutions, + # only helpers defined in helpers directory will be included. + # # == Migrations & seed data # # Engines can have their own migrations. The default path for migrations is exactly the same @@ -410,6 +405,24 @@ module Rails @railties ||= self.class::Railties.new(config) end + def helpers + @helpers ||= begin + helpers = Module.new + + helpers_paths = if config.respond_to?(:helpers_paths) + config.helpers_paths + else + paths["app/helpers"].existent + end + + all = ActionController::Base.all_helpers_from_path(helpers_paths) + ActionController::Base.modules_for_helpers(all).each do |mod| + helpers.send(:include, mod) + end + helpers + end + end + def app @app ||= begin config.middleware = config.middleware.merge_into(default_middleware_stack) @@ -427,8 +440,7 @@ module Rails def env_config @env_config ||= { - 'action_dispatch.routes' => routes, - 'action_dispatch.asset_path' => config.asset_path + 'action_dispatch.routes' => routes } end @@ -509,13 +521,9 @@ module Rails require environment if environment end - initializer :append_asset_paths do - config.asset_path ||= default_asset_path - - public_path = paths["public"].first - if config.compiled_asset_path && File.exist?(public_path) - config.static_asset_paths[config.compiled_asset_path] = public_path - end + initializer :append_assets_path do |app| + app.config.assets.paths.unshift *paths["vendor/assets"].existent + app.config.assets.paths.unshift *paths["app/assets"].existent end initializer :prepend_helpers_path do |app| @@ -537,42 +545,29 @@ module Rails rake_tasks do next if self.is_a?(Rails::Application) + next unless has_migrations? namespace railtie_name do - desc "Shortcut for running both rake #{railtie_name}:install:migrations and #{railtie_name}:install:assets" - task :install do - Rake::Task["#{railtie_name}:install:migrations"].invoke - Rake::Task["#{railtie_name}:install:assets"].invoke - end - namespace :install do - # TODO Add assets copying to this list - # TODO Skip this if there is no paths["db/migrate"] for the engine desc "Copy migrations from #{railtie_name} to application" task :migrations do ENV["FROM"] = railtie_name Rake::Task["railties:install:migrations"].invoke end - - desc "Copy assets from #{railtie_name} to application" - task :assets do - ENV["FROM"] = railtie_name - Rake::Task["railties:install:assets"].invoke - end end end end protected - def default_asset_path - "/#{railtie_name}%s" - end - def routes? defined?(@routes) end + def has_migrations? + paths["db/migrate"].first.present? + end + def find_root_with_flag(flag, default=nil) root_path = self.class.called_from diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 4f458b0aee..241db4b0a9 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -5,7 +5,7 @@ module Rails class Configuration < ::Rails::Railtie::Configuration attr_reader :root attr_writer :middleware, :eager_load_paths, :autoload_once_paths, :autoload_paths - attr_accessor :plugins, :asset_path + attr_accessor :plugins def initialize(root=nil) super() @@ -40,6 +40,7 @@ module Rails @paths ||= begin paths = Rails::Paths::Root.new(@root) paths.add "app", :eager_load => true, :glob => "*" + paths.add "app/assets", :glob => "*" paths.add "app/controllers", :eager_load => true paths.add "app/helpers", :eager_load => true paths.add "app/models", :eager_load => true @@ -55,10 +56,8 @@ module Rails paths.add "db" paths.add "db/migrate" paths.add "db/seeds", :with => "db/seeds.rb" - paths.add "public" - paths.add "public/javascripts" - paths.add "public/stylesheets" paths.add "vendor", :load_path => true + paths.add "vendor/assets", :glob => "*" paths.add "vendor/plugins" paths end @@ -79,10 +78,6 @@ module Rails def autoload_paths @autoload_paths ||= paths.autoload_paths end - - def compiled_asset_path - asset_path % "" if asset_path - end end end end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index be61b087f9..9be395e989 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -24,9 +24,12 @@ module Rails :rails => { :actions => '-a', :orm => '-o', + :javascripts => '-j', + :javascript_engine => '-je', :resource_controller => '-c', :scaffold_controller => '-c', :stylesheets => '-y', + :stylesheet_engine => '-se', :template_engine => '-e', :test_framework => '-t' }, @@ -43,15 +46,18 @@ module Rails DEFAULT_OPTIONS = { :rails => { + :assets => true, :force_plural => false, :helper => true, - :assets => true, - :orm => nil, :integration_tool => nil, + :javascripts => true, + :javascript_engine => nil, + :orm => nil, :performance_tool => nil, :resource_controller => :controller, :scaffold_controller => :scaffold_controller, :stylesheets => true, + :stylesheet_engine => nil, :test_framework => nil, :template_engine => :erb }, diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 481fa95068..3cc3762cee 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -10,6 +10,8 @@ module Rails module Generators class AppBase < Base DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) + JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql ) + DATABASES.concat(JDBC_DATABASES) JAVASCRIPTS = %w( jquery prototype ) attr_accessor :rails_template @@ -156,12 +158,15 @@ module Rails end def gem_for_database - # %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) + # %w( mysql oracle postgresql sqlite3 frontbase ibm_db jdbcmysql jdbcsqlite3 jdbcpostgresql ) case options[:database] when "oracle" then "ruby-oci8" when "postgresql" then "pg" when "frontbase" then "ruby-frontbase" when "mysql" then "mysql2" + when "jdbcmysql" then "activerecord-jdbcmysql-adapter" + when "jdbcsqlite3" then "activerecord-jdbcsqlite3-adapter" + when "jdbcpostgresql" then "activerecord-jdbcpostgresql-adapter" else options[:database] end end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 93d6c1f827..bc55efa261 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -89,10 +89,6 @@ module Rails directory "public", "public", :recursive => false end - def images - directory "public/images" - end - def script directory "script" do |content| "#{shebang}\n" + content @@ -106,7 +102,8 @@ module Rails empty_directory_with_gitkeep "test/integration" empty_directory_with_gitkeep "test/unit" - copy_file "test/performance/browsing_test.rb" + template "test/performance/browsing_test.rb" + template "test/test_helper.rb" end def tmp @@ -137,7 +134,7 @@ module Rails def vendor_stylesheets empty_directory_with_gitkeep "vendor/assets/stylesheets" end - + def vendor_plugins empty_directory_with_gitkeep "vendor/plugins" end @@ -165,7 +162,7 @@ module Rails if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end - + if !options[:skip_javascript] && !JAVASCRIPTS.include?(options[:javascript]) raise Error, "Invalid value for --javascript option. Supported for preconfiguration are: #{JAVASCRIPTS.join(", ")}." end @@ -218,10 +215,6 @@ module Rails build(:public_directory) end - def create_public_image_files - build(:images) - end - def create_script_files build(:script) end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 0cee7deb72..9f2346028a 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -6,8 +6,9 @@ source 'http://rubygems.org' # Asset template engines <%= "gem 'json'\n" if RUBY_VERSION < "1.9.2" -%> -gem 'sass', '~> 3.1.0.alpha' +gem 'sass' gem 'coffee-script' +# gem 'uglifier' # Use unicorn as the web server # gem 'unicorn' diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/images/rails.png b/railties/lib/rails/generators/rails/app/templates/app/assets/images/rails.png Binary files differnew file mode 100644 index 0000000000..d5edc04e65 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/images/rails.png diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml new file mode 100644 index 0000000000..ca807c9f3f --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -0,0 +1,30 @@ +# MySQL. Versions 4.1 and 5.0 are recommended. +# +# Install the MySQL driver: +# gem install activerecord-jdbcmysql-adapter +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +development: + adapter: jdbcmysql + database: <%= app_name %>_development + username: root + password: + host: localhost + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: jdbcmysql + database: <%= app_name %>_test + username: root + password: + host: localhost + +production: + adapter: jdbcmysql + database: <%= app_name %>_production + username: root + password: + host: localhost diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml new file mode 100644 index 0000000000..a228aca5d2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml @@ -0,0 +1,48 @@ +# PostgreSQL. Versions 7.4 and 8.x are supported. +# +# Install the pg driver: +# gem install pg +# On Mac OS X with macports: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +development: + adapter: jdbcpostgresql + encoding: unicode + database: <%= app_name %>_development + username: <%= app_name %> + password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # The server defaults to notice. + #min_messages: warning + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: jdbcpostgresql + encoding: unicode + database: <%= app_name %>_test + username: <%= app_name %> + password: + +production: + adapter: jdbcpostgresql + encoding: unicode + database: <%= app_name %>_production + username: <%= app_name %> + password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml new file mode 100644 index 0000000000..30776b3b4e --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml @@ -0,0 +1,17 @@ +# SQLite version 3.x +# gem 'activerecord-jdbcsqlite3-adapter' + +development: + adapter: jdbcsqlite3 + database: db/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: jdbcsqlite3 + database: db/test.sqlite3 + +production: + adapter: jdbcsqlite3 + database: db/production.sqlite3 diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 874cc403ba..ce28e41b91 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -15,6 +15,10 @@ # (comment out if your front-end server doesn't support this) config.action_dispatch.x_sendfile_header = "X-Sendfile" # Use 'X-Accel-Redirect' for nginx + # Compress both stylesheets and JavaScripts + # config.assets.js_compressor = :uglifier + config.assets.css_compressor = :scss + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -30,6 +34,9 @@ # Enable serving of images, stylesheets, and javascripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" + # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + # config.assets.precompile += %w( search.js ) + # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false diff --git a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt index 9a2efa68a7..9a2efa68a7 100644 --- a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb +++ b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/public/index.html b/railties/lib/rails/generators/rails/app/templates/public/index.html index 13a203dd08..9d9811a5bf 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/index.html +++ b/railties/lib/rails/generators/rails/app/templates/public/index.html @@ -59,7 +59,7 @@ #header { - background-image: url("images/rails.png"); + background-image: url("/assets/rails.png"); background-repeat: no-repeat; background-position: top left; height: 64px; diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb index a8f7aeac7d..a8f7aeac7d 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb index 59239d1a03..80beb7abfe 100644 --- a/railties/lib/rails/generators/rails/assets/assets_generator.rb +++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb @@ -1,36 +1,38 @@ module Rails module Generators - # TODO: Add hooks for using other asset pipelines, like Less class AssetsGenerator < NamedBase - def create_asset_files + class_option :javascripts, :type => :boolean, :desc => "Generate javascripts" + class_option :stylesheets, :type => :boolean, :desc => "Generate stylesheets" + + class_option :javascript_engine, :desc => "Engine for javascripts" + class_option :stylesheet_engine, :desc => "Engine for stylesheets" + + def create_javascript_files + return unless options.javascripts? copy_file "javascript.#{javascript_extension}", - File.join('app/assets/javascripts', "#{file_name}.#{javascript_extension}") + File.join('app/assets/javascripts', class_path, "#{asset_name}.#{javascript_extension}") + end + def create_stylesheet_files + return unless options.stylesheets? copy_file "stylesheet.#{stylesheet_extension}", - File.join('app/assets/stylesheets', "#{file_name}.#{stylesheet_extension}") + File.join('app/assets/stylesheets', class_path, "#{asset_name}.#{stylesheet_extension}") end - - private - def javascript_extension - using_coffee? ? "js.coffee" : "js" + + protected + + def asset_name + file_name end - - def using_coffee? - require 'coffee-script' - defined?(CoffeeScript) - rescue LoadError - false + + def javascript_extension + options.javascript_engine.present? ? + "js.#{options.javascript_engine}" : "js" end - + def stylesheet_extension - using_sass? ? "css.scss" : "css" - end - - def using_sass? - require 'sass' - defined?(Sass) - rescue LoadError - false + options.stylesheet_engine.present? ? + "css.#{options.stylesheet_engine}" : "css" end end end diff --git a/railties/lib/rails/generators/rails/assets/templates/javascript.js.coffee b/railties/lib/rails/generators/rails/assets/templates/javascript.js.coffee index 09b2da094a..761567942f 100644 --- a/railties/lib/rails/generators/rails/assets/templates/javascript.js.coffee +++ b/railties/lib/rails/generators/rails/assets/templates/javascript.js.coffee @@ -1,3 +1,3 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. -// You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb index 3cf8410d1e..126aadb88d 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -9,10 +9,17 @@ module Rails end def app - if options[:mountable] + if mountable? directory "app" template "#{app_templates_dir}/app/views/layouts/application.html.erb.tt", "app/views/layouts/#{name}/application.html.erb" + empty_directory_with_gitkeep "app/assets/images" + elsif full? + empty_directory_with_gitkeep "app/models" + empty_directory_with_gitkeep "app/controllers" + empty_directory_with_gitkeep "app/views" + empty_directory_with_gitkeep "app/helpers" + empty_directory_with_gitkeep "app/assets/images" end end @@ -61,8 +68,12 @@ task :default => :test end end + PASSTHROUGH_OPTIONS = [ + :skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip + ] + def generate_test_dummy(force = false) - opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip) + opts = (options || {}).slice(*PASSTHROUGH_OPTIONS) opts[:force] = force invoke Rails::Generators::AppGenerator, @@ -84,7 +95,7 @@ task :default => :test remove_file "doc" remove_file "Gemfile" remove_file "lib/tasks" - remove_file "public/images/rails.png" + remove_file "app/assets/images/rails.png" remove_file "public/index.html" remove_file "public/robots.txt" remove_file "README" @@ -94,26 +105,36 @@ task :default => :test end def stylesheets - empty_directory_with_gitkeep "public/stylesheets" if options[:mountable] + if mountable? + copy_file "#{app_templates_dir}/app/assets/stylesheets/application.css", + "app/assets/stylesheets/application.css" + elsif full? + empty_directory_with_gitkeep "app/assets/stylesheets" + end end def javascripts - return unless options[:mountable] - - empty_directory "#{app_templates_dir}/public/javascripts" + return if options.skip_javascript? - unless options[:skip_javascript] - copy_file "#{app_templates_dir}/public/javascripts/#{options[:javascript]}.js", "public/javascripts/#{options[:javascript]}.js" - copy_file "#{app_templates_dir}/public/javascripts/#{options[:javascript]}_ujs.js", "public/javascripts/rails.js" + if mountable? + copy_file "#{app_templates_dir}/app/assets/javascripts/application.js.tt", + "app/assets/javascripts/application.js" + copy_file "#{app_templates_dir}/vendor/assets/javascripts/#{options[:javascript]}.js", + "vendor/assets/javascripts/#{options[:javascript]}.js" + copy_file "#{app_templates_dir}/vendor/assets/javascripts/#{options[:javascript]}_ujs.js", + "vendor/assets/javascripts/#{options[:javascript]}_ujs.js" if options[:javascript] == "prototype" - copy_file "#{app_templates_dir}/public/javascripts/controls.js", "public/javascripts/controls.js" - copy_file "#{app_templates_dir}/public/javascripts/dragdrop.js", "public/javascripts/dragdrop.js" - copy_file "#{app_templates_dir}/public/javascripts/effects.js", "public/javascripts/effects.js" + copy_file "#{app_templates_dir}/vendor/assets/javascripts/controls.js", + "vendor/assets/javascripts/controls.js" + copy_file "#{app_templates_dir}/vendor/assets/javascripts/dragdrop.js", + "vendor/assets/javascripts/dragdrop.js" + copy_file "#{app_templates_dir}/vendor/assets/javascripts/effects.js", + "vendor/assets/javascripts/effects.js" end + elsif full? + empty_directory_with_gitkeep "app/assets/javascripts" end - - copy_file "#{app_templates_dir}/public/javascripts/application.js", "public/javascripts/application.js" end def script(force = false) @@ -130,17 +151,17 @@ task :default => :test alias_method :plugin_path, :app_path - class_option :dummy_path, :type => :string, :default => "test/dummy", - :desc => "Create dummy application at given path" + class_option :dummy_path, :type => :string, :default => "test/dummy", + :desc => "Create dummy application at given path" - class_option :full, :type => :boolean, :default => false, - :desc => "Generate rails engine with integration tests" + class_option :full, :type => :boolean, :default => false, + :desc => "Generate rails engine with integration tests" - class_option :mountable, :type => :boolean, :default => false, - :desc => "Generate mountable isolated application" + class_option :mountable, :type => :boolean, :default => false, + :desc => "Generate mountable isolated application" - class_option :skip_gemspec, :type => :boolean, :default => false, - :desc => "Skip gemspec file" + class_option :skip_gemspec, :type => :boolean, :default => false, + :desc => "Skip gemspec file" def initialize(*args) raise Error, "Options should be given after the plugin name. For details run: rails plugin --help" if args[0].blank? @@ -180,6 +201,10 @@ task :default => :test build(:javascripts) end + def create_images_directory + build(:images) + end + def create_script_files build(:script) end @@ -200,6 +225,7 @@ task :default => :test public_task :apply_rails_template, :bundle_if_dev_or_edge protected + def app_templates_dir "../../app/templates" end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb index dd4d2da4eb..824caecb24 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb +++ b/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb @@ -5,9 +5,8 @@ class NavigationTest < ActionDispatch::IntegrationTest fixtures :all <% end -%> - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index 779f933785..6eef0dbe5b 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -6,8 +6,27 @@ module Rails remove_hook_for :resource_controller remove_class_option :actions + class_option :stylesheets, :type => :boolean, :desc => "Generate stylesheets" + class_option :stylesheet_engine, :desc => "Engine for stylesheets" + hook_for :scaffold_controller, :required => true - hook_for :stylesheets + + def copy_stylesheets_file + if behavior == :invoke && options.stylesheets? + template "scaffold.#{stylesheet_extension}", "app/assets/stylesheets/scaffold.#{stylesheet_extension}" + end + end + + hook_for :assets do |assets| + invoke assets, [controller_name] + end + + private + + def stylesheet_extension + options.stylesheet_engine.present? ? + "css.#{options.stylesheet_engine}" : "css" + end end end end diff --git a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index 1ae7000299..1ae7000299 100644 --- a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css diff --git a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css.scss b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css.scss index 45116b53f6..45116b53f6 100644 --- a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css.scss +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css.scss diff --git a/railties/lib/rails/generators/rails/stylesheets/USAGE b/railties/lib/rails/generators/rails/stylesheets/USAGE deleted file mode 100644 index 59e5495d0b..0000000000 --- a/railties/lib/rails/generators/rails/stylesheets/USAGE +++ /dev/null @@ -1,5 +0,0 @@ -Description: - Copies scaffold stylesheets to public/stylesheets/. - -Examples: - `rails generate stylesheets` diff --git a/railties/lib/rails/generators/rails/stylesheets/stylesheets_generator.rb b/railties/lib/rails/generators/rails/stylesheets/stylesheets_generator.rb deleted file mode 100644 index d06db25292..0000000000 --- a/railties/lib/rails/generators/rails/stylesheets/stylesheets_generator.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Rails - module Generators - class StylesheetsGenerator < Base - def copy_stylesheets_file - if behavior == :invoke - template "scaffold.#{stylesheet_extension}", "app/assets/stylesheets/scaffold.#{stylesheet_extension}" - end - end - - private - def stylesheet_extension - using_sass? ? "css.scss" : "css" - end - - def using_sass? - require 'sass' - defined?(Sass) - rescue LoadError - false - end - end - end -end diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb index 11a73ebad7..0bc5fd8ca2 100644 --- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb @@ -3,10 +3,9 @@ require 'test_helper' <% module_namespacing do -%> class <%= class_name %>ControllerTest < ActionController::TestCase <% if actions.empty? -%> - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end <% else -%> <% for action in actions -%> test "should get <%= action %>" do diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb index de0823749c..e7a06e4a73 100644 --- a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb +++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb @@ -3,8 +3,7 @@ require 'test_helper' class <%= class_name %>Test < ActionDispatch::IntegrationTest fixtures :all - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb index b62c7fd279..c05102290c 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb @@ -13,10 +13,9 @@ class <%= class_name %>Test < ActionMailer::TestCase <% end -%> <% if actions.blank? -%> - # replace this with your real tests - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end <% end -%> end <% end -%> diff --git a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb index 6f79879838..c9bc7d5b90 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb +++ b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb @@ -2,9 +2,8 @@ require 'test_helper' <% module_namespacing do -%> class <%= class_name %>Test < ActiveSupport::TestCase - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end <% end -%> diff --git a/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb index cd116f5ce9..28aa23626a 100644 --- a/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb +++ b/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb @@ -2,9 +2,8 @@ require 'test_helper' <% module_namespacing do -%> class <%= class_name %>ObserverTest < ActiveSupport::TestCase - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end <% end -%> diff --git a/railties/lib/rails/generators/test_unit/plugin/templates/%file_name%_test.rb.tt b/railties/lib/rails/generators/test_unit/plugin/templates/%file_name%_test.rb.tt index 3e0bc29d3a..0cbae1120e 100644 --- a/railties/lib/rails/generators/test_unit/plugin/templates/%file_name%_test.rb.tt +++ b/railties/lib/rails/generators/test_unit/plugin/templates/%file_name%_test.rb.tt @@ -1,8 +1,7 @@ require 'test_helper' class <%= class_name %>Test < ActiveSupport::TestCase - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index 2c7b5bc048..bfd2a73aeb 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -67,13 +67,6 @@ module Rails super || @@options.key?(name.to_sym) end - # static_asset_paths is a Hash containing asset_paths - # with associated public folders, like: - # { "/" => "/app/public", "/my_engine" => "app/engines/my_engine/public" } - def static_asset_paths - @@static_asset_paths ||= ActiveSupport::OrderedHash.new - end - private def method_missing(name, *args, &blk) diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 4d09b0c2c0..166d518f7c 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -12,7 +12,6 @@ $VERBOSE = nil routes statistics tmp - railties ).each do |task| load "rails/tasks/#{task}.rake" end diff --git a/railties/lib/rails/tasks/assets.rake b/railties/lib/rails/tasks/assets.rake index 396ce728a1..158df31749 100644 --- a/railties/lib/rails/tasks/assets.rake +++ b/railties/lib/rails/tasks/assets.rake @@ -1,5 +1,9 @@ namespace :assets do - task :compile => :environment do + desc "Compile all the assets named in config.assets.precompile" + task :precompile => :environment do + # Give assets access to asset_path + ActionView::Helpers::SprocketsHelper + assets = Rails.application.config.assets.precompile Rails.application.assets.precompile(*assets) end diff --git a/railties/lib/rails/tasks/railties.rake b/railties/lib/rails/tasks/railties.rake deleted file mode 100644 index 16703879cf..0000000000 --- a/railties/lib/rails/tasks/railties.rake +++ /dev/null @@ -1,29 +0,0 @@ -namespace :railties do - namespace :install do - # desc "Copies missing assets from Railties (e.g. plugins, engines). You can specify Railties to use with FROM=railtie1,railtie2" - task :assets => :rails_env do - require 'rails/generators/base' - Rails.application.initialize! - - to_load = ENV["FROM"].blank? ? :all : ENV["FROM"].split(",").map {|n| n.strip } - app_public_path = Rails.application.paths["public"].first - - Rails.application.railties.all do |railtie| - next unless to_load == :all || to_load.include?(railtie.railtie_name) - - if railtie.respond_to?(:paths) && (path = railtie.paths["public"].first) && - (assets_dir = railtie.config.compiled_asset_path) && File.exist?(path) - - Rails::Generators::Base.source_root(path) - copier = Rails::Generators::Base.new - Dir[File.join(path, "**/*")].each do |file| - relative = file.gsub(/^#{path}\//, '') - if File.file?(file) - copier.copy_file relative, File.join(app_public_path, assets_dir, relative) - end - end - end - end - end - end -end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 62697b1bf9..ab3eb4c9e7 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -258,6 +258,18 @@ module ApplicationTests assert_equal res, last_response.body # value should be unchanged end + test "sets all Active Record models to whitelist all attributes by default" do + add_to_config <<-RUBY + config.active_record.whitelist_attributes = true + RUBY + + require "#{app_path}/config/environment" + + assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, + ActiveRecord::Base.active_authorizers[:default].class + assert_equal [""], ActiveRecord::Base.active_authorizers[:default].to_a + end + test "registers interceptors with ActionMailer" do add_to_config <<-RUBY config.action_mailer.interceptors = MyMailInterceptor diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index dfb7adf459..1902484301 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -8,6 +8,8 @@ DEFAULT_APP_FILES = %w( Gemfile Rakefile config.ru + app/assets/javascripts + app/assets/stylesheets app/controllers app/helpers app/mailers @@ -21,9 +23,7 @@ DEFAULT_APP_FILES = %w( lib lib/tasks log - public/images - public/javascripts - public/stylesheets + app/assets/images script/rails test/fixtures test/functional @@ -31,11 +31,9 @@ DEFAULT_APP_FILES = %w( test/performance test/unit vendor + vendor/assets vendor/plugins - tmp/sessions - tmp/sockets tmp/cache - tmp/pids ) class AppGeneratorTest < Rails::Generators::TestCase @@ -49,8 +47,9 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_application_controller_and_layout_files run_generator - assert_file "app/views/layouts/application.html.erb", /stylesheet_link_tag :all/ - assert_no_file "public/stylesheets/application.css" + assert_file "app/views/layouts/application.html.erb", /stylesheet_link_tag\s+"application"/ + assert_file "app/views/layouts/application.html.erb", /javascript_include_tag\s+"application"/ + assert_file "app/assets/stylesheets/application.css" end def test_invalid_application_name_raises_an_error @@ -132,6 +131,24 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile", /^gem\s+["']mysql2["']$/ end + def test_config_jdbcmysql_database + run_generator([destination_root, "-d", "jdbcmysql"]) + assert_file "config/database.yml", /jdbcmysql/ + assert_file "Gemfile", /^gem\s+["']activerecord-jdbcmysql-adapter["']$/ + end + + def test_config_jdbcsqlite3_database + run_generator([destination_root, "-d", "jdbcsqlite3"]) + assert_file "config/database.yml", /jdbcsqlite3/ + assert_file "Gemfile", /^gem\s+["']activerecord-jdbcsqlite3-adapter["']$/ + end + + def test_config_jdbcpostgresql_database + run_generator([destination_root, "-d", "jdbcpostgresql"]) + assert_file "config/database.yml", /jdbcpostgresql/ + assert_file "Gemfile", /^gem\s+["']activerecord-jdbcpostgresql-adapter["']$/ + end + def test_generator_if_skip_active_record_is_given run_generator [destination_root, "--skip-active-record"] assert_no_file "config/database.yml" @@ -149,37 +166,37 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_jquery_and_test_unit_are_added_by_default run_generator assert_file "config/application.rb", /#\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(prototype effects dragdrop controls rails\)/ - assert_file "public/javascripts/application.js" - assert_file "public/javascripts/jquery.js" - assert_file "public/javascripts/rails.js" + assert_file "app/assets/javascripts/application.js" + assert_file "vendor/assets/javascripts/jquery.js" + assert_file "vendor/assets/javascripts/jquery_ujs.js" assert_file "test" end def test_javascript_is_skipped_if_required run_generator [destination_root, "--skip-javascript"] assert_file "config/application.rb", /^\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(\)/ - assert_file "public/javascripts/application.js" - assert_no_file "public/javascripts/jquery.js" - assert_no_file "public/javascripts/rails.js" + assert_file "app/assets/javascripts/application.js" + assert_no_file "vendor/assets/javascripts/jquery.js" + assert_no_file "vendor/assets/javascripts/jquery_ujs.js" end def test_config_prototype_javascript_library run_generator [destination_root, "-j", "prototype"] assert_file "config/application.rb", /^\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(prototype effects dragdrop controls rails\)/ - assert_file "public/javascripts/application.js" - assert_file "public/javascripts/prototype.js" - assert_file "public/javascripts/effects.js" - assert_file "public/javascripts/dragdrop.js" - assert_file "public/javascripts/controls.js" - assert_file "public/javascripts/rails.js", /prototype/ + assert_file "app/assets/javascripts/application.js" + assert_file "vendor/assets/javascripts/prototype.js" + assert_file "vendor/assets/javascripts/effects.js" + assert_file "vendor/assets/javascripts/dragdrop.js" + assert_file "vendor/assets/javascripts/controls.js" + assert_file "vendor/assets/javascripts/prototype_ujs.js", /prototype/ end def test_config_jquery_javascript_library run_generator [destination_root, "-j", "jquery"] assert_file "config/application.rb", /#\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(prototype effects dragdrop controls rails\)/ - assert_file "public/javascripts/application.js" - assert_file "public/javascripts/jquery.js" - assert_file "public/javascripts/rails.js", /jQuery/ + assert_file "app/assets/javascripts/application.js" + assert_file "vendor/assets/javascripts/jquery.js" + assert_file "vendor/assets/javascripts/jquery_ujs.js", /jQuery/ end def test_template_from_dir_pwd diff --git a/railties/test/generators/assets_generator_test.rb b/railties/test/generators/assets_generator_test.rb index a2a40a28d4..375632e5bc 100644 --- a/railties/test/generators/assets_generator_test.rb +++ b/railties/test/generators/assets_generator_test.rb @@ -1,32 +1,26 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/assets/assets_generator' -# FOXME: Silence the 'Could not find task "using_coffee?"' message in tests due to the public stub +# FIXME: Silence the 'Could not find task "using_coffee?"' message in tests due to the public stub class AssetsGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(posts) - def test_vanilla_assets + def test_assets run_generator - assert_file "app/assets/javascripts/posts.js" - assert_file "app/assets/stylesheets/posts.css" + assert_file "app/assets/javascripts/posts.js.coffee" + assert_file "app/assets/stylesheets/posts.css.scss" end def test_skipping_assets - content = run_generator ["posts", "--skip-assets"] - assert_no_file "app/assets/javascripts/posts.js" - assert_no_file "app/assets/stylesheets/posts.css" - end - - def test_coffee_javascript - self.generator_class.any_instance.stubs(:using_coffee?).returns(true) - run_generator - assert_file "app/assets/javascripts/posts.js.coffee" + content = run_generator ["posts", "--no-stylesheets", "--no-javascripts"] + assert_no_file "app/assets/javascripts/posts.js.coffee" + assert_no_file "app/assets/stylesheets/posts.css.scss" end - def test_sass_stylesheet - self.generator_class.any_instance.stubs(:using_sass?).returns(true) - run_generator - assert_file "app/assets/stylesheets/posts.css.scss" + def test_vanilla_assets + run_generator ["posts", "--no-javascript-engine", "--no-stylesheet-engine"] + assert_file "app/assets/javascripts/posts.js" + assert_file "app/assets/stylesheets/posts.css" end end diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 655d8ad450..2dfc91c683 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -39,8 +39,8 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_invokes_assets run_generator - assert_file "app/assets/javascripts/account.js" - assert_file "app/assets/stylesheets/account.css" + assert_file "app/assets/javascripts/account.js.coffee" + assert_file "app/assets/stylesheets/account.css.scss" end def test_invokes_default_test_framework diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index d61a02d32f..eb56e8d1a4 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -248,7 +248,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase assert_file "test/unit/helpers/test_app/product_lines_helper_test.rb" # Stylesheets - assert_file "public/stylesheets/scaffold.css" + assert_file "app/assets/stylesheets/scaffold.css.scss" end def test_scaffold_on_revoke @@ -279,7 +279,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase assert_no_file "test/unit/helpers/test_app/product_lines_helper_test.rb" # Stylesheets (should not be removed) - assert_file "public/stylesheets/scaffold.css" + assert_file "app/assets/stylesheets/scaffold.css.scss" end def test_scaffold_with_namespace_on_invoke @@ -320,7 +320,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase assert_file "test/unit/helpers/test_app/admin/roles_helper_test.rb" # Stylesheets - assert_file "public/stylesheets/scaffold.css" + assert_file "app/assets/stylesheets/scaffold.css.scss" end def test_scaffold_with_namespace_on_revoke @@ -352,6 +352,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase assert_no_file "test/unit/helpers/test_app/admin/roles_helper_test.rb" # Stylesheets (should not be removed) - assert_file "public/stylesheets/scaffold.css" + assert_file "app/assets/stylesheets/scaffold.css.scss" end end diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_new_generator_test.rb index 402fd25b14..fb956a8335 100644 --- a/railties/test/generators/plugin_new_generator_test.rb +++ b/railties/test/generators/plugin_new_generator_test.rb @@ -95,32 +95,33 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase def test_skipping_javascripts_without_mountable_option run_generator - assert_no_file "public/javascripts/jquery.js" - assert_no_file "public/javascripts/rails.js" - assert_no_file "public/javascripts/application.js" + assert_no_file "app/assets/javascripts/application.js" + assert_no_file "vendor/assets/javascripts/jquery.js" + assert_no_file "vendor/assets/javascripts/jquery_ujs.js" end def test_javascripts_generation run_generator [destination_root, "--mountable"] - assert_file "public/javascripts/jquery.js" - assert_file "public/javascripts/rails.js" - assert_file "public/javascripts/application.js" + assert_file "app/assets/javascripts/application.js" + assert_file "vendor/assets/javascripts/jquery.js" + assert_file "vendor/assets/javascripts/jquery_ujs.js" end def test_skip_javascripts run_generator [destination_root, "--skip-javascript", "--mountable"] - assert_no_file "public/javascripts/jquery.js" - assert_no_file "public/javascripts/rails.js" + assert_no_file "app/assets/javascripts/application.js" + assert_no_file "vendor/assets/javascripts/jquery.js" + assert_no_file "vendor/assets/javascripts/jquery_ujs.js" end - def test_ensure_that_javascript_option_is_passed_to_app_generator - run_generator [destination_root, "--javascript", "prototype"] - assert_file "test/dummy/public/javascripts/prototype.js" - end - - def test_ensure_that_skip_javascript_option_is_passed_to_app_generator - run_generator [destination_root, "--skip_javascript"] - assert_no_file "test/dummy/public/javascripts/jquery.js" + def test_config_prototype_javascript_library + run_generator [destination_root, "-j", "prototype", "--mountable"] + assert_file "app/assets/javascripts/application.js" + assert_file "vendor/assets/javascripts/prototype.js" + assert_file "vendor/assets/javascripts/effects.js" + assert_file "vendor/assets/javascripts/dragdrop.js" + assert_file "vendor/assets/javascripts/controls.js" + assert_file "vendor/assets/javascripts/prototype_ujs.js", /prototype/ end def test_template_from_dir_pwd @@ -139,11 +140,18 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--full", "--skip_active_record"] FileUtils.cd destination_root `bundle install` - assert_match(/2 tests, 2 assertions, 0 failures, 0 errors/, `bundle exec rake test`) + assert_match(/1 tests, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test`) end def test_creating_engine_in_full_mode run_generator [destination_root, "--full"] + assert_file "app/assets/javascripts" + assert_file "app/assets/stylesheets" + assert_file "app/assets/images" + assert_file "app/models" + assert_file "app/controllers" + assert_file "app/views" + assert_file "app/helpers" assert_file "config/routes.rb", /Rails.application.routes.draw do/ assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < Rails::Engine\n end\nend/ assert_file "lib/bukkits.rb", /require "bukkits\/engine"/ @@ -155,6 +163,9 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase def test_create_mountable_application_with_mountable_option run_generator [destination_root, "--mountable"] + assert_file "app/assets/javascripts" + assert_file "app/assets/stylesheets" + assert_file "app/assets/images" assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/ assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/ assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/ @@ -218,3 +229,4 @@ protected silence(:stdout){ generator.send(*args, &block) } end end + diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index df787f61ba..4b07f8bcbe 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -79,8 +79,10 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/helpers/product_lines_helper.rb" assert_file "test/unit/helpers/product_lines_helper_test.rb" - # Stylesheets - assert_file "public/stylesheets/scaffold.css" + # Assets + assert_file "app/assets/stylesheets/scaffold.css.scss" + assert_file "app/assets/javascripts/product_lines.js.coffee" + assert_file "app/assets/stylesheets/product_lines.css.scss" end def test_scaffold_on_revoke @@ -110,8 +112,10 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_no_file "app/helpers/product_lines_helper.rb" assert_no_file "test/unit/helpers/product_lines_helper_test.rb" - # Stylesheets (should not be removed) - assert_file "public/stylesheets/scaffold.css" + # Assets + assert_file "app/assets/stylesheets/scaffold.css.scss", /&:visited/ + assert_no_file "app/assets/javascripts/product_lines.js.coffee" + assert_no_file "app/assets/stylesheets/product_lines.css.scss" end def test_scaffold_with_namespace_on_invoke @@ -184,8 +188,10 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/helpers/admin/roles_helper.rb" assert_file "test/unit/helpers/admin/roles_helper_test.rb" - # Stylesheets - assert_file "public/stylesheets/scaffold.css" + # Assets + assert_file "app/assets/stylesheets/scaffold.css.scss", /&:visited/ + assert_file "app/assets/javascripts/admin/roles.js.coffee" + assert_file "app/assets/stylesheets/admin/roles.css.scss" end def test_scaffold_with_namespace_on_revoke @@ -216,8 +222,10 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_no_file "app/helpers/admin/roles_helper.rb" assert_no_file "test/unit/helpers/admin/roles_helper_test.rb" - # Stylesheets (should not be removed) - assert_file "public/stylesheets/scaffold.css" + # Assets + assert_file "app/assets/stylesheets/scaffold.css.scss" + assert_no_file "app/assets/javascripts/admin/roles.js.coffee" + assert_no_file "app/assets/stylesheets/admin/roles.css.scss" end def test_scaffold_generator_on_revoke_does_not_mutilate_legacy_map_parameter @@ -235,6 +243,34 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "config/routes.rb", /\.routes\.draw do\s*\|map\|\s*$/ end + def test_scaffold_generator_no_assets + run_generator [ "posts", "--no-assets" ] + assert_file "app/assets/stylesheets/scaffold.css.scss" + assert_no_file "app/assets/javascripts/posts.js.coffee" + assert_no_file "app/assets/stylesheets/posts.css.scss" + end + + def test_scaffold_generator_no_stylesheets + run_generator [ "posts", "--no-stylesheets" ] + assert_no_file "app/assets/stylesheets/scaffold.css.scss" + assert_file "app/assets/javascripts/posts.js.coffee" + assert_no_file "app/assets/stylesheets/posts.css.scss" + end + + def test_scaffold_generator_no_javascripts + run_generator [ "posts", "--no-javascripts" ] + assert_file "app/assets/stylesheets/scaffold.css.scss" + assert_no_file "app/assets/javascripts/posts.js.coffee" + assert_file "app/assets/stylesheets/posts.css.scss" + end + + def test_scaffold_generator_no_negines + run_generator [ "posts", "--no-javascript-engine", "--no-stylesheet-engine" ] + assert_file "app/assets/stylesheets/scaffold.css" + assert_file "app/assets/javascripts/posts.js" + assert_file "app/assets/stylesheets/posts.css" + end + def test_scaffold_generator_outputs_error_message_on_missing_attribute_type content = capture(:stderr) { run_generator ["post", "title:string", "body"]} assert_match(/Missing type for attribute 'body'/, content) diff --git a/railties/test/generators/stylesheets_generator_test.rb b/railties/test/generators/stylesheets_generator_test.rb deleted file mode 100644 index 4b86cdd1c7..0000000000 --- a/railties/test/generators/stylesheets_generator_test.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/stylesheets/stylesheets_generator' - -class StylesheetsGeneratorTest < Rails::Generators::TestCase - include GeneratorsTestHelper - - def test_copy_scss_stylesheet - self.generator_class.any_instance.stubs(:using_sass?).returns(true) - run_generator - assert_file "app/assets/stylesheets/scaffold.css.scss" - end - - def test_copy_css_stylesheet - run_generator - assert_file "app/assets/stylesheets/scaffold.css" - end - - def test_stylesheets_are_not_deleted_on_revoke - run_generator - run_generator [], :behavior => :revoke - assert_file "app/assets/stylesheets/scaffold.css" - end -end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index c5b1cb9a80..f2261540ca 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -90,6 +90,7 @@ module TestHelpers end module Generation + # Build an application by invoking the generator and going through the whole stack. def build_app(options = {}) FileUtils.rm_rf(app_path) FileUtils.cp_r(tmp_path('app_template'), app_path) @@ -115,6 +116,8 @@ module TestHelpers add_to_config 'config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"; config.session_store :cookie_store, :key => "_myapp_session"; config.active_support.deprecation = :log' end + # Make a very basic app, without creating the whole directory structure. + # This is faster and simpler than the method above. def make_basic_app require "rails" require "action_controller/railtie" diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 20797a2b0c..b3cf9ad449 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -1,8 +1,7 @@ require "isolation/abstract_unit" require "railties/shared_tests" -require 'stringio' -require 'rack/test' -require 'rack/file' +require "stringio" +require "rack/test" module RailtiesTest class EngineTest < Test::Unit::TestCase @@ -188,6 +187,7 @@ module RailtiesTest end RUBY + require "rack/file" boot_rails env = Rack::MockRequest.env_for("/") @@ -199,194 +199,6 @@ module RailtiesTest assert_equal Rails.application.routes, env['action_dispatch.routes'] end - test "it allows to set asset_path" do - @plugin.write "lib/bukkits.rb", <<-RUBY - class Bukkits - class Engine < ::Rails::Engine - end - end - RUBY - - - @plugin.write "config/routes.rb", <<-RUBY - Bukkits::Engine.routes.draw do - match "/foo" => "foo#index" - end - RUBY - - @plugin.write "app/controllers/foo_controller.rb", <<-RUBY - class FooController < ActionController::Base - def index - render :index - end - end - RUBY - - @plugin.write "app/views/foo/index.html.erb", <<-ERB - <%= image_path("foo.png") %> - <%= javascript_include_tag("foo") %> - <%= stylesheet_link_tag("foo") %> - ERB - - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - mount Bukkits::Engine => "/bukkits" - end - RUBY - - add_to_config 'config.asset_path = "/omg%s"' - - boot_rails - - # should set asset_path with engine name by default - assert_equal "/bukkits_engine%s", ::Bukkits::Engine.config.asset_path - - ::Bukkits::Engine.config.asset_path = "/bukkits%s" - - get("/bukkits/foo") - stripped_body = last_response.body.split("\n").map(&:strip).join - - expected = "/omg/bukkits/images/foo.png" + - "<script src=\"/omg/bukkits/javascripts/foo.js\" type=\"text/javascript\"></script>" + - "<link href=\"/omg/bukkits/stylesheets/foo.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />" - assert_equal expected, stripped_body - end - - test "default application's asset_path" do - @plugin.write "config/routes.rb", <<-RUBY - Bukkits::Engine.routes.draw do - match "/foo" => "foo#index" - end - RUBY - - @plugin.write "app/controllers/foo_controller.rb", <<-RUBY - class FooController < ActionController::Base - def index - render :inline => '<%= image_path("foo.png") %>' - end - end - RUBY - - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - mount Bukkits::Engine => "/bukkits" - end - RUBY - - boot_rails - - get("/bukkits/foo") - assert_equal "/bukkits/images/foo.png", last_response.body.strip - end - - test "engine's files are served via ActionDispatch::Static" do - add_to_config "config.serve_static_assets = true" - - @plugin.write "lib/bukkits.rb", <<-RUBY - class Bukkits - class Engine < ::Rails::Engine - engine_name :bukkits - end - end - RUBY - - @plugin.write "public/bukkits.html", "/bukkits/bukkits.html" - app_file "public/app.html", "/app.html" - app_file "public/bukkits/file_from_app.html", "/bukkits/file_from_app.html" - - boot_rails - - get("/app.html") - assert_equal File.read(File.join(app_path, "public/app.html")), last_response.body - - get("/bukkits/bukkits.html") - assert_equal File.read(File.join(@plugin.path, "public/bukkits.html")), last_response.body - - get("/bukkits/file_from_app.html") - assert_equal File.read(File.join(app_path, "public/bukkits/file_from_app.html")), last_response.body - end - - test "an applications files are given priority over an engines files when served via ActionDispatch::Static" do - add_to_config "config.serve_static_assets = true" - - @plugin.write "lib/bukkits.rb", <<-RUBY - class Bukkits - class Engine < ::Rails::Engine - engine_name :bukkits - end - end - RUBY - - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - mount Bukkits::Engine => "/bukkits" - end - RUBY - - @plugin.write "public/bukkits.html", "in engine" - - app_file "public/bukkits/bukkits.html", "in app" - - boot_rails - - get('/bukkits/bukkits.html') - - assert_equal 'in app', last_response.body.strip - end - - test "shared engine should include application's helpers and own helpers" do - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - match "/foo" => "bukkits/foo#index", :as => "foo" - match "/foo/show" => "bukkits/foo#show" - match "/foo/bar" => "bukkits/foo#bar" - end - RUBY - - app_file "app/helpers/some_helper.rb", <<-RUBY - module SomeHelper - def something - "Something... Something... Something..." - end - end - RUBY - - @plugin.write "app/helpers/bar_helper.rb", <<-RUBY - module BarHelper - def bar - "It's a bar." - end - end - RUBY - - @plugin.write "app/controllers/bukkits/foo_controller.rb", <<-RUBY - class Bukkits::FooController < ActionController::Base - def index - render :inline => "<%= something %>" - end - - def show - render :text => foo_path - end - - def bar - render :inline => "<%= bar %>" - end - end - RUBY - - boot_rails - - get("/foo") - assert_equal "Something... Something... Something...", last_response.body - - get("/foo/show") - assert_equal "/foo", last_response.body - - get("/foo/bar") - assert_equal "It's a bar.", last_response.body - end - test "isolated engine should include only its own routes and helpers" do @plugin.write "lib/bukkits.rb", <<-RUBY module Bukkits @@ -772,10 +584,7 @@ module RailtiesTest assert_equal Bukkits::Engine.instance, Rails::Engine.find(engine_path) end - test "ensure that engine properly sets assets directories" do - add_to_config("config.action_dispatch.show_exceptions = false") - add_to_config("config.serve_static_assets = true") - + test "gather isolated engine's helpers in Engine#helpers" do @plugin.write "lib/bukkits.rb", <<-RUBY module Bukkits class Engine < ::Rails::Engine @@ -784,56 +593,40 @@ module RailtiesTest end RUBY - @plugin.write "public/stylesheets/foo.css", "" - @plugin.write "public/javascripts/foo.js", "" - - @plugin.write "app/views/layouts/bukkits/application.html.erb", <<-RUBY - <%= stylesheet_link_tag :all %> - <%= javascript_include_tag :all %> - <%= yield %> + app_file "app/helpers/some_helper.rb", <<-RUBY + module SomeHelper + def foo + 'foo' + end + end RUBY - @plugin.write "app/controllers/bukkits/home_controller.rb", <<-RUBY + @plugin.write "app/helpers/bukkits/engine_helper.rb", <<-RUBY module Bukkits - class HomeController < ActionController::Base - def index - render :text => "Good morning!", :layout => "bukkits/application" + module EngineHelper + def bar + 'bar' end end end RUBY - @plugin.write "config/routes.rb", <<-RUBY - Bukkits::Engine.routes.draw do - match "/home" => "home#index" - end - RUBY - - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - mount Bukkits::Engine => "/bukkits" + @plugin.write "app/helpers/engine_helper.rb", <<-RUBY + module EngineHelper + def baz + 'baz' + end end RUBY - require 'rack/test' - extend Rack::Test::Methods + add_to_config("config.action_dispatch.show_exceptions = false") boot_rails - require "#{rails_root}/config/environment" - assert_equal File.join(@plugin.path, "public"), Bukkits::HomeController.assets_dir - assert_equal File.join(@plugin.path, "public/stylesheets"), Bukkits::HomeController.stylesheets_dir - assert_equal File.join(@plugin.path, "public/javascripts"), Bukkits::HomeController.javascripts_dir - - assert_equal File.join(app_path, "public"), ActionController::Base.assets_dir - assert_equal File.join(app_path, "public/stylesheets"), ActionController::Base.stylesheets_dir - assert_equal File.join(app_path, "public/javascripts"), ActionController::Base.javascripts_dir - - get "/bukkits/home" - - assert_match %r{bukkits/stylesheets/foo.css}, last_response.body - assert_match %r{bukkits/javascripts/foo.js}, last_response.body + methods = Bukkits::Engine.helpers.public_instance_methods.sort + expected = ["bar", "baz"] + assert_equal expected, methods end private diff --git a/railties/test/railties/shared_tests.rb b/railties/test/railties/shared_tests.rb index 3eb79d57c8..e975950b85 100644 --- a/railties/test/railties/shared_tests.rb +++ b/railties/test/railties/shared_tests.rb @@ -10,42 +10,16 @@ module RailtiesTest @app ||= Rails.application end - def test_install_migrations_and_assets - @plugin.write "public/javascripts/foo.js", "doSomething()" + def test_serving_sprockets_assets + @plugin.write "app/assets/javascripts/engine.js.erb", "<%= :alert %>();" - @plugin.write "db/migrate/1_create_users.rb", <<-RUBY - class CreateUsers < ActiveRecord::Migration - end - RUBY - - app_file "db/migrate/1_create_sessions.rb", <<-RUBY - class CreateSessions < ActiveRecord::Migration - end - RUBY - - add_to_config "ActiveRecord::Base.timestamped_migrations = false" - - Dir.chdir(app_path) do - `rake bukkits:install` - assert File.exists?("#{app_path}/db/migrate/2_create_users.rb") - assert File.exists?(app_path("public/bukkits/javascripts/foo.js")) - end - end - - def test_copying_assets - @plugin.write "public/javascripts/foo.js", "doSomething()" - @plugin.write "public/stylesheets/foo.css", "h1 { font-size: 10000px }" - @plugin.write "public/images/img.png", "" - - Dir.chdir(app_path) do - `rake bukkits:install:assets --trace` + boot_rails + require 'rack/test' + require 'coffee_script' + extend Rack::Test::Methods - assert File.exists?(app_path("public/bukkits/javascripts/foo.js")) - assert_equal "doSomething()\n", File.read(app_path("public/bukkits/javascripts/foo.js")) - assert File.exists?(app_path("public/bukkits/stylesheets/foo.css")) - assert_equal "h1 { font-size: 10000px }\n", File.read(app_path("public/bukkits/stylesheets/foo.css")) - assert File.exists?(app_path("public/bukkits/images/img.png")) - end + get "/assets/engine.js" + assert_match "alert();", last_response.body end def test_copying_migrations diff --git a/tasks/release.rb b/tasks/release.rb index a605fed160..01950b227d 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -61,6 +61,32 @@ directory "dist" end end +namespace :changelog do + task :release_date do + FRAMEWORKS.each do |fw| + require 'date' + replace = '\1(' + Date.today.strftime('%B %d, %Y') + ')' + fname = File.join fw, 'CHANGELOG' + + contents = File.read(fname).sub(/^([^(]*)\(unreleased\)/, replace) + File.open(fname, 'wb') { |f| f.write contents } + end + end + + task :release_summary do + FRAMEWORKS.each do |fw| + puts "## #{fw}" + fname = File.join fw, 'CHANGELOG' + contents = File.readlines fname + contents.shift + changes = [] + changes << contents.shift until contents.first =~ /^\*Rails \d+\.\d+\.\d+/ + puts changes.reject { |change| change.strip.empty? }.join + puts + end + end +end + namespace :all do task :build => FRAMEWORKS.map { |f| "#{f}:build" } + ['rails:build'] task :install => FRAMEWORKS.map { |f| "#{f}:install" } + ['rails:install'] |