diff options
130 files changed, 1257 insertions, 601 deletions
@@ -5,7 +5,7 @@ gemspec if ENV['AREL'] gem "arel", :path => ENV['AREL'] else - gem "arel", '~> 2.1.0' + gem "arel", '~> 2.1.3' end gem "coffee-script" @@ -19,6 +19,7 @@ group :doc do gem "rdoc", "~> 3.4" gem "horo", "= 1.0.3" gem "RedCloth", "~> 4.2" if RUBY_VERSION < "1.9.3" + gem "w3c_validators" end # AS diff --git a/RAILS_VERSION b/RAILS_VERSION index 90040cdc46..dd8b7cd23b 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -3.1.0.rc1 +3.2.0.beta diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 5654e54d7a..b74ba3cb0d 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,9 +1,9 @@ module ActionMailer module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index f4b6464bdc..b0f7d0bc11 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,9 @@ *Rails 3.2.0 (unreleased)* +* send_file now guess the mime type [Esad Hajdarevic] + +* Mime type entries for PDF, ZIP and other formats were added [Esad Hajdarevic] + * Generate hidden input before select with :multiple option set to true. This is useful when you rely on the fact that when no options is set, the state of select will be sent to rails application. Without hidden field @@ -26,6 +30,8 @@ *Rails 3.1.0 (unreleased)* +* Make sure respond_with with :js tries to render a template in all cases [José Valim] + * json_escape will now return a SafeBuffer string if it receives SafeBuffer string [tenderlove] * Make sure escape_js returns SafeBuffer string if it receives SafeBuffer string [Prem Sichanugrist] @@ -59,8 +65,6 @@ You can read more about this change in http://groups.google.com/group/rubyonrails-security/browse_thread/thread/2e516e7acc96c4fb -* Added 'ActionView::Helpers::FormHelper.fields_for_with_index', similar to fields_for but allows to have access to the current iteration index [Jorge Bejar] - * Warn if we cannot verify CSRF token authenticity [José Valim] * Allow AM/PM format in datetime selectors [Aditya Sanghi] diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 16b1e78b4d..642fbcb8e6 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -18,13 +18,13 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('rack-cache', '~> 1.0.1') + s.add_dependency('rack-cache', '~> 1.0.2') s.add_dependency('builder', '~> 3.0.0') s.add_dependency('i18n', '~> 0.6') s.add_dependency('rack', '~> 1.3.0') s.add_dependency('rack-test', '~> 0.6.0') s.add_dependency('rack-mount', '~> 0.8.1') s.add_dependency('sprockets', '= 2.0.0.beta.10') - s.add_dependency('tzinfo', '~> 0.3.27') + s.add_dependency('tzinfo', '~> 0.3.29') s.add_dependency('erubis', '~> 2.7.0') end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 8d813a8e38..35e29398e6 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -10,7 +10,7 @@ module ActionController format = payload[:format] format = format.to_s.upcase if format.is_a?(Symbol) - info " Processing by #{payload[:controller]}##{payload[:action]} as #{format}" + info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}" info " Parameters: #{params.inspect}" unless params.empty? end @@ -20,10 +20,11 @@ module ActionController status = payload[:status] if status.nil? && payload[:exception].present? - status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil - end + status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil + end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? + message << "\n" info(message) end @@ -59,4 +60,4 @@ module ActionController end end -ActionController::LogSubscriber.attach_to :action_controller
\ No newline at end of file +ActionController::LogSubscriber.attach_to :action_controller diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 623a9873fc..50827d8107 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -26,8 +26,11 @@ module ActionController #:nodoc: # 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>:type</tt> - specifies an HTTP content type. + # You can specify either a string or a symbol for a registered type register with + # <tt>Mime::Type.register</tt>, for example :json + # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, default type 'application/octet-stream' will be used. # * <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'. @@ -84,6 +87,8 @@ module ActionController #:nodoc: # * <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 + # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, default type 'application/octet-stream' will be used. # * <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'. @@ -108,6 +113,8 @@ module ActionController #:nodoc: private def send_file_headers!(options) + type_provided = options.has_key?(:type) + options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) [:type, :disposition].each do |arg| raise ArgumentError, ":#{arg} option required" if options[arg].nil? @@ -123,6 +130,10 @@ module ActionController #:nodoc: raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension self.content_type = extension else + if !type_provided && options[:filename] + # If type wasn't provided, try guessing from file extension. + content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.tr('.','')) || content_type + end self.content_type = content_type end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index ebadb29ea7..f3b7357e64 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -162,6 +162,11 @@ module ActionController #:nodoc: navigation_behavior(e) end + # to_js simply tries to render a template. If no template is found, raises the error. + def to_js + default_render + end + # All other formats follow the procedure below. First we try to render a # template, if the template is not available, we verify if the resource # responds to :to_format and display it. diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index d2ba052c8d..f0c29825ba 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -4,7 +4,6 @@ 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 diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 68f37d2f65..3da4f91051 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -7,6 +7,15 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv + +Mime::Type.register "image/png", :png, [], %w(png) +Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe) +Mime::Type.register "image/gif", :gif, [], %w(gif) +Mime::Type.register "image/bmp", :bmp, [], %w(bmp) +Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff) + +Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) + Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) Mime::Type.register "application/rss+xml", :rss Mime::Type.register "application/atom+xml", :atom @@ -19,5 +28,8 @@ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form # http://www.json.org/JSONRequest.html Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) +Mime::Type.register "application/pdf", :pdf, [], %w(pdf) +Mime::Type.register "application/zip", :zip, [], %w(zip) + # Create Mime::ALL but do not add it to the SET. Mime::ALL = Mime::Type.new("*/*", :all, []) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 8cee9ecdc4..1c312f2587 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -129,6 +129,11 @@ module ActionDispatch @cookies[name.to_s] end + def key?(name) + @cookies.key?(name.to_s) + end + alias :has_key? :key? + def update(other_hash) @cookies.update other_hash.stringify_keys self diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 82c4fadb50..49aef0bf72 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -182,10 +182,12 @@ module ActionDispatch if record.is_a?(Symbol) || record.is_a?(String) route << record - else + elsif record route << ActiveModel::Naming.route_key(record) route = [route.join("_").singularize] if inflection == :singular route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + else + raise ArgumentError, "Nil location provided. Can't build URI." end route << routing_type(options) diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 367c295cf5..f668b81b45 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -39,7 +39,7 @@ module ActionDispatch # # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary) def fixture_file_upload(path, mime_type = nil, binary = false) - fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path) + fixture_path = self.class.fixture_path if self.class.respond_to?(:fixture_path) Rack::Test::UploadedFile.new("#{fixture_path}#{path}", mime_type, binary) end end diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index fcf0eb9565..add6b56425 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,9 +1,9 @@ module ActionPack module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/actionpack/lib/action_view/asset_paths.rb b/actionpack/lib/action_view/asset_paths.rb index 2b1fe545a6..4dd02755d3 100644 --- a/actionpack/lib/action_view/asset_paths.rb +++ b/actionpack/lib/action_view/asset_paths.rb @@ -4,7 +4,7 @@ module ActionView class AssetPaths #:nodoc: attr_reader :config, :controller - def initialize(config, controller) + def initialize(config, controller = nil) @config = config @controller = controller end @@ -13,21 +13,29 @@ module ActionView # 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) + # + # When include_host is true and the asset host does not specify the protocol + # the protocol parameter specifies how the protocol will be added. + # When :relative (default), the protocol will be determined by the client using current protocol + # When :request, the protocol will be the request protocol + # Otherwise, the protocol is used (E.g. :http, :https, etc) + def compute_public_path(source, dir, ext = nil, include_host = true, protocol = nil) source = source.to_s return source if is_uri?(source) source = rewrite_extension(source, dir, ext) if ext 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 = rewrite_relative_url_root(source, relative_url_root) if has_request? + source = rewrite_host_and_protocol(source, protocol) if include_host source end + # Return the filesystem path for the source + def compute_source_path(source, dir, ext) + source = rewrite_extension(source, dir, ext) if ext + File.join(config.assets_dir, dir, source) + end + def is_uri?(path) path =~ %r{^[-a-z]+://|^cid:|^//} end @@ -46,13 +54,45 @@ module ActionView 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 + def has_request? + controller.respond_to?(:request) + end + + def rewrite_host_and_protocol(source, protocol = nil) host = compute_asset_host(source) - if has_request && host && !is_uri?(host) - host = "#{controller.request.protocol}#{host}" + if host && !is_uri?(host) + if (protocol || default_protocol) == :request && !has_request? + host = nil + else + host = "#{compute_protocol(protocol)}#{host}" + end + end + host.nil? ? source : "#{host}#{source}" + end + + def compute_protocol(protocol) + protocol ||= default_protocol + case protocol + when :relative + "//" + when :request + unless @controller + invalid_asset_host!("The protocol requested was :request. Consider using :relative instead.") + end + @controller.request.protocol + else + "#{protocol}://" end - "#{host}#{source}" + end + + def default_protocol + protocol = @config.action_controller.default_asset_host_protocol if @config.action_controller.present? + protocol ||= @config.default_asset_host_protocol + protocol || (has_request? ? :request : :relative) + end + + def invalid_asset_host!(help_message) + raise ActionController::RoutingError, "This asset host cannot be computed without a request in scope. #{help_message}" end # Pick an asset host for this source. Returns +nil+ if no host is set, @@ -61,19 +101,45 @@ module ActionView # or the value returned from invoking call on an object responding to call # (proc or otherwise). def compute_asset_host(source) - if host = config.asset_host + if host = asset_host_config if 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) + args = [source] + arity = arity_of(host) + if arity > 1 && !has_request? + invalid_asset_host!("Remove the second argument to your asset_host Proc if you do not need the request.") end + args << current_request if (arity > 1 || arity < 0) && has_request? + host.call(*args) else (host =~ /%d/) ? host % (source.hash % 4) : host end end end + + def relative_url_root + config = controller.config if controller.respond_to?(:config) + config ||= config.action_controller if config.action_controller.present? + config ||= config + config.relative_url_root + end + + def asset_host_config + if config.action_controller.present? + config.action_controller.asset_host + else + config.asset_host + end + end + + # Returns the current request if one exists. + def current_request + controller.request if has_request? + end + + # Returns the arity of a callable + def arity_of(callable) + callable.respond_to?(:arity) ? callable.arity : callable.method(:call).arity + end + end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 7970176d37..0c3f011c92 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -347,7 +347,7 @@ module ActionView src = options[:src] = path_to_image(source) unless src =~ /^cid:/ - options[:alt] = options.fetch(:alt){ File.basename(src, '.*').capitalize } + options[:alt] = options.fetch(:alt){ image_alt(src) } end if size = options.delete(:size) @@ -362,6 +362,10 @@ module ActionView tag("img", options) end + def image_alt(src) + File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').capitalize + end + # Returns an html video tag for the +sources+. If +sources+ is a string, # a single video tag will be returned. If +sources+ is an array, a video # tag with nested source tags for each source will be returned. The 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 e4662a2919..3c05173a1b 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 @@ -60,12 +60,16 @@ module ActionView private - def path_to_asset(source, include_host = true) - asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host) + def path_to_asset(source, include_host = true, protocol = nil) + asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host, protocol) + end + + def path_to_asset_source(source) + asset_paths.compute_source_path(source, asset_name.to_s.pluralize, extension) end def compute_paths(*args) - expand_sources(*args).collect { |source| asset_paths.compute_public_path(source, asset_name.pluralize, extension, false) } + expand_sources(*args).collect { |source| path_to_asset_source(source) } end def expand_sources(sources, recursive) @@ -92,7 +96,7 @@ module ActionView def ensure_sources!(sources) sources.each do |source| - asset_file_path!(path_to_asset(source, false)) + asset_file_path!(path_to_asset_source(source)) end end @@ -123,19 +127,14 @@ module ActionView # Set mtime to the latest of the combined files to allow for # consistent ETag without a shared filesystem. - mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max + mt = asset_paths.map { |p| File.mtime(asset_file_path!(p)) }.max File.utime(mt, mt, joined_asset_path) end - def asset_file_path(path) - File.join(config.assets_dir, path.split('?').first) - end - - def asset_file_path!(path, error_if_file_is_uri = false) - if asset_paths.is_uri?(path) + def asset_file_path!(absolute_path, error_if_file_is_uri = false) + if asset_paths.is_uri?(absolute_path) raise(Errno::ENOENT, "Asset file #{path} is uri and cannot be merged into single file") if error_if_file_is_uri else - absolute_path = asset_file_path(path) raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) return absolute_path end 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 12a304b395..8b35aa8896 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,3 +1,4 @@ +require 'thread' require 'active_support/core_ext/file' module ActionView 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 e4f11c9bc7..8c25d38bbd 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 @@ -16,7 +16,8 @@ module ActionView end def asset_tag(source, options) - tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source)) }.merge(options), false, false) + # We force the :request protocol here to avoid a double-download bug in IE7 and IE8 + tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source, true, :request)) }.merge(options), false, false) end def custom_dir @@ -52,7 +53,7 @@ module ActionView # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). # Full paths from the document root will be passed through. # Used internally by +stylesheet_link_tag+ to build the stylesheet path. - # + # # ==== Examples # stylesheet_path "style" # => /stylesheets/style.css # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css @@ -60,11 +61,7 @@ module ActionView # 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 - asset_path(source, 'css') - else - asset_paths.compute_public_path(source, 'stylesheets', 'css') - end + asset_paths.compute_public_path(source, 'stylesheets', 'css', true, :request) end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 0ef2357368..3a30263b49 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -4,6 +4,7 @@ require 'action_view/helpers/tag_helper' require 'action_view/helpers/form_tag_helper' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/module/method_names' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/array/extract_options' @@ -567,18 +568,13 @@ module ActionView # ... # <% end %> # - # In addition, you may want to have access to the current iteration index. - # In that case, you can use a similar method called fields_for_with_index - # which receives a block with an extra parameter: + # When projects is already an association on Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: # - # <%= form_for @person do |person_form| %> - # ... - # <%= person_form.fields_for_with_index :projects do |project_fields, index| %> - # Position: <%= index %> - # Name: <%= project_fields.text_field :name %> - # <% end %> - # ... - # <% end %> + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects + # end # # If you want to destroy any of the associated models through the # form, you have to enable it first using the <tt>:allow_destroy</tt> @@ -1153,7 +1149,7 @@ module ActionView options["name"] ||= tag_name_with_index(@auto_index) options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } else - options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '') + options["name"] ||= tag_name + (options['multiple'] ? '[]' : '') options["id"] = options.fetch("id"){ tag_id } end end @@ -1233,13 +1229,6 @@ module ActionView RUBY_EVAL end - # Check +fields_for+ for docs and examples. - def fields_for_with_index(record_name, record_object = nil, fields_options = {}, &block) - index = fields_options[:index] || options[:child_index] || nested_child_index(@object_name) - block_with_index = Proc.new{ |obj| block.call(obj, index) } - fields_for(record_name, record_object, fields_options, &block_with_index) - end - def fields_for(record_name, record_object = nil, fields_options = {}, &block) fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options? fields_options[:builder] ||= options[:builder] diff --git a/actionpack/lib/action_view/log_subscriber.rb b/actionpack/lib/action_view/log_subscriber.rb index 29ffbd6fdd..bf90d012bf 100644 --- a/actionpack/lib/action_view/log_subscriber.rb +++ b/actionpack/lib/action_view/log_subscriber.rb @@ -4,7 +4,7 @@ module ActionView # Provides functionality so that Rails can output logs from Action View. class LogSubscriber < ActiveSupport::LogSubscriber def render_template(event) - message = "Rendered #{from_rails_root(event.payload[:identifier])}" + message = " Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] message << (" (%.1fms)" % event.duration) info(message) diff --git a/railties/lib/rails/tasks/assets.rake b/actionpack/lib/sprockets/assets.rake index 0236350576..0236350576 100644 --- a/railties/lib/rails/tasks/assets.rake +++ b/actionpack/lib/sprockets/assets.rake diff --git a/actionpack/lib/sprockets/helpers/rails_helper.rb b/actionpack/lib/sprockets/helpers/rails_helper.rb index 9f1f0f3b68..7d709de9e2 100644 --- a/actionpack/lib/sprockets/helpers/rails_helper.rb +++ b/actionpack/lib/sprockets/helpers/rails_helper.rb @@ -1,4 +1,4 @@ -require "action_view/helpers" +require "action_view" module Sprockets module Helpers @@ -10,7 +10,16 @@ module Sprockets @asset_paths ||= begin config = self.config if respond_to?(:config) controller = self.controller if respond_to?(:controller) - RailsHelper::AssetPaths.new(config, controller) + config ||= Rails.application.config + if config.action_controller.present? + config.action_controller.default_asset_host_protocol ||= :relative + else + config.default_asset_host_protocol ||= :relative + end + paths = RailsHelper::AssetPaths.new(config, controller) + paths.asset_environment = asset_environment + paths.asset_prefix = asset_prefix + paths end end @@ -50,7 +59,7 @@ module Sprockets 'rel' => "stylesheet", 'type' => "text/css", 'media' => "screen", - 'href' => asset_path(source, 'css', body) + 'href' => asset_path(source, 'css', body, :request) }.merge(options.stringify_keys) tag 'link', tag_options @@ -58,35 +67,59 @@ module Sprockets end.join("\n").html_safe end + def asset_path(source, default_ext = nil, body = false, protocol = nil) + source = source.logical_path if source.respond_to?(:logical_path) + path = asset_paths.compute_public_path(source, 'assets', default_ext, true, protocol) + body ? "#{path}?body=1" : path + end + private def debug_assets? params[:debug_assets] == '1' || params[:debug_assets] == 'true' end - def asset_path(source, default_ext = nil, body = false) - source = source.logical_path if source.respond_to?(:logical_path) - path = asset_paths.compute_public_path(source, Rails.application.config.assets.prefix, default_ext, true) - body ? "#{path}?body=1" : path + # Override to specify an alternative prefix for asset path generation. + # When combined with a custom +asset_environment+, this can be used to + # implement themes that can take advantage of the asset pipeline. + # + # If you only want to change where the assets are mounted, refer to + # +config.assets.prefix+ instead. + def asset_prefix + Rails.application.config.assets.prefix + end + + # Override to specify an alternative asset environment for asset + # path generation. The environment should already have been mounted + # at the prefix returned by +asset_prefix+. + def asset_environment + Rails.application.assets end class AssetPaths < ::ActionView::AssetPaths #:nodoc: - def compute_public_path(source, dir, ext=nil, include_host=true) - super(source, Rails.application.config.assets.prefix, ext, include_host) + attr_accessor :asset_environment, :asset_prefix + + def compute_public_path(source, dir, ext=nil, include_host=true, protocol=nil) + super(source, asset_prefix, ext, include_host, protocol) + end + + # Return the filesystem path for the source + def compute_source_path(source, ext) + asset_for(source, ext) end def asset_for(source, ext) source = source.to_s return nil if is_uri?(source) source = rewrite_extension(source, nil, ext) - assets[source] + asset_environment[source] end def rewrite_asset_path(source, dir) if source[0] == ?/ source else - assets.path(source, performing_caching?, dir) + asset_environment.path(source, performing_caching?, dir) end end @@ -98,13 +131,9 @@ module Sprockets 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 + config.action_controller.present? ? config.action_controller.perform_caching : config.perform_caching end end end diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb index ab5101f6fc..d1e97c594e 100644 --- a/actionpack/lib/sprockets/railtie.rb +++ b/actionpack/lib/sprockets/railtie.rb @@ -1,16 +1,12 @@ module Sprockets autoload :Helpers, "sprockets/helpers" + # TODO: Get rid of config.assets.enabled class Railtie < ::Rails::Railtie - def self.using_coffee? - require 'coffee-script' - defined?(CoffeeScript) - rescue LoadError - false + rake_tasks do + load "sprockets/assets.rake" end - config.app_generators.javascript_engine :coffee if using_coffee? - # Configure ActionController to use sprockets. initializer "sprockets.set_configs", :after => "action_controller.set_configs" do |app| ActiveSupport.on_load(:action_controller) do diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb index f9e47d5118..20d11377f6 100644 --- a/actionpack/test/activerecord/polymorphic_routes_test.rb +++ b/actionpack/test/activerecord/polymorphic_routes_test.rb @@ -87,6 +87,14 @@ class PolymorphicRoutesTest < ActionController::TestCase end end + def test_with_nil + with_test_routes do + assert_raise ArgumentError, "Nil location provided. Can't build URI." do + polymorphic_url(nil) + end + end + end + def test_with_record with_test_routes do @project.save diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index 26270571cf..afb2d39955 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -509,7 +509,7 @@ end class RespondWithController < ActionController::Base respond_to :html, :json respond_to :xml, :except => :using_resource_with_block - respond_to :js, :only => [ :using_resource_with_block, :using_resource ] + respond_to :js, :only => [ :using_resource_with_block, :using_resource, :using_hash_resource ] def using_resource respond_with(resource) @@ -575,11 +575,6 @@ protected def resource Customer.new("david", request.delete? ? nil : 13) end - - def _render_js(js, options) - self.content_type ||= Mime::JS - self.response_body = js.respond_to?(:to_js) ? js.to_js : js - end end class InheritedRespondWithController < RespondWithController @@ -638,6 +633,20 @@ class RespondWithControllerTest < ActionController::TestCase end end + def test_using_resource_with_js_simply_tries_to_render_the_template + @request.accept = "text/javascript" + get :using_resource + assert_equal "text/javascript", @response.content_type + assert_equal "alert(\"Hi\");", @response.body + end + + def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found + @request.accept = "text/javascript" + assert_raise ActionView::MissingTemplate do + get :using_hash_resource + end + end + def test_using_hash_resource @request.accept = "application/xml" get :using_hash_resource @@ -650,6 +659,13 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal %Q[{"result":{"name":"david","id":13}}], @response.body end + def test_using_hash_resource_with_post + @request.accept = "application/json" + assert_raise ArgumentError, "Nil location provided. Can't build URI." do + post :using_hash_resource + end + end + def test_using_resource_with_block @request.accept = "*/*" get :using_resource_with_block diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index c7c8360ae6..8f885ff28e 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -138,6 +138,25 @@ class SendFileTest < ActionController::TestCase @controller.headers = {} assert_raise(ArgumentError){ @controller.send(:send_file_headers!, options) } end + + def test_send_file_headers_guess_type_from_extension + { + 'image.png' => 'image/png', + 'image.jpeg' => 'image/jpeg', + 'image.jpg' => 'image/jpeg', + 'image.tif' => 'image/tiff', + 'image.gif' => 'image/gif', + 'movie.mpg' => 'video/mpeg', + 'file.zip' => 'application/zip', + 'file.unk' => 'application/octet-stream', + 'zip' => 'application/octet-stream' + }.each do |filename,expected_type| + options = { :filename => filename } + @controller.headers = {} + @controller.send(:send_file_headers!, options) + assert_equal expected_type, @controller.content_type + end + end %w(file data).each do |method| define_method "test_send_#{method}_status" do diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index 6265e78030..043d44500a 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -649,6 +649,13 @@ XML end + def test_fixture_path_is_accessed_from_self_instead_of_active_support_test_case + TestTest.stubs(:fixture_path).returns(FILES_DIR) + + uploaded_file = fixture_file_upload('/mona_lisa.jpg', 'image/png') + assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + end + def test_test_uploaded_file_with_binary filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 8e5fd97cc6..fb67ecb07d 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -148,6 +148,15 @@ class CookiesTest < ActionController::TestCase @request.host = "www.nextangle.com" end + def test_key_methods + assert !request.cookie_jar.key?(:foo) + assert !request.cookie_jar.has_key?("foo") + + request.cookie_jar[:foo] = :bar + assert request.cookie_jar.key?(:foo) + assert request.cookie_jar.has_key?("foo") + end + def test_setting_cookie get :authenticate assert_cookie_header "user_name=david; path=/" diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 11cf68fdb3..851fb59d51 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -52,7 +52,7 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse application with trailing star" do accept = "application/*" - expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::RSS, Mime::ATOM, Mime::YAML, Mime::URL_ENCODED_FORM, Mime::JSON, Mime::PDF] + expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::RSS, Mime::ATOM, Mime::YAML, Mime::URL_ENCODED_FORM, Mime::JSON, Mime::PDF, Mime::ZIP] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end @@ -86,12 +86,12 @@ class MimeTypeTest < ActiveSupport::TestCase test "custom type" do begin - Mime::Type.register("image/gif", :gif) + Mime::Type.register("image/foo", :foo) assert_nothing_raised do - assert_equal Mime::GIF, Mime::SET.last + assert_equal Mime::FOO, Mime::SET.last end ensure - Mime::Type.unregister(:gif) + Mime::Type.unregister(:FOO) end end diff --git a/actionpack/test/fixtures/respond_with/using_resource.js.erb b/actionpack/test/fixtures/respond_with/using_resource.js.erb new file mode 100644 index 0000000000..4417680bce --- /dev/null +++ b/actionpack/test/fixtures/respond_with/using_resource.js.erb @@ -0,0 +1 @@ +alert("Hi");
\ No newline at end of file diff --git a/actionpack/test/fixtures/sprockets/alternate/stylesheets/style.css b/actionpack/test/fixtures/sprockets/alternate/stylesheets/style.css new file mode 100644 index 0000000000..bfb90bfa48 --- /dev/null +++ b/actionpack/test/fixtures/sprockets/alternate/stylesheets/style.css @@ -0,0 +1 @@ +/* Different from other style.css */
\ No newline at end of file diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 2abc806e97..df61901b44 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -424,6 +424,14 @@ class AssetTagHelperTest < ActionView::TestCase PathToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end + def test_image_alt + [nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix| + assert_equal 'Rails', image_alt("#{prefix}rails.png") + assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png") + assert_equal 'Avatar-0000', image_alt("#{prefix}avatar-0000.png") + end + end + def test_image_tag ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end @@ -854,7 +862,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_caching_stylesheet_link_tag_when_caching_on ENV["RAILS_ASSET_ID"] = "" - @controller.config.asset_host = 'http://a0.example.com' + @controller.config.asset_host = 'a0.example.com' config.perform_caching = true assert_dom_equal( @@ -964,7 +972,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_caching_stylesheet_link_tag_when_caching_on_with_proc_asset_host ENV["RAILS_ASSET_ID"] = "" - @controller.config.asset_host = Proc.new { |source| "http://a#{source.length}.example.com" } + @controller.config.asset_host = Proc.new { |source| "a#{source.length}.example.com" } config.perform_caching = true assert_equal '/stylesheets/styles.css'.length, 23 @@ -1091,13 +1099,23 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase end def test_should_compute_proper_path_with_asset_host - @controller.config.asset_host = "http://assets.example.com" + @controller.config.asset_host = "assets.example.com" assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag) - assert_dom_equal(%(http://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr")) - assert_dom_equal(%(http://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style")) - assert_dom_equal(%(http://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png")) - assert_dom_equal(%(<img alt="Mouse" onmouseover="this.src='http://assets.example.com/collaboration/hieraki/images/mouse_over.png'" onmouseout="this.src='http://assets.example.com/collaboration/hieraki/images/mouse.png'" src="http://assets.example.com/collaboration/hieraki/images/mouse.png" />), image_tag("mouse.png", :mouseover => "/images/mouse_over.png")) - assert_dom_equal(%(<img alt="Mouse2" onmouseover="this.src='http://assets.example.com/collaboration/hieraki/images/mouse_over2.png'" onmouseout="this.src='http://assets.example.com/collaboration/hieraki/images/mouse2.png'" src="http://assets.example.com/collaboration/hieraki/images/mouse2.png" />), image_tag("mouse2.png", :mouseover => image_path("mouse_over2.png"))) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png")) + assert_dom_equal(%(<img alt="Mouse" onmouseover="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse_over.png'" onmouseout="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse.png'" src="gopher://assets.example.com/collaboration/hieraki/images/mouse.png" />), image_tag("mouse.png", :mouseover => "/images/mouse_over.png")) + assert_dom_equal(%(<img alt="Mouse2" onmouseover="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse_over2.png'" onmouseout="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse2.png'" src="gopher://assets.example.com/collaboration/hieraki/images/mouse2.png" />), image_tag("mouse2.png", :mouseover => image_path("mouse_over2.png"))) + end + + def test_should_compute_proper_path_with_asset_host_and_default_protocol + @controller.config.asset_host = "assets.example.com" + @controller.config.default_asset_host_protocol = :request + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png")) + assert_dom_equal(%(<img alt="Mouse" onmouseover="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse_over.png'" onmouseout="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse.png'" src="gopher://assets.example.com/collaboration/hieraki/images/mouse.png" />), image_tag("mouse.png", :mouseover => "/images/mouse_over.png")) + assert_dom_equal(%(<img alt="Mouse2" onmouseover="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse_over2.png'" onmouseout="this.src='gopher://assets.example.com/collaboration/hieraki/images/mouse2.png'" src="gopher://assets.example.com/collaboration/hieraki/images/mouse2.png" />), image_tag("mouse2.png", :mouseover => image_path("mouse_over2.png"))) end def test_should_ignore_asset_host_on_complete_url @@ -1115,12 +1133,12 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path('xml.png')) end - def test_asset_host_without_protocol_should_use_request_protocol + def test_asset_host_without_protocol_should_be_protocol_relative @controller.config.asset_host = 'a.example.com' assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_path('xml.png') end - def test_asset_host_without_protocol_should_use_request_protocol_even_if_path_present + def test_asset_host_without_protocol_should_be_protocol_relative_even_if_path_present @controller.config.asset_host = 'a.example.com/files/go/here' assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_path('xml.png') end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index bf65a9359b..cc3d2cddf7 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -974,22 +974,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_with_index_and_parent_fields - form_for(@post, :index => 1) do |c| - concat c.text_field(:title) - concat c.fields_for_with_index('comment', @comment, :index => 1) { |r, comment_index| - concat r.text_field(:name, "data-index" => comment_index) - } - end - - expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do - "<input name='post[1][title]' size='30' type='text' id='post_1_title' value='Hello World' />" + - "<input name='post[1][comment][1][name]' size='30' type='text' id='post_1_comment_1_name' value='new comment' data-index='1' />" - end - - assert_dom_equal expected, output_buffer - end - def test_form_for_with_index_and_nested_fields_for output_buffer = form_for(@post, :index => 1) do |f| concat f.fields_for(:comment, @post) { |c| @@ -1046,20 +1030,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_with_index_radio_button - form_for(@post) do |f| - concat f.fields_for_with_index(:comment, @post, :index => 5) { |c, index| - concat c.radio_button(:title, "hello", "data-index" => index) - } - end - - expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do - "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' data-index='5' />" - end - - assert_dom_equal expected, output_buffer - end - def test_nested_fields_for_with_auto_index_on_both form_for(@post, :as => "post[]") do |f| concat f.fields_for("comment[]", @post) { |c| @@ -1259,29 +1229,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_with_existing_records_on_a_nested_attributes_collection_association - @post.comments = Array.new(2) { |id| Comment.new(id + 1) } - - form_for(@post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for_with_index(:comments, comment) { |cf, index| - concat cf.text_field(:name, "data-index" => index) - } - end - end - - expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" data-index="0" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" data-index="1" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' - end - - assert_dom_equal expected, output_buffer - end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) @@ -1309,33 +1256,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id - @post.comments = Array.new(2) { |id| Comment.new(id + 1) } - @post.author = Author.new(321) - - form_for(@post) do |f| - concat f.text_field(:title) - concat f.fields_for(:author) { |af| - concat af.text_field(:name) - } - @post.comments.each do |comment| - concat f.fields_for_with_index(:comments, comment, :include_id => false) { |cf, index| - concat cf.text_field(:name, 'data-index' => index) - } - end - end - - expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" data-index="0" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" data-index="1" />' - end - - assert_dom_equal expected, output_buffer - end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) @@ -1457,28 +1377,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_with_new_records_on_a_nested_attributes_collection_association - @post.comments = [Comment.new, Comment.new] - - form_for(@post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for_with_index(:comments, comment) { |cf, index| - concat cf.text_field(:name, "data-index" => index) - } - end - end - - expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="new comment" data-index="0" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" data-index="1" />' - end - - assert_dom_equal expected, output_buffer - end - - def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new(321), Comment.new] @@ -1501,29 +1399,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_with_existing_and_new_records_on_a_nested_attributes_collection_association - @post.comments = [Comment.new(321), Comment.new] - - form_for(@post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for_with_index(:comments, comment) { |cf, index| - concat cf.text_field(:name, "data-index" => index) - } - end - end - - expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" data-index="0" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" data-index="1" />' - end - - assert_dom_equal expected, output_buffer - end - - def test_nested_fields_for_with_an_empty_supplied_attributes_collection form_for(@post) do |f| concat f.text_field(:title) diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index f3969895ae..a4599a3f00 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -378,6 +378,13 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_without_multiple + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"></select>", + select(:post, :category, "", {}, :multiple => false) + ) + end + def test_select_with_boolean_method @post = Post.new @post.allow_comments = false diff --git a/actionpack/test/template/sprockets_helper_test.rb b/actionpack/test/template/sprockets_helper_test.rb index b1317d0a35..f11d1bba15 100644 --- a/actionpack/test/template/sprockets_helper_test.rb +++ b/actionpack/test/template/sprockets_helper_test.rb @@ -31,18 +31,21 @@ class SprocketsHelperTest < ActionView::TestCase Rails.stubs(:application).returns(application) application.stubs(:config).returns(config) application.stubs(:assets).returns(@assets) - - config.perform_caching = true + @config = config + @config.action_controller ||= ActiveSupport::InheritableOptions.new + @config.perform_caching = true end def url_for(*args) "http://www.example.com" end - test "asset path" do + test "asset_path" do assert_equal "/assets/logo-9c0a079bdd7701d7e729bd956823d153.png", asset_path("logo.png") + end + test "asset_path with root relative assets" do assert_equal "/images/logo", asset_path("/images/logo") assert_equal "/images/logo.gif", @@ -50,13 +53,73 @@ class SprocketsHelperTest < ActionView::TestCase assert_equal "/dir/audio", asset_path("/dir/audio") - + end + + test "asset_path with absolute urls" do 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 "with a simple asset host the url should default to protocol relative" do + @controller.config.asset_host = "assets-%d.example.com" + assert_match %r{//assets-\d.example.com/assets/logo-[0-9a-f]+.png}, + asset_path("logo.png") + end + + test "with a simple asset host the url can be changed to use the request protocol" do + @controller.config.asset_host = "assets-%d.example.com" + @controller.config.default_asset_host_protocol = :request + assert_match %r{http://assets-\d.example.com/assets/logo-[0-9a-f]+.png}, + asset_path("logo.png") + end + + test "With a proc asset host that returns no protocol the url should be protocol relative" do + @controller.config.asset_host = Proc.new do |asset| + "assets-999.example.com" + end + assert_match %r{//assets-999.example.com/assets/logo-[0-9a-f]+.png}, + asset_path("logo.png") + end + + test "with a proc asset host that returns a protocol the url use it" do + @controller.config.asset_host = Proc.new do |asset| + "http://assets-999.example.com" + end + assert_match %r{http://assets-999.example.com/assets/logo-[0-9a-f]+.png}, + asset_path("logo.png") + end + + test "stylesheets served with a controller in scope can access the request" do + config.asset_host = Proc.new do |asset, request| + assert_not_nil request + "http://assets-666.example.com" + end + assert_match %r{http://assets-666.example.com/assets/logo-[0-9a-f]+.png}, + asset_path("logo.png") + end + + test "stylesheets served without a controller in scope cannot access the request" do + remove_instance_variable("@controller") + @config.action_controller.asset_host = Proc.new do |asset, request| + fail "This should not have been called." + end + assert_raises ActionController::RoutingError do + asset_path("logo.png") + end + end + + test "stylesheets served without a controller in do not use asset hosts when the default protocol is :request" do + remove_instance_variable("@controller") + @config.action_controller.asset_host = "assets-%d.example.com" + @config.action_controller.default_asset_host_protocol = :request + @config.action_controller.perform_caching = true + + assert_equal "/assets/logo-9c0a079bdd7701d7e729bd956823d153.png", + asset_path("logo.png") + end + test "asset path with relative url root" do @controller.config.relative_url_root = "/collaboration/hieraki" assert_equal "/collaboration/hieraki/images/logo.gif", @@ -134,4 +197,16 @@ class SprocketsHelperTest < ActionView::TestCase assert_equal "<link href=\"/assets/style-d41d8cd98f00b204e9800998ecf8427e.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />\n<link href=\"/assets/extra-d41d8cd98f00b204e9800998ecf8427e.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />", stylesheet_link_tag("style", "extra") end + + test "alternate asset prefix" do + stubs(:asset_prefix).returns("/themes/test") + assert_equal "/themes/test/style-d41d8cd98f00b204e9800998ecf8427e.css", asset_path("style", "css") + end + + test "alternate asset environment" do + assets = Sprockets::Environment.new + assets.paths << FIXTURES.join("sprockets/alternate/stylesheets") + stubs(:asset_environment).returns(assets) + assert_equal "/assets/style-df0b97ad35a8e1f7f61097461f77c19a.css", asset_path("style", "css") + end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 3b412d3dd7..166cccf161 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -156,7 +156,7 @@ module ActiveModel rescue TypeError, NoMethodError end - changed_attributes[attr] = value + changed_attributes[attr] = value unless changed_attributes.include?(attr) end # Handle <tt>reset_*!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index a7b4706906..3f9feb7631 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/array/wrap' require 'active_model/mass_assignment_security/permission_set' require 'active_model/mass_assignment_security/sanitizer' @@ -110,8 +111,11 @@ module ActiveModel options = args.extract_options! role = options[:as] || :default - self._protected_attributes = protected_attributes_configs.dup - self._protected_attributes[role] = self.protected_attributes(role) + args + self._protected_attributes = protected_attributes_configs.dup + + Array.wrap(role).each do |name| + self._protected_attributes[name] = self.protected_attributes(name) + args + end self._active_authorizer = self._protected_attributes end @@ -169,8 +173,11 @@ module ActiveModel options = args.extract_options! role = options[:as] || :default - self._accessible_attributes = accessible_attributes_configs.dup - self._accessible_attributes[role] = self.accessible_attributes(role) + args + self._accessible_attributes = accessible_attributes_configs.dup + + Array.wrap(role).each do |name| + self._accessible_attributes[name] = self.accessible_attributes(name) + args + end self._active_authorizer = self._accessible_attributes end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 0405705b35..4fbccd7419 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -32,8 +32,15 @@ module ActiveModel # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # - # The remainder of the examples in this section assume +include_root_in_json+ - # is false. + # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in: + # + # user = User.find(1) + # user.as_json(root: false) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # The remainder of the examples in this section assume include_root_in_json is set to + # <tt>false</tt>. # # Without any +options+, the returned JSON string will include all the model's # attributes. For example: @@ -83,7 +90,12 @@ module ActiveModel def as_json(options = nil) hash = serializable_hash(options) - if include_root_in_json + include_root = include_root_in_json + if options.try(:key?, :root) + include_root = options[:root] + end + + if include_root custom_root = options && options[:root] hash = { custom_root || self.class.model_name.element => hash } end @@ -91,9 +103,9 @@ module ActiveModel hash end - def from_json(json) + def from_json(json, include_root=include_root_in_json) hash = ActiveSupport::JSON.decode(json) - hash = hash.values.first if include_root_in_json + hash = hash.values.first if include_root self.attributes = hash self end diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index 09684ac4df..dbda55ca7c 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,9 +1,9 @@ module ActiveModel module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 858ae9cb69..98244a6290 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -106,4 +106,13 @@ class DirtyTest < ActiveModel::TestCase assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end + test "changing the same attribute multiple times retains the correct original value" do + @model.name = "Otto" + @model.save + @model.name = "DudeFella ManGuy" + @model.name = "Mr. Manfredgensonton" + assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change + assert_equal @model.name_was, "Otto" + end + end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index a778240827..be07e59a2f 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -43,6 +43,20 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal expected, sanitized end + def test_attributes_accessible_with_roles_given_as_array + user = Account.new + expected = { "name" => "John Smith", "email" => "john@smith.com" } + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) + assert_equal expected, sanitized + end + + def test_attributes_accessible_with_admin_role_when_roles_given_as_array + user = Account.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 + def test_attributes_protected_by_default firm = Firm.new expected = { } diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 500a5c575f..5e1e7d897a 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -8,6 +8,12 @@ class Contact include ActiveModel::Serializers::JSON include ActiveModel::Validations + def attributes=(hash) + hash.each do |k, v| + instance_variable_set("@#{k}", v) + end + end + def attributes instance_values end unless method_defined?(:attributes) @@ -34,7 +40,7 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"preferences":\{"shows":"anime"\}}, json end - test "should not include root in json" do + test "should not include root in json (class method)" do begin Contact.include_root_in_json = false json = @contact.to_json @@ -50,6 +56,13 @@ class JsonSerializationTest < ActiveModel::TestCase end end + test "should not include root in json (option)" do + + json = @contact.to_json(:root => false) + + assert_no_match %r{^\{"contact":\{}, json + end + test "should include custom root in json" do json = @contact.to_json(:root => 'json_contact') @@ -135,6 +148,44 @@ class JsonSerializationTest < ActiveModel::TestCase end end + test "from_json should set the object's attributes" do + json = @contact.to_json + result = Contact.new.from_json(json) + + assert_equal result.name, @contact.name + assert_equal result.age, @contact.age + assert_equal Time.parse(result.created_at), @contact.created_at + assert_equal result.awesome, @contact.awesome + assert_equal result.preferences, @contact.preferences + end + + test "from_json should work without a root (method parameter)" do + json = @contact.to_json(:root => false) + result = Contact.new.from_json(json, false) + + assert_equal result.name, @contact.name + assert_equal result.age, @contact.age + assert_equal Time.parse(result.created_at), @contact.created_at + assert_equal result.awesome, @contact.awesome + assert_equal result.preferences, @contact.preferences + end + + test "from_json should work without a root (class attribute)" do + begin + Contact.include_root_in_json = false + json = @contact.to_json + result = Contact.new.from_json(json) + + assert_equal result.name, @contact.name + assert_equal result.age, @contact.age + assert_equal Time.parse(result.created_at), @contact.created_at + assert_equal result.awesome, @contact.awesome + assert_equal result.preferences, @contact.preferences + ensure + Contact.include_root_in_json = true + end + end + test "custom as_json should be honored when generating json" do def @contact.as_json(options); { :name => name, :created_at => created_at }; end json = @contact.to_json diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb index 53b37369ff..1d123fa58c 100644 --- a/activemodel/test/models/mass_assignment_specific.rb +++ b/activemodel/test/models/mass_assignment_specific.rb @@ -20,6 +20,14 @@ class Person public :sanitize_for_mass_assignment end +class Account + include ActiveModel::MassAssignmentSecurity + attr_accessible :name, :email, :as => [:default, :admin] + attr_accessible :admin, :as => :admin + + public :sanitize_for_mass_assignment +end + class Firm include ActiveModel::MassAssignmentSecurity @@ -65,4 +73,4 @@ end class TightDescendant < TightPerson attr_accessible :phone_number attr_accessible :super_powers, :as => :admin -end
\ No newline at end of file +end diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index ead2a244e5..4113a16c12 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,17 @@ *Rails 3.1.0 (unreleased)* +* ActiveRecord::MacroReflection::AssociationReflection#build_record has a new method signature. + + Before: def build_association(*options) + After: def build_association(*options, &block) + + Users who are redefining this method to extend functionality should ensure that the block is + passed through to ActiveRecord::Base#new. + + This change is necessary to fix https://github.com/rails/rails/issues/1842. + + [Jon Leighton] + * AR#pluralize_table_names can be used to singularize/pluralize table name of an individual model: class User < ActiveRecord::Base diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index b7e23faa09..8ac4d9a225 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -21,6 +21,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 2.1.1') - s.add_dependency('tzinfo', '~> 0.3.27') + s.add_dependency('arel', '~> 2.1.3') + s.add_dependency('tzinfo', '~> 0.3.29') end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index bd98cf2f46..92ed844a2e 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -9,7 +9,7 @@ module ActiveRecord # table_joins is an array of arel joins which might conflict with the aliases we assign here def initialize(table_joins = []) - @aliases = Hash.new + @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) } @table_joins = table_joins end @@ -26,8 +26,6 @@ module ActiveRecord def aliased_name_for(table_name, aliased_name = nil) aliased_name ||= table_name - initialize_count_for(table_name) if aliases[table_name].nil? - if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 @@ -36,8 +34,6 @@ module ActiveRecord # Otherwise, we need to use an alias aliased_name = connection.table_alias_for(aliased_name) - initialize_count_for(aliased_name) if aliases[aliased_name].nil? - # Update the count aliases[aliased_name] += 1 @@ -49,32 +45,24 @@ module ActiveRecord end end - def pluralize(table_name, base) - base.pluralize_table_names ? table_name.to_s.pluralize : table_name.to_s - end - private - def initialize_count_for(name) - aliases[name] = 0 - - unless Arel::Table === table_joins - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = connection.quote_table_name(name).downcase + def initial_count_for(name) + return 0 if Arel::Table === table_joins - aliases[name] += table_joins.map { |join| - # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ - ).size - }.sum - end + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = connection.quote_table_name(name).downcase - aliases[name] + table_joins.map { |join| + # Table names + table aliases + join.left.downcase.scan( + /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + ).size + }.sum end def truncate(name) - name[0..connection.table_alias_length-3] + name.slice(0, connection.table_alias_length - 2) end def connection diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index c3aee90124..daadc8aa81 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -224,6 +224,15 @@ module ActiveRecord def association_class @reflection.klass end + + def build_record(attributes, options) + reflection.build_association(attributes, options) do |record| + record.assign_attributes( + create_scope.except(*record.changed), + :without_protection => true + ) + end + end end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 337a0d48f1..224e0095d9 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -423,12 +423,6 @@ module ActiveRecord scoped.scope_for_create.stringify_keys end - def build_record(attributes, options) - record = reflection.build_association(attributes, options) - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) - record - end - def delete_or_destroy(records, method) records = records.flatten records.each { |record| raise_on_type_mismatch(record) } diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 5267116c42..7f7dec467a 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -58,10 +58,10 @@ module ActiveRecord alias_method :new, :build - def respond_to?(*args) + def respond_to?(name, include_private = false) super || - (load_target && target.respond_to?(*args)) || - @association.klass.respond_to?(*args) + (load_target && target.respond_to?(name, include_private)) || + @association.klass.respond_to?(name, include_private) end def method_missing(method, *args, &block) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 6b8201973a..2131edbc20 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -19,7 +19,7 @@ module ActiveRecord if owner.persisted? && save && !record.save nullify_owner_attributes(record) - set_owner_attributes(target) + set_owner_attributes(target) if target raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index 87e33891a5..f83138195c 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -32,8 +32,7 @@ module ActiveRecord end def table_alias_for(reflection, join = false) - name = alias_tracker.pluralize(reflection.name, reflection.active_record) - name << "_#{alias_suffix}" + name = "#{reflection.plural_name}_#{alias_suffix}" name << "_join" if join name end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 7256dd5288..779f8164cc 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -68,7 +68,8 @@ module ActiveRecord private def associated_records_by_owner - owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq + owners_map = owners_by_key + owner_keys = owners_map.keys.compact if klass.nil? || owner_keys.empty? records = [] @@ -84,7 +85,7 @@ module ActiveRecord records.each do |record| owner_key = record[association_key_name].to_s - owners_by_key[owner_key].each do |owner| + owners_map[owner_key].each do |owner| records_by_owner[owner] << record end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index ce1f2a5543..6b010064d5 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -26,8 +26,7 @@ module ActiveRecord end def build(attributes = {}, options = {}) - record = reflection.build_association(attributes, options) - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) + record = build_record(attributes, options) yield(record) if block_given? set_new_record(record) record diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 5833c65893..d0687ed0b6 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -61,7 +61,7 @@ module ActiveRecord end end - def respond_to?(*args) + def respond_to?(name, include_private = false) self.class.define_attribute_methods unless self.class.attribute_methods_generated? super end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 7d4182291a..9a50a20fbc 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -99,8 +99,9 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "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}" if @attributes.has_key?(attr_name.to_s) + method = "_#{attr_name}" + if respond_to? method + send method if @attributes.has_key?(attr_name.to_s) else _read_attribute attr_name end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 6a5d282973..3b8df4f2f1 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -709,6 +709,12 @@ module ActiveRecord #:nodoc: connection_pool.columns_hash[table_name] end + # Returns a hash where the keys are column names and the values are + # default values when instantiating the AR object for this table. + def column_defaults + connection_pool.column_defaults[table_name] + end + # Returns an array of column names as strings. def column_names @column_names ||= columns.map { |column| column.name } @@ -1204,11 +1210,11 @@ MSG end def current_scope #:nodoc: - Thread.current[:"#{self}_current_scope"] + Thread.current["#{self}_current_scope"] end def current_scope=(scope) #:nodoc: - Thread.current[:"#{self}_current_scope"] = scope + Thread.current["#{self}_current_scope"] = scope end # Use this macro in your model to set a default scope for all operations on @@ -1527,6 +1533,7 @@ MSG @marked_for_destruction = false @previously_changed = {} @changed_attributes = {} + @relation = nil ensure_proper_type set_serialized_attributes @@ -1535,9 +1542,8 @@ MSG assign_attributes(attributes, options) if attributes - result = yield self if block_given? + yield self if block_given? run_callbacks :initialize - result end # Populate +coder+ with attributes about this record that should be @@ -1568,6 +1574,7 @@ MSG # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] + @relation = nil set_serialized_attributes @@ -1702,27 +1709,24 @@ MSG return unless new_attributes attributes = new_attributes.stringify_keys - role = options[:as] || :default - multi_parameter_attributes = [] + @mass_assignment_options = options unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, role) + attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) end attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] + elsif respond_to?("#{k}=") + send("#{k}=", v) else - method_name = "#{k}=" - if respond_to?(method_name) - method(method_name).arity == -2 ? send(method_name, v, options) : send(method_name, v) - else - raise(UnknownAttributeError, "unknown attribute: #{k}") - end + raise(UnknownAttributeError, "unknown attribute: #{k}") end end + @mass_assignment_options = nil assign_multiparameter_attributes(multi_parameter_attributes) end @@ -1777,16 +1781,12 @@ MSG # Note also that destroying a record preserves its ID in the model instance, so deleted # models are still comparable. def ==(comparison_object) - comparison_object.equal?(self) || + super || comparison_object.instance_of?(self.class) && id.present? && comparison_object.id == id end - - # Delegates to == - def eql?(comparison_object) - self == comparison_object - end + alias :eql? :== # Delegates to id in order to allow two records of the same type and id to work with something like: # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] @@ -1804,6 +1804,15 @@ MSG @attributes.frozen? end + # Allows sort on objects + def <=>(other_object) + if other_object.is_a?(self.class) + self.to_key <=> other_object.to_key + else + nil + end + end + # Backport dup from 1.9 so that initialize_dup() gets called unless Object.respond_to?(:initialize_dup) def dup # :nodoc: @@ -1882,12 +1891,33 @@ MSG value end + def mass_assignment_options + @mass_assignment_options ||= {} + end + + def mass_assignment_role + mass_assignment_options[:as] || :default + end + private + # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements + # of the array, and then rescues from the possible NoMethodError. If those elements are + # ActiveRecord::Base's, then this triggers the various method_missing's that we have, + # which significantly impacts upon performance. + # + # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. + # + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ + def to_ary # :nodoc: + nil + end + def set_serialized_attributes - (@attributes.keys & self.class.serialized_attributes.keys).each do |key| - coder = self.class.serialized_attributes[key] - @attributes[key] = coder.load @attributes[key] + sattrs = self.class.serialized_attributes + + sattrs.each do |key, coder| + @attributes[key] = coder.load @attributes[key] if @attributes.key?(key) end end @@ -1897,8 +1927,9 @@ MSG # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. # No such attribute would be set for objects of the Message class in that example. def ensure_proper_type - unless self.class.descends_from_active_record? - write_attribute(self.class.inheritance_column, self.class.sti_name) + klass = self.class + if klass.finder_needs_type_condition? + write_attribute(klass.inheritance_column, klass.sti_name) end end @@ -2072,8 +2103,10 @@ MSG end def populate_with_current_scope_attributes - self.class.scoped.scope_for_create.each do |att,value| - respond_to?("#{att}=") && send("#{att}=", value) + return unless self.class.scope_attributes? + + self.class.scope_attributes.each do |att,value| + send("#{att}=", value) if respond_to?("#{att}=") end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 090f6bc6fe..ddfdb05297 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -60,6 +60,7 @@ module ActiveRecord attr_accessor :automatic_reconnect attr_reader :spec, :connections attr_reader :columns, :columns_hash, :primary_keys, :tables + attr_reader :column_defaults # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -106,6 +107,12 @@ module ActiveRecord }] end + @column_defaults = Hash.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col.default] + }] + end + @primary_keys = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| table_exists?(table_name) ? conn.primary_key(table_name) : 'id' @@ -133,6 +140,7 @@ module ActiveRecord def clear_cache! @columns.clear @columns_hash.clear + @column_defaults.clear @tables.clear end @@ -140,6 +148,7 @@ module ActiveRecord def clear_table_cache!(table_name) @columns.delete table_name @columns_hash.delete table_name + @column_defaults.delete table_name @primary_keys.delete table_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 6d638628df..29dd0a9ea3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -940,7 +940,7 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = orders.collect { |s| s =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : s } + order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') } 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}" } diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 3afa257a76..6cfce6e573 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -73,7 +73,7 @@ module ActiveRecord # <tt>locking_enabled?</tt> at this point as # <tt>@attributes</tt> may not have been initialized yet. - if lock_optimistically && result.include?(self.class.locking_column) + if result.key?(self.class.locking_column) && lock_optimistically result[self.class.locking_column] ||= 0 end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c98abab091..2eeff7e36f 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -48,7 +48,7 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| + [:create_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args) # def create_table(*args) record(:"#{method}", args) # record(:create_table, args) diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 14db7a6cd6..0313abe456 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -40,6 +40,25 @@ module ActiveRecord end end + ## + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + if current_scope + current_scope.scope_for_create + else + scope = relation.clone + scope.default_scoped = true + scope.scope_for_create + end + end + + ## + # Are there default attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope || default_scopes.any? + end + # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. # diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index f51fd21077..53617059d0 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -276,15 +276,15 @@ module ActiveRecord type = (reflection.collection? ? :collection : :one_to_one) - # def pirate_attributes=(attributes, assignment_opts = {}) - # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, assignment_opts) + # def pirate_attributes=(attributes) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) # end class_eval <<-eoruby, __FILE__, __LINE__ + 1 if method_defined?(:#{association_name}_attributes=) remove_method(:#{association_name}_attributes=) end - def #{association_name}_attributes=(attributes, assignment_opts = {}) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, assignment_opts) + def #{association_name}_attributes=(attributes) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options) end eoruby else diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 367a1be4d9..f425118f59 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -319,9 +319,7 @@ module ActiveRecord # that a new instance, or one populated from a passed-in Hash, still has all the attributes # that instances loaded from the database would. def attributes_from_column_definition - Hash[self.class.columns.map do |column| - [column.name, column.default] - end] + self.class.column_defaults.dup end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 83909b0766..399f3c5d03 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -377,8 +377,7 @@ db_namespace = namespace :db do dbfile = abcs[Rails.env]['database'] || abcs[Rails.env]['dbfile'] `sqlite3 #{dbfile} .schema > db/#{Rails.env}_structure.sql` when 'sqlserver' - `scptxfr /s #{abcs[Rails.env]['host']} /d #{abcs[Rails.env]['database']} /I /f db\\#{Rails.env}_structure.sql /q /A /r` - `scptxfr /s #{abcs[Rails.env]['host']} /d #{abcs[Rails.env]['database']} /I /F db\ /q /A /r` + `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f db\\#{Rails.env}_structure.sql -A -U` when "firebird" set_firebird_env(abcs[Rails.env]) db_string = firebird_db_string(abcs[Rails.env]) @@ -423,7 +422,7 @@ db_namespace = namespace :db do dbfile = abcs['test']['database'] || abcs['test']['dbfile'] `sqlite3 #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` when 'sqlserver' - `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{Rails.env}_structure.sql` + `sqlcmd -S #{abcs['test']['host']} -d #{abcs['test']['database']} -U #{abcs['test']['username']} -P #{abcs['test']['password']} -i db\\#{Rails.env}_structure.sql` when 'oci', 'oracle' ActiveRecord::Base.establish_connection(:test) IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split(";\n\n").each do |ddl| @@ -453,9 +452,11 @@ db_namespace = namespace :db do dbfile = abcs['test']['database'] || abcs['test']['dbfile'] File.delete(dbfile) if File.exist?(dbfile) when 'sqlserver' - dropfkscript = "#{abcs['test']['host']}.#{abcs['test']['database']}.DP1".gsub(/\\/,'-') - `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{dropfkscript}` - `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{Rails.env}_structure.sql` + test = abcs.deep_dup['test'] + test_database = test['database'] + test['database'] = 'master' + ActiveRecord::Base.establish_connection(test) + ActiveRecord::Base.connection.recreate_database!(test_database) when "oci", "oracle" ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl| diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6df473d011..a2324039cf 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -80,12 +80,6 @@ module ActiveRecord # Abstract base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection - attr_reader :active_record - - def initialize(macro, name, options, active_record) - @macro, @name, @options, @active_record = macro, name, options, active_record - end - # Returns the name of the macro. # # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt> @@ -104,6 +98,19 @@ module ActiveRecord # <tt>has_many :clients</tt> returns +{}+ attr_reader :options + attr_reader :active_record + + attr_reader :plural_name # :nodoc: + + def initialize(macro, name, options, active_record) + @macro = macro + @name = name + @options = options + @active_record = active_record + @plural_name = active_record.pluralize_table_names ? + name.to_s.pluralize : name.to_s + end + # Returns the class for the macro. # # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class @@ -123,7 +130,11 @@ module ActiveRecord # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, # and +other_aggregation+ has an options hash assigned to it. def ==(other_aggregation) - other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record + super || + other_aggregation.kind_of?(self.class) && + name == other_aggregation.name && + other_aggregation.options && + active_record == other_aggregation.active_record end def sanitized_conditions #:nodoc: @@ -168,25 +179,8 @@ module ActiveRecord # Returns a new, unsaved instance of the associated class. +options+ will # be passed to the class's constructor. - def build_association(*options) - klass.new(*options) - end - - # Creates a new instance of the associated class, and immediately saves it - # with ActiveRecord::Base#save. +options+ will be passed to the class's - # creation method. Returns the newly created object. - def create_association(*options) - klass.create(*options) - end - - # Creates a new instance of the associated class, and immediately saves it - # with ActiveRecord::Base#save!. +options+ will be passed to the class's - # creation method. If the created record doesn't pass validations, then an - # exception will be raised. - # - # Returns the newly created object. - def create_association!(*options) - klass.create!(*options) + def build_association(*options, &block) + klass.new(*options, &block) end def table_name diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 317af8a15d..9cd146b857 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -6,7 +6,7 @@ module ActiveRecord JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder, :reverse_order] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches @@ -29,6 +29,7 @@ module ActiveRecord SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} @extensions = [] + @create_with_value = {} end def insert(values) @@ -403,7 +404,7 @@ module ActiveRecord end def scope_for_create - @scope_for_create ||= where_values_hash.merge(@create_with_value || {}) + @scope_for_create ||= where_values_hash.merge(create_with_value) end def eager_loading? diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index aabe5c269b..9c5f603c01 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -146,10 +146,16 @@ module ActiveRecord if options.except(:distinct).present? apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct]) else - if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) - construct_relation_for_association_calculations.calculate(operation, column_name, options) + relation = with_default_scope + + if relation.equal?(self) + if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) + construct_relation_for_association_calculations.calculate(operation, column_name, options) + else + perform_calculation(operation, column_name, options) + end else - perform_calculation(operation, column_name, options) + relation.calculate(operation, column_name, options) end end rescue ThrowResult @@ -196,7 +202,7 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: # Postgresql doesn't like ORDER BY when there are no GROUP BY - relation = with_default_scope.reorder(nil) + relation = reorder(nil) if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. @@ -245,7 +251,7 @@ module ActiveRecord "#{field} AS #{aliaz}" } - relation = with_default_scope.except(:group).group(group.join(',')) + relation = except(:group).group(group.join(',')) relation.select_values = select_values calculated_data = @klass.connection.select_all(relation.to_sql) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 7fbbefeea2..ec2e4d4dcd 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -137,7 +137,7 @@ module ActiveRecord def create_with(value) relation = clone - relation.create_with_value = value && (@create_with_value || {}).merge(value) + relation.create_with_value = value ? create_with_value.merge(value) : {} relation end @@ -307,7 +307,7 @@ module ActiveRecord order_query.map do |o| case o - when Arel::Nodes::Ordering + when Arel::Nodes::Ascending, Arel::Nodes::Descending o.reverse when String, Symbol o.to_s.split(',').collect do |s| diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 69706b5ead..ba882beca9 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -55,7 +55,7 @@ module ActiveRecord merged_relation.lock_value = r.lock_value unless merged_relation.lock_value - merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value + merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty? # Apply scope extension modules merged_relation.send :apply_modules, r.extensions diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 43fe1cdee5..838aa8fb1e 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,9 +1,9 @@ module ActiveRecord module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 9af1f7249f..7e331658a3 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1557,4 +1557,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, contract.hi_count assert_equal 1, contract.bye_count end + + def test_association_attributes_are_available_to_after_initialize + car = Car.create(:name => 'honda') + bulb = car.bulbs.build + + assert_equal car.id, bulb.attributes_after_initialize['car_id'] + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 299688d840..26931e3e85 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -345,6 +345,17 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert orig_ship.destroyed? end + def test_creation_failure_due_to_new_record_should_raise_error + pirate = pirates(:redbeard) + new_ship = Ship.new + + assert_raise(ActiveRecord::RecordNotSaved) do + pirate.ship = new_ship + end + assert_nil pirate.ship + assert_nil new_ship.pirate_id + end + def test_replacement_failure_due_to_existing_record_should_raise_error pirate = pirates(:blackbeard) pirate.ship.name = nil @@ -438,4 +449,11 @@ class HasOneAssociationsTest < ActiveRecord::TestCase bulb = car.create_bulb!{ |b| b.color = 'Red' } assert_equal 'RED!', bulb.color end + + def test_association_attributes_are_available_to_after_initialize + car = Car.create(:name => 'honda') + bulb = car.create_bulb + + assert_equal car.id, bulb.attributes_after_initialize['car_id'] + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 2224097f04..5fef3faab9 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -478,6 +478,19 @@ class BasicsTest < ActiveRecord::TestCase def test_hashing assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end + + def test_comparison + topic_1 = Topic.create! + topic_2 = Topic.create! + + assert_equal [topic_2, topic_1].sort, [topic_1, topic_2] + end + + def test_comparison_with_different_objects + topic = Topic.create + category = Category.create(:name => "comparison") + assert_nil topic <=> category + end def test_readonly_attributes assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 0f79c99e1a..36007255fa 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -29,7 +29,12 @@ module ActiveRecord assert_equal [[:create_table, [:horses]]], recorder.commands end - def test_unknown_commands_raise_exception + def test_unknown_commands_delegate + recorder = CommandRecorder.new(stub(:foo => 'bar')) + assert_equal 'bar', recorder.foo + end + + def test_unknown_commands_raise_exception_if_they_cannot_delegate @recorder.record :execute, ['some sql'] assert_raises(ActiveRecord::IrreversibleMigration) do @recorder.inverse diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 58c78ab058..41312e8661 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -76,7 +76,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil) + reflection = MacroReflection.new(:company, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index a6e08f95d0..f2d177d834 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -15,6 +15,18 @@ class RelationScopingTest < ActiveRecord::TestCase assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order end + def test_reverse_order_with_arel_node + assert_equal Developer.order("id DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).reverse_order + end + + def test_reverse_order_with_multiple_arel_nodes + assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).order(Developer.arel_table[:name].desc).reverse_order + end + + def test_reverse_order_with_arel_nodes_and_strings + assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order("id DESC").order(Developer.arel_table[:name].desc).reverse_order + end + def test_double_reverse_order_produces_original_order assert_equal Developer.order("name DESC"), Developer.order("name DESC").reverse_order.reverse_order end @@ -472,6 +484,13 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 'Jamis', jamis.name end + # FIXME: I don't know if this is *desired* behavior, but it is *today's* + # behavior. + def test_create_with_empty_hash_will_not_reset + jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new + assert_equal 'Aaron', jamis.name + end + def test_unscoped_with_named_scope_should_not_have_default_scope assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor @@ -498,4 +517,11 @@ class DefaultScopingTest < ActiveRecord::TestCase lowest_salary_dev = DeveloperOrderedBySalary.find(developers(:poor_jamis).id) assert_equal lowest_salary_dev, DeveloperOrderedBySalary.last end + + def test_default_scope_include_with_count + d = DeveloperWithIncludes.create! + d.audit_logs.create! :message => 'foo' + + assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count + end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 12e58bd340..b23ead6feb 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_single_values - assert_equal [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder, :reverse_order].map(&:to_s).sort, + assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order].map(&:to_s).sort, Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index aa94c9b87e..5a95e68eb4 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -176,6 +176,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal entrants(:first).name, entrants.first.name end + def test_finding_with_cross_table_order_and_limit + tags = Tag.includes(:taggings). + order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)"). + limit(1).to_a + assert_equal 1, tags.length + end + def test_finding_with_complex_order_and_limit tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a assert_equal 1, tags.length diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 0dcc8d5970..efb98b66e7 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -4,13 +4,18 @@ class Bulb < ActiveRecord::Base attr_protected :car_id, :frickinawesome - attr_reader :scope_after_initialize + attr_reader :scope_after_initialize, :attributes_after_initialize after_initialize :record_scope_after_initialize def record_scope_after_initialize @scope_after_initialize = self.class.scoped end + after_initialize :record_attributes_after_initialize + def record_attributes_after_initialize + @attributes_after_initialize = attributes.dup + end + def color=(color) self[:color] = color.upcase + "!" end @@ -28,4 +33,4 @@ class Bulb < ActiveRecord::Base end class CustomBulb < Bulb -end
\ No newline at end of file +end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 98d6aa22f7..f182a7fa97 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -91,6 +91,12 @@ class DeveloperWithSelect < ActiveRecord::Base default_scope select('name') end +class DeveloperWithIncludes < ActiveRecord::Base + self.table_name = 'developers' + has_many :audit_logs, :foreign_key => :developer_id + default_scope includes(:audit_logs) +end + class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' default_scope :order => 'salary DESC' diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb index 440b504344..d53374b261 100644 --- a/activeresource/lib/active_resource/version.rb +++ b/activeresource/lib/active_resource/version.rb @@ -1,9 +1,9 @@ module ActiveResource module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 23f25d6197..034d03533f 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.2.0 (unreleased)* +* The inflector understands acronyms. [dlee] + * Deprecated ActiveSupport::Memoizable in favor of Ruby memoization pattern [José Valim] * Added Time#all_day/week/quarter/year as a way of generating ranges (example: Event.where(created_at: Time.now.all_week)) [DHH] diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index a94446acde..a2d2719de7 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -12,12 +12,12 @@ module ActiveSupport class Configuration < ActiveSupport::InheritableOptions def compile_methods! - self.class.compile_methods!(keys.reject {|key| respond_to?(key)}) + self.class.compile_methods!(keys) end # compiles reader methods so we don't have to go through method_missing def self.compile_methods!(keys) - keys.each do |key| + keys.reject { |m| method_defined?(m) }.each do |key| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{key}; _get(#{key.inspect}); end RUBY diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index f420270dc4..4834eca8b1 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -40,7 +40,7 @@ class Array if object.nil? [] elsif object.respond_to?(:to_ary) - object.to_ary + object.to_ary || [object] else [object] end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 871f5cef3b..be94ae1565 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -12,7 +12,7 @@ class Module end EOS - unless options[:instance_reader] == false + unless options[:instance_reader] == false || options[:instance_accessor] == false class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} @@#{sym} @@ -31,7 +31,7 @@ class Module end EOS - unless options[:instance_writer] == false + unless options[:instance_writer] == false || options[:instance_accessor] == false class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym}=(obj) @@#{sym} = obj @@ -53,6 +53,10 @@ class Module # end # # AppConfiguration.google_api_key = "overriding the api key!" + # + # To opt out of the instance writer method, pass :instance_writer => false. + # To opt out of the instance reader method, pass :instance_reader => false. + # To opt out of both instance methods, pass :instance_accessor => false. def mattr_accessor(*syms) mattr_reader(*syms) mattr_writer(*syms) diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 2f0676f567..002688d6c0 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -1,5 +1,4 @@ require 'active_support/inflector/methods' -require 'active_support/inflector/inflections' require 'active_support/inflector/transliterate' # String inflections define new methods on the String class to transform names for different purposes. diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 07cb2187e3..3bf4edbdef 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -67,7 +67,7 @@ class Object end end -class Fixnum +class Numeric def html_safe? true end diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index d5d55b7207..90bb62f57b 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -20,10 +20,61 @@ module ActiveSupport @__instance__ ||= new end - attr_reader :plurals, :singulars, :uncountables, :humans + attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex def initialize - @plurals, @singulars, @uncountables, @humans = [], [], [], [] + @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/ + end + + # Specifies a new acronym. An acronym must be specified as it will appear in a camelized string. An underscore + # string that contains the acronym will retain the acronym when passed to `camelize`, `humanize`, or `titleize`. + # A camelized string that contains the acronym will maintain the acronym when titleized or humanized, and will + # convert the acronym into a non-delimited single lowercase word when passed to +underscore+. + # + # Examples: + # acronym 'HTML' + # titleize 'html' #=> 'HTML' + # camelize 'html' #=> 'HTML' + # underscore 'MyHTML' #=> 'my_html' + # + # The acronym, however, must occur as a delimited unit and not be part of another word for conversions to recognize it: + # + # acronym 'HTTP' + # camelize 'my_http_delimited' #=> 'MyHTTPDelimited' + # camelize 'https' #=> 'Https', not 'HTTPs' + # underscore 'HTTPS' #=> 'http_s', not 'https' + # + # acronym 'HTTPS' + # camelize 'https' #=> 'HTTPS' + # underscore 'HTTPS' #=> 'https' + # + # Note: Acronyms that are passed to `pluralize` will no longer be recognized, since the acronym will not occur as + # a delimited unit in the pluralized result. To work around this, you must specify the pluralized form as an + # acronym as well: + # + # acronym 'API' + # camelize(pluralize('api')) #=> 'Apis' + # + # acronym 'APIs' + # camelize(pluralize('api')) #=> 'APIs' + # + # `acronym` may be used to specify any word that contains an acronym or otherwise needs to maintain a non-standard + # capitalization. The only restriction is that the word must begin with a capital letter. + # + # Examples: + # acronym 'RESTful' + # underscore 'RESTful' #=> 'restful' + # underscore 'RESTfulController' #=> 'restful_controller' + # titleize 'RESTfulController' #=> 'RESTful Controller' + # camelize 'restful' #=> 'RESTful' + # camelize 'restful_controller' #=> 'RESTfulController' + # + # acronym 'McDonald' + # underscore 'McDonald' #=> 'mcdonald' + # camelize 'mcdonald' #=> 'McDonald' + def acronym(word) + @acronyms[word.downcase] = word + @acronym_regex = /#{@acronyms.values.join("|")}/ end # Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression. @@ -117,95 +168,5 @@ module ActiveSupport Inflections.instance end end - - # Returns the plural form of the word in the string. - # - # Examples: - # "post".pluralize # => "posts" - # "octopus".pluralize # => "octopi" - # "sheep".pluralize # => "sheep" - # "words".pluralize # => "words" - # "CamelOctopus".pluralize # => "CamelOctopi" - def pluralize(word) - result = word.to_s.dup - - if word.empty? || inflections.uncountables.include?(result.downcase) - result - else - inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } - result - end - end - - # The reverse of +pluralize+, returns the singular form of a word in a string. - # - # Examples: - # "posts".singularize # => "post" - # "octopi".singularize # => "octopus" - # "sheep".singularize # => "sheep" - # "word".singularize # => "word" - # "CamelOctopi".singularize # => "CamelOctopus" - def singularize(word) - result = word.to_s.dup - - if inflections.uncountables.any? { |inflection| result =~ /\b(#{inflection})\Z/i } - result - else - inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } - result - end - end - - # Capitalizes the first word and turns underscores into spaces and strips a - # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output. - # - # Examples: - # "employee_salary" # => "Employee salary" - # "author_id" # => "Author" - def humanize(lower_case_and_underscored_word) - result = lower_case_and_underscored_word.to_s.dup - - inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } - result.gsub(/_id$/, "").gsub(/_/, " ").capitalize - end - - # Capitalizes all the words and replaces some characters in the string to create - # a nicer looking title. +titleize+ is meant for creating pretty output. It is not - # used in the Rails internals. - # - # +titleize+ is also aliased as as +titlecase+. - # - # Examples: - # "man from the boondocks".titleize # => "Man From The Boondocks" - # "x-men: the last stand".titleize # => "X Men: The Last Stand" - def titleize(word) - humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize } - end - - # Create the name of a table like Rails does for models to table names. This method - # uses the +pluralize+ method on the last word in the string. - # - # Examples - # "RawScaledScorer".tableize # => "raw_scaled_scorers" - # "egg_and_ham".tableize # => "egg_and_hams" - # "fancyCategory".tableize # => "fancy_categories" - def tableize(class_name) - pluralize(underscore(class_name)) - end - - # Create a class name from a plural table name like Rails does for table names to models. - # Note that this returns a string and not a Class. (To convert to an actual class - # follow +classify+ with +constantize+.) - # - # Examples: - # "egg_and_hams".classify # => "EggAndHam" - # "posts".classify # => "Post" - # - # Singular names are not handled correctly: - # "business".classify # => "Busines" - def classify(table_name) - # strip out any leading schema name - camelize(singularize(table_name.to_s.sub(/.*\./, ''))) - end end end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index a2c4f7bfda..3d28d33f40 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,3 +1,5 @@ +require 'active_support/inflector/inflections' + module ActiveSupport # The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without, # and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept @@ -10,6 +12,44 @@ module ActiveSupport module Inflector extend self + # Returns the plural form of the word in the string. + # + # Examples: + # "post".pluralize # => "posts" + # "octopus".pluralize # => "octopi" + # "sheep".pluralize # => "sheep" + # "words".pluralize # => "words" + # "CamelOctopus".pluralize # => "CamelOctopi" + def pluralize(word) + result = word.to_s.dup + + if word.empty? || inflections.uncountables.include?(result.downcase) + result + else + inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result + end + end + + # The reverse of +pluralize+, returns the singular form of a word in a string. + # + # Examples: + # "posts".singularize # => "post" + # "octopi".singularize # => "octopus" + # "sheep".singularize # => "sheep" + # "word".singularize # => "word" + # "CamelOctopi".singularize # => "CamelOctopus" + def singularize(word) + result = word.to_s.dup + + if inflections.uncountables.any? { |inflection| result =~ /\b(#{inflection})\Z/i } + result + else + inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result + end + end + # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+ # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase. # @@ -25,12 +65,14 @@ module ActiveSupport # though there are cases where that does not hold: # # "SSLError".underscore.camelize # => "SslError" - def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true) - if first_letter_in_uppercase - lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } + def camelize(term, uppercase_first_letter = true) + string = term.to_s + if uppercase_first_letter + string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize } else - lower_case_and_underscored_word.to_s[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1] + string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end + string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::') end # Makes an underscored, lowercase form from the expression in the string. @@ -48,13 +90,66 @@ module ActiveSupport def underscore(camel_cased_word) word = camel_cased_word.to_s.dup word.gsub!(/::/, '/') - word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') + word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') word.tr!("-", "_") word.downcase! word end + # Capitalizes the first word and turns underscores into spaces and strips a + # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output. + # + # Examples: + # "employee_salary" # => "Employee salary" + # "author_id" # => "Author" + def humanize(lower_case_and_underscored_word) + result = lower_case_and_underscored_word.to_s.dup + inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result.gsub!(/_id$/, "") + result.gsub(/(_)?([a-z\d]*)/i) { "#{$1 && ' '}#{inflections.acronyms[$2] || $2.downcase}" }.gsub(/^\w/) { $&.upcase } + end + + # Capitalizes all the words and replaces some characters in the string to create + # a nicer looking title. +titleize+ is meant for creating pretty output. It is not + # used in the Rails internals. + # + # +titleize+ is also aliased as as +titlecase+. + # + # Examples: + # "man from the boondocks".titleize # => "Man From The Boondocks" + # "x-men: the last stand".titleize # => "X Men: The Last Stand" + def titleize(word) + humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize } + end + + # Create the name of a table like Rails does for models to table names. This method + # uses the +pluralize+ method on the last word in the string. + # + # Examples + # "RawScaledScorer".tableize # => "raw_scaled_scorers" + # "egg_and_ham".tableize # => "egg_and_hams" + # "fancyCategory".tableize # => "fancy_categories" + def tableize(class_name) + pluralize(underscore(class_name)) + end + + # Create a class name from a plural table name like Rails does for table names to models. + # Note that this returns a string and not a Class. (To convert to an actual class + # follow +classify+ with +constantize+.) + # + # Examples: + # "egg_and_hams".classify # => "EggAndHam" + # "posts".classify # => "Post" + # + # Singular names are not handled correctly: + # "business".classify # => "Busines" + def classify(table_name) + # strip out any leading schema name + camelize(singularize(table_name.to_s.sub(/.*\./, ''))) + end + # Replaces underscores with dashes in the string. # # Example: diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index 8d8e6ebc58..bf81567d22 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -36,6 +36,10 @@ module ActiveSupport #:nodoc: self[name] end end + + def respond_to?(name) + true + end end class InheritableOptions < OrderedOptions diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index e135872bf6..bd8e7f907a 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,9 +1,9 @@ module ActiveSupport module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index c6d8191298..2e5ea2c360 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -58,16 +58,26 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase child = Class.new(parent) parent.config.bar = :foo - assert !parent.config.respond_to?(:bar) - assert !child.config.respond_to?(:bar) - assert !child.new.config.respond_to?(:bar) + assert_method_not_defined parent.config, :bar + assert_method_not_defined child.config, :bar + assert_method_not_defined child.new.config, :bar parent.config.compile_methods! assert_equal :foo, parent.config.bar assert_equal :foo, child.new.config.bar - assert_respond_to parent.config, :bar - assert_respond_to child.config, :bar - assert_respond_to child.new.config, :bar + assert_method_defined parent.config, :bar + assert_method_defined child.config, :bar + assert_method_defined child.new.config, :bar + end + + def assert_method_defined(object, method) + methods = object.public_methods.map(&:to_s) + assert methods.include?(method.to_s), "Expected #{methods.inspect} to include #{method.to_s.inspect}" + end + + def assert_method_not_defined(object, method) + methods = object.public_methods.map(&:to_s) + assert !methods.include?(method.to_s), "Expected #{methods.inspect} to not include #{method.to_s.inspect}" end end diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index 0e5407bc35..e532010b18 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -456,8 +456,9 @@ class ArrayWrapperTests < Test::Unit::TestCase assert_equal [o], Array.wrap(o) end - def test_wrap_returns_nil_if_to_ary_returns_nil - assert_nil Array.wrap(NilToAry.new) + def test_wrap_returns_wrapped_if_to_ary_returns_nil + o = NilToAry.new + assert_equal [o], Array.wrap(o) end def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index 118fb070a0..29889b51e0 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -7,6 +7,7 @@ class ModuleAttributeAccessorTest < Test::Unit::TestCase mattr_accessor :foo mattr_accessor :bar, :instance_writer => false mattr_reader :shaq, :instance_reader => false + mattr_accessor :camp, :instance_accessor => false end @class = Class.new @class.instance_eval { include m } @@ -37,4 +38,10 @@ class ModuleAttributeAccessorTest < Test::Unit::TestCase assert_respond_to @module, :shaq assert !@object.respond_to?(:shaq) end + + def test_should_not_create_instance_accessors + assert_respond_to @module, :camp + assert !@object.respond_to?(:camp) + assert !@object.respond_to?(:camp=) + end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 7a8cda160d..0360cf2901 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -353,6 +353,10 @@ class OutputSafetyTest < ActiveSupport::TestCase test "A fixnum is safe by default" do assert 5.html_safe? end + + test "a float is safe by default" do + assert 5.7.html_safe? + end test "An object is unsafe by default" do assert !@object.html_safe? diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 95f18126d4..b9e299af75 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -94,6 +94,88 @@ class InflectorTest < Test::Unit::TestCase assert_equal('capital', ActiveSupport::Inflector.camelize('Capital', false)) end + def test_camelize_with_underscores + assert_equal("CamelCase", ActiveSupport::Inflector.camelize('Camel_Case')) + end + + def test_acronyms + ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym("API") + inflect.acronym("HTML") + inflect.acronym("HTTP") + inflect.acronym("RESTful") + inflect.acronym("W3C") + inflect.acronym("PhD") + inflect.acronym("RoR") + inflect.acronym("SSL") + end + + # camelize underscore humanize titleize + [ + ["API", "api", "API", "API"], + ["APIController", "api_controller", "API controller", "API Controller"], + ["Nokogiri::HTML", "nokogiri/html", "Nokogiri/HTML", "Nokogiri/HTML"], + ["HTTPAPI", "http_api", "HTTP API", "HTTP API"], + ["HTTP::Get", "http/get", "HTTP/get", "HTTP/Get"], + ["SSLError", "ssl_error", "SSL error", "SSL Error"], + ["RESTful", "restful", "RESTful", "RESTful"], + ["RESTfulController", "restful_controller", "RESTful controller", "RESTful Controller"], + ["IHeartW3C", "i_heart_w3c", "I heart W3C", "I Heart W3C"], + ["PhDRequired", "phd_required", "PhD required", "PhD Required"], + ["IRoRU", "i_ror_u", "I RoR u", "I RoR U"], + ["RESTfulHTTPAPI", "restful_http_api", "RESTful HTTP API", "RESTful HTTP API"], + + # misdirection + ["Capistrano", "capistrano", "Capistrano", "Capistrano"], + ["CapiController", "capi_controller", "Capi controller", "Capi Controller"], + ["HttpsApis", "https_apis", "Https apis", "Https Apis"], + ["Html5", "html5", "Html5", "Html5"], + ["Restfully", "restfully", "Restfully", "Restfully"], + ["RoRails", "ro_rails", "Ro rails", "Ro Rails"] + ].each do |camel, under, human, title| + assert_equal(camel, ActiveSupport::Inflector.camelize(under)) + assert_equal(camel, ActiveSupport::Inflector.camelize(camel)) + assert_equal(under, ActiveSupport::Inflector.underscore(under)) + assert_equal(under, ActiveSupport::Inflector.underscore(camel)) + assert_equal(title, ActiveSupport::Inflector.titleize(under)) + assert_equal(title, ActiveSupport::Inflector.titleize(camel)) + assert_equal(human, ActiveSupport::Inflector.humanize(under)) + end + end + + def test_acronym_override + ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym("API") + inflect.acronym("LegacyApi") + end + + assert_equal("LegacyApi", ActiveSupport::Inflector.camelize("legacyapi")) + assert_equal("LegacyAPI", ActiveSupport::Inflector.camelize("legacy_api")) + assert_equal("SomeLegacyApi", ActiveSupport::Inflector.camelize("some_legacyapi")) + assert_equal("Nonlegacyapi", ActiveSupport::Inflector.camelize("nonlegacyapi")) + end + + def test_acronyms_camelize_lower + ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym("API") + inflect.acronym("HTML") + end + + assert_equal("htmlAPI", ActiveSupport::Inflector.camelize("html_api", false)) + assert_equal("htmlAPI", ActiveSupport::Inflector.camelize("htmlAPI", false)) + assert_equal("htmlAPI", ActiveSupport::Inflector.camelize("HTMLAPI", false)) + end + + def test_underscore_acronym_sequence + ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym("API") + inflect.acronym("HTML5") + inflect.acronym("HTML") + end + + assert_equal("html5_html_api", ActiveSupport::Inflector.underscore("HTML5HTMLAPI")) + end + def test_underscore CamelToUnderscore.each do |camel, underscore| assert_equal(underscore, ActiveSupport::Inflector.underscore(camel)) @@ -148,8 +230,8 @@ class InflectorTest < Test::Unit::TestCase end def test_parameterize_with_custom_separator - StringToParameterized.each do |some_string, parameterized_string| - assert_equal(parameterized_string.gsub('-', '_'), ActiveSupport::Inflector.parameterize(some_string, '_')) + StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| + assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_')) end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index ec9d92794c..0cb1f70657 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -168,6 +168,7 @@ module InflectorTestCases StringToParameterizeWithNoSeparator = { "Donald E. Knuth" => "donaldeknuth", + "With-some-dashes" => "with-some-dashes", "Random text with *(bad)* characters" => "randomtextwithbadcharacters", "Trailing bad characters!@#" => "trailingbadcharacters", "!@#Leading bad characters" => "leadingbadcharacters", @@ -179,6 +180,8 @@ module InflectorTestCases StringToParameterizeWithUnderscore = { "Donald E. Knuth" => "donald_e_knuth", "Random text with *(bad)* characters" => "random_text_with_bad_characters", + "With-some-dashes" => "with-some-dashes", + "Retain_underscore" => "retain_underscore", "Trailing bad characters!@#" => "trailing_bad_characters", "!@#Leading bad characters" => "leading_bad_characters", "Squeeze separators" => "squeeze_separators", diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 49cefc6e82..3575175517 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -224,9 +224,9 @@ class TimeZoneTest < Test::Unit::TestCase end def test_formatted_offset_positive - zone = ActiveSupport::TimeZone['Moscow'] - assert_equal "+03:00", zone.formatted_offset - assert_equal "+0300", zone.formatted_offset(false) + zone = ActiveSupport::TimeZone['New Delhi'] + assert_equal "+05:30", zone.formatted_offset + assert_equal "+0530", zone.formatted_offset(false) end def test_formatted_offset_negative @@ -257,7 +257,7 @@ class TimeZoneTest < Test::Unit::TestCase end def test_to_s - assert_equal "(GMT+03:00) Moscow", ActiveSupport::TimeZone['Moscow'].to_s + assert_equal "(GMT+05:30) New Delhi", ActiveSupport::TimeZone['New Delhi'].to_s end def test_all_sorted diff --git a/ci/cruise_config.rb b/ci/cruise_config.rb index 9c7fa98a70..b64cd8aaea 100644 --- a/ci/cruise_config.rb +++ b/ci/cruise_config.rb @@ -1,9 +1,9 @@ Project.configure do |project| - # Send email notifications about broken and fixed builds to core mailing list - if Socket.gethostname =~ /ci.rubyonrails.org/ && ENV['ENABLE_RAILS_CI_EMAILS'] == 'true' - project.email_notifier.emails = ['rubyonrails-core@googlegroups.com'] - end - project.build_command = 'sudo gem update --system && ruby ci/ci_build.rb' - project.email_notifier.from = 'thewoolleyman@gmail.com' + project.email_notifier.from = 'rails-ci@wyeworks.com' + + # project.campfire_notifier.account = 'rails' + # project.campfire_notifier.token = '' + # project.campfire_notifier.room = 'Rails 3' + # project.campfire_notifier.ssl = true end diff --git a/ci/site_config.rb b/ci/site_config.rb index 09d5b550e8..f9db39ed57 100644 --- a/ci/site_config.rb +++ b/ci/site_config.rb @@ -45,16 +45,16 @@ Configuration.dashboard_refresh_interval = 60.seconds # Site-wide setting for the email "from" field. This can also be set on per-project basis, # through project.email.notifier.from attribute -Configuration.email_from = 'thewoolleyman+railsci@gmail.com' +Configuration.email_from = 'rails-ci@wyeworks.com' # Root URL of the dashboard application. Setting this attribute allows various notifiers to include a link to the # build page in the notification message. -Configuration.dashboard_url = 'http://ci.rubyonrails.org/' +Configuration.dashboard_url = 'http://rails-ci.wyeworks.com/' # If you don't want to allow triggering builds through dashboard Build Now button. Useful when you host CC.rb as a # public web site (such as http://cruisecontrolrb.thoughtworks.com/projects - try clicking on Build Now button there # and see what happens): -# Configuration.disable_build_now = true +Configuration.disable_build_now = true # If you want to only allow one project to build at a time, uncomment this line # by default, cruise allows multiple projects to build at a time diff --git a/railties/CHANGELOG b/railties/CHANGELOG index e8b4a32056..a9b4deb551 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -8,6 +8,8 @@ *Rails 3.1.0 (unreleased)* +* Make sprockets/railtie require explicit and add --skip-sprockets to app generator [José Valim] + * Added Rails.groups that automatically handles Rails.env and ENV["RAILS_GROUPS"] [José Valim] * The new rake task assets:clean removes precompiled assets. [fxn] diff --git a/railties/bin/rails b/railties/bin/rails new file mode 100755 index 0000000000..a7d6938e0d --- /dev/null +++ b/railties/bin/rails @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +require "rails/cli"
\ No newline at end of file diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 7f33cc4df5..a0ed85cf01 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -1496,7 +1496,15 @@ end That may be handy to compute method names in a language that follows that convention, for example JavaScript. -INFO: As a rule of thumb you can think of +camelize+ as the inverse of +underscore+, though there are cases where that does not hold: <tt>"SSLError".underscore.camelize</tt> gives back <tt>"SslError"</tt>. +INFO: As a rule of thumb you can think of +camelize+ as the inverse of +underscore+, though there are cases where that does not hold: <tt>"SSLError".underscore.camelize</tt> gives back <tt>"SslError"</tt>. To support cases such as this, Active Support allows you to specify acronyms in +config/initializers/inflections.rb+: + +<ruby> +ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym 'SSL' +end + +"SSLError".underscore.camelize #=> "SSLError" +</ruby> +camelize+ is aliased to +camelcase+. @@ -2241,8 +2249,8 @@ The method +Array.wrap+ wraps its argument in an array unless it is already an a Specifically: * If the argument is +nil+ an empty list is returned. -* Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned. -* Otherwise, returns an array with the argument as its single element. +* Otherwise, if the argument responds to +to_ary+ it is invoked, and if the value of +to_ary+ is not +nil+, it is returned. +* Otherwise, an array with the argument as its single element is returned. <ruby> Array.wrap(nil) # => [] diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 1d5b0c0c11..340699419b 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -661,9 +661,9 @@ require 'active_support/inflections' require 'active_support/core_ext/string/inflections' </ruby> -The +active_support/inflector/methods+ file has already been required by +active_support/autoload+ and so won't be loaded again here. +The +active_support/inflector/methods+ file has already been required by +active_support/autoload+ and so won't be loaded again here. The +activesupport/lib/active_support/inflector/inflections.rb+ is required by +active_support/inflector/methods+. -h4. +activesupport/lib/active_support/inflector/inflections.rb+ +h4. +active_support/inflections+ 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". diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb index 82775b7e3b..01ceb80972 100644 --- a/railties/lib/rails/all.rb +++ b/railties/lib/rails/all.rb @@ -6,6 +6,7 @@ require "rails" action_mailer active_resource rails/test_unit + sprockets ).each do |framework| begin require "#{framework}/railtie" diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 52c89274e7..eb6fcd5dd7 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -538,15 +538,9 @@ module Rails end initializer :append_assets_path do |app| - if app.config.assets.respond_to?(:prepend_path) - app.config.assets.prepend_path(*paths["vendor/assets"].existent) - app.config.assets.prepend_path(*paths["lib/assets"].existent) - app.config.assets.prepend_path(*paths["app/assets"].existent) - else - app.config.assets.paths.unshift(*paths["vendor/assets"].existent) - app.config.assets.paths.unshift(*paths["lib/assets"].existent) - app.config.assets.paths.unshift(*paths["app/assets"].existent) - end + app.config.assets.paths.unshift(*paths["vendor/assets"].existent) + app.config.assets.paths.unshift(*paths["lib/assets"].existent) + app.config.assets.paths.unshift(*paths["app/assets"].existent) end initializer :prepend_helpers_path do |app| @@ -592,7 +586,7 @@ module Rails end def has_migrations? - paths["db/migrate"].first.present? + paths["db/migrate"].existent.any? end def find_root_with_flag(flag, default=nil) diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index ea9f023511..fdafc91fcb 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -53,7 +53,7 @@ module Rails :helper => true, :integration_tool => nil, :javascripts => true, - :javascript_engine => nil, + :javascript_engine => :js, :orm => false, :performance_tool => nil, :resource_controller => :controller, diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 378eceb4c3..bbdd000ad9 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -37,6 +37,9 @@ module Rails class_option :skip_active_record, :type => :boolean, :aliases => "-O", :default => false, :desc => "Skip Active Record files" + class_option :skip_sprockets, :type => :boolean, :aliases => "-S", :default => false, + :desc => "Skip Sprockets files" + class_option :database, :type => :string, :aliases => "-d", :default => "sqlite3", :desc => "Preconfigure for selected database (options: #{DATABASES.join('/')})" @@ -124,7 +127,7 @@ module Rails end def include_all_railties? - !options[:skip_active_record] && !options[:skip_test_unit] + !options[:skip_active_record] && !options[:skip_test_unit] && !options[:skip_sprockets] end def comment_if(value) @@ -195,6 +198,16 @@ module Rails end end + def assets_gemfile_entry + <<-GEMFILE.strip_heredoc + group :assets do + gem 'sass-rails', :git => 'git://github.com/rails/sass-rails' + gem 'coffee-rails', :git => 'git://github.com/rails/coffee-rails' + gem 'uglifier' + end + GEMFILE + end + def javascript_gemfile_entry "gem '#{options[:javascript]}-rails'" unless options[:skip_javascript] end diff --git a/railties/lib/rails/generators/js/assets/assets_generator.rb b/railties/lib/rails/generators/js/assets/assets_generator.rb new file mode 100644 index 0000000000..d134a9e392 --- /dev/null +++ b/railties/lib/rails/generators/js/assets/assets_generator.rb @@ -0,0 +1,13 @@ +require "rails/generators/named_base" + +module Js + module Generators + class AssetsGenerator < Rails::Generators::NamedBase + source_root File.expand_path("../templates", __FILE__) + + def copy_javascript + copy_file "javascript.js", File.join('app/assets/javascripts', class_path, "#{file_name}.js") + end + end + end +end diff --git a/railties/lib/rails/generators/js/assets/templates/javascript.js b/railties/lib/rails/generators/js/assets/templates/javascript.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/railties/lib/rails/generators/js/assets/templates/javascript.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 242677cc65..6af9d299aa 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -88,6 +88,7 @@ module Rails def lib empty_directory "lib" empty_directory_with_gitkeep "lib/tasks" + empty_directory_with_gitkeep "lib/assets" end def log diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index cd674ddbdd..88eea40b1b 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -9,11 +9,7 @@ source 'http://rubygems.org' # Gems used only for assets and not required # in production environments by default. -group :assets do - gem 'sass-rails' - gem 'coffee-script' - gem 'uglifier' -end +<%= assets_gemfile_entry %> <%= javascript_gemfile_entry %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index 7f623a7af9..eaa31e7386 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -8,6 +8,7 @@ require 'rails/all' require "action_controller/railtie" require "action_mailer/railtie" require "active_resource/railtie" +<%= comment_if :skip_sprockets %> require "sprockets/railtie" <%= comment_if :skip_test_unit %> require "rails/test_unit/railtie" <% end -%> @@ -46,7 +47,9 @@ module <%= app_const_base %> # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] + <% unless options.skip_sprockets? %> # Enable the asset pipeline config.assets.enabled = true + <% end %> end end diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb index db3422fe83..2e7f25a0b7 100644 --- a/railties/lib/rails/generators/rails/assets/assets_generator.rb +++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb @@ -7,21 +7,14 @@ module Rails 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', class_path, "#{asset_name}.#{javascript_extension}") - end - protected def asset_name file_name end - def javascript_extension - options.javascript_engine.present? ? - "js.#{options.javascript_engine}" : "js" + hook_for :javascript_engine do |javascript_engine| + invoke javascript_engine, [name] if options[:javascripts] end hook_for :stylesheet_engine do |stylesheet_engine| diff --git a/railties/lib/rails/generators/rails/assets/templates/javascript.js.coffee b/railties/lib/rails/generators/rails/assets/templates/javascript.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/railties/lib/rails/generators/rails/assets/templates/javascript.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# 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/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb index 8b68280a5e..4864ead960 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb @@ -1,13 +1,15 @@ require File.expand_path('../boot', __FILE__) -<% unless options[:skip_active_record] -%> +<% if include_all_railties? -%> require 'rails/all' <% else -%> -# require "active_record/railtie" +# Pick the frameworks you want: +<%= comment_if :skip_active_record %>require "active_record/railtie" require "action_controller/railtie" require "action_mailer/railtie" require "active_resource/railtie" -require "rails/test_unit/railtie" +<%= comment_if :skip_sprockets %> require "sprockets/railtie" +<%= comment_if :skip_test_unit %> require "rails/test_unit/railtie" <% end -%> Bundler.require diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index d05e031f56..a1e15092b2 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -78,6 +78,10 @@ module Rails Rails::VERSION::STRING end + property 'JavaScript Runtime' do + ExecJS.runtime.name + end + # Versions of each Rails framework (Active Record, Action Pack, # Active Resource, Action Mailer, and Active Support). frameworks.each do |framework| diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 166d518f7c..9807000578 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,7 +3,6 @@ $VERBOSE = nil # Load Rails rakefile extensions %w( annotations - assets documentation framework log diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 28dc40379b..b9877a83b5 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -79,10 +79,14 @@ task :test do Rake::Task[task].invoke nil rescue => e - task + { :task => task, :exception => e } end end.compact - abort "Errors running #{errors * ', '}!" if errors.any? + + if errors.any? + puts errors.map { |e| "Errors running #{e[:task]}! #{e[:exception].inspect}" }.join("\n") + abort + end end namespace :test do diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index 3d6ecb9d30..254227ecf7 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -1,9 +1,9 @@ module Rails module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 9c535f4f90..b917128885 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -15,6 +15,9 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG', 'README.rdoc', 'bin/**/*', 'guides/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' + s.bindir = 'bin' + s.executables = ['rails'] + s.rdoc_options << '--exclude' << '.' s.add_dependency('rake', '>= 0.8.7') diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 44ac6615de..b61e2851bf 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -59,6 +59,32 @@ module ApplicationTests Dir.chdir(app_path){ `rake stats` } end + def test_rake_test_error_output + Dir.chdir(app_path){ `rake db:migrate` } + + app_file "config/database.yml", <<-RUBY + development: + RUBY + + app_file "test/unit/one_unit_test.rb", <<-RUBY + RUBY + + app_file "test/functional/one_functional_test.rb", <<-RUBY + raise RuntimeError + RUBY + + app_file "test/integration/one_integration_test.rb", <<-RUBY + raise RuntimeError + RUBY + + silence_stderr do + output = Dir.chdir(app_path){ `rake test` } + assert_match /Errors running test:units! #<ActiveRecord::AdapterNotSpecified/, output + assert_match /Errors running test:functionals! #<RuntimeError/, output + assert_match /Errors running test:integration! #<RuntimeError/, output + end + end + def test_rake_routes_output_strips_anchors_from_http_verbs app_file "config/routes.rb", <<-RUBY AppTemplate::Application.routes.draw do diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 03d185165f..81f0bf5e82 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -10,6 +10,7 @@ DEFAULT_APP_FILES = %w( config.ru app/assets/javascripts app/assets/stylesheets + app/assets/images app/controllers app/helpers app/mailers @@ -22,8 +23,8 @@ DEFAULT_APP_FILES = %w( doc lib lib/tasks + lib/assets log - app/assets/images script/rails test/fixtures test/functional @@ -47,11 +48,12 @@ class AppGeneratorTest < Rails::Generators::TestCase ::DEFAULT_APP_FILES end - def test_application_controller_and_layout_files + def test_assets run_generator 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" + assert_file "config/application.rb", /config\.assets\.enabled = true/ end def test_invalid_application_name_raises_an_error @@ -123,13 +125,21 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_config_database_is_added_by_default run_generator assert_file "config/database.yml", /sqlite3/ - assert_file "Gemfile", /^gem\s+["']sqlite3["']$/ + unless defined?(JRUBY_VERSION) + assert_file "Gemfile", /^gem\s+["']sqlite3["']$/ + else + assert_file "Gemfile", /^gem\s+["']activerecord-jdbcsqlite3-adapter["']$/ + end end def test_config_another_database run_generator([destination_root, "-d", "mysql"]) assert_file "config/database.yml", /mysql/ - assert_file "Gemfile", /^gem\s+["']mysql2["']$/ + unless defined?(JRUBY_VERSION) + assert_file "Gemfile", /^gem\s+["']mysql2["']$/ + else + assert_file "Gemfile", /^gem\s+["']activerecord-jdbcmysql-adapter["']$/ + end end def test_config_jdbcmysql_database @@ -171,15 +181,20 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_if_skip_active_record_is_given run_generator [destination_root, "--skip-active-record"] assert_no_file "config/database.yml" + assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ assert_file "test/test_helper.rb" do |helper_content| assert_no_match(/fixtures :all/, helper_content) end assert_file "test/performance/browsing_test.rb" end - def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given - run_generator [destination_root, "--skip-active-record"] - assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ + def test_generator_if_skip_active_record_is_given + run_generator [destination_root, "--skip-sprockets"] + assert_file "config/application.rb" do |content| + assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content) + assert_no_match(/config\.assets\.enabled = true/, content) + end + assert_file "test/performance/browsing_test.rb" end def test_creation_of_a_test_directory diff --git a/railties/test/generators/assets_generator_test.rb b/railties/test/generators/assets_generator_test.rb index 2d20982d04..044e0b6bc6 100644 --- a/railties/test/generators/assets_generator_test.rb +++ b/railties/test/generators/assets_generator_test.rb @@ -8,19 +8,13 @@ class AssetsGeneratorTest < Rails::Generators::TestCase def test_assets run_generator - assert_file "app/assets/javascripts/posts.js.coffee" + assert_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end def test_skipping_assets content = run_generator ["posts", "--no-stylesheets", "--no-javascripts"] - assert_no_file "app/assets/javascripts/posts.js.coffee" + assert_no_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end - - def test_vanilla_assets - run_generator ["posts", "--no-javascript-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 3adf7be118..c3fa9ebb03 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -39,7 +39,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_invokes_assets run_generator - assert_file "app/assets/javascripts/account.js.coffee" + assert_file "app/assets/javascripts/account.js" assert_file "app/assets/stylesheets/account.css" end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index c3c6015291..2db8090621 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -81,7 +81,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/product_lines.js.coffee" + assert_file "app/assets/javascripts/product_lines.js" assert_file "app/assets/stylesheets/product_lines.css" end @@ -114,7 +114,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ - assert_no_file "app/assets/javascripts/product_lines.js.coffee" + assert_no_file "app/assets/javascripts/product_lines.js" assert_no_file "app/assets/stylesheets/product_lines.css" end @@ -190,7 +190,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ - assert_file "app/assets/javascripts/admin/roles.js.coffee" + assert_file "app/assets/javascripts/admin/roles.js" assert_file "app/assets/stylesheets/admin/roles.css" end @@ -224,7 +224,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/admin/roles.js.coffee" + assert_no_file "app/assets/javascripts/admin/roles.js" assert_no_file "app/assets/stylesheets/admin/roles.css" end @@ -246,28 +246,21 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase def test_scaffold_generator_no_assets run_generator [ "posts", "--no-assets" ] assert_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js.coffee" + assert_no_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_stylesheets run_generator [ "posts", "--no-stylesheets" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js.coffee" + assert_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_javascripts run_generator [ "posts", "--no-javascripts" ] assert_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js.coffee" - assert_file "app/assets/stylesheets/posts.css" - end - - def test_scaffold_generator_no_engines - run_generator [ "posts", "--no-javascript-engine" ] - assert_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js" + assert_no_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index fe59dcd52b..08dd1ebbc2 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -1,7 +1,8 @@ RAILS_ISOLATED_ENGINE = true require "isolation/abstract_unit" -require "#{RAILS_FRAMEWORK_ROOT}/railties/lib/rails/generators/test_case" +require 'generators/generators_test_helper' +require "rails/generators/test_case" module RailtiesTests class GeneratorTest < Rails::Generators::TestCase diff --git a/railties/test/railties/shared_tests.rb b/railties/test/railties/shared_tests.rb index 8cbc76db71..9a64b7c64e 100644 --- a/railties/test/railties/shared_tests.rb +++ b/railties/test/railties/shared_tests.rb @@ -84,6 +84,15 @@ module RailtiesTest end end + def test_no_rake_task_without_migrations + boot_rails + require 'rake' + require 'rdoc/task' + require 'rake/testtask' + Rails.application.load_tasks + assert !Rake::Task.task_defined?('bukkits:install:migrations') + end + def test_puts_its_lib_directory_on_load_path boot_rails require "another" diff --git a/version.rb b/version.rb index 3d6ecb9d30..254227ecf7 100644 --- a/version.rb +++ b/version.rb @@ -1,9 +1,9 @@ module Rails module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end |