From 45d41f0dadd9fa171f306ff356770c4492726f30 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 19 Jun 2008 16:25:27 +0200 Subject: integrating I18n into Rails --- actionpack/lib/action_view.rb | 2 + .../action_view/helpers/active_record_helper.rb | 33 +- actionpack/lib/action_view/helpers/date_helper.rb | 81 ++-- .../lib/action_view/helpers/form_options_helper.rb | 52 +- .../lib/action_view/helpers/number_helper.rb | 18 +- actionpack/lib/action_view/lang/en-US.rb | 93 ++++ .../template/active_record_helper_i18n_test.rb | 47 ++ actionpack/test/template/date_helper_i18n_test.rb | 99 ++++ .../test/template/form_options_helper_i18n_test.rb | 26 + .../test/template/number_helper_i18n_test.rb | 27 ++ activerecord/lib/active_record.rb | 2 + activerecord/lib/active_record/lang/en-US.rb | 25 + activerecord/lib/active_record/validations.rb | 184 ++++--- activerecord/test/cases/validations_i18n_test.rb | 539 +++++++++++++++++++++ .../active_support/core_ext/array/conversions.rb | 14 +- activesupport/lib/active_support/vendor.rb | 7 + activesupport/lib/active_support/vendor/i18n-0.0.1 | 1 + 17 files changed, 1082 insertions(+), 168 deletions(-) create mode 100644 actionpack/lib/action_view/lang/en-US.rb create mode 100644 actionpack/test/template/active_record_helper_i18n_test.rb create mode 100644 actionpack/test/template/date_helper_i18n_test.rb create mode 100644 actionpack/test/template/form_options_helper_i18n_test.rb create mode 100644 actionpack/test/template/number_helper_i18n_test.rb create mode 100644 activerecord/lib/active_record/lang/en-US.rb create mode 100644 activerecord/test/cases/validations_i18n_test.rb create mode 160000 activesupport/lib/active_support/vendor/i18n-0.0.1 diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 973020a768..33d50a61c4 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -32,6 +32,8 @@ require 'action_view/base' require 'action_view/partials' require 'action_view/template_error' +require 'action_view/lang/en-US.rb' + ActionView::Base.class_eval do include ActionView::Partials diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index f3f204cc97..5ad9d5f76d 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -151,12 +151,17 @@ module ActionView # instance yourself and set it up. View the source of this method to see how easy it is. def error_messages_for(*params) options = params.extract_options!.symbolize_keys + if object = options.delete(:object) objects = [object].flatten else objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact end - count = objects.inject(0) {|sum, object| sum + object.errors.count } + + count = objects.inject(0) {|sum, object| sum + object.errors.count } + locale = options[:locale] + locale ||= request.locale if respond_to?(:request) + unless count.zero? html = {} [:id, :class].each do |key| @@ -168,21 +173,29 @@ module ActionView end end options[:object_name] ||= params.first - options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message) - options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message) - error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join - contents = '' - contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank? - contents << content_tag(:p, options[:message]) unless options[:message].blank? - contents << content_tag(:ul, error_messages) + I18n.with_options :locale => locale, :scope => [:active_record, :error] do |locale| + header_message = if options.include?(:header_message) + options[:header_message] + else + object_name = options[:object_name].to_s.gsub('_', ' ') + locale.t :header_message, :count => count, :object_name => object_name + end + message = options.include?(:message) ? options[:message] : locale.t(:message) + error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join + + contents = '' + contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank? + contents << content_tag(:p, message) unless message.blank? + contents << content_tag(:ul, error_messages) - content_tag(:div, contents, html) + content_tag(:div, contents, html) + end else '' end end - + private def all_input_tags(record, record_name, options) input_block = options[:input_block] || default_input_block diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 7ed6272898..0337be0744 100755 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -58,35 +58,43 @@ module ActionView # distance_of_time_in_words(to_time, from_time, true) # => over 6 years # distance_of_time_in_words(Time.now, Time.now) # => less than a minute # - def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) + def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) + locale = options[:locale] + locale ||= request.locale if respond_to?(:request) + from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) distance_in_minutes = (((to_time - from_time).abs)/60).round distance_in_seconds = ((to_time - from_time).abs).round - case distance_in_minutes - when 0..1 - return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds - case distance_in_seconds - when 0..4 then 'less than 5 seconds' - when 5..9 then 'less than 10 seconds' - when 10..19 then 'less than 20 seconds' - when 20..39 then 'half a minute' - when 40..59 then 'less than a minute' - else '1 minute' - end + I18n.with_options :locale => locale, :scope => :'datetime.distance_in_words' do |locale| + case distance_in_minutes + when 0..1 + return distance_in_minutes == 0 ? + locale.t(:less_than_x_minutes, :count => 1) : + locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds + + case distance_in_seconds + when 0..4 then locale.t :less_than_x_seconds, :count => 5 + when 5..9 then locale.t :less_than_x_seconds, :count => 10 + when 10..19 then locale.t :less_than_x_seconds, :count => 20 + when 20..39 then locale.t :half_a_minute + when 40..59 then locale.t :less_than_x_minutes, :count => 1 + else locale.t :x_minutes, :count => 1 + end - when 2..44 then "#{distance_in_minutes} minutes" - when 45..89 then 'about 1 hour' - when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" - when 1440..2879 then '1 day' - when 2880..43199 then "#{(distance_in_minutes / 1440).round} days" - when 43200..86399 then 'about 1 month' - when 86400..525599 then "#{(distance_in_minutes / 43200).round} months" - when 525600..1051199 then 'about 1 year' - else "over #{(distance_in_minutes / 525600).round} years" + when 2..44 then locale.t :x_minutes, :count => distance_in_minutes + when 45..89 then locale.t :about_x_hours, :count => 1 + when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round + when 1440..2879 then locale.t :x_days, :count => 1 + when 2880..43199 then locale.t :x_days, :count => (distance_in_minutes / 1440).round + when 43200..86399 then locale.t :about_x_months, :count => 1 + when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes / 43200).round + when 525600..1051199 then locale.t :about_x_years, :count => 1 + else locale.t :over_x_years, :count => (distance_in_minutes / 525600).round + end end - end + end # Like distance_of_time_in_words, but where to_time is fixed to Time.now. # @@ -498,13 +506,19 @@ module ActionView # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # def select_month(date, options = {}, html_options = {}) + locale = options[:locale] + locale ||= request.locale if respond_to?(:request) + val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' if options[:use_hidden] hidden_html(options[:field_name] || 'month', val, options) else month_options = [] - month_names = options[:use_month_names] || (options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES) + month_names = options[:use_month_names] || begin + (options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names').t locale + end month_names.unshift(nil) if month_names.size < 13 + 1.upto(12) do |month_number| month_name = if options[:use_month_numbers] month_number @@ -522,7 +536,7 @@ module ActionView end select_html(options[:field_name] || 'month', month_options.join, options, html_options) end - end + end # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius # can be changed using the :start_year and :end_year keys in the +options+. Both ascending and descending year @@ -612,15 +626,17 @@ module ActionView private def date_or_time_select(options, html_options = {}) + locale = options[:locale] + defaults = { :discard_type => true } options = defaults.merge(options) datetime = value(object) datetime ||= default_time_from_options(options[:default]) unless options[:include_blank] - + position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } - order = (options[:order] ||= [:year, :month, :day]) - + order = options[:order] ||= :'date.order'.t(locale) + # Discard explicit and implicit by not being included in the :order discard = {} discard[:year] = true if options[:discard_year] or !order.include?(:year) @@ -629,19 +645,19 @@ module ActionView discard[:hour] = true if options[:discard_hour] discard[:minute] = true if options[:discard_minute] or discard[:hour] discard[:second] = true unless options[:include_seconds] && !discard[:minute] - + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid # (otherwise it could be 31 and february wouldn't be a valid date) if datetime && discard[:day] && !discard[:month] datetime = datetime.change(:day => 1) end - + # Maintain valid dates by including hidden fields for discarded elements [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } - + # Ensure proper ordering of :hour, :minute and :second [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) } - + date_or_time_select = '' order.reverse.each do |param| # Send hidden fields for discarded elements once output has started @@ -656,9 +672,8 @@ module ActionView when :second then options[:include_seconds] ? " : " : "" else "" end) - end - + date_or_time_select end diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index b3f8e63c1b..c782c0a816 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -276,16 +276,25 @@ module ActionView # that they will be listed above the rest of the (long) list. # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. - def country_options_for_select(selected = nil, priority_countries = nil) + def country_options_for_select(*args) + options = args.extract_options! + + locale = options[:locale] + locale ||= request.locale if respond_to?(:request) + + selected, priority_countries = *args + countries = :'countries.names'.t options[:locale] country_options = "" if priority_countries + # TODO priority_countries need to be translated? country_options += options_for_select(priority_countries, selected) country_options += "\n" end - return country_options + options_for_select(COUNTRIES, selected) + return country_options + options_for_select(countries, selected) end + # Returns a string of option tags for pretty much any time zone in the # world. Supply a TimeZone name as +selected+ to have it marked as the @@ -340,43 +349,8 @@ module ActionView end # All the countries included in the country_options output. - COUNTRIES = ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", - "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", - "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", - "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", - "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", - "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", - "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", - "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", - "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", - "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", - "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", - "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", - "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)", - "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", - "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", - "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", - "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", - "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of", - "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", - "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", - "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", - "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", - "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", - "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", - "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", - "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", - "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", - "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", - "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", - "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", - "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", - "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", - "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", - "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", - "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", - "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", - "Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES") + # only included for backwards compatibility, please use the I18n interface + COUNTRIES = :'countries.names'.t 'en-US' unless const_defined?("COUNTRIES") end class InstanceTag #:nodoc: diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 120bb4cc1f..9d98036f2d 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -69,13 +69,19 @@ module ActionView # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) - options = options.stringify_keys - precision = options["precision"] || 2 - unit = options["unit"] || "$" - separator = precision > 0 ? options["separator"] || "." : "" - delimiter = options["delimiter"] || "," - format = options["format"] || "%u%n" + options = options.symbolize_keys + + locale = options[:locale] + locale ||= request.locale if respond_to?(:request) + defaults = :'currency.format'.t(locale) || {} + precision = options[:precision] || defaults[:precision] + unit = options[:unit] || defaults[:unit] + separator = options[:separator] || defaults[:separator] + delimiter = options[:delimiter] || defaults[:delimiter] + format = options[:format] || defaults[:format] + separator = '' if precision == 0 + begin parts = number_with_precision(number, precision).split('.') format.gsub(/%n/, number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s).gsub(/%u/, unit) diff --git a/actionpack/lib/action_view/lang/en-US.rb b/actionpack/lib/action_view/lang/en-US.rb new file mode 100644 index 0000000000..659f96a5f3 --- /dev/null +++ b/actionpack/lib/action_view/lang/en-US.rb @@ -0,0 +1,93 @@ +I18n.backend.add_translations :'en-US', { + :date => { + :formats => { + :default => "%Y-%m-%d", + :short => "%b %d", + :long => "%B %d, %Y", + }, + :day_names => Date::DAYNAMES, + :abbr_day_names => Date::ABBR_DAYNAMES, + :month_names => Date::MONTHNAMES, + :abbr_month_names => Date::ABBR_MONTHNAMES, + :order => [:year, :month, :day] + }, + :time => { + :formats => { + :default => "%a, %d %b %Y %H:%M:%S %z", + :short => "%d %b %H:%M", + :long => "%B %d, %Y %H:%M", + }, + :am => 'am', + :pm => 'pm' + }, + :datetime => { + :distance_in_words => { + :half_a_minute => 'half a minute', + :less_than_x_seconds => ['less than 1 second', 'less than {{count}} seconds'], + :x_seconds => ['1 second', '{{count}} seconds'], + :less_than_x_minutes => ['less than a minute', 'less than {{count}} minutes'], + :x_minutes => ['1 minute', '{{count}} minutes'], + :about_x_hours => ['about 1 hour', 'about {{count}} hours'], + :x_days => ['1 day', '{{count}} days'], + :about_x_months => ['about 1 month', 'about {{count}} months'], + :x_months => ['1 month', '{{count}} months'], + :about_x_years => ['about 1 year', 'about {{count}} year'], + :over_x_years => ['over 1 year', 'over {{count}} years'] + } + }, + :currency => { + :format => { + :unit => '$', + :precision => 2, + :separator => '.', + :delimiter => ',', + :format => '%u%n', + } + }, + :countries => { + :names => ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", + "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", + "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", + "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", + "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", + "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", + "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", + "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", + "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", + "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", + "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", + "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", + "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", + "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)", + "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", + "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", + "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", + "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", + "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", + "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", + "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", + "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", + "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", + "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", + "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", + "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", + "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", + "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", + "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", + "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", + "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", + "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", + "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", + "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", + "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", + "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", + "Yemen", "Zambia", "Zimbabwe"] + }, + :active_record => { + :error => { + :header_message => ["1 error prohibited this {{object_name}} from being saved", "{{count}} errors prohibited this {{object_name}} from being saved"], + :message => "There were problems with the following fields:" + } + } +} diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb new file mode 100644 index 0000000000..057fb9bd1a --- /dev/null +++ b/actionpack/test/template/active_record_helper_i18n_test.rb @@ -0,0 +1,47 @@ +require 'abstract_unit' + +class ActiveRecordHelperI18nTest < Test::Unit::TestCase + include ActionView::Helpers::ActiveRecordHelper + + attr_reader :request + def setup + @request = mock + @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) + stubs(:content_tag).returns 'content_tag' + + I18n.stubs(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns "1 error prohibited this from being saved" + I18n.stubs(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' + end + + def test_error_messages_for_given_a_locale_it_does_not_check_request_for_locale + request.expects(:locale).never + @object.errors.stubs(:count).returns 0 + error_messages_for(:object => @object, :locale => 'en-US') + end + + def test_error_messages_for_given_no_locale_it_checks_request_for_locale + request.expects(:locale).returns 'en-US' + @object.errors.stubs(:count).returns 0 + error_messages_for(:object => @object) + end + + def test_error_messages_for_given_a_header_message_option_it_does_not_translate_header_message + I18n.expects(:translate).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').never + error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en-US') + end + + def test_error_messages_for_given_no_header_message_option_it_translates_header_message + I18n.expects(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns 'header message' + error_messages_for(:object => @object, :locale => 'en-US') + end + + def test_error_messages_for_given_a_message_option_it_does_not_translate_message + I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).never + error_messages_for(:object => @object, :message => 'message', :locale => 'en-US') + end + + def test_error_messages_for_given_no_message_option_it_translates_message + I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' + error_messages_for(:object => @object, :locale => 'en-US') + end +end \ No newline at end of file diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb new file mode 100644 index 0000000000..9b7c03a400 --- /dev/null +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -0,0 +1,99 @@ +require 'abstract_unit' + +class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase + include ActionView::Helpers::DateHelper + attr_reader :request + + def setup + @request = mock + @from = Time.mktime(2004, 6, 6, 21, 45, 0) + end + + # distance_of_time_in_words + + def test_distance_of_time_in_words_given_a_locale_it_does_not_check_request_for_locale + request.expects(:locale).never + distance_of_time_in_words @from, @from + 1.second, false, :locale => 'en-US' + end + + def test_distance_of_time_in_words_given_no_locale_it_checks_request_for_locale + request.expects(:locale).returns 'en-US' + distance_of_time_in_words @from, @from + 1.second + end + + def test_distance_of_time_in_words_calls_i18n + { # with include_seconds + [2.seconds, true] => [:'less_than_x_seconds', 5], + [9.seconds, true] => [:'less_than_x_seconds', 10], + [19.seconds, true] => [:'less_than_x_seconds', 20], + [30.seconds, true] => [:'half_a_minute', nil], + [59.seconds, true] => [:'less_than_x_minutes', 1], + [60.seconds, true] => [:'x_minutes', 1], + + # without include_seconds + [29.seconds, false] => [:'less_than_x_minutes', 1], + [60.seconds, false] => [:'x_minutes', 1], + [44.minutes, false] => [:'x_minutes', 44], + [61.minutes, false] => [:'about_x_hours', 1], + [24.hours, false] => [:'x_days', 1], + [30.days, false] => [:'about_x_months', 1], + [60.days, false] => [:'x_months', 2], + [1.year, false] => [:'about_x_years', 1], + [3.years, false] => [:'over_x_years', 3] + + }.each do |passed, expected| + assert_distance_of_time_in_words_translates_key passed, expected + end + end + + def assert_distance_of_time_in_words_translates_key(passed, expected) + diff, include_seconds = *passed + key, count = *expected + to = @from + diff + + options = {:locale => 'en-US', :scope => :'datetime.distance_in_words'} + options[:count] = count if count + + I18n.expects(:t).with(key, options) + distance_of_time_in_words(@from, to, include_seconds, :locale => 'en-US') + end +end + +class DateHelperSelectTagsI18nTests < Test::Unit::TestCase + include ActionView::Helpers::DateHelper + attr_reader :request + + def setup + @request = mock + I18n.stubs(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES + end + + # select_month + + def test_select_month_given_use_month_names_option_does_not_translate_monthnames + I18n.expects(:translate).never + select_month(8, :locale => 'en-US', :use_month_names => Date::MONTHNAMES) + end + + def test_select_month_translates_monthnames + I18n.expects(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES + select_month(8, :locale => 'en-US') + end + + def test_select_month_given_use_short_month_option_translates_abbr_monthnames + I18n.expects(:translate).with(:'date.abbr_month_names', 'en-US').returns Date::ABBR_MONTHNAMES + select_month(8, :locale => 'en-US', :use_short_month => true) + end + + # date_or_time_select + + def test_date_or_time_select_given_an_order_options_does_not_translate_order + I18n.expects(:translate).never + datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en-US') + end + + def test_date_or_time_select_given_no_order_options_translates_order + I18n.expects(:translate).with(:'date.order', 'en-US').returns [:year, :month, :day] + datetime_select('post', 'updated_at', :locale => 'en-US') + end +end \ No newline at end of file diff --git a/actionpack/test/template/form_options_helper_i18n_test.rb b/actionpack/test/template/form_options_helper_i18n_test.rb new file mode 100644 index 0000000000..c9fc0768bb --- /dev/null +++ b/actionpack/test/template/form_options_helper_i18n_test.rb @@ -0,0 +1,26 @@ +require 'abstract_unit' + +class FormOptionsHelperI18nTests < Test::Unit::TestCase + include ActionView::Helpers::FormOptionsHelper + attr_reader :request + + def setup + @request = mock + end + + def test_country_options_for_select_given_a_locale_it_does_not_check_request_for_locale + request.expects(:locale).never + country_options_for_select :locale => 'en-US' + end + + def test_country_options_for_select_given_no_locale_it_checks_request_for_locale + request.expects(:locale).returns 'en-US' + country_options_for_select + end + + def test_country_options_for_select_translates_country_names + countries = ActionView::Helpers::FormOptionsHelper::COUNTRIES + I18n.expects(:translate).with(:'countries.names', 'en-US').returns countries + country_options_for_select :locale => 'en-US' + end +end \ No newline at end of file diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb new file mode 100644 index 0000000000..47cb035f56 --- /dev/null +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -0,0 +1,27 @@ +require 'abstract_unit' + +class NumberHelperI18nTests < Test::Unit::TestCase + include ActionView::Helpers::NumberHelper + + attr_reader :request + def setup + @request = mock + @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} + I18n.backend.add_translations 'en-US', :currency => {:format => @defaults} + end + + def test_number_to_currency_given_a_locale_it_does_not_check_request_for_locale + request.expects(:locale).never + number_to_currency(1, :locale => 'en-US') + end + + def test_number_to_currency_given_no_locale_it_checks_request_for_locale + request.expects(:locale).returns 'en-US' + number_to_currency(1) + end + + def test_number_to_currency_translates_currency_formats + I18n.expects(:translate).with(:'currency.format', 'en-US').returns @defaults + number_to_currency(1, :locale => 'en-US') + end +end \ No newline at end of file diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d4f7170305..b379bd26f8 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -80,3 +80,5 @@ end require 'active_record/connection_adapters/abstract_adapter' require 'active_record/schema_dumper' + +require 'active_record/lang/en-US.rb' diff --git a/activerecord/lib/active_record/lang/en-US.rb b/activerecord/lib/active_record/lang/en-US.rb new file mode 100644 index 0000000000..7c3bcfd85e --- /dev/null +++ b/activerecord/lib/active_record/lang/en-US.rb @@ -0,0 +1,25 @@ +I18n.backend.add_translations :'en-US', { + :active_record => { + :error_messages => { + :inclusion => "is not included in the list", + :exclusion => "is reserved", + :invalid => "is invalid", + :confirmation => "doesn't match confirmation", + :accepted => "must be accepted", + :empty => "can't be empty", + :blank => "can't be blank", + :too_long => "is too long (maximum is {{count}} characters)", + :too_short => "is too short (minimum is {{count}} characters)", + :wrong_length => "is the wrong length (should be {{count}} characters)", + :taken => "has already been taken", + :not_a_number => "is not a number", + :greater_than => "must be greater than {{count}}", + :greater_than_or_equal_to => "must be greater than or equal to {{count}}", + :equal_to => "must be equal to {{count}}", + :less_than => "must be less than {{count}}", + :less_than_or_equal_to => "must be less than or equal to {{count}}", + :odd => "must be odd", + :even => "must be even" + } + } +} \ No newline at end of file diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index c4e370d017..f54fb80137 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -23,30 +23,30 @@ module ActiveRecord @base, @errors = base, {} end - @@default_error_messages = { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is %d characters)", - :too_short => "is too short (minimum is %d characters)", - :wrong_length => "is the wrong length (should be %d characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than %d", - :greater_than_or_equal_to => "must be greater than or equal to %d", - :equal_to => "must be equal to %d", - :less_than => "must be less than %d", - :less_than_or_equal_to => "must be less than or equal to %d", - :odd => "must be odd", - :even => "must be even" - } - - # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. - cattr_accessor :default_error_messages + # @@default_error_messages = { + # :inclusion => "is not included in the list", + # :exclusion => "is reserved", + # :invalid => "is invalid", + # :confirmation => "doesn't match confirmation", + # :accepted => "must be accepted", + # :empty => "can't be empty", + # :blank => "can't be blank", + # :too_long => "is too long (maximum is %d characters)", + # :too_short => "is too short (minimum is %d characters)", + # :wrong_length => "is the wrong length (should be %d characters)", + # :taken => "has already been taken", + # :not_a_number => "is not a number", + # :greater_than => "must be greater than %d", + # :greater_than_or_equal_to => "must be greater than or equal to %d", + # :equal_to => "must be equal to %d", + # :less_than => "must be less than %d", + # :less_than_or_equal_to => "must be less than or equal to %d", + # :odd => "must be odd", + # :even => "must be even" + # } + # + # # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. + # cattr_accessor :default_error_messages # Adds an error to the base object instead of any particular attribute. This is used @@ -61,27 +61,34 @@ module ActiveRecord # for the same attribute and ensure that this error object returns false when asked if empty?. More than one # error can be added to the same +attribute+ in which case an array will be returned on a call to on(attribute). # If no +msg+ is supplied, "invalid" is assumed. - def add(attribute, msg = @@default_error_messages[:invalid]) - @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? - @errors[attribute.to_s] << msg - end + def add(attribute, message = nil) + message ||= :"active_record.error_messages.invalid".t + @errors[attribute.to_s] ||= [] + @errors[attribute.to_s] << message + end # Will add an error message to each of the attributes in +attributes+ that is empty. - def add_on_empty(attributes, msg = @@default_error_messages[:empty]) + def add_on_empty(attributes, custom_message = nil) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - is_empty = value.respond_to?("empty?") ? value.empty? : false - add(attr, msg) unless !value.nil? && !is_empty + is_empty = value.respond_to?("empty?") ? value.empty? : false + add(attr, generate_message(attr, :empty, :default => custom_message)) unless !value.nil? && !is_empty end end # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). - def add_on_blank(attributes, msg = @@default_error_messages[:blank]) + def add_on_blank(attributes, custom_message = nil) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - add(attr, msg) if value.blank? + add(attr, generate_message(attr, :blank, :default => custom_message)) if value.blank? end end + + def generate_message(attr, key, options = {}) + scope = [:active_record, :error_messages] + key.t(options.merge(:scope => scope + [:custom, @base.class.name.downcase, attr])) || + key.t(options.merge(:scope => scope)) + end # Returns true if the specified +attribute+ has errors associated with it. # @@ -166,22 +173,25 @@ module ActiveRecord # company = Company.create(:address => '123 First St.') # company.errors.full_messages # => # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] - def full_messages + def full_messages(options = {}) full_messages = [] + locale = options[:locale] @errors.each_key do |attr| - @errors[attr].each do |msg| - next if msg.nil? - + @errors[attr].each do |message| + next unless message + if attr == "base" - full_messages << msg + full_messages << message else - full_messages << @base.class.human_attribute_name(attr) + " " + msg + key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}" + attr_name = key.t(locale) || @base.class.human_attribute_name(attr) + full_messages << attr_name + " " + message end end end full_messages - end + end # Returns true if no errors have been added. def empty? @@ -388,15 +398,18 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + message = record.errors.generate_message(attr_name, :confirmation, :default => configuration[:message]) + record.errors.add(attr_name, message) + end end - end + end # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: # @@ -422,7 +435,7 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } + configuration = { :on => :save, :allow_nil => true, :accept => "1" } configuration.update(attr_names.extract_options!) db_cols = begin @@ -434,7 +447,10 @@ module ActiveRecord attr_accessor(*names) validates_each(attr_names,configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] + unless value == configuration[:accept] + message = record.errors.generate_message(attr_name, :accepted, :default => configuration[:message]) + record.errors.add(attr_name, message) + end end end @@ -461,7 +477,7 @@ module ActiveRecord # method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) # can't use validates_each here, because it cannot cope with nonexistent attributes, @@ -505,11 +521,7 @@ module ActiveRecord # method, proc or string should return or evaluate to a true or false value. def validates_length_of(*attrs) # Merge given options with defaults. - options = { - :too_long => ActiveRecord::Errors.default_error_messages[:too_long], - :too_short => ActiveRecord::Errors.default_error_messages[:too_short], - :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] - }.merge(DEFAULT_VALIDATION_OPTIONS) + options = {}.merge(DEFAULT_VALIDATION_OPTIONS) options.update(attrs.extract_options!.symbolize_keys) # Ensure that one and only one range option is specified. @@ -531,15 +543,14 @@ module ActiveRecord when :within, :in raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - too_short = options[:too_short] % option_value.begin - too_long = options[:too_long] % option_value.end - validates_each(attrs, options) do |record, attr, value| value = value.split(//) if value.kind_of?(String) if value.nil? or value.size < option_value.begin - record.errors.add(attr, too_short) + message = record.errors.generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + record.errors.add(attr, message) elsif value.size > option_value.end - record.errors.add(attr, too_long) + message = record.errors.generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) + record.errors.add(attr, message) end end when :is, :minimum, :maximum @@ -549,11 +560,14 @@ module ActiveRecord validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } - message = (options[:message] || options[message_options[option]]) % option_value - validates_each(attrs, options) do |record, attr, value| value = value.split(//) if value.kind_of?(String) - record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] + unless !value.nil? and value.size.method(validity_checks[option])[option_value] + key = message_options[option] + custom_message = options[:message] || options[key] + message = record.errors.generate_message(attr, key, :default => custom_message, :count => option_value) + record.errors.add(attr, message) + end end end end @@ -595,7 +609,7 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_uniqueness_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true } + configuration = { :case_sensitive => true } configuration.update(attr_names.extract_options!) validates_each(attr_names,configuration) do |record, attr_name, value| @@ -654,8 +668,11 @@ module ActiveRecord if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? found = results.any? { |a| a[attr_name.to_s] == value } end - - record.errors.add(attr_name, configuration[:message]) if found + + if found + message = record.errors.generate_message(attr_name, :taken, :default => configuration[:message]) + record.errors.add(attr_name, message) + end end end end @@ -685,13 +702,16 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } + configuration = { :on => :save, :with => nil } configuration.update(attr_names.extract_options!) raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with] + unless value.to_s =~ configuration[:with] + message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -715,7 +735,7 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } + configuration = { :on => :save, :with => nil } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -723,7 +743,10 @@ module ActiveRecord raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) + unless enum.include?(value) + message = record.errors.generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -747,7 +770,7 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } + configuration = { :on => :save, :with => nil } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -755,7 +778,10 @@ module ActiveRecord raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) + if enum.include?(value) + message = record.errors.generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -791,12 +817,14 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_associated(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless - (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -844,7 +872,8 @@ module ActiveRecord if configuration[:only_integer] unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + record.errors.add(attr_name, message) next end raw_value = raw_value.to_i @@ -852,7 +881,8 @@ module ActiveRecord begin raw_value = Kernel.Float(raw_value.to_s) rescue ArgumentError, TypeError - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + record.errors.add(attr_name, message) next end end @@ -860,10 +890,12 @@ module ActiveRecord numericality_options.each do |option| case option when :odd, :even - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + message = record.errors.generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) + record.errors.add(attr_name, message) + end else - message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option] - message = message % configuration[option] if configuration[option] + message = record.errors.generate_message(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] end end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb new file mode 100644 index 0000000000..eb454fca20 --- /dev/null +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -0,0 +1,539 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class ActiveRecordValidationsI18nTests < Test::Unit::TestCase + def setup + reset_callbacks Topic + @topic = Topic.new + I18n.backend.add_translations('en-US', :active_record => {:error_messages => {:custom => nil}}) + end + + def teardown + reset_callbacks Topic + load 'active_record/lang/en-US.rb' + end + + def unique_topic + @unique ||= Topic.create :title => 'unique!' + end + + def replied_topic + @replied_topic ||= begin + topic = Topic.create(:title => "topic") + topic.replies << Reply.new + topic + end + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + # ActiveRecord::Errors + + def test_errors_generate_message_translates_custom_model_attribute_key + global_scope = [:active_record, :error_messages] + custom_scope = global_scope + [:custom, 'topic', :title] + + I18n.expects(:translate).with(:invalid, :scope => custom_scope).returns 'translation' + I18n.expects(:translate).with(:invalid, :scope => global_scope).never + + @topic.errors.generate_message :title, :invalid + end + + def test_errors_generate_message_given_a_custom_message_translates_custom_model_attribute_key_with_custom_message_as_default + custom_scope = [:active_record, :error_messages, :custom, 'topic', :title] + + I18n.expects(:translate).with(:invalid, :scope => custom_scope, :default => 'default from class def').returns 'translation' + @topic.errors.generate_message :title, :invalid, :default => 'default from class def' + end + + def test_errors_generate_message_given_no_custom_message_falls_back_to_global_default_key_translation + global_scope = [:active_record, :error_messages] + custom_scope = global_scope + [:custom, 'topic', :title] + + I18n.stubs(:translate).with(:invalid, :scope => custom_scope).returns nil + I18n.expects(:translate).with(:invalid, :scope => global_scope) + @topic.errors.generate_message :title, :invalid + end + + def test_errors_add_given_no_message_it_translates_invalid + I18n.expects(:translate).with(:"active_record.error_messages.invalid") + @topic.errors.add :title + end + + def test_errors_add_on_empty_generates_message + @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) + @topic.errors.add_on_empty :title + end + + def test_errors_add_on_empty_generates_message_with_custom_default_message + @topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'}) + @topic.errors.add_on_empty :title, 'custom' + end + + def test_errors_add_on_blank_generates_message + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @topic.errors.add_on_blank :title + end + + def test_errors_add_on_blank_generates_message_with_custom_default_message + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @topic.errors.add_on_blank :title, 'custom' + end + + def test_errors_full_messages_translates_human_attribute_name_for_model_attributes + @topic.errors.instance_variable_set :@errors, { 'title' => 'empty' } + I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US').returns('Title') + @topic.errors.full_messages :locale => 'en-US' + end + + + # ActiveRecord::Validations + + # validates_confirmation_of + + def test_validates_confirmation_of_generates_message + Topic.validates_confirmation_of :title + @topic.title_confirmation = 'foo' + @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil}) + @topic.valid? + end + + def test_validates_confirmation_of_generates_message_with_custom_default_message + Topic.validates_confirmation_of :title, :message => 'custom' + @topic.title_confirmation = 'foo' + @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'}) + @topic.valid? + end + + def test_validates_confirmation_of_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + + Topic.validates_confirmation_of :title + @topic.title_confirmation = 'foo' + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_confirmation_of_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + + Topic.validates_confirmation_of :title + @topic.title_confirmation = 'foo' + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_acceptance_of + + def test_validates_acceptance_of_generates_message + Topic.validates_acceptance_of :title, :allow_nil => false + @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) + @topic.valid? + end + + def test_validates_acceptance_of_generates_message_with_custom_default_message + Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false + @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) + @topic.valid? + end + + def test_validates_acceptance_of_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + + Topic.validates_acceptance_of :title, :allow_nil => false + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_acceptance_of_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + + Topic.validates_acceptance_of :title, :allow_nil => false + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_presence_of + + def test_validates_presence_of_generates_message + Topic.validates_presence_of :title + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @topic.valid? + end + + def test_validates_presence_of_generates_message_with_custom_default_message + Topic.validates_presence_of :title, :message => 'custom' + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @topic.valid? + end + + def test_validates_presence_of_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + + Topic.validates_presence_of :title + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_presence_of_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + + Topic.validates_presence_of :title + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_length_of :within + + def test_validates_length_of_within_generates_message + Topic.validates_length_of :title, :within => 3..5 + @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_custom_default_message + Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom' + @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) + @topic.valid? + end + + def test_validates_length_of_within_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + + Topic.validates_length_of :title, :within => 3..5 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + + Topic.validates_length_of :title, :within => 3..5 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_length_of :is + + def test_validates_length_of_is_generates_message + Topic.validates_length_of :title, :is => 5 + @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_is_generates_message_with_custom_default_message + Topic.validates_length_of :title, :is => 5, :message => 'custom' + @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'}) + @topic.valid? + end + + def test_validates_length_of_within_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_uniqueness_of + + def test_validates_uniqueness_of_generates_message + Topic.validates_uniqueness_of :title + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil}) + @topic.valid? + end + + def test_validates_uniqueness_of_generates_message_with_custom_default_message + Topic.validates_uniqueness_of :title, :message => 'custom' + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom'}) + @topic.valid? + end + + def test_validates_length_of_within_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_format_of + + def test_validates_format_of_generates_message + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @topic.title = '72x' + @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil}) + @topic.valid? + end + + def test_validates_format_of_generates_message_with_custom_default_message + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom' + @topic.title = '72x' + @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'}) + @topic.valid? + end + + def test_validates_format_of_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_format_of_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_inclusion_of + + def test_validates_inclusion_of_generates_message + Topic.validates_inclusion_of :title, :in => %w(a b c) + @topic.title = 'z' + @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil}) + @topic.valid? + end + + def test_validates_inclusion_of_generates_message_with_custom_default_message + Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom' + @topic.title = 'z' + @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'}) + @topic.valid? + end + + def test_validates_inclusion_of_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + + Topic.validates_inclusion_of :title, :in => %w(a b c) + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_inclusion_of_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + + Topic.validates_inclusion_of :title, :in => %w(a b c) + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_exclusion_of + + def test_validates_exclusion_of_generates_message + Topic.validates_exclusion_of :title, :in => %w(a b c) + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil}) + @topic.valid? + end + + def test_validates_exclusion_of_generates_message_with_custom_default_message + Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom' + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'}) + @topic.valid? + end + + def test_validates_exclusion_of_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} + + Topic.validates_exclusion_of :title, :in => %w(a b c) + @topic.title = 'a' + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_exclusion_of_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} + + Topic.validates_exclusion_of :title, :in => %w(a b c) + @topic.title = 'a' + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_numericality_of :only_integer + + def test_validates_numericality_of_only_integer_generates_message + Topic.validates_numericality_of :title, :only_integer => true + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom' + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @topic.valid? + end + + def test_validates_numericality_of_only_integer_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + + Topic.validates_numericality_of :title, :only_integer => true + @topic.title = 'a' + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_numericality_of_only_integer_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + + Topic.validates_numericality_of :title, :only_integer => true + @topic.title = 'a' + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_numericality_of :odd + + def test_validates_numericality_of_odd_generates_message + Topic.validates_numericality_of :title, :only_integer => true, :odd => true + @topic.title = 0 + @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom' + @topic.title = 0 + @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'}) + @topic.valid? + end + + def test_validates_numericality_of_odd_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} + + Topic.validates_numericality_of :title, :only_integer => true, :odd => true + @topic.title = 0 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_numericality_of_odd_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} + + Topic.validates_numericality_of :title, :only_integer => true, :odd => true + @topic.title = 0 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_numericality_of :less_than + + def test_validates_numericality_of_less_than_generates_message + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @topic.title = 1 + @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom' + @topic.title = 1 + @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'}) + @topic.valid? + end + + def test_validates_numericality_of_less_than_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} + + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @topic.title = 1 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_numericality_of_less_than_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} + + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @topic.title = 1 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_associated + + def test_validates_associated_generates_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + def test_validates_associated_generates_message_with_custom_default_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + def test_validates_associated_finds_custom_model_key_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal 'custom message', replied_topic.errors.on(:replies) + end + + def test_validates_associated_finds_global_default_translation + I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal 'global message', replied_topic.errors.on(:replies) + end +end \ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index a9882828ca..08e608d346 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -6,10 +6,15 @@ module ActiveSupport #:nodoc: module Conversions # Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options: # * :connector - The word used to join the last element in arrays with two or more elements (default: "and") - # * :skip_last_comma - Set to true to return "a, b and c" instead of "a, b, and c". - def to_sentence(options = {}) - options.assert_valid_keys(:connector, :skip_last_comma) - options.reverse_merge! :connector => 'and', :skip_last_comma => false + # * :skip_last_comma - Set to true to return "a, b and c" instead of "a, b, and c". + def to_sentence(options = {}) + options.assert_valid_keys(:connector, :skip_last_comma, :locale) + + locale = options[:locale] + locale ||= request.locale if respond_to?(:request) + + default = :'support.array.sentence_connector'.t(locale) + options.reverse_merge! :connector => default, :skip_last_comma => false options[:connector] = "#{options[:connector]} " unless options[:connector].nil? || options[:connector].strip == '' case length @@ -23,6 +28,7 @@ module ActiveSupport #:nodoc: "#{self[0...-1].join(', ')}#{options[:skip_last_comma] ? '' : ','} #{options[:connector]}#{self[-1]}" end end + # Calls to_param on all its elements and joins the result with # slashes. This is used by url_for in Action Pack. diff --git a/activesupport/lib/active_support/vendor.rb b/activesupport/lib/active_support/vendor.rb index a02e42f791..bca2664fa4 100644 --- a/activesupport/lib/active_support/vendor.rb +++ b/activesupport/lib/active_support/vendor.rb @@ -23,4 +23,11 @@ begin gem 'tzinfo', '~> 0.3.9' rescue Gem::LoadError $:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.9" +end + +begin + gem 'i18n', '~> 0.3.9' +rescue Gem::LoadError + $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.0.1/lib" # TODO + require 'i18n' end \ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1 b/activesupport/lib/active_support/vendor/i18n-0.0.1 new file mode 160000 index 0000000000..70ab0f3cc5 --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1 @@ -0,0 +1 @@ +Subproject commit 70ab0f3cc5921cc67e09741939a08b2582d707cb -- cgit v1.2.3 From 4a8486a1b1aa28d4cab5571b55301917221870e9 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 19 Jun 2008 16:43:30 +0200 Subject: add lang file for active_support --- activesupport/lib/active_support.rb | 1 + activesupport/lib/active_support/lang/en-US.rb | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 activesupport/lib/active_support/lang/en-US.rb diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 1a8603e892..acdb3056d2 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -55,6 +55,7 @@ require 'active_support/multibyte' require 'active_support/base64' require 'active_support/time_with_zone' +require 'active_support/lang/en-US.rb' Inflector = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Inflector', 'ActiveSupport::Inflector') Dependencies = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Dependencies', 'ActiveSupport::Dependencies') diff --git a/activesupport/lib/active_support/lang/en-US.rb b/activesupport/lib/active_support/lang/en-US.rb new file mode 100644 index 0000000000..8732927f48 --- /dev/null +++ b/activesupport/lib/active_support/lang/en-US.rb @@ -0,0 +1,7 @@ +I18n.backend.add_translations :'en-US', { + :support => { + :array => { + :sentence_connector => 'and' + } + } +} \ No newline at end of file -- cgit v1.2.3 From fdb5f810dc41f54f8cdb9c51154ff8987362c13a Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 19 Jun 2008 16:55:47 +0200 Subject: I18n has not been released as a gem, yet --- activesupport/lib/active_support/vendor.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/activesupport/lib/active_support/vendor.rb b/activesupport/lib/active_support/vendor.rb index bca2664fa4..381471b833 100644 --- a/activesupport/lib/active_support/vendor.rb +++ b/activesupport/lib/active_support/vendor.rb @@ -25,9 +25,10 @@ rescue Gem::LoadError $:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.9" end -begin - gem 'i18n', '~> 0.3.9' -rescue Gem::LoadError - $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.0.1/lib" # TODO +# TODO I18n gem has not been released yet +# begin +# gem 'i18n', '~> 0.0.1' +# rescue Gem::LoadError + $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.0.1/lib" require 'i18n' -end \ No newline at end of file +# end \ No newline at end of file -- cgit v1.2.3 From 2fe4d350e98d7f825cf3d1f9233075a5a79e32a1 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 19 Jun 2008 18:31:11 +0200 Subject: make ActiveRecord::Errors.default_error_messages look up translated error messages --- activerecord/lib/active_record/validations.rb | 33 ++++++--------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index f54fb80137..0ca68989d2 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -18,37 +18,18 @@ module ActiveRecord # determine whether the object is in a valid state to be saved. See usage example in Validations. class Errors include Enumerable + + class << self + def default_error_messages + # TODO deprecate this? + :'active_record.error_messages'.t + end + end def initialize(base) # :nodoc: @base, @errors = base, {} end - # @@default_error_messages = { - # :inclusion => "is not included in the list", - # :exclusion => "is reserved", - # :invalid => "is invalid", - # :confirmation => "doesn't match confirmation", - # :accepted => "must be accepted", - # :empty => "can't be empty", - # :blank => "can't be blank", - # :too_long => "is too long (maximum is %d characters)", - # :too_short => "is too short (minimum is %d characters)", - # :wrong_length => "is the wrong length (should be %d characters)", - # :taken => "has already been taken", - # :not_a_number => "is not a number", - # :greater_than => "must be greater than %d", - # :greater_than_or_equal_to => "must be greater than or equal to %d", - # :equal_to => "must be equal to %d", - # :less_than => "must be less than %d", - # :less_than_or_equal_to => "must be less than or equal to %d", - # :odd => "must be odd", - # :even => "must be even" - # } - # - # # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. - # cattr_accessor :default_error_messages - - # Adds an error to the base object instead of any particular attribute. This is used # to report errors that don't tie to any specific attribute, but rather to the object # as a whole. These error messages don't get prepended with any field name when iterating -- cgit v1.2.3 From b09c6e7444cf05f986e601bcc22cf17ede7b63bd Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 19 Jun 2008 19:08:14 +0200 Subject: add a generic tranlate view helper --- actionpack/lib/action_view/helpers/i18n_helper.rb | 19 +++++++++++++ actionpack/test/template/i18n_helper_test.rb | 33 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 actionpack/lib/action_view/helpers/i18n_helper.rb create mode 100644 actionpack/test/template/i18n_helper_test.rb diff --git a/actionpack/lib/action_view/helpers/i18n_helper.rb b/actionpack/lib/action_view/helpers/i18n_helper.rb new file mode 100644 index 0000000000..1b1d1f301d --- /dev/null +++ b/actionpack/lib/action_view/helpers/i18n_helper.rb @@ -0,0 +1,19 @@ +module ActionView + module Helpers + module I18nHelper + def translate(*args) + # inserts the locale or current request locale to the argument list if no locale + # has been passed or the locale has been passed as part of the options hash + options = args.extract_options! + if args.size != 2 + locale = options.delete :locale + locale ||= request.locale if respond_to? :request + args << locale if locale + end + args << options unless options.empty? + I18n.translate *args + end + alias :t :translate + end + end +end \ No newline at end of file diff --git a/actionpack/test/template/i18n_helper_test.rb b/actionpack/test/template/i18n_helper_test.rb new file mode 100644 index 0000000000..598731568c --- /dev/null +++ b/actionpack/test/template/i18n_helper_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'action_view/helpers/i18n_helper' + +class I18nHelperTests < Test::Unit::TestCase + include ActionView::Helpers::I18nHelper + + attr_reader :request + def setup + @request = stub :locale => 'en-US' + I18n.stubs(:translate).with(:'foo.bar', 'en-US').returns 'Foo Bar' + end + + def test_translate_given_a_locale_argument_it_does_not_check_request_for_locale + request.expects(:locale).never + assert_equal 'Foo Bar', translate(:'foo.bar', :locale => 'en-US') + end + + def test_translate_given_a_locale_option_it_does_not_check_request_for_locale + request.expects(:locale).never + I18n.expects(:translate).with(:'foo.bar', 'en-US').returns 'Foo Bar' + assert_equal 'Foo Bar', translate(:'foo.bar', :locale => 'en-US') + end + + def test_translate_given_no_locale_it_checks_request_for_locale + request.expects(:locale).returns 'en-US' + assert_equal 'Foo Bar', translate(:'foo.bar') + end + + def test_translate_delegates_to_i18n_translate + I18n.expects(:translate).with(:'foo.bar', 'en-US').returns 'Foo Bar' + assert_equal 'Foo Bar', translate(:'foo.bar') + end +end \ No newline at end of file -- cgit v1.2.3 From 585c8c17c303fc46fcf014a644a541eae6cb5ffd Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 20 Jun 2008 09:13:20 +0200 Subject: rename Backend::Simple#add_translations to set_translations because it overwrites existing translations --- .../lib/action_view/helpers/form_options_helper.rb | 2 +- actionpack/lib/action_view/lang/en-US.rb | 2 +- .../test/template/number_helper_i18n_test.rb | 2 +- activerecord/lib/active_record/lang/en-US.rb | 2 +- activerecord/lib/active_record/validations.rb | 2 +- activerecord/test/cases/validations_i18n_test.rb | 80 +++++++++++----------- activesupport/lib/active_support/lang/en-US.rb | 2 +- activesupport/lib/active_support/vendor/i18n-0.0.1 | 2 +- 8 files changed, 47 insertions(+), 47 deletions(-) diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index c782c0a816..a6b9e65a77 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -349,7 +349,7 @@ module ActionView end # All the countries included in the country_options output. - # only included for backwards compatibility, please use the I18n interface + # deprecated. please use :'countries.names'.t directly COUNTRIES = :'countries.names'.t 'en-US' unless const_defined?("COUNTRIES") end diff --git a/actionpack/lib/action_view/lang/en-US.rb b/actionpack/lib/action_view/lang/en-US.rb index 659f96a5f3..70eb1b79de 100644 --- a/actionpack/lib/action_view/lang/en-US.rb +++ b/actionpack/lib/action_view/lang/en-US.rb @@ -1,4 +1,4 @@ -I18n.backend.add_translations :'en-US', { +I18n.backend.set_translations :'en-US', { :date => { :formats => { :default => "%Y-%m-%d", diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index 47cb035f56..d002ad4a2f 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -7,7 +7,7 @@ class NumberHelperI18nTests < Test::Unit::TestCase def setup @request = mock @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} - I18n.backend.add_translations 'en-US', :currency => {:format => @defaults} + I18n.backend.set_translations 'en-US', :currency => {:format => @defaults} end def test_number_to_currency_given_a_locale_it_does_not_check_request_for_locale diff --git a/activerecord/lib/active_record/lang/en-US.rb b/activerecord/lib/active_record/lang/en-US.rb index 7c3bcfd85e..f307f40f1a 100644 --- a/activerecord/lib/active_record/lang/en-US.rb +++ b/activerecord/lib/active_record/lang/en-US.rb @@ -1,4 +1,4 @@ -I18n.backend.add_translations :'en-US', { +I18n.backend.set_translations :'en-US', { :active_record => { :error_messages => { :inclusion => "is not included in the list", diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 0ca68989d2..bcb204f1ba 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -21,7 +21,7 @@ module ActiveRecord class << self def default_error_messages - # TODO deprecate this? + # ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use :'active_record.error_messages'.t.") :'active_record.error_messages'.t end end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index eb454fca20..8f8574c242 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -6,7 +6,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def setup reset_callbacks Topic @topic = Topic.new - I18n.backend.add_translations('en-US', :active_record => {:error_messages => {:custom => nil}}) + I18n.backend.set_translations('en-US', :active_record => {:error_messages => {:custom => nil}}) end def teardown @@ -113,8 +113,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_confirmation_of_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @@ -123,7 +123,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_confirmation_of_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @@ -147,8 +147,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_acceptance_of_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? @@ -156,7 +156,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_acceptance_of_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? @@ -179,8 +179,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_presence_of_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} Topic.validates_presence_of :title @topic.valid? @@ -188,7 +188,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_presence_of_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} Topic.validates_presence_of :title @topic.valid? @@ -211,8 +211,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} Topic.validates_length_of :title, :within => 3..5 @topic.valid? @@ -220,7 +220,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} Topic.validates_length_of :title, :within => 3..5 @topic.valid? @@ -243,8 +243,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -252,7 +252,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -277,8 +277,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -286,7 +286,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -311,8 +311,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_format_of_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? @@ -320,7 +320,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_format_of_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? @@ -345,8 +345,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_inclusion_of_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? @@ -354,7 +354,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_inclusion_of_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? @@ -379,8 +379,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_exclusion_of_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @@ -389,7 +389,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_exclusion_of_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @@ -415,8 +415,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_only_integer_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @@ -425,7 +425,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_only_integer_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @@ -451,8 +451,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_odd_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @@ -461,7 +461,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_odd_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @@ -487,8 +487,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_less_than_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @@ -497,7 +497,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_less_than_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @@ -521,8 +521,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_associated_finds_custom_model_key_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_associated :replies replied_topic.valid? @@ -530,7 +530,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_associated_finds_global_default_translation - I18n.backend.add_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_associated :replies replied_topic.valid? diff --git a/activesupport/lib/active_support/lang/en-US.rb b/activesupport/lib/active_support/lang/en-US.rb index 8732927f48..5b8e04363e 100644 --- a/activesupport/lib/active_support/lang/en-US.rb +++ b/activesupport/lib/active_support/lang/en-US.rb @@ -1,4 +1,4 @@ -I18n.backend.add_translations :'en-US', { +I18n.backend.set_translations :'en-US', { :support => { :array => { :sentence_connector => 'and' diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1 b/activesupport/lib/active_support/vendor/i18n-0.0.1 index 70ab0f3cc5..1af3435539 160000 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1 +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1 @@ -1 +1 @@ -Subproject commit 70ab0f3cc5921cc67e09741939a08b2582d707cb +Subproject commit 1af3435539b4a0729c13d21c5df037a635fe98c1 -- cgit v1.2.3 From c1e2506494107892a0962b8491cd234f77949c08 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sat, 21 Jun 2008 11:27:19 +0200 Subject: Changed process of storing translations from the client libraries to the backend: clients now can pass a block to backend#populate which can contain code to load and register translations. This makes sense for backends that persist their translations (e.g. to db) so the repeated loading and passing of translations throughout the server startup would be wasted resources. --- actionpack/lib/action_view.rb | 4 +- actionpack/lib/action_view/lang/en-US.rb | 2 +- .../test/template/number_helper_i18n_test.rb | 2 +- activerecord/lib/active_record.rb | 5 +- activerecord/lib/active_record/lang/en-US.rb | 2 +- activerecord/test/cases/validations_i18n_test.rb | 80 +++++++++++----------- activesupport/lib/active_support.rb | 5 +- activesupport/lib/active_support/lang/en-US.rb | 2 +- activesupport/lib/active_support/vendor/i18n-0.0.1 | 2 +- 9 files changed, 56 insertions(+), 48 deletions(-) diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 33d50a61c4..dff487377f 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -32,7 +32,9 @@ require 'action_view/base' require 'action_view/partials' require 'action_view/template_error' -require 'action_view/lang/en-US.rb' +I18n.backend.populate do + require 'action_view/lang/en-US.rb' +end ActionView::Base.class_eval do include ActionView::Partials diff --git a/actionpack/lib/action_view/lang/en-US.rb b/actionpack/lib/action_view/lang/en-US.rb index 70eb1b79de..6b5345ed90 100644 --- a/actionpack/lib/action_view/lang/en-US.rb +++ b/actionpack/lib/action_view/lang/en-US.rb @@ -1,4 +1,4 @@ -I18n.backend.set_translations :'en-US', { +I18n.backend.store_translations :'en-US', { :date => { :formats => { :default => "%Y-%m-%d", diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index d002ad4a2f..b75af03378 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -7,7 +7,7 @@ class NumberHelperI18nTests < Test::Unit::TestCase def setup @request = mock @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} - I18n.backend.set_translations 'en-US', :currency => {:format => @defaults} + I18n.backend.store_translations 'en-US', :currency => {:format => @defaults} end def test_number_to_currency_given_a_locale_it_does_not_check_request_for_locale diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index b379bd26f8..71882833d4 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -81,4 +81,7 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/schema_dumper' -require 'active_record/lang/en-US.rb' +I18n.backend.populate do + require 'active_record/lang/en-US.rb' +end + diff --git a/activerecord/lib/active_record/lang/en-US.rb b/activerecord/lib/active_record/lang/en-US.rb index f307f40f1a..b31e13ed3a 100644 --- a/activerecord/lib/active_record/lang/en-US.rb +++ b/activerecord/lib/active_record/lang/en-US.rb @@ -1,4 +1,4 @@ -I18n.backend.set_translations :'en-US', { +I18n.backend.store_translations :'en-US', { :active_record => { :error_messages => { :inclusion => "is not included in the list", diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 8f8574c242..de844bf5a6 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -6,7 +6,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def setup reset_callbacks Topic @topic = Topic.new - I18n.backend.set_translations('en-US', :active_record => {:error_messages => {:custom => nil}}) + I18n.backend.store_translations('en-US', :active_record => {:error_messages => {:custom => nil}}) end def teardown @@ -113,8 +113,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_confirmation_of_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @@ -123,7 +123,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_confirmation_of_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @@ -147,8 +147,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_acceptance_of_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? @@ -156,7 +156,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_acceptance_of_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? @@ -179,8 +179,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_presence_of_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} Topic.validates_presence_of :title @topic.valid? @@ -188,7 +188,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_presence_of_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} Topic.validates_presence_of :title @topic.valid? @@ -211,8 +211,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} Topic.validates_length_of :title, :within => 3..5 @topic.valid? @@ -220,7 +220,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} Topic.validates_length_of :title, :within => 3..5 @topic.valid? @@ -243,8 +243,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -252,7 +252,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -277,8 +277,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -286,7 +286,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_length_of_within_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} Topic.validates_length_of :title, :is => 5 @topic.valid? @@ -311,8 +311,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_format_of_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? @@ -320,7 +320,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_format_of_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? @@ -345,8 +345,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_inclusion_of_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? @@ -354,7 +354,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_inclusion_of_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? @@ -379,8 +379,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_exclusion_of_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @@ -389,7 +389,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_exclusion_of_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @@ -415,8 +415,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_only_integer_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @@ -425,7 +425,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_only_integer_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @@ -451,8 +451,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_odd_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @@ -461,7 +461,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_odd_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @@ -487,8 +487,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_less_than_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @@ -497,7 +497,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_numericality_of_less_than_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @@ -521,8 +521,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_associated_finds_custom_model_key_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_associated :replies replied_topic.valid? @@ -530,7 +530,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end def test_validates_associated_finds_global_default_translation - I18n.backend.set_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} Topic.validates_associated :replies replied_topic.valid? diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index acdb3056d2..0de948dda9 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -55,7 +55,10 @@ require 'active_support/multibyte' require 'active_support/base64' require 'active_support/time_with_zone' -require 'active_support/lang/en-US.rb' + +I18n.backend.populate do + require 'active_support/lang/en-US.rb' +end Inflector = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Inflector', 'ActiveSupport::Inflector') Dependencies = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Dependencies', 'ActiveSupport::Dependencies') diff --git a/activesupport/lib/active_support/lang/en-US.rb b/activesupport/lib/active_support/lang/en-US.rb index 5b8e04363e..aa06fe14bd 100644 --- a/activesupport/lib/active_support/lang/en-US.rb +++ b/activesupport/lib/active_support/lang/en-US.rb @@ -1,4 +1,4 @@ -I18n.backend.set_translations :'en-US', { +I18n.backend.store_translations :'en-US', { :support => { :array => { :sentence_connector => 'and' diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1 b/activesupport/lib/active_support/vendor/i18n-0.0.1 index 1af3435539..8e43afa38a 160000 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1 +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1 @@ -1 +1 @@ -Subproject commit 1af3435539b4a0729c13d21c5df037a635fe98c1 +Subproject commit 8e43afa38aa007d1de6d6acf44d43143c403d13f -- cgit v1.2.3 From 428aa24d24032d382dc3d9ccf131e0c874043dbd Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sat, 21 Jun 2008 11:35:02 +0200 Subject: Renamed lang/ to locale/ because that's what we seem to standarize on. Also, in future this place can be used for data/code that's not literally translations but conceptually belongs to the locale (like custom pluralization algorithms etc.). --- actionpack/lib/action_view.rb | 2 +- actionpack/lib/action_view/lang/en-US.rb | 93 ------------------------ actionpack/lib/action_view/locale/en-US.rb | 93 ++++++++++++++++++++++++ activerecord/lib/active_record.rb | 2 +- activerecord/lib/active_record/lang/en-US.rb | 25 ------- activerecord/lib/active_record/locale/en-US.rb | 25 +++++++ activerecord/test/cases/validations_i18n_test.rb | 2 +- activesupport/lib/active_support.rb | 2 +- activesupport/lib/active_support/lang/en-US.rb | 7 -- activesupport/lib/active_support/locale/en-US.rb | 7 ++ 10 files changed, 129 insertions(+), 129 deletions(-) delete mode 100644 actionpack/lib/action_view/lang/en-US.rb create mode 100644 actionpack/lib/action_view/locale/en-US.rb delete mode 100644 activerecord/lib/active_record/lang/en-US.rb create mode 100644 activerecord/lib/active_record/locale/en-US.rb delete mode 100644 activesupport/lib/active_support/lang/en-US.rb create mode 100644 activesupport/lib/active_support/locale/en-US.rb diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index dff487377f..067a871f79 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -33,7 +33,7 @@ require 'action_view/partials' require 'action_view/template_error' I18n.backend.populate do - require 'action_view/lang/en-US.rb' + require 'action_view/locale/en-US.rb' end ActionView::Base.class_eval do diff --git a/actionpack/lib/action_view/lang/en-US.rb b/actionpack/lib/action_view/lang/en-US.rb deleted file mode 100644 index 6b5345ed90..0000000000 --- a/actionpack/lib/action_view/lang/en-US.rb +++ /dev/null @@ -1,93 +0,0 @@ -I18n.backend.store_translations :'en-US', { - :date => { - :formats => { - :default => "%Y-%m-%d", - :short => "%b %d", - :long => "%B %d, %Y", - }, - :day_names => Date::DAYNAMES, - :abbr_day_names => Date::ABBR_DAYNAMES, - :month_names => Date::MONTHNAMES, - :abbr_month_names => Date::ABBR_MONTHNAMES, - :order => [:year, :month, :day] - }, - :time => { - :formats => { - :default => "%a, %d %b %Y %H:%M:%S %z", - :short => "%d %b %H:%M", - :long => "%B %d, %Y %H:%M", - }, - :am => 'am', - :pm => 'pm' - }, - :datetime => { - :distance_in_words => { - :half_a_minute => 'half a minute', - :less_than_x_seconds => ['less than 1 second', 'less than {{count}} seconds'], - :x_seconds => ['1 second', '{{count}} seconds'], - :less_than_x_minutes => ['less than a minute', 'less than {{count}} minutes'], - :x_minutes => ['1 minute', '{{count}} minutes'], - :about_x_hours => ['about 1 hour', 'about {{count}} hours'], - :x_days => ['1 day', '{{count}} days'], - :about_x_months => ['about 1 month', 'about {{count}} months'], - :x_months => ['1 month', '{{count}} months'], - :about_x_years => ['about 1 year', 'about {{count}} year'], - :over_x_years => ['over 1 year', 'over {{count}} years'] - } - }, - :currency => { - :format => { - :unit => '$', - :precision => 2, - :separator => '.', - :delimiter => ',', - :format => '%u%n', - } - }, - :countries => { - :names => ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", - "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", - "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", - "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", - "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", - "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", - "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", - "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", - "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", - "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", - "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", - "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", - "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", - "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)", - "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", - "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", - "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", - "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", - "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of", - "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", - "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", - "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", - "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", - "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", - "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", - "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", - "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", - "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", - "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", - "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", - "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", - "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", - "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", - "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", - "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", - "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", - "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", - "Yemen", "Zambia", "Zimbabwe"] - }, - :active_record => { - :error => { - :header_message => ["1 error prohibited this {{object_name}} from being saved", "{{count}} errors prohibited this {{object_name}} from being saved"], - :message => "There were problems with the following fields:" - } - } -} diff --git a/actionpack/lib/action_view/locale/en-US.rb b/actionpack/lib/action_view/locale/en-US.rb new file mode 100644 index 0000000000..6b5345ed90 --- /dev/null +++ b/actionpack/lib/action_view/locale/en-US.rb @@ -0,0 +1,93 @@ +I18n.backend.store_translations :'en-US', { + :date => { + :formats => { + :default => "%Y-%m-%d", + :short => "%b %d", + :long => "%B %d, %Y", + }, + :day_names => Date::DAYNAMES, + :abbr_day_names => Date::ABBR_DAYNAMES, + :month_names => Date::MONTHNAMES, + :abbr_month_names => Date::ABBR_MONTHNAMES, + :order => [:year, :month, :day] + }, + :time => { + :formats => { + :default => "%a, %d %b %Y %H:%M:%S %z", + :short => "%d %b %H:%M", + :long => "%B %d, %Y %H:%M", + }, + :am => 'am', + :pm => 'pm' + }, + :datetime => { + :distance_in_words => { + :half_a_minute => 'half a minute', + :less_than_x_seconds => ['less than 1 second', 'less than {{count}} seconds'], + :x_seconds => ['1 second', '{{count}} seconds'], + :less_than_x_minutes => ['less than a minute', 'less than {{count}} minutes'], + :x_minutes => ['1 minute', '{{count}} minutes'], + :about_x_hours => ['about 1 hour', 'about {{count}} hours'], + :x_days => ['1 day', '{{count}} days'], + :about_x_months => ['about 1 month', 'about {{count}} months'], + :x_months => ['1 month', '{{count}} months'], + :about_x_years => ['about 1 year', 'about {{count}} year'], + :over_x_years => ['over 1 year', 'over {{count}} years'] + } + }, + :currency => { + :format => { + :unit => '$', + :precision => 2, + :separator => '.', + :delimiter => ',', + :format => '%u%n', + } + }, + :countries => { + :names => ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", + "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", + "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", + "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", + "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", + "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", + "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", + "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", + "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", + "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", + "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", + "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", + "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", + "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)", + "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", + "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", + "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", + "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", + "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", + "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", + "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", + "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", + "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", + "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", + "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", + "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", + "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", + "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", + "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", + "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", + "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", + "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", + "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", + "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", + "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", + "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", + "Yemen", "Zambia", "Zimbabwe"] + }, + :active_record => { + :error => { + :header_message => ["1 error prohibited this {{object_name}} from being saved", "{{count}} errors prohibited this {{object_name}} from being saved"], + :message => "There were problems with the following fields:" + } + } +} diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 71882833d4..17a7949959 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -82,6 +82,6 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/schema_dumper' I18n.backend.populate do - require 'active_record/lang/en-US.rb' + require 'active_record/locale/en-US.rb' end diff --git a/activerecord/lib/active_record/lang/en-US.rb b/activerecord/lib/active_record/lang/en-US.rb deleted file mode 100644 index b31e13ed3a..0000000000 --- a/activerecord/lib/active_record/lang/en-US.rb +++ /dev/null @@ -1,25 +0,0 @@ -I18n.backend.store_translations :'en-US', { - :active_record => { - :error_messages => { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is {{count}} characters)", - :too_short => "is too short (minimum is {{count}} characters)", - :wrong_length => "is the wrong length (should be {{count}} characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than {{count}}", - :greater_than_or_equal_to => "must be greater than or equal to {{count}}", - :equal_to => "must be equal to {{count}}", - :less_than => "must be less than {{count}}", - :less_than_or_equal_to => "must be less than or equal to {{count}}", - :odd => "must be odd", - :even => "must be even" - } - } -} \ No newline at end of file diff --git a/activerecord/lib/active_record/locale/en-US.rb b/activerecord/lib/active_record/locale/en-US.rb new file mode 100644 index 0000000000..b31e13ed3a --- /dev/null +++ b/activerecord/lib/active_record/locale/en-US.rb @@ -0,0 +1,25 @@ +I18n.backend.store_translations :'en-US', { + :active_record => { + :error_messages => { + :inclusion => "is not included in the list", + :exclusion => "is reserved", + :invalid => "is invalid", + :confirmation => "doesn't match confirmation", + :accepted => "must be accepted", + :empty => "can't be empty", + :blank => "can't be blank", + :too_long => "is too long (maximum is {{count}} characters)", + :too_short => "is too short (minimum is {{count}} characters)", + :wrong_length => "is the wrong length (should be {{count}} characters)", + :taken => "has already been taken", + :not_a_number => "is not a number", + :greater_than => "must be greater than {{count}}", + :greater_than_or_equal_to => "must be greater than or equal to {{count}}", + :equal_to => "must be equal to {{count}}", + :less_than => "must be less than {{count}}", + :less_than_or_equal_to => "must be less than or equal to {{count}}", + :odd => "must be odd", + :even => "must be even" + } + } +} \ No newline at end of file diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index de844bf5a6..10e36b1512 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -11,7 +11,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def teardown reset_callbacks Topic - load 'active_record/lang/en-US.rb' + load 'active_record/locale/en-US.rb' end def unique_topic diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 0de948dda9..de50aafe16 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -57,7 +57,7 @@ require 'active_support/base64' require 'active_support/time_with_zone' I18n.backend.populate do - require 'active_support/lang/en-US.rb' + require 'active_support/locale/en-US.rb' end Inflector = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Inflector', 'ActiveSupport::Inflector') diff --git a/activesupport/lib/active_support/lang/en-US.rb b/activesupport/lib/active_support/lang/en-US.rb deleted file mode 100644 index aa06fe14bd..0000000000 --- a/activesupport/lib/active_support/lang/en-US.rb +++ /dev/null @@ -1,7 +0,0 @@ -I18n.backend.store_translations :'en-US', { - :support => { - :array => { - :sentence_connector => 'and' - } - } -} \ No newline at end of file diff --git a/activesupport/lib/active_support/locale/en-US.rb b/activesupport/lib/active_support/locale/en-US.rb new file mode 100644 index 0000000000..aa06fe14bd --- /dev/null +++ b/activesupport/lib/active_support/locale/en-US.rb @@ -0,0 +1,7 @@ +I18n.backend.store_translations :'en-US', { + :support => { + :array => { + :sentence_connector => 'and' + } + } +} \ No newline at end of file -- cgit v1.2.3 From 8bfdabbd8b5137f91d8bcddc8c3d18961c8e316b Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sat, 21 Jun 2008 17:50:37 +0200 Subject: incorporate #translate usage with several default keys (use first default key that resolves to a translation). this might, depending on the backend implementation save some expensive lookups (like db lookups) --- activerecord/lib/active_record/validations.rb | 19 ++++- activerecord/test/cases/validations_i18n_test.rb | 86 +++++++++------------- activesupport/lib/active_support/vendor/i18n-0.0.1 | 2 +- 3 files changed, 52 insertions(+), 55 deletions(-) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index bcb204f1ba..49d3c59ca7 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -66,9 +66,11 @@ module ActiveRecord end def generate_message(attr, key, options = {}) - scope = [:active_record, :error_messages] - key.t(options.merge(:scope => scope + [:custom, @base.class.name.downcase, attr])) || - key.t(options.merge(:scope => scope)) + msgs = base_classes(@base.class).map{|klass| :"custom.#{klass.name.underscore}.#{attr}.#{key}"} + msgs << options[:default] if options[:default] + msgs << key + + I18n.t options.merge(:default => msgs, :scope => [:active_record, :error_messages]) end # Returns true if the specified +attribute+ has errors associated with it. @@ -217,6 +219,17 @@ module ActiveRecord full_messages.each { |msg| e.error(msg) } end end + + protected + + # TODO maybe this should be on ActiveRecord::Base, maybe #self_and_descendents_from_active_record + def base_classes(klass) + classes = [klass] + while klass != klass.base_class + classes << klass = klass.superclass + end + classes + end end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 10e36b1512..37a7c1ce49 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -40,31 +40,15 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase global_scope = [:active_record, :error_messages] custom_scope = global_scope + [:custom, 'topic', :title] - I18n.expects(:translate).with(:invalid, :scope => custom_scope).returns 'translation' - I18n.expects(:translate).with(:invalid, :scope => global_scope).never - - @topic.errors.generate_message :title, :invalid + I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid] + @topic.errors.generate_message :title, :invalid, :default => 'default from class def' end - def test_errors_generate_message_given_a_custom_message_translates_custom_model_attribute_key_with_custom_message_as_default + def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti custom_scope = [:active_record, :error_messages, :custom, 'topic', :title] - I18n.expects(:translate).with(:invalid, :scope => custom_scope, :default => 'default from class def').returns 'translation' - @topic.errors.generate_message :title, :invalid, :default => 'default from class def' - end - - def test_errors_generate_message_given_no_custom_message_falls_back_to_global_default_key_translation - global_scope = [:active_record, :error_messages] - custom_scope = global_scope + [:custom, 'topic', :title] - - I18n.stubs(:translate).with(:invalid, :scope => custom_scope).returns nil - I18n.expects(:translate).with(:invalid, :scope => global_scope) - @topic.errors.generate_message :title, :invalid - end - - def test_errors_add_given_no_message_it_translates_invalid - I18n.expects(:translate).with(:"active_record.error_messages.invalid") - @topic.errors.add :title + I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid] + Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def' end def test_errors_add_on_empty_generates_message @@ -115,7 +99,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_confirmation_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} - + Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @topic.valid? @@ -124,7 +108,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_confirmation_of_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} - + Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @topic.valid? @@ -133,13 +117,13 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase # validates_acceptance_of - + def test_validates_acceptance_of_generates_message Topic.validates_acceptance_of :title, :allow_nil => false @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) @topic.valid? end - + def test_validates_acceptance_of_generates_message_with_custom_default_message Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) @@ -149,7 +133,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_acceptance_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} - + Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? assert_equal 'custom message', @topic.errors.on(:title) @@ -157,7 +141,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_acceptance_of_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} - + Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? assert_equal 'global message', @topic.errors.on(:title) @@ -165,7 +149,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase # validates_presence_of - + def test_validates_presence_of_generates_message Topic.validates_presence_of :title @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) @@ -181,7 +165,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_presence_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} - + Topic.validates_presence_of :title @topic.valid? assert_equal 'custom message', @topic.errors.on(:title) @@ -189,7 +173,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_presence_of_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} - + Topic.validates_presence_of :title @topic.valid? assert_equal 'global message', @topic.errors.on(:title) @@ -197,7 +181,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase # validates_length_of :within - + def test_validates_length_of_within_generates_message Topic.validates_length_of :title, :within => 3..5 @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) @@ -213,7 +197,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_length_of_within_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} - + Topic.validates_length_of :title, :within => 3..5 @topic.valid? assert_equal 'custom message', @topic.errors.on(:title) @@ -221,7 +205,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_length_of_within_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} - + Topic.validates_length_of :title, :within => 3..5 @topic.valid? assert_equal 'global message', @topic.errors.on(:title) @@ -245,7 +229,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_length_of_within_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} - + Topic.validates_length_of :title, :is => 5 @topic.valid? assert_equal 'custom message', @topic.errors.on(:title) @@ -253,7 +237,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_length_of_within_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} - + Topic.validates_length_of :title, :is => 5 @topic.valid? assert_equal 'global message', @topic.errors.on(:title) @@ -279,7 +263,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_length_of_within_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} - + Topic.validates_length_of :title, :is => 5 @topic.valid? assert_equal 'custom message', @topic.errors.on(:title) @@ -287,7 +271,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_length_of_within_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} - + Topic.validates_length_of :title, :is => 5 @topic.valid? assert_equal 'global message', @topic.errors.on(:title) @@ -313,7 +297,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_format_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} - + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? assert_equal 'custom message', @topic.errors.on(:title) @@ -321,7 +305,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_format_of_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} - + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? assert_equal 'global message', @topic.errors.on(:title) @@ -347,7 +331,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_inclusion_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} - + Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? assert_equal 'custom message', @topic.errors.on(:title) @@ -355,7 +339,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_inclusion_of_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} - + Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? assert_equal 'global message', @topic.errors.on(:title) @@ -381,7 +365,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_exclusion_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} - + Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @topic.valid? @@ -390,7 +374,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_exclusion_of_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}} - + Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @topic.valid? @@ -417,7 +401,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_numericality_of_only_integer_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} - + Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? @@ -426,7 +410,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_numericality_of_only_integer_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} - + Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? @@ -453,7 +437,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_numericality_of_odd_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} - + Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @topic.valid? @@ -462,7 +446,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_numericality_of_odd_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}} - + Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @topic.valid? @@ -489,7 +473,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_numericality_of_less_than_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} - + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @topic.valid? @@ -498,7 +482,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_numericality_of_less_than_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}} - + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @topic.valid? @@ -523,7 +507,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_associated_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} - + Topic.validates_associated :replies replied_topic.valid? assert_equal 'custom message', replied_topic.errors.on(:replies) @@ -531,7 +515,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_validates_associated_finds_global_default_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} - + Topic.validates_associated :replies replied_topic.valid? assert_equal 'global message', replied_topic.errors.on(:replies) diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1 b/activesupport/lib/active_support/vendor/i18n-0.0.1 index 8e43afa38a..20c331666b 160000 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1 +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1 @@ -1 +1 @@ -Subproject commit 8e43afa38aa007d1de6d6acf44d43143c403d13f +Subproject commit 20c331666b3b6a21791d4cded53c3d8654fba714 -- cgit v1.2.3 From 55e2e2e8b4efbe6fdb0a921c19cd8be5650eab0a Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sat, 21 Jun 2008 18:12:59 +0200 Subject: experimental DeprecatedConstantToMethodProxy --- activesupport/lib/active_support/deprecation.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index ebdaf86146..36933b007d 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -194,6 +194,23 @@ module ActiveSupport ActiveSupport::Deprecation.warn("#{@old_const} is deprecated! Use #{@new_const} instead.", callstack) end end + + class DeprecatedConstantToMethodProxy < DeprecationProxy #:nodoc: + def initialize(old_const, new_target, new_method) + @old_const = old_const + @new_target = new_target + @new_method = new_method + end + + private + def target + @new_target.__send__(@new_method) + end + + def warn(callstack, called, args) + ActiveSupport::Deprecation.warn("#{@old_const} is deprecated! Use #{@new_target.inspect}.#{@new_method} instead.", callstack) + end + end end end -- cgit v1.2.3 From d897acfbb11aaa2d7f3138e1f9772546ecb6f981 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sun, 22 Jun 2008 11:39:10 +0200 Subject: remove DeprecatedConstantToMethodProxy again --- activesupport/lib/active_support/deprecation.rb | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 36933b007d..ebdaf86146 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -194,23 +194,6 @@ module ActiveSupport ActiveSupport::Deprecation.warn("#{@old_const} is deprecated! Use #{@new_const} instead.", callstack) end end - - class DeprecatedConstantToMethodProxy < DeprecationProxy #:nodoc: - def initialize(old_const, new_target, new_method) - @old_const = old_const - @new_target = new_target - @new_method = new_method - end - - private - def target - @new_target.__send__(@new_method) - end - - def warn(callstack, called, args) - ActiveSupport::Deprecation.warn("#{@old_const} is deprecated! Use #{@new_target.inspect}.#{@new_method} instead.", callstack) - end - end end end -- cgit v1.2.3 From 20d6630c1bb70f09e1f6a135bd3f9d690ad28250 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sun, 22 Jun 2008 11:41:51 +0200 Subject: Replaced country_options_for_select with old, untranslated version and moved country-related helpers to a new FormCountryHelper helper module so that they can easily be moved to a plugin. Updated tests accordingly. --- .../lib/action_view/helpers/form_country_helper.rb | 86 +++ .../lib/action_view/helpers/form_options_helper.rb | 32 +- actionpack/lib/action_view/locale/en-US.rb | 40 -- .../test/template/form_country_helper_test.rb | 772 +++++++++++++++++++++ .../test/template/form_options_helper_i18n_test.rb | 26 - .../test/template/form_options_helper_test.rb | 766 +------------------- 6 files changed, 862 insertions(+), 860 deletions(-) create mode 100644 actionpack/lib/action_view/helpers/form_country_helper.rb create mode 100644 actionpack/test/template/form_country_helper_test.rb delete mode 100644 actionpack/test/template/form_options_helper_i18n_test.rb diff --git a/actionpack/lib/action_view/helpers/form_country_helper.rb b/actionpack/lib/action_view/helpers/form_country_helper.rb new file mode 100644 index 0000000000..b2d2b7741f --- /dev/null +++ b/actionpack/lib/action_view/helpers/form_country_helper.rb @@ -0,0 +1,86 @@ +require 'action_view/helpers/form_options_helper' + +module ActionView + module Helpers + module FormCountryHelper + # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. + def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options) + end + + # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to + # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so + # that they will be listed above the rest of the (long) list. + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def country_options_for_select(selected = nil, priority_countries = nil) + country_options = "" + + if priority_countries + country_options += options_for_select(priority_countries, selected) + country_options += "\n" + end + + return country_options + options_for_select(COUNTRIES, selected) + end + + private + + # All the countries included in the country_options output. + COUNTRIES = ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", + "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", + "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", + "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", + "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", + "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", + "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", + "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", + "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", + "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", + "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", + "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", + "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)", + "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", + "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", + "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", + "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", + "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", + "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", + "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", + "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", + "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", + "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", + "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", + "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", + "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", + "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", + "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", + "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", + "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", + "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", + "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", + "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", + "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", + "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", + "Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES") + + end + + class InstanceTag #:nodoc: + include FormCountryHelper + + def to_country_select_tag(priority_countries, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + value = value(object) + content_tag("select", + add_options( + country_options_for_select(value, priority_countries), + options, value + ), html_options + ) + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index a6b9e65a77..8ce092b015 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -133,11 +133,6 @@ module ActionView InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) end - # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. - def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) - InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options) - end - # Return select and option tags for the given object and method, using # #time_zone_options_for_select to generate the list of option tags. # @@ -270,30 +265,6 @@ module ActionView options_for_select += '' end end - - # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to - # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so - # that they will be listed above the rest of the (long) list. - # - # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. - def country_options_for_select(*args) - options = args.extract_options! - - locale = options[:locale] - locale ||= request.locale if respond_to?(:request) - - selected, priority_countries = *args - countries = :'countries.names'.t options[:locale] - country_options = "" - - if priority_countries - # TODO priority_countries need to be translated? - country_options += options_for_select(priority_countries, selected) - country_options += "\n" - end - - return country_options + options_for_select(countries, selected) - end # Returns a string of option tags for pretty much any time zone in the @@ -349,8 +320,7 @@ module ActionView end # All the countries included in the country_options output. - # deprecated. please use :'countries.names'.t directly - COUNTRIES = :'countries.names'.t 'en-US' unless const_defined?("COUNTRIES") + COUNTRIES = ActiveSupport::Deprecation::DeprecatedConstantProxy.new 'COUNTRIES', 'ActionView::Helpers::FormCountryHelper::COUNTRIES' end class InstanceTag #:nodoc: diff --git a/actionpack/lib/action_view/locale/en-US.rb b/actionpack/lib/action_view/locale/en-US.rb index 6b5345ed90..20d668a9e1 100644 --- a/actionpack/lib/action_view/locale/en-US.rb +++ b/actionpack/lib/action_view/locale/en-US.rb @@ -44,46 +44,6 @@ I18n.backend.store_translations :'en-US', { :format => '%u%n', } }, - :countries => { - :names => ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", - "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", - "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", - "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", - "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", - "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", - "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", - "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", - "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", - "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", - "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", - "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", - "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", - "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)", - "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", - "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", - "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", - "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", - "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of", - "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", - "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", - "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", - "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", - "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", - "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", - "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", - "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", - "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", - "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", - "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", - "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", - "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", - "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", - "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", - "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", - "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", - "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", - "Yemen", "Zambia", "Zimbabwe"] - }, :active_record => { :error => { :header_message => ["1 error prohibited this {{object_name}} from being saved", "{{count}} errors prohibited this {{object_name}} from being saved"], diff --git a/actionpack/test/template/form_country_helper_test.rb b/actionpack/test/template/form_country_helper_test.rb new file mode 100644 index 0000000000..224b2e21c2 --- /dev/null +++ b/actionpack/test/template/form_country_helper_test.rb @@ -0,0 +1,772 @@ +require 'abstract_unit' + +class FormCountryHelperTest < ActionView::TestCase + tests ActionView::Helpers::FormCountryHelper + + silence_warnings do + Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin) + end + + def test_country_select + @post = Post.new + @post.origin = "Denmark" + expected_select = <<-COUNTRIES + +COUNTRIES + assert_dom_equal(expected_select[0..-2], country_select("post", "origin")) + end + + def test_country_select_with_priority_countries + @post = Post.new + @post.origin = "Denmark" + expected_select = <<-COUNTRIES + +COUNTRIES + assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"])) + end + + def test_country_select_with_selected_priority_country + @post = Post.new + @post.origin = "New Zealand" + expected_select = <<-COUNTRIES + +COUNTRIES + assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"])) + end +end \ No newline at end of file diff --git a/actionpack/test/template/form_options_helper_i18n_test.rb b/actionpack/test/template/form_options_helper_i18n_test.rb deleted file mode 100644 index c9fc0768bb..0000000000 --- a/actionpack/test/template/form_options_helper_i18n_test.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'abstract_unit' - -class FormOptionsHelperI18nTests < Test::Unit::TestCase - include ActionView::Helpers::FormOptionsHelper - attr_reader :request - - def setup - @request = mock - end - - def test_country_options_for_select_given_a_locale_it_does_not_check_request_for_locale - request.expects(:locale).never - country_options_for_select :locale => 'en-US' - end - - def test_country_options_for_select_given_no_locale_it_checks_request_for_locale - request.expects(:locale).returns 'en-US' - country_options_for_select - end - - def test_country_options_for_select_translates_country_names - countries = ActionView::Helpers::FormOptionsHelper::COUNTRIES - I18n.expects(:translate).with(:'countries.names', 'en-US').returns countries - country_options_for_select :locale => 'en-US' - end -end \ No newline at end of file diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 3f89a5e426..5ba81aac02 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -412,769 +412,6 @@ class FormOptionsHelperTest < ActionView::TestCase assert_dom_equal expected, collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true, :name => 'post[author_name][]' }, :multiple => true) end - def test_country_select - @post = Post.new - @post.origin = "Denmark" - expected_select = <<-COUNTRIES - -COUNTRIES - assert_dom_equal(expected_select[0..-2], country_select("post", "origin")) - end - - def test_country_select_with_priority_countries - @post = Post.new - @post.origin = "Denmark" - expected_select = <<-COUNTRIES - -COUNTRIES - assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"])) - end - - def test_country_select_with_selected_priority_country - @post = Post.new - @post.origin = "New Zealand" - expected_select = <<-COUNTRIES - -COUNTRIES - assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"])) - end - def test_time_zone_select @firm = Firm.new("D") html = time_zone_select( "firm", "time_zone" ) @@ -1327,4 +564,7 @@ COUNTRIES html end + def test_countries_is_deprectated + assert_deprecated(/COUNTRIES/) { ActionView::Helpers::FormOptionsHelper::COUNTRIES.size } + end end -- cgit v1.2.3 From 67fce4671e8bcfe2aa670a89195b20837546183a Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sun, 22 Jun 2008 13:49:08 +0200 Subject: crap, an array never has a request, stupid. --- activesupport/lib/active_support/core_ext/array/conversions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 08e608d346..80d91a6cbd 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -11,7 +11,7 @@ module ActiveSupport #:nodoc: options.assert_valid_keys(:connector, :skip_last_comma, :locale) locale = options[:locale] - locale ||= request.locale if respond_to?(:request) + locale ||= self.locale if respond_to?(:locale) default = :'support.array.sentence_connector'.t(locale) options.reverse_merge! :connector => default, :skip_last_comma => false -- cgit v1.2.3 From 3533dc68120ed40a4ec44ed9900c9035108cfcf1 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sun, 22 Jun 2008 13:49:38 +0200 Subject: check self.locale instead of request.locale in helpers --- actionpack/lib/action_view/helpers/active_record_helper.rb | 2 +- actionpack/lib/action_view/helpers/date_helper.rb | 4 ++-- actionpack/lib/action_view/helpers/number_helper.rb | 2 +- actionpack/test/template/active_record_helper_i18n_test.rb | 6 +++--- actionpack/test/template/date_helper_i18n_test.rb | 8 ++++---- actionpack/test/template/number_helper_i18n_test.rb | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index 5ad9d5f76d..716e303a5d 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -160,7 +160,7 @@ module ActionView count = objects.inject(0) {|sum, object| sum + object.errors.count } locale = options[:locale] - locale ||= request.locale if respond_to?(:request) + locale ||= self.locale if respond_to?(:locale) unless count.zero? html = {} diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 0337be0744..dbb5d458bf 100755 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -60,7 +60,7 @@ module ActionView # def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) locale = options[:locale] - locale ||= request.locale if respond_to?(:request) + locale ||= self.locale if respond_to?(:locale) from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) @@ -507,7 +507,7 @@ module ActionView # def select_month(date, options = {}, html_options = {}) locale = options[:locale] - locale ||= request.locale if respond_to?(:request) + locale ||= self.locale if respond_to?(:locale) val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' if options[:use_hidden] diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 9d98036f2d..dc56817c12 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -72,7 +72,7 @@ module ActionView options = options.symbolize_keys locale = options[:locale] - locale ||= request.locale if respond_to?(:request) + locale ||= self.locale if respond_to?(:locale) defaults = :'currency.format'.t(locale) || {} precision = options[:precision] || defaults[:precision] diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb index 057fb9bd1a..3a2197ac93 100644 --- a/actionpack/test/template/active_record_helper_i18n_test.rb +++ b/actionpack/test/template/active_record_helper_i18n_test.rb @@ -5,22 +5,22 @@ class ActiveRecordHelperI18nTest < Test::Unit::TestCase attr_reader :request def setup - @request = mock @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) stubs(:content_tag).returns 'content_tag' + stubs(:locale) I18n.stubs(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns "1 error prohibited this from being saved" I18n.stubs(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' end def test_error_messages_for_given_a_locale_it_does_not_check_request_for_locale - request.expects(:locale).never + expects(:locale).never @object.errors.stubs(:count).returns 0 error_messages_for(:object => @object, :locale => 'en-US') end def test_error_messages_for_given_no_locale_it_checks_request_for_locale - request.expects(:locale).returns 'en-US' + expects(:locale).returns 'en-US' @object.errors.stubs(:count).returns 0 error_messages_for(:object => @object) end diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index 9b7c03a400..f245ca1fc8 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -5,19 +5,19 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase attr_reader :request def setup - @request = mock + stubs(:locale) @from = Time.mktime(2004, 6, 6, 21, 45, 0) end # distance_of_time_in_words def test_distance_of_time_in_words_given_a_locale_it_does_not_check_request_for_locale - request.expects(:locale).never + expects(:locale).never distance_of_time_in_words @from, @from + 1.second, false, :locale => 'en-US' end def test_distance_of_time_in_words_given_no_locale_it_checks_request_for_locale - request.expects(:locale).returns 'en-US' + expects(:locale).returns 'en-US' distance_of_time_in_words @from, @from + 1.second end @@ -64,7 +64,7 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase attr_reader :request def setup - @request = mock + # stubs(:locale) I18n.stubs(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES end diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index b75af03378..5db60ece04 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -5,18 +5,18 @@ class NumberHelperI18nTests < Test::Unit::TestCase attr_reader :request def setup - @request = mock + stubs(:locale) @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} I18n.backend.store_translations 'en-US', :currency => {:format => @defaults} end def test_number_to_currency_given_a_locale_it_does_not_check_request_for_locale - request.expects(:locale).never + expects(:locale).never number_to_currency(1, :locale => 'en-US') end def test_number_to_currency_given_no_locale_it_checks_request_for_locale - request.expects(:locale).returns 'en-US' + expects(:locale).returns 'en-US' number_to_currency(1) end -- cgit v1.2.3 From 0dddba41fcfcd28de2ce1a88a23514fbde53afcf Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 23 Jun 2008 14:33:29 +0200 Subject: rather cosmetic improvements of test coverage --- .../lib/action_view/helpers/number_helper.rb | 2 +- activerecord/lib/active_record/validations.rb | 8 +-- activerecord/test/cases/validations_i18n_test.rb | 62 ++++++++++++++++++++-- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index dc56817c12..4373d063bb 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -70,7 +70,7 @@ module ActionView # # => 1234567890,50 £ def number_to_currency(number, options = {}) options = options.symbolize_keys - + locale = options[:locale] locale ||= self.locale if respond_to?(:locale) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 49d3c59ca7..5bbd10394c 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -21,8 +21,8 @@ module ActiveRecord class << self def default_error_messages - # ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use :'active_record.error_messages'.t.") - :'active_record.error_messages'.t + ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use 'active_record.error_messages'.t.") + 'active_record.error_messages'.t end end @@ -163,7 +163,7 @@ module ActiveRecord @errors.each_key do |attr| @errors[attr].each do |message| next unless message - + if attr == "base" full_messages << message else @@ -872,7 +872,7 @@ module ActiveRecord end raw_value = raw_value.to_i else - begin + begin raw_value = Kernel.Float(raw_value.to_s) rescue ArgumentError, TypeError message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 37a7c1ce49..158ff69e57 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -34,6 +34,12 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end end + def test_default_error_messages_is_deprecated + assert_deprecated('ActiveRecord::Errors.default_error_messages') do + ActiveRecord::Errors.default_error_messages + end + end + # ActiveRecord::Errors def test_errors_generate_message_translates_custom_model_attribute_key @@ -182,18 +188,32 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase # validates_length_of :within - def test_validates_length_of_within_generates_message + def test_validates_length_of_within_generates_message_with_title_too_short Topic.validates_length_of :title, :within => 3..5 @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) @topic.valid? end - def test_validates_length_of_within_generates_message_with_custom_default_message + def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom' @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) @topic.valid? end + def test_validates_length_of_within_generates_message_with_title_too_long + Topic.validates_length_of :title, :within => 3..5 + @topic.title = 'this title is too long' + @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message + Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom' + @topic.title = 'this title is too long' + @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) + @topic.valid? + end + def test_validates_length_of_within_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} @@ -382,7 +402,43 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end - # validates_numericality_of :only_integer + # validates_numericality_of without :only_integer + + def test_validates_numericality_of_generates_message + Topic.validates_numericality_of :title + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :message => 'custom' + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @topic.valid? + end + + def test_validates_numericality_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + + Topic.validates_numericality_of :title + @topic.title = 'a' + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_numericality_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + + Topic.validates_numericality_of :title, :only_integer => true + @topic.title = 'a' + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_numericality_of with :only_integer def test_validates_numericality_of_only_integer_generates_message Topic.validates_numericality_of :title, :only_integer => true -- cgit v1.2.3 From 2ee9f2a0303cba95b2d8073fc7e22ec75229a8ee Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 23 Jun 2008 14:34:01 +0200 Subject: remove generic translate helpers --- actionpack/lib/action_view/helpers/i18n_helper.rb | 19 ------------- actionpack/test/template/i18n_helper_test.rb | 33 ----------------------- 2 files changed, 52 deletions(-) delete mode 100644 actionpack/lib/action_view/helpers/i18n_helper.rb delete mode 100644 actionpack/test/template/i18n_helper_test.rb diff --git a/actionpack/lib/action_view/helpers/i18n_helper.rb b/actionpack/lib/action_view/helpers/i18n_helper.rb deleted file mode 100644 index 1b1d1f301d..0000000000 --- a/actionpack/lib/action_view/helpers/i18n_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActionView - module Helpers - module I18nHelper - def translate(*args) - # inserts the locale or current request locale to the argument list if no locale - # has been passed or the locale has been passed as part of the options hash - options = args.extract_options! - if args.size != 2 - locale = options.delete :locale - locale ||= request.locale if respond_to? :request - args << locale if locale - end - args << options unless options.empty? - I18n.translate *args - end - alias :t :translate - end - end -end \ No newline at end of file diff --git a/actionpack/test/template/i18n_helper_test.rb b/actionpack/test/template/i18n_helper_test.rb deleted file mode 100644 index 598731568c..0000000000 --- a/actionpack/test/template/i18n_helper_test.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'abstract_unit' -require 'action_view/helpers/i18n_helper' - -class I18nHelperTests < Test::Unit::TestCase - include ActionView::Helpers::I18nHelper - - attr_reader :request - def setup - @request = stub :locale => 'en-US' - I18n.stubs(:translate).with(:'foo.bar', 'en-US').returns 'Foo Bar' - end - - def test_translate_given_a_locale_argument_it_does_not_check_request_for_locale - request.expects(:locale).never - assert_equal 'Foo Bar', translate(:'foo.bar', :locale => 'en-US') - end - - def test_translate_given_a_locale_option_it_does_not_check_request_for_locale - request.expects(:locale).never - I18n.expects(:translate).with(:'foo.bar', 'en-US').returns 'Foo Bar' - assert_equal 'Foo Bar', translate(:'foo.bar', :locale => 'en-US') - end - - def test_translate_given_no_locale_it_checks_request_for_locale - request.expects(:locale).returns 'en-US' - assert_equal 'Foo Bar', translate(:'foo.bar') - end - - def test_translate_delegates_to_i18n_translate - I18n.expects(:translate).with(:'foo.bar', 'en-US').returns 'Foo Bar' - assert_equal 'Foo Bar', translate(:'foo.bar') - end -end \ No newline at end of file -- cgit v1.2.3 From c178a87b4326edd491922136c0a55bf4b889473d Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 23 Jun 2008 14:37:50 +0200 Subject: remove call to self.locale from helpers --- actionpack/lib/action_view/helpers/active_record_helper.rb | 5 +---- actionpack/lib/action_view/helpers/date_helper.rb | 5 +---- actionpack/lib/action_view/helpers/number_helper.rb | 9 +++------ 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index 716e303a5d..4ff16cd70c 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -159,9 +159,6 @@ module ActionView end count = objects.inject(0) {|sum, object| sum + object.errors.count } - locale = options[:locale] - locale ||= self.locale if respond_to?(:locale) - unless count.zero? html = {} [:id, :class].each do |key| @@ -174,7 +171,7 @@ module ActionView end options[:object_name] ||= params.first - I18n.with_options :locale => locale, :scope => [:active_record, :error] do |locale| + I18n.with_options :locale => options[:locale], :scope => [:active_record, :error] do |locale| header_message = if options.include?(:header_message) options[:header_message] else diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index dbb5d458bf..6ac4171fd5 100755 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -59,15 +59,12 @@ module ActionView # distance_of_time_in_words(Time.now, Time.now) # => less than a minute # def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) - locale = options[:locale] - locale ||= self.locale if respond_to?(:locale) - from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) distance_in_minutes = (((to_time - from_time).abs)/60).round distance_in_seconds = ((to_time - from_time).abs).round - I18n.with_options :locale => locale, :scope => :'datetime.distance_in_words' do |locale| + I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale| case distance_in_minutes when 0..1 return distance_in_minutes == 0 ? diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 4373d063bb..3e0d5b1db4 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -69,12 +69,9 @@ module ActionView # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) - options = options.symbolize_keys - - locale = options[:locale] - locale ||= self.locale if respond_to?(:locale) - - defaults = :'currency.format'.t(locale) || {} + options = options.symbolize_keys + defaults = :'currency.format'.t(options[:locale]) || {} + precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] separator = options[:separator] || defaults[:separator] -- cgit v1.2.3 From ac66865ea3d0eb0de8e19fef49293feb9e61281b Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 23 Jun 2008 14:49:02 +0200 Subject: update tests according to removal of self.locale from helpers --- actionpack/test/template/active_record_helper_i18n_test.rb | 13 ------------- actionpack/test/template/date_helper_i18n_test.rb | 12 ------------ actionpack/test/template/number_helper_i18n_test.rb | 11 ----------- 3 files changed, 36 deletions(-) diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb index 3a2197ac93..d1b92c7e4d 100644 --- a/actionpack/test/template/active_record_helper_i18n_test.rb +++ b/actionpack/test/template/active_record_helper_i18n_test.rb @@ -7,24 +7,11 @@ class ActiveRecordHelperI18nTest < Test::Unit::TestCase def setup @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) stubs(:content_tag).returns 'content_tag' - stubs(:locale) I18n.stubs(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns "1 error prohibited this from being saved" I18n.stubs(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' end - def test_error_messages_for_given_a_locale_it_does_not_check_request_for_locale - expects(:locale).never - @object.errors.stubs(:count).returns 0 - error_messages_for(:object => @object, :locale => 'en-US') - end - - def test_error_messages_for_given_no_locale_it_checks_request_for_locale - expects(:locale).returns 'en-US' - @object.errors.stubs(:count).returns 0 - error_messages_for(:object => @object) - end - def test_error_messages_for_given_a_header_message_option_it_does_not_translate_header_message I18n.expects(:translate).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').never error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en-US') diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index f245ca1fc8..3e1eed61fd 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -5,22 +5,11 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase attr_reader :request def setup - stubs(:locale) @from = Time.mktime(2004, 6, 6, 21, 45, 0) end # distance_of_time_in_words - def test_distance_of_time_in_words_given_a_locale_it_does_not_check_request_for_locale - expects(:locale).never - distance_of_time_in_words @from, @from + 1.second, false, :locale => 'en-US' - end - - def test_distance_of_time_in_words_given_no_locale_it_checks_request_for_locale - expects(:locale).returns 'en-US' - distance_of_time_in_words @from, @from + 1.second - end - def test_distance_of_time_in_words_calls_i18n { # with include_seconds [2.seconds, true] => [:'less_than_x_seconds', 5], @@ -64,7 +53,6 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase attr_reader :request def setup - # stubs(:locale) I18n.stubs(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES end diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index 5db60ece04..bee9ceaa71 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -5,21 +5,10 @@ class NumberHelperI18nTests < Test::Unit::TestCase attr_reader :request def setup - stubs(:locale) @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} I18n.backend.store_translations 'en-US', :currency => {:format => @defaults} end - def test_number_to_currency_given_a_locale_it_does_not_check_request_for_locale - expects(:locale).never - number_to_currency(1, :locale => 'en-US') - end - - def test_number_to_currency_given_no_locale_it_checks_request_for_locale - expects(:locale).returns 'en-US' - number_to_currency(1) - end - def test_number_to_currency_translates_currency_formats I18n.expects(:translate).with(:'currency.format', 'en-US').returns @defaults number_to_currency(1, :locale => 'en-US') -- cgit v1.2.3 From 77177441d1bd8f62c5b6a990ddee155061df661c Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 23 Jun 2008 14:49:47 +0200 Subject: including rcov shell scripts for reference --- actionpack/test/i18n_coverage | 9 +++++++++ activerecord/test/i18n_coverage | 6 ++++++ 2 files changed, 15 insertions(+) create mode 100755 actionpack/test/i18n_coverage create mode 100755 activerecord/test/i18n_coverage diff --git a/actionpack/test/i18n_coverage b/actionpack/test/i18n_coverage new file mode 100755 index 0000000000..57b54e9d47 --- /dev/null +++ b/actionpack/test/i18n_coverage @@ -0,0 +1,9 @@ +rcov -x abstract_unit.rb \ +-i action_view/helpers/number_helper.rb,action_view/helpers/date_helper.rb,action_view/helpers/active_record_helper.rb \ +template/number_helper_i18n_test.rb \ +template/date_helper_i18n_test.rb \ +template/active_record_helper_i18n_test.rb \ + +# template/number_helper_test.rb \ +# template/date_helper_test.rb \ +# template/active_record_helper_test.rb \ No newline at end of file diff --git a/activerecord/test/i18n_coverage b/activerecord/test/i18n_coverage new file mode 100755 index 0000000000..1589a6c06f --- /dev/null +++ b/activerecord/test/i18n_coverage @@ -0,0 +1,6 @@ +rcov -I connections/native_mysql \ +-x cases/helper,config,connection,models \ +-i active_record/validations.rb \ +cases/validations_i18n_test.rb \ + +# cases/validations_test.rb \ No newline at end of file -- cgit v1.2.3 From 8461526f346b8d8387ba3b74221fbeefef3aefeb Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 23 Jun 2008 14:55:07 +0200 Subject: silence deprecation warning during validations test --- activerecord/test/cases/validations_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 7b71647d25..ad27ac951c 100755 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -853,7 +853,9 @@ class ValidationsTest < ActiveRecord::TestCase end def test_validates_length_with_globally_modified_error_message - ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' + ActiveSupport::Deprecation.silence do + ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' + end Topic.validates_length_of :title, :minimum => 10 t = Topic.create(:title => 'too short') assert !t.valid? -- cgit v1.2.3 From 66c2508ebbca06e551255a76cc47a1608f091992 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Fri, 27 Jun 2008 15:00:55 +0200 Subject: Make sure mocha is available --- .../template/active_record_helper_i18n_test.rb | 48 +- actionpack/test/template/date_helper_i18n_test.rb | 134 ++--- .../test/template/number_helper_i18n_test.rb | 16 +- activerecord/test/cases/validations_i18n_test.rb | 576 +++++++++++---------- 4 files changed, 413 insertions(+), 361 deletions(-) diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb index d1b92c7e4d..d78b0e4c0c 100644 --- a/actionpack/test/template/active_record_helper_i18n_test.rb +++ b/actionpack/test/template/active_record_helper_i18n_test.rb @@ -4,31 +4,33 @@ class ActiveRecordHelperI18nTest < Test::Unit::TestCase include ActionView::Helpers::ActiveRecordHelper attr_reader :request - def setup - @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) - stubs(:content_tag).returns 'content_tag' - - I18n.stubs(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns "1 error prohibited this from being saved" - I18n.stubs(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' - end + uses_mocha 'active_record_helper_i18n_test' do + def setup + @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) + stubs(:content_tag).returns 'content_tag' - def test_error_messages_for_given_a_header_message_option_it_does_not_translate_header_message - I18n.expects(:translate).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').never - error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en-US') - end + I18n.stubs(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns "1 error prohibited this from being saved" + I18n.stubs(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' + end - def test_error_messages_for_given_no_header_message_option_it_translates_header_message - I18n.expects(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns 'header message' - error_messages_for(:object => @object, :locale => 'en-US') - end - - def test_error_messages_for_given_a_message_option_it_does_not_translate_message - I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).never - error_messages_for(:object => @object, :message => 'message', :locale => 'en-US') - end + def test_error_messages_for_given_a_header_message_option_it_does_not_translate_header_message + I18n.expects(:translate).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').never + error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en-US') + end + + def test_error_messages_for_given_no_header_message_option_it_translates_header_message + I18n.expects(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns 'header message' + error_messages_for(:object => @object, :locale => 'en-US') + end + + def test_error_messages_for_given_a_message_option_it_does_not_translate_message + I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).never + error_messages_for(:object => @object, :message => 'message', :locale => 'en-US') + end - def test_error_messages_for_given_no_message_option_it_translates_message - I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' - error_messages_for(:object => @object, :locale => 'en-US') + def test_error_messages_for_given_no_message_option_it_translates_message + I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' + error_messages_for(:object => @object, :locale => 'en-US') + end end end \ No newline at end of file diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index 3e1eed61fd..aeb06c55ea 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -8,43 +8,45 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase @from = Time.mktime(2004, 6, 6, 21, 45, 0) end - # distance_of_time_in_words + uses_mocha 'date_helper_distance_of_time_in_words_i18n_test' do + # distance_of_time_in_words - def test_distance_of_time_in_words_calls_i18n - { # with include_seconds - [2.seconds, true] => [:'less_than_x_seconds', 5], - [9.seconds, true] => [:'less_than_x_seconds', 10], - [19.seconds, true] => [:'less_than_x_seconds', 20], - [30.seconds, true] => [:'half_a_minute', nil], - [59.seconds, true] => [:'less_than_x_minutes', 1], - [60.seconds, true] => [:'x_minutes', 1], - - # without include_seconds - [29.seconds, false] => [:'less_than_x_minutes', 1], - [60.seconds, false] => [:'x_minutes', 1], - [44.minutes, false] => [:'x_minutes', 44], - [61.minutes, false] => [:'about_x_hours', 1], - [24.hours, false] => [:'x_days', 1], - [30.days, false] => [:'about_x_months', 1], - [60.days, false] => [:'x_months', 2], - [1.year, false] => [:'about_x_years', 1], - [3.years, false] => [:'over_x_years', 3] - - }.each do |passed, expected| - assert_distance_of_time_in_words_translates_key passed, expected + def test_distance_of_time_in_words_calls_i18n + { # with include_seconds + [2.seconds, true] => [:'less_than_x_seconds', 5], + [9.seconds, true] => [:'less_than_x_seconds', 10], + [19.seconds, true] => [:'less_than_x_seconds', 20], + [30.seconds, true] => [:'half_a_minute', nil], + [59.seconds, true] => [:'less_than_x_minutes', 1], + [60.seconds, true] => [:'x_minutes', 1], + + # without include_seconds + [29.seconds, false] => [:'less_than_x_minutes', 1], + [60.seconds, false] => [:'x_minutes', 1], + [44.minutes, false] => [:'x_minutes', 44], + [61.minutes, false] => [:'about_x_hours', 1], + [24.hours, false] => [:'x_days', 1], + [30.days, false] => [:'about_x_months', 1], + [60.days, false] => [:'x_months', 2], + [1.year, false] => [:'about_x_years', 1], + [3.years, false] => [:'over_x_years', 3] + + }.each do |passed, expected| + assert_distance_of_time_in_words_translates_key passed, expected + end end - end - - def assert_distance_of_time_in_words_translates_key(passed, expected) - diff, include_seconds = *passed - key, count = *expected - to = @from + diff - options = {:locale => 'en-US', :scope => :'datetime.distance_in_words'} - options[:count] = count if count - - I18n.expects(:t).with(key, options) - distance_of_time_in_words(@from, to, include_seconds, :locale => 'en-US') + def assert_distance_of_time_in_words_translates_key(passed, expected) + diff, include_seconds = *passed + key, count = *expected + to = @from + diff + + options = {:locale => 'en-US', :scope => :'datetime.distance_in_words'} + options[:count] = count if count + + I18n.expects(:t).with(key, options) + distance_of_time_in_words(@from, to, include_seconds, :locale => 'en-US') + end end end @@ -52,36 +54,38 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase include ActionView::Helpers::DateHelper attr_reader :request - def setup - I18n.stubs(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES - end - - # select_month - - def test_select_month_given_use_month_names_option_does_not_translate_monthnames - I18n.expects(:translate).never - select_month(8, :locale => 'en-US', :use_month_names => Date::MONTHNAMES) - end - - def test_select_month_translates_monthnames - I18n.expects(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES - select_month(8, :locale => 'en-US') - end - - def test_select_month_given_use_short_month_option_translates_abbr_monthnames - I18n.expects(:translate).with(:'date.abbr_month_names', 'en-US').returns Date::ABBR_MONTHNAMES - select_month(8, :locale => 'en-US', :use_short_month => true) - end - - # date_or_time_select - - def test_date_or_time_select_given_an_order_options_does_not_translate_order - I18n.expects(:translate).never - datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en-US') - end - - def test_date_or_time_select_given_no_order_options_translates_order - I18n.expects(:translate).with(:'date.order', 'en-US').returns [:year, :month, :day] - datetime_select('post', 'updated_at', :locale => 'en-US') + uses_mocha 'date_helper_select_tags_i18n_tests' do + def setup + I18n.stubs(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES + end + + # select_month + + def test_select_month_given_use_month_names_option_does_not_translate_monthnames + I18n.expects(:translate).never + select_month(8, :locale => 'en-US', :use_month_names => Date::MONTHNAMES) + end + + def test_select_month_translates_monthnames + I18n.expects(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES + select_month(8, :locale => 'en-US') + end + + def test_select_month_given_use_short_month_option_translates_abbr_monthnames + I18n.expects(:translate).with(:'date.abbr_month_names', 'en-US').returns Date::ABBR_MONTHNAMES + select_month(8, :locale => 'en-US', :use_short_month => true) + end + + # date_or_time_select + + def test_date_or_time_select_given_an_order_options_does_not_translate_order + I18n.expects(:translate).never + datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en-US') + end + + def test_date_or_time_select_given_no_order_options_translates_order + I18n.expects(:translate).with(:'date.order', 'en-US').returns [:year, :month, :day] + datetime_select('post', 'updated_at', :locale => 'en-US') + end end end \ No newline at end of file diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index bee9ceaa71..be40ddbc88 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -4,13 +4,15 @@ class NumberHelperI18nTests < Test::Unit::TestCase include ActionView::Helpers::NumberHelper attr_reader :request - def setup - @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} - I18n.backend.store_translations 'en-US', :currency => {:format => @defaults} - end + uses_mocha 'number_helper_i18n_tests' do + def setup + @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} + I18n.backend.store_translations 'en-US', :currency => {:format => @defaults} + end - def test_number_to_currency_translates_currency_formats - I18n.expects(:translate).with(:'currency.format', 'en-US').returns @defaults - number_to_currency(1, :locale => 'en-US') + def test_number_to_currency_translates_currency_formats + I18n.expects(:translate).with(:'currency.format', 'en-US').returns @defaults + number_to_currency(1, :locale => 'en-US') + end end end \ No newline at end of file diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 158ff69e57..53e90f4d53 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -41,66 +41,307 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end # ActiveRecord::Errors + uses_mocha 'ActiveRecord::Errors' do + def test_errors_generate_message_translates_custom_model_attribute_key + global_scope = [:active_record, :error_messages] + custom_scope = global_scope + [:custom, 'topic', :title] + + I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid] + @topic.errors.generate_message :title, :invalid, :default => 'default from class def' + end + + def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti + custom_scope = [:active_record, :error_messages, :custom, 'topic', :title] + + I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid] + Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def' + end + + def test_errors_add_on_empty_generates_message + @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) + @topic.errors.add_on_empty :title + end + + def test_errors_add_on_empty_generates_message_with_custom_default_message + @topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'}) + @topic.errors.add_on_empty :title, 'custom' + end + + def test_errors_add_on_blank_generates_message + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @topic.errors.add_on_blank :title + end + + def test_errors_add_on_blank_generates_message_with_custom_default_message + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @topic.errors.add_on_blank :title, 'custom' + end + + def test_errors_full_messages_translates_human_attribute_name_for_model_attributes + @topic.errors.instance_variable_set :@errors, { 'title' => 'empty' } + I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US').returns('Title') + @topic.errors.full_messages :locale => 'en-US' + end + end - def test_errors_generate_message_translates_custom_model_attribute_key - global_scope = [:active_record, :error_messages] - custom_scope = global_scope + [:custom, 'topic', :title] + # ActiveRecord::Validations + uses_mocha 'ActiveRecord::Validations' do + # validates_confirmation_of w/ mocha + + def test_validates_confirmation_of_generates_message + Topic.validates_confirmation_of :title + @topic.title_confirmation = 'foo' + @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil}) + @topic.valid? + end + + def test_validates_confirmation_of_generates_message_with_custom_default_message + Topic.validates_confirmation_of :title, :message => 'custom' + @topic.title_confirmation = 'foo' + @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'}) + @topic.valid? + end - I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid] - @topic.errors.generate_message :title, :invalid, :default => 'default from class def' - end - - def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti - custom_scope = [:active_record, :error_messages, :custom, 'topic', :title] + # validates_acceptance_of w/ mocha + + def test_validates_acceptance_of_generates_message + Topic.validates_acceptance_of :title, :allow_nil => false + @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) + @topic.valid? + end + + def test_validates_acceptance_of_generates_message_with_custom_default_message + Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false + @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) + @topic.valid? + end - I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid] - Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def' - end - - def test_errors_add_on_empty_generates_message - @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) - @topic.errors.add_on_empty :title - end - - def test_errors_add_on_empty_generates_message_with_custom_default_message - @topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'}) - @topic.errors.add_on_empty :title, 'custom' - end - - def test_errors_add_on_blank_generates_message - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) - @topic.errors.add_on_blank :title - end - - def test_errors_add_on_blank_generates_message_with_custom_default_message - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) - @topic.errors.add_on_blank :title, 'custom' - end - - def test_errors_full_messages_translates_human_attribute_name_for_model_attributes - @topic.errors.instance_variable_set :@errors, { 'title' => 'empty' } - I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US').returns('Title') - @topic.errors.full_messages :locale => 'en-US' - end - - - # ActiveRecord::Validations - - # validates_confirmation_of - - def test_validates_confirmation_of_generates_message - Topic.validates_confirmation_of :title - @topic.title_confirmation = 'foo' - @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil}) - @topic.valid? + # validates_presence_of w/ mocha + + def test_validates_presence_of_generates_message + Topic.validates_presence_of :title + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @topic.valid? + end + + def test_validates_presence_of_generates_message_with_custom_default_message + Topic.validates_presence_of :title, :message => 'custom' + @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short + Topic.validates_length_of :title, :within => 3..5 + @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message + Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom' + @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long + Topic.validates_length_of :title, :within => 3..5 + @topic.title = 'this title is too long' + @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message + Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom' + @topic.title = 'this title is too long' + @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) + @topic.valid? + end + + # validates_length_of :within w/ mocha + + def test_validates_length_of_within_generates_message_with_title_too_short + Topic.validates_length_of :title, :within => 3..5 + @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message + Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom' + @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long + Topic.validates_length_of :title, :within => 3..5 + @topic.title = 'this title is too long' + @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message + Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom' + @topic.title = 'this title is too long' + @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) + @topic.valid? + end + + # validates_length_of :is w/ mocha + + def test_validates_length_of_is_generates_message + Topic.validates_length_of :title, :is => 5 + @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil}) + @topic.valid? + end + + def test_validates_length_of_is_generates_message_with_custom_default_message + Topic.validates_length_of :title, :is => 5, :message => 'custom' + @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'}) + @topic.valid? + end + + # validates_uniqueness_of w/ mocha + + def test_validates_uniqueness_of_generates_message + Topic.validates_uniqueness_of :title + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil}) + @topic.valid? + end + + def test_validates_uniqueness_of_generates_message_with_custom_default_message + Topic.validates_uniqueness_of :title, :message => 'custom' + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom'}) + @topic.valid? + end + + # validates_format_of w/ mocha + + def test_validates_format_of_generates_message + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @topic.title = '72x' + @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil}) + @topic.valid? + end + + def test_validates_format_of_generates_message_with_custom_default_message + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom' + @topic.title = '72x' + @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'}) + @topic.valid? + end + + # validates_inclusion_of w/ mocha + + def test_validates_inclusion_of_generates_message + Topic.validates_inclusion_of :title, :in => %w(a b c) + @topic.title = 'z' + @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil}) + @topic.valid? + end + + def test_validates_inclusion_of_generates_message_with_custom_default_message + Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom' + @topic.title = 'z' + @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'}) + @topic.valid? + end + + # validates_exclusion_of w/ mocha + + def test_validates_exclusion_of_generates_message + Topic.validates_exclusion_of :title, :in => %w(a b c) + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil}) + @topic.valid? + end + + def test_validates_exclusion_of_generates_message_with_custom_default_message + Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom' + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'}) + @topic.valid? + end + + # validates_numericality_of without :only_integer w/ mocha + + def test_validates_numericality_of_generates_message + Topic.validates_numericality_of :title + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :message => 'custom' + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @topic.valid? + end + + # validates_numericality_of with :only_integer w/ mocha + + def test_validates_numericality_of_only_integer_generates_message + Topic.validates_numericality_of :title, :only_integer => true + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom' + @topic.title = 'a' + @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @topic.valid? + end + + # validates_numericality_of :odd w/ mocha + + def test_validates_numericality_of_odd_generates_message + Topic.validates_numericality_of :title, :only_integer => true, :odd => true + @topic.title = 0 + @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom' + @topic.title = 0 + @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'}) + @topic.valid? + end + + # validates_numericality_of :less_than w/ mocha + + def test_validates_numericality_of_less_than_generates_message + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @topic.title = 1 + @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil}) + @topic.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom' + @topic.title = 1 + @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'}) + @topic.valid? + end + + # validates_associated w/ mocha + + def test_validates_associated_generates_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + def test_validates_associated_generates_message_with_custom_default_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end end - def test_validates_confirmation_of_generates_message_with_custom_default_message - Topic.validates_confirmation_of :title, :message => 'custom' - @topic.title_confirmation = 'foo' - @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'}) - @topic.valid? - end + # validates_confirmation_of w/o mocha def test_validates_confirmation_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} @@ -121,20 +362,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_acceptance_of - - def test_validates_acceptance_of_generates_message - Topic.validates_acceptance_of :title, :allow_nil => false - @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) - @topic.valid? - end - - def test_validates_acceptance_of_generates_message_with_custom_default_message - Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false - @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) - @topic.valid? - end + # validates_acceptance_of w/o mocha def test_validates_acceptance_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} @@ -153,21 +381,8 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_presence_of - - def test_validates_presence_of_generates_message - Topic.validates_presence_of :title - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) - @topic.valid? - end - - def test_validates_presence_of_generates_message_with_custom_default_message - Topic.validates_presence_of :title, :message => 'custom' - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) - @topic.valid? - end - + # validates_presence_of w/o mocha + def test_validates_presence_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} @@ -185,34 +400,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_length_of :within - - def test_validates_length_of_within_generates_message_with_title_too_short - Topic.validates_length_of :title, :within => 3..5 - @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message - Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom' - @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_long - Topic.validates_length_of :title, :within => 3..5 - @topic.title = 'this title is too long' - @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message - Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom' - @topic.title = 'this title is too long' - @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) - @topic.valid? - end + # validates_length_of :within w/o mocha def test_validates_length_of_within_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} @@ -231,20 +419,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_length_of :is - - def test_validates_length_of_is_generates_message - Topic.validates_length_of :title, :is => 5 - @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_is_generates_message_with_custom_default_message - Topic.validates_length_of :title, :is => 5, :message => 'custom' - @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'}) - @topic.valid? - end + # validates_length_of :is w/o mocha def test_validates_length_of_within_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} @@ -263,22 +438,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_uniqueness_of - - def test_validates_uniqueness_of_generates_message - Topic.validates_uniqueness_of :title - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil}) - @topic.valid? - end - - def test_validates_uniqueness_of_generates_message_with_custom_default_message - Topic.validates_uniqueness_of :title, :message => 'custom' - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom'}) - @topic.valid? - end + # validates_uniqueness_of w/o mocha def test_validates_length_of_within_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} @@ -298,21 +458,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end - # validates_format_of - - def test_validates_format_of_generates_message - Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ - @topic.title = '72x' - @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil}) - @topic.valid? - end - - def test_validates_format_of_generates_message_with_custom_default_message - Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom' - @topic.title = '72x' - @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'}) - @topic.valid? - end + # validates_format_of w/o mocha def test_validates_format_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} @@ -331,22 +477,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_inclusion_of - - def test_validates_inclusion_of_generates_message - Topic.validates_inclusion_of :title, :in => %w(a b c) - @topic.title = 'z' - @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil}) - @topic.valid? - end - - def test_validates_inclusion_of_generates_message_with_custom_default_message - Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom' - @topic.title = 'z' - @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'}) - @topic.valid? - end + # validates_inclusion_of w/o mocha def test_validates_inclusion_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} @@ -365,22 +496,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_exclusion_of - - def test_validates_exclusion_of_generates_message - Topic.validates_exclusion_of :title, :in => %w(a b c) - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil}) - @topic.valid? - end - - def test_validates_exclusion_of_generates_message_with_custom_default_message - Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom' - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'}) - @topic.valid? - end + # validates_exclusion_of w/o mocha def test_validates_exclusion_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} @@ -401,22 +517,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_numericality_of without :only_integer - - def test_validates_numericality_of_generates_message - Topic.validates_numericality_of :title - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :message => 'custom' - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) - @topic.valid? - end + # validates_numericality_of without :only_integer w/o mocha def test_validates_numericality_of_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} @@ -437,22 +538,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_numericality_of with :only_integer - - def test_validates_numericality_of_only_integer_generates_message - Topic.validates_numericality_of :title, :only_integer => true - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom' - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) - @topic.valid? - end + # validates_numericality_of with :only_integer w/o mocha def test_validates_numericality_of_only_integer_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} @@ -473,22 +559,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_numericality_of :odd - - def test_validates_numericality_of_odd_generates_message - Topic.validates_numericality_of :title, :only_integer => true, :odd => true - @topic.title = 0 - @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_odd_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom' - @topic.title = 0 - @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'}) - @topic.valid? - end + # validates_numericality_of :odd w/o mocha def test_validates_numericality_of_odd_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} @@ -509,22 +580,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase assert_equal 'global message', @topic.errors.on(:title) end - - # validates_numericality_of :less_than - - def test_validates_numericality_of_less_than_generates_message - Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 - @topic.title = 1 - @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_odd_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom' - @topic.title = 1 - @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'}) - @topic.valid? - end + # validates_numericality_of :less_than w/o mocha def test_validates_numericality_of_less_than_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} @@ -546,19 +602,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase end - # validates_associated - - def test_validates_associated_generates_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? - end - - def test_validates_associated_generates_message_with_custom_default_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? - end + # validates_associated w/o mocha def test_validates_associated_finds_custom_model_key_translation I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} -- cgit v1.2.3 From 6982acb0793fb6e59f52cab4062344a88e3691ce Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Tue, 1 Jul 2008 16:52:48 +0200 Subject: Experimental I18n charset support for ActionMailer --- actionmailer/lib/action_mailer.rb | 4 + actionmailer/lib/action_mailer/base.rb | 14 ++- actionmailer/lib/action_mailer/locale/en-US.rb | 3 + actionmailer/test/i18n_test.rb | 133 +++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 actionmailer/lib/action_mailer/locale/en-US.rb create mode 100644 actionmailer/test/i18n_test.rb diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 2e324d4637..806edf1f9e 100755 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -49,4 +49,8 @@ ActionMailer::Base.class_eval do helper MailHelper end +I18n.backend.populate do + require 'action_mailer/locale/en-US.rb' +end + silence_warnings { TMail::Encoder.const_set("MAX_LINE_LEN", 200) } diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 1518e23dfe..e787c1b8da 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -307,10 +307,6 @@ module ActionMailer #:nodoc: # Specify the CC addresses for the message. adv_attr_accessor :cc - # Specify the charset to use for the message. This defaults to the - # +default_charset+ specified for ActionMailer::Base. - adv_attr_accessor :charset - # Specify the content type for the message. This defaults to text/plain # in most cases, but can be automatically set in some situations. adv_attr_accessor :content_type @@ -348,6 +344,15 @@ module ActionMailer #:nodoc: # have multiple mailer methods share the same template. adv_attr_accessor :template + # Specify the charset to use for the message. + # It performs a lookup, on the specified charset, then on the charset from + # the current locale, and, finally, on the +default_charset+ specified + # for ActionMailer::Base. + def charset(charset = nil) + @charset ||= charset || :'charset'.t || @@default_charset + end + attr_writer :charset + # Override the mailer name, which defaults to an inflected version of the # mailer's class name. If you want to use a template in a non-standard # location, you can use this to specify that location. @@ -517,7 +522,6 @@ module ActionMailer #:nodoc: # mailer. Subclasses may override this method to provide different # defaults. def initialize_defaults(method_name) - @charset ||= @@default_charset.dup @content_type ||= @@default_content_type.dup @implicit_parts_order ||= @@default_implicit_parts_order.dup @template ||= method_name diff --git a/actionmailer/lib/action_mailer/locale/en-US.rb b/actionmailer/lib/action_mailer/locale/en-US.rb new file mode 100644 index 0000000000..369f2d1a1c --- /dev/null +++ b/actionmailer/lib/action_mailer/locale/en-US.rb @@ -0,0 +1,3 @@ +I18n.backend.store_translations :'en-US', { + :charset => 'utf-8' +} \ No newline at end of file diff --git a/actionmailer/test/i18n_test.rb b/actionmailer/test/i18n_test.rb new file mode 100644 index 0000000000..92b128bce6 --- /dev/null +++ b/actionmailer/test/i18n_test.rb @@ -0,0 +1,133 @@ +require 'abstract_unit' + +class I18nMailer < ActionMailer::Base + def use_locale_charset(recipient) + recipients recipient + subject "using locale charset" + from "tester@example.com" + body "x" + end + + def use_explicit_charset(recipient) + recipients recipient + subject "using explicit charset" + from "tester@example.com" + body "x" + charset "iso-8859-2" + end + + def multiparted(recipient) + recipients recipient + subject "Multiparted" + from "tester@example.com" + body "x" + + part "text/html" do |p| + p.body = "multiparted iso-8859-1 html" + end + + part :content_type => "text/plain", + :body => "multiparted utf-8 text", + :charset => 'utf-8' + end + + def rxml_template(recipient) + recipients recipient + subject "rendering rxml template" + from "tester@example.com" + end + + def initialize_defaults(method_name) + super + mailer_name "test_mailer" + end +end + +I18n.backend.store_translations :'en-GB', { } +I18n.backend.store_translations :'de-DE', { + :charset => 'iso-8859-1' +} + +class I18nTest < Test::Unit::TestCase + def setup + @charset = 'utf-8' + @recipient = 'test@localhost' + end + + def test_should_use_locale_charset + assert_equal @charset, mail.charset + end + + def test_should_use_default_charset_if_no_current_locale + uses_locale nil do + assert_equal @charset, mail.charset + end + end + + def test_mail_headers_should_contains_current_charset + uses_locale 'de-DE' do + assert_match /iso-8859-1/, mail.header['content-type'].body + end + end + + def test_should_use_charset_from_current_locale + uses_locale 'de-DE' do + assert_equal 'iso-8859-1', mail.charset + end + end + + def test_should_raise_exception_if_current_locale_doesnt_specify_a_charset + assert_raise I18n::MissingTranslationData do + uses_locale 'en-GB' do + mail + end + end + end + + def test_should_use_explicit_charset + assert_equal 'iso-8859-2', mail('use_explicit_charset').charset + end + + def test_mail_parts_charsets + uses_locale 'de-DE' do + charsets = mail('multiparted').parts.map(&:charset) + assert_equal 'iso-8859-1', charsets[0] + assert_equal 'iso-8859-1', charsets[1] + assert_equal 'utf-8', charsets[2] + end + end + + def test_mail_parts_headers + uses_locale 'de-DE' do + content_types = mail('multiparted').parts.map(&:header).map do |header| + header['content-type'].body + end + assert_match /iso-8859-1/, content_types[0] + assert_match /iso-8859-1/, content_types[1] + assert_match /utf-8/, content_types[2] + end + end + + # TODO: this case depends on XML Builder, + # should we pass Builder::XmlMarkup.new :encoding => charset_from_i18n ? + def _ignore_test_rxml_template_should_use_current_charset + uses_locale 'de-DE' do + assert_equal "\n", + mail('rxml_template').body.strip + end + end + + private + def mail(method = 'use_locale_charset') + I18nMailer.__send__('create_' + method, @recipient) + end + + def uses_locale(locale, &block) + begin + I18n.locale = locale + yield + ensure + I18n.locale = I18n.default_locale + end + end +end -- cgit v1.2.3 From 7403c825a05af320e20f1b7e20b0c565081ede89 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Wed, 2 Jul 2008 17:51:34 +0200 Subject: Fixed Date and Time localization for ActiveSupport --- actionpack/lib/action_view/locale/en-US.rb | 21 ------- activesupport/lib/active_support/locale/en-US.rb | 21 +++++++ activesupport/test/core_ext/i18n_test.rb | 75 ++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 activesupport/test/core_ext/i18n_test.rb diff --git a/actionpack/lib/action_view/locale/en-US.rb b/actionpack/lib/action_view/locale/en-US.rb index 20d668a9e1..3adb199681 100644 --- a/actionpack/lib/action_view/locale/en-US.rb +++ b/actionpack/lib/action_view/locale/en-US.rb @@ -1,25 +1,4 @@ I18n.backend.store_translations :'en-US', { - :date => { - :formats => { - :default => "%Y-%m-%d", - :short => "%b %d", - :long => "%B %d, %Y", - }, - :day_names => Date::DAYNAMES, - :abbr_day_names => Date::ABBR_DAYNAMES, - :month_names => Date::MONTHNAMES, - :abbr_month_names => Date::ABBR_MONTHNAMES, - :order => [:year, :month, :day] - }, - :time => { - :formats => { - :default => "%a, %d %b %Y %H:%M:%S %z", - :short => "%d %b %H:%M", - :long => "%B %d, %Y %H:%M", - }, - :am => 'am', - :pm => 'pm' - }, :datetime => { :distance_in_words => { :half_a_minute => 'half a minute', diff --git a/activesupport/lib/active_support/locale/en-US.rb b/activesupport/lib/active_support/locale/en-US.rb index aa06fe14bd..51324a90bf 100644 --- a/activesupport/lib/active_support/locale/en-US.rb +++ b/activesupport/lib/active_support/locale/en-US.rb @@ -3,5 +3,26 @@ I18n.backend.store_translations :'en-US', { :array => { :sentence_connector => 'and' } + }, + :date => { + :formats => { + :default => "%Y-%m-%d", + :short => "%b %d", + :long => "%B %d, %Y", + }, + :day_names => Date::DAYNAMES, + :abbr_day_names => Date::ABBR_DAYNAMES, + :month_names => Date::MONTHNAMES, + :abbr_month_names => Date::ABBR_MONTHNAMES, + :order => [:year, :month, :day] + }, + :time => { + :formats => { + :default => "%a, %d %b %Y %H:%M:%S %z", + :short => "%d %b %H:%M", + :long => "%B %d, %Y %H:%M", + }, + :am => 'am', + :pm => 'pm' } } \ No newline at end of file diff --git a/activesupport/test/core_ext/i18n_test.rb b/activesupport/test/core_ext/i18n_test.rb new file mode 100644 index 0000000000..a67b6a5d8c --- /dev/null +++ b/activesupport/test/core_ext/i18n_test.rb @@ -0,0 +1,75 @@ +require 'abstract_unit' + +class I18nTest < Test::Unit::TestCase + def setup + @date = Date.parse("2008-7-2") + @time = Time.utc(2008, 7, 2, 16, 47, 1) + end + + uses_mocha 'I18nTimeZoneTest' do + def test_time_zone_localization_with_default_format + Time.zone.stubs(:now).returns Time.local(2000) + assert_equal "Sat, 01 Jan 2000 00:00:00 +0100", Time.zone.now.l + end + end + + def test_date_localization_should_use_default_format + assert_equal "2008-07-02", @date.l + end + + def test_date_localization_with_default_format + assert_equal "2008-07-02", @date.l(nil, :default) + end + + def test_date_localization_with_short_format + assert_equal "Jul 02", @date.l(nil, :short) + end + + def test_date_localization_with_long_format + assert_equal "July 02, 2008", @date.l(nil, :long) + end + + def test_time_localization_should_use_default_format + assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", @time.l + end + + def test_time_localization_with_default_format + assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", @time.l(nil, :default) + end + + def test_time_localization_with_short_format + assert_equal "02 Jul 16:47", @time.l(nil, :short) + end + + def test_time_localization_with_long_format + assert_equal "July 02, 2008 16:47", @time.l(nil, :long) + end + + def test_day_names + assert_equal Date::DAYNAMES, :'date.day_names'.t + end + + def test_abbr_day_names + assert_equal Date::ABBR_DAYNAMES, :'date.abbr_day_names'.t + end + + def test_month_names + assert_equal Date::MONTHNAMES, :'date.month_names'.t + end + + def test_abbr_month_names + assert_equal Date::ABBR_MONTHNAMES, :'date.abbr_month_names'.t + end + + def test_date_order + assert_equal [:year, :month, :day], :'date.order'.t + end + + def test_time_am + assert_equal 'am', :'time.am'.t + end + + def test_time_pm + assert_equal 'pm', :'time.pm'.t + end +end -- cgit v1.2.3 From 8f74ba96c47e77e18ce363c8e7cd2fc9196faf7a Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Wed, 2 Jul 2008 19:21:07 +0200 Subject: remove core extensions in favor of I18n#translate and I18n#localize --- actionmailer/lib/action_mailer/base.rb | 2 +- actionpack/lib/action_view/helpers/date_helper.rb | 5 +- .../lib/action_view/helpers/number_helper.rb | 2 +- activerecord/lib/active_record/validations.rb | 6 +- activerecord/test/cases/validations_i18n_test.rb | 2 +- .../active_support/core_ext/array/conversions.rb | 2 +- activesupport/lib/active_support/vendor/i18n-0.0.1 | 2 +- activesupport/test/core_ext/i18n_test.rb | 75 ---------------------- activesupport/test/i18n_test.rb | 75 ++++++++++++++++++++++ 9 files changed, 86 insertions(+), 85 deletions(-) delete mode 100644 activesupport/test/core_ext/i18n_test.rb create mode 100644 activesupport/test/i18n_test.rb diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index e787c1b8da..f7da90d10f 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -349,7 +349,7 @@ module ActionMailer #:nodoc: # the current locale, and, finally, on the +default_charset+ specified # for ActionMailer::Base. def charset(charset = nil) - @charset ||= charset || :'charset'.t || @@default_charset + @charset ||= charset || I18n.translate(:charset) || @@default_charset end attr_writer :charset diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 6ac4171fd5..d306c7a742 100755 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -512,7 +512,8 @@ module ActionView else month_options = [] month_names = options[:use_month_names] || begin - (options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names').t locale + key = options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' + I18n.translate key, locale end month_names.unshift(nil) if month_names.size < 13 @@ -632,7 +633,7 @@ module ActionView position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } - order = options[:order] ||= :'date.order'.t(locale) + order = options[:order] ||= I18n.translate(:'date.order', locale) # Discard explicit and implicit by not being included in the :order discard = {} diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 3e0d5b1db4..981589437d 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -70,7 +70,7 @@ module ActionView # # => 1234567890,50 £ def number_to_currency(number, options = {}) options = options.symbolize_keys - defaults = :'currency.format'.t(options[:locale]) || {} + defaults = I18n.translate(:'currency.format', options[:locale]) || {} precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 5bbd10394c..5245f65869 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -22,7 +22,7 @@ module ActiveRecord class << self def default_error_messages ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use 'active_record.error_messages'.t.") - 'active_record.error_messages'.t + I18n.translate 'active_record.error_messages' end end @@ -43,7 +43,7 @@ module ActiveRecord # error can be added to the same +attribute+ in which case an array will be returned on a call to on(attribute). # If no +msg+ is supplied, "invalid" is assumed. def add(attribute, message = nil) - message ||= :"active_record.error_messages.invalid".t + message ||= I18n.translate :"active_record.error_messages.invalid" @errors[attribute.to_s] ||= [] @errors[attribute.to_s] << message end @@ -168,7 +168,7 @@ module ActiveRecord full_messages << message else key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}" - attr_name = key.t(locale) || @base.class.human_attribute_name(attr) + attr_name = I18n.translate(key, locale, :raise => true) rescue @base.class.human_attribute_name(attr) full_messages << attr_name + " " + message end end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 53e90f4d53..840fcd81d0 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -79,7 +79,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @topic.errors.instance_variable_set :@errors, { 'title' => 'empty' } - I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US').returns('Title') + I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US', :raise => true).returns('Title') @topic.errors.full_messages :locale => 'en-US' end end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 80d91a6cbd..80bf1404de 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -13,7 +13,7 @@ module ActiveSupport #:nodoc: locale = options[:locale] locale ||= self.locale if respond_to?(:locale) - default = :'support.array.sentence_connector'.t(locale) + default = I18n.translate(:'support.array.sentence_connector', locale) options.reverse_merge! :connector => default, :skip_last_comma => false options[:connector] = "#{options[:connector]} " unless options[:connector].nil? || options[:connector].strip == '' diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1 b/activesupport/lib/active_support/vendor/i18n-0.0.1 index 20c331666b..970bc7ab5f 160000 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1 +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1 @@ -1 +1 @@ -Subproject commit 20c331666b3b6a21791d4cded53c3d8654fba714 +Subproject commit 970bc7ab5faa94e41ee4a56bc8913c144c1cdd19 diff --git a/activesupport/test/core_ext/i18n_test.rb b/activesupport/test/core_ext/i18n_test.rb deleted file mode 100644 index a67b6a5d8c..0000000000 --- a/activesupport/test/core_ext/i18n_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'abstract_unit' - -class I18nTest < Test::Unit::TestCase - def setup - @date = Date.parse("2008-7-2") - @time = Time.utc(2008, 7, 2, 16, 47, 1) - end - - uses_mocha 'I18nTimeZoneTest' do - def test_time_zone_localization_with_default_format - Time.zone.stubs(:now).returns Time.local(2000) - assert_equal "Sat, 01 Jan 2000 00:00:00 +0100", Time.zone.now.l - end - end - - def test_date_localization_should_use_default_format - assert_equal "2008-07-02", @date.l - end - - def test_date_localization_with_default_format - assert_equal "2008-07-02", @date.l(nil, :default) - end - - def test_date_localization_with_short_format - assert_equal "Jul 02", @date.l(nil, :short) - end - - def test_date_localization_with_long_format - assert_equal "July 02, 2008", @date.l(nil, :long) - end - - def test_time_localization_should_use_default_format - assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", @time.l - end - - def test_time_localization_with_default_format - assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", @time.l(nil, :default) - end - - def test_time_localization_with_short_format - assert_equal "02 Jul 16:47", @time.l(nil, :short) - end - - def test_time_localization_with_long_format - assert_equal "July 02, 2008 16:47", @time.l(nil, :long) - end - - def test_day_names - assert_equal Date::DAYNAMES, :'date.day_names'.t - end - - def test_abbr_day_names - assert_equal Date::ABBR_DAYNAMES, :'date.abbr_day_names'.t - end - - def test_month_names - assert_equal Date::MONTHNAMES, :'date.month_names'.t - end - - def test_abbr_month_names - assert_equal Date::ABBR_MONTHNAMES, :'date.abbr_month_names'.t - end - - def test_date_order - assert_equal [:year, :month, :day], :'date.order'.t - end - - def test_time_am - assert_equal 'am', :'time.am'.t - end - - def test_time_pm - assert_equal 'pm', :'time.pm'.t - end -end diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb new file mode 100644 index 0000000000..e6d90f09c1 --- /dev/null +++ b/activesupport/test/i18n_test.rb @@ -0,0 +1,75 @@ +require 'abstract_unit' + +class I18nTest < Test::Unit::TestCase + def setup + @date = Date.parse("2008-7-2") + @time = Time.utc(2008, 7, 2, 16, 47, 1) + end + + uses_mocha 'I18nTimeZoneTest' do + def test_time_zone_localization_with_default_format + Time.zone.stubs(:now).returns Time.local(2000) + assert_equal "Sat, 01 Jan 2000 00:00:00 +0100", I18n.localize(Time.zone.now) + end + end + + def test_date_localization_should_use_default_format + assert_equal "2008-07-02", I18n.localize(@date) + end + + def test_date_localization_with_default_format + assert_equal "2008-07-02", I18n.localize(@date, nil, :default) + end + + def test_date_localization_with_short_format + assert_equal "Jul 02", I18n.localize(@date, nil, :short) + end + + def test_date_localization_with_long_format + assert_equal "July 02, 2008", I18n.localize(@date, nil, :long) + end + + def test_time_localization_should_use_default_format + assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", I18n.localize(@time) + end + + def test_time_localization_with_default_format + assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", I18n.localize(@time, nil, :default) + end + + def test_time_localization_with_short_format + assert_equal "02 Jul 16:47", I18n.localize(@time, nil, :short) + end + + def test_time_localization_with_long_format + assert_equal "July 02, 2008 16:47", I18n.localize(@time, nil, :long) + end + + def test_day_names + assert_equal Date::DAYNAMES, I18n.translate(:'date.day_names') + end + + def test_abbr_day_names + assert_equal Date::ABBR_DAYNAMES, I18n.translate(:'date.abbr_day_names') + end + + def test_month_names + assert_equal Date::MONTHNAMES, I18n.translate(:'date.month_names') + end + + def test_abbr_month_names + assert_equal Date::ABBR_MONTHNAMES, I18n.translate(:'date.abbr_month_names') + end + + def test_date_order + assert_equal [:year, :month, :day], I18n.translate(:'date.order') + end + + def test_time_am + assert_equal 'am', I18n.translate(:'time.am') + end + + def test_time_pm + assert_equal 'pm', I18n.translate(:'time.pm') + end +end -- cgit v1.2.3 From d41e4c1c3d6e6259f1cfc0cdbd4fc30fee0f866a Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Thu, 3 Jul 2008 11:50:18 +0200 Subject: Make sure ActionMailer use default charset if no defined by current locale --- actionmailer/lib/action_mailer/base.rb | 4 ++-- actionmailer/test/i18n_test.rb | 26 ++++++++++++-------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f7da90d10f..a1a7f55d2b 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -346,10 +346,10 @@ module ActionMailer #:nodoc: # Specify the charset to use for the message. # It performs a lookup, on the specified charset, then on the charset from - # the current locale, and, finally, on the +default_charset+ specified + # the current locale, and, in the end, on the +default_charset+ specified # for ActionMailer::Base. def charset(charset = nil) - @charset ||= charset || I18n.translate(:charset) || @@default_charset + @charset ||= charset || I18n.translate(:charset, :default => @@default_charset) end attr_writer :charset diff --git a/actionmailer/test/i18n_test.rb b/actionmailer/test/i18n_test.rb index 92b128bce6..a775128519 100644 --- a/actionmailer/test/i18n_test.rb +++ b/actionmailer/test/i18n_test.rb @@ -59,37 +59,35 @@ class I18nTest < Test::Unit::TestCase end def test_should_use_default_charset_if_no_current_locale - uses_locale nil do + with_locale nil do assert_equal @charset, mail.charset end end def test_mail_headers_should_contains_current_charset - uses_locale 'de-DE' do + with_locale 'de-DE' do assert_match /iso-8859-1/, mail.header['content-type'].body end end def test_should_use_charset_from_current_locale - uses_locale 'de-DE' do + with_locale 'de-DE' do assert_equal 'iso-8859-1', mail.charset end end - - def test_should_raise_exception_if_current_locale_doesnt_specify_a_charset - assert_raise I18n::MissingTranslationData do - uses_locale 'en-GB' do - mail - end + + def test_should_use_default_charset_if_missing_for_current_locale + with_locale 'en-GB' do + assert_equal @charset, mail.charset end end - + def test_should_use_explicit_charset assert_equal 'iso-8859-2', mail('use_explicit_charset').charset end def test_mail_parts_charsets - uses_locale 'de-DE' do + with_locale 'de-DE' do charsets = mail('multiparted').parts.map(&:charset) assert_equal 'iso-8859-1', charsets[0] assert_equal 'iso-8859-1', charsets[1] @@ -98,7 +96,7 @@ class I18nTest < Test::Unit::TestCase end def test_mail_parts_headers - uses_locale 'de-DE' do + with_locale 'de-DE' do content_types = mail('multiparted').parts.map(&:header).map do |header| header['content-type'].body end @@ -111,7 +109,7 @@ class I18nTest < Test::Unit::TestCase # TODO: this case depends on XML Builder, # should we pass Builder::XmlMarkup.new :encoding => charset_from_i18n ? def _ignore_test_rxml_template_should_use_current_charset - uses_locale 'de-DE' do + with_locale 'de-DE' do assert_equal "\n", mail('rxml_template').body.strip end @@ -122,7 +120,7 @@ class I18nTest < Test::Unit::TestCase I18nMailer.__send__('create_' + method, @recipient) end - def uses_locale(locale, &block) + def with_locale(locale, &block) begin I18n.locale = locale yield -- cgit v1.2.3 From e1a7f83fca862fd7472ef6b80f8b6a8d33849a8e Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 4 Jul 2008 22:22:20 +0200 Subject: use :default for human_attribute_name --- activerecord/lib/active_record/validations.rb | 2 +- activerecord/test/cases/validations_i18n_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 5245f65869..8ba09b3992 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -168,7 +168,7 @@ module ActiveRecord full_messages << message else key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}" - attr_name = I18n.translate(key, locale, :raise => true) rescue @base.class.human_attribute_name(attr) + attr_name = I18n.translate(key, locale, :default => @base.class.human_attribute_name(attr)) full_messages << attr_name + " " + message end end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 840fcd81d0..5be518c547 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -79,7 +79,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @topic.errors.instance_variable_set :@errors, { 'title' => 'empty' } - I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US', :raise => true).returns('Title') + I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US', :default => 'Title').returns('Title') @topic.errors.full_messages :locale => 'en-US' end end -- cgit v1.2.3 From 8305d6759abe2b8511ccee35fd0998e6fb0dffd0 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 4 Jul 2008 22:26:53 +0200 Subject: Reverting changes to ActionMailer Revert "Make sure ActionMailer use default charset if no defined by current locale" This reverts commit d41e4c1c3d6e6259f1cfc0cdbd4fc30fee0f866a. --- actionmailer/lib/action_mailer/base.rb | 4 ++-- actionmailer/test/i18n_test.rb | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index a1a7f55d2b..f7da90d10f 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -346,10 +346,10 @@ module ActionMailer #:nodoc: # Specify the charset to use for the message. # It performs a lookup, on the specified charset, then on the charset from - # the current locale, and, in the end, on the +default_charset+ specified + # the current locale, and, finally, on the +default_charset+ specified # for ActionMailer::Base. def charset(charset = nil) - @charset ||= charset || I18n.translate(:charset, :default => @@default_charset) + @charset ||= charset || I18n.translate(:charset) || @@default_charset end attr_writer :charset diff --git a/actionmailer/test/i18n_test.rb b/actionmailer/test/i18n_test.rb index a775128519..92b128bce6 100644 --- a/actionmailer/test/i18n_test.rb +++ b/actionmailer/test/i18n_test.rb @@ -59,35 +59,37 @@ class I18nTest < Test::Unit::TestCase end def test_should_use_default_charset_if_no_current_locale - with_locale nil do + uses_locale nil do assert_equal @charset, mail.charset end end def test_mail_headers_should_contains_current_charset - with_locale 'de-DE' do + uses_locale 'de-DE' do assert_match /iso-8859-1/, mail.header['content-type'].body end end def test_should_use_charset_from_current_locale - with_locale 'de-DE' do + uses_locale 'de-DE' do assert_equal 'iso-8859-1', mail.charset end end - - def test_should_use_default_charset_if_missing_for_current_locale - with_locale 'en-GB' do - assert_equal @charset, mail.charset + + def test_should_raise_exception_if_current_locale_doesnt_specify_a_charset + assert_raise I18n::MissingTranslationData do + uses_locale 'en-GB' do + mail + end end end - + def test_should_use_explicit_charset assert_equal 'iso-8859-2', mail('use_explicit_charset').charset end def test_mail_parts_charsets - with_locale 'de-DE' do + uses_locale 'de-DE' do charsets = mail('multiparted').parts.map(&:charset) assert_equal 'iso-8859-1', charsets[0] assert_equal 'iso-8859-1', charsets[1] @@ -96,7 +98,7 @@ class I18nTest < Test::Unit::TestCase end def test_mail_parts_headers - with_locale 'de-DE' do + uses_locale 'de-DE' do content_types = mail('multiparted').parts.map(&:header).map do |header| header['content-type'].body end @@ -109,7 +111,7 @@ class I18nTest < Test::Unit::TestCase # TODO: this case depends on XML Builder, # should we pass Builder::XmlMarkup.new :encoding => charset_from_i18n ? def _ignore_test_rxml_template_should_use_current_charset - with_locale 'de-DE' do + uses_locale 'de-DE' do assert_equal "\n", mail('rxml_template').body.strip end @@ -120,7 +122,7 @@ class I18nTest < Test::Unit::TestCase I18nMailer.__send__('create_' + method, @recipient) end - def with_locale(locale, &block) + def uses_locale(locale, &block) begin I18n.locale = locale yield -- cgit v1.2.3 From 34bd1e95c7a08c9266fc99281417dad4a19cdf73 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 4 Jul 2008 22:34:32 +0200 Subject: Reverting changes to ActionMailer Revert "Experimental I18n charset support for ActionMailer" This reverts commit 6982acb0793fb6e59f52cab4062344a88e3691ce. Conflicts: actionmailer/lib/action_mailer/base.rb --- actionmailer/lib/action_mailer.rb | 4 - actionmailer/lib/action_mailer/base.rb | 9 -- actionmailer/lib/action_mailer/locale/en-US.rb | 3 - actionmailer/test/i18n_test.rb | 133 ------------------------- 4 files changed, 149 deletions(-) delete mode 100644 actionmailer/lib/action_mailer/locale/en-US.rb delete mode 100644 actionmailer/test/i18n_test.rb diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 806edf1f9e..2e324d4637 100755 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -49,8 +49,4 @@ ActionMailer::Base.class_eval do helper MailHelper end -I18n.backend.populate do - require 'action_mailer/locale/en-US.rb' -end - silence_warnings { TMail::Encoder.const_set("MAX_LINE_LEN", 200) } diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f7da90d10f..b1f23583a6 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -344,15 +344,6 @@ module ActionMailer #:nodoc: # have multiple mailer methods share the same template. adv_attr_accessor :template - # Specify the charset to use for the message. - # It performs a lookup, on the specified charset, then on the charset from - # the current locale, and, finally, on the +default_charset+ specified - # for ActionMailer::Base. - def charset(charset = nil) - @charset ||= charset || I18n.translate(:charset) || @@default_charset - end - attr_writer :charset - # Override the mailer name, which defaults to an inflected version of the # mailer's class name. If you want to use a template in a non-standard # location, you can use this to specify that location. diff --git a/actionmailer/lib/action_mailer/locale/en-US.rb b/actionmailer/lib/action_mailer/locale/en-US.rb deleted file mode 100644 index 369f2d1a1c..0000000000 --- a/actionmailer/lib/action_mailer/locale/en-US.rb +++ /dev/null @@ -1,3 +0,0 @@ -I18n.backend.store_translations :'en-US', { - :charset => 'utf-8' -} \ No newline at end of file diff --git a/actionmailer/test/i18n_test.rb b/actionmailer/test/i18n_test.rb deleted file mode 100644 index 92b128bce6..0000000000 --- a/actionmailer/test/i18n_test.rb +++ /dev/null @@ -1,133 +0,0 @@ -require 'abstract_unit' - -class I18nMailer < ActionMailer::Base - def use_locale_charset(recipient) - recipients recipient - subject "using locale charset" - from "tester@example.com" - body "x" - end - - def use_explicit_charset(recipient) - recipients recipient - subject "using explicit charset" - from "tester@example.com" - body "x" - charset "iso-8859-2" - end - - def multiparted(recipient) - recipients recipient - subject "Multiparted" - from "tester@example.com" - body "x" - - part "text/html" do |p| - p.body = "multiparted iso-8859-1 html" - end - - part :content_type => "text/plain", - :body => "multiparted utf-8 text", - :charset => 'utf-8' - end - - def rxml_template(recipient) - recipients recipient - subject "rendering rxml template" - from "tester@example.com" - end - - def initialize_defaults(method_name) - super - mailer_name "test_mailer" - end -end - -I18n.backend.store_translations :'en-GB', { } -I18n.backend.store_translations :'de-DE', { - :charset => 'iso-8859-1' -} - -class I18nTest < Test::Unit::TestCase - def setup - @charset = 'utf-8' - @recipient = 'test@localhost' - end - - def test_should_use_locale_charset - assert_equal @charset, mail.charset - end - - def test_should_use_default_charset_if_no_current_locale - uses_locale nil do - assert_equal @charset, mail.charset - end - end - - def test_mail_headers_should_contains_current_charset - uses_locale 'de-DE' do - assert_match /iso-8859-1/, mail.header['content-type'].body - end - end - - def test_should_use_charset_from_current_locale - uses_locale 'de-DE' do - assert_equal 'iso-8859-1', mail.charset - end - end - - def test_should_raise_exception_if_current_locale_doesnt_specify_a_charset - assert_raise I18n::MissingTranslationData do - uses_locale 'en-GB' do - mail - end - end - end - - def test_should_use_explicit_charset - assert_equal 'iso-8859-2', mail('use_explicit_charset').charset - end - - def test_mail_parts_charsets - uses_locale 'de-DE' do - charsets = mail('multiparted').parts.map(&:charset) - assert_equal 'iso-8859-1', charsets[0] - assert_equal 'iso-8859-1', charsets[1] - assert_equal 'utf-8', charsets[2] - end - end - - def test_mail_parts_headers - uses_locale 'de-DE' do - content_types = mail('multiparted').parts.map(&:header).map do |header| - header['content-type'].body - end - assert_match /iso-8859-1/, content_types[0] - assert_match /iso-8859-1/, content_types[1] - assert_match /utf-8/, content_types[2] - end - end - - # TODO: this case depends on XML Builder, - # should we pass Builder::XmlMarkup.new :encoding => charset_from_i18n ? - def _ignore_test_rxml_template_should_use_current_charset - uses_locale 'de-DE' do - assert_equal "\n", - mail('rxml_template').body.strip - end - end - - private - def mail(method = 'use_locale_charset') - I18nMailer.__send__('create_' + method, @recipient) - end - - def uses_locale(locale, &block) - begin - I18n.locale = locale - yield - ensure - I18n.locale = I18n.default_locale - end - end -end -- cgit v1.2.3 From a865d19516a716762b46ca29212bac668dd7c4a0 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 4 Jul 2008 22:45:53 +0200 Subject: reverting changes to ActionMailer --- actionmailer/lib/action_mailer/base.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index b1f23583a6..1518e23dfe 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -307,6 +307,10 @@ module ActionMailer #:nodoc: # Specify the CC addresses for the message. adv_attr_accessor :cc + # Specify the charset to use for the message. This defaults to the + # +default_charset+ specified for ActionMailer::Base. + adv_attr_accessor :charset + # Specify the content type for the message. This defaults to text/plain # in most cases, but can be automatically set in some situations. adv_attr_accessor :content_type @@ -513,6 +517,7 @@ module ActionMailer #:nodoc: # mailer. Subclasses may override this method to provide different # defaults. def initialize_defaults(method_name) + @charset ||= @@default_charset.dup @content_type ||= @@default_content_type.dup @implicit_parts_order ||= @@default_implicit_parts_order.dup @template ||= method_name -- cgit v1.2.3 From c9ed2c9bd24b9f4cdfcb692151f87ba900469e71 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sun, 6 Jul 2008 19:00:55 +0200 Subject: add a translation helper --- .../lib/action_view/helpers/translation_helper.rb | 16 +++++++++ .../test/template/translation_helper_test.rb | 42 ++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 actionpack/lib/action_view/helpers/translation_helper.rb create mode 100644 actionpack/test/template/translation_helper_test.rb diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb new file mode 100644 index 0000000000..0bfe6bf771 --- /dev/null +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -0,0 +1,16 @@ +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers + module TranslationHelper + def translate(*args) + key, locale, options = I18n.send :process_translate_arguments, *args + I18n.translate key, locale, options.merge(:raise => true) + + rescue I18n::MissingTranslationData => e + keys = I18n.send :normalize_translation_keys, locale, key, options[:scope] + content_tag('span', keys.join(', '), :class => 'translation_missing') + end + end + end +end \ No newline at end of file diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb new file mode 100644 index 0000000000..e97bcdb731 --- /dev/null +++ b/actionpack/test/template/translation_helper_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' + +class TranslationHelperTest < Test::Unit::TestCase + include ActionView::Helpers::TagHelper + include ActionView::Helpers::TranslationHelper + + attr_reader :request + uses_mocha 'translation_helper_test' do + def setup + end + + def test_delegates_to_i18n_setting_the_raise_option + I18n.expects(:translate).with(:foo, 'en-US', :raise => true) + translate :foo, 'en-US' + end + + def test_returns_missing_translation_message_wrapped_into_span + expected = 'en-US, foo' + assert_equal expected, translate(:foo) + end + + # def test_error_messages_for_given_a_header_message_option_it_does_not_translate_header_message + # I18n.expects(:translate).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').never + # error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en-US') + # end + # + # def test_error_messages_for_given_no_header_message_option_it_translates_header_message + # I18n.expects(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns 'header message' + # error_messages_for(:object => @object, :locale => 'en-US') + # end + # + # def test_error_messages_for_given_a_message_option_it_does_not_translate_message + # I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).never + # error_messages_for(:object => @object, :message => 'message', :locale => 'en-US') + # end + # + # def test_error_messages_for_given_no_message_option_it_translates_message + # I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' + # error_messages_for(:object => @object, :locale => 'en-US') + # end + end +end \ No newline at end of file -- cgit v1.2.3 From 84816ae981a8598e5e401eb1b9b805de840fefc9 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sun, 6 Jul 2008 21:20:02 +0200 Subject: align with changes in i18n --- actionpack/lib/action_view/helpers/date_helper.rb | 5 ++--- actionpack/lib/action_view/helpers/number_helper.rb | 2 +- actionpack/lib/action_view/helpers/translation_helper.rb | 6 +++--- actionpack/test/template/date_helper_i18n_test.rb | 8 ++++---- actionpack/test/template/number_helper_i18n_test.rb | 2 +- activerecord/lib/active_record/validations.rb | 5 ++--- activerecord/test/cases/validations_i18n_test.rb | 6 +++--- .../lib/active_support/core_ext/array/conversions.rb | 5 +---- activesupport/lib/active_support/vendor/i18n-0.0.1 | 2 +- activesupport/test/i18n_test.rb | 12 ++++++------ 10 files changed, 24 insertions(+), 29 deletions(-) diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index d306c7a742..61fff42d5a 100755 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -504,7 +504,6 @@ module ActionView # def select_month(date, options = {}, html_options = {}) locale = options[:locale] - locale ||= self.locale if respond_to?(:locale) val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' if options[:use_hidden] @@ -513,7 +512,7 @@ module ActionView month_options = [] month_names = options[:use_month_names] || begin key = options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' - I18n.translate key, locale + I18n.translate key, :locale => locale end month_names.unshift(nil) if month_names.size < 13 @@ -633,7 +632,7 @@ module ActionView position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } - order = options[:order] ||= I18n.translate(:'date.order', locale) + order = options[:order] ||= I18n.translate(:'date.order', :locale => locale) # Discard explicit and implicit by not being included in the :order discard = {} diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 981589437d..6bb8263794 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -70,7 +70,7 @@ module ActionView # # => 1234567890,50 £ def number_to_currency(number, options = {}) options = options.symbolize_keys - defaults = I18n.translate(:'currency.format', options[:locale]) || {} + defaults = I18n.translate(:'currency.format', :locale => options[:locale]) || {} precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 0bfe6bf771..c13c2dfc04 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -4,11 +4,11 @@ module ActionView module Helpers module TranslationHelper def translate(*args) - key, locale, options = I18n.send :process_translate_arguments, *args - I18n.translate key, locale, options.merge(:raise => true) + args << args.extract_options!.merge(:raise => true) + I18n.translate *args rescue I18n::MissingTranslationData => e - keys = I18n.send :normalize_translation_keys, locale, key, options[:scope] + keys = I18n.send :normalize_translation_keys, e.locale, e.key, e.options[:scope] content_tag('span', keys.join(', '), :class => 'translation_missing') end end diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index aeb06c55ea..aca3593921 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -56,7 +56,7 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase uses_mocha 'date_helper_select_tags_i18n_tests' do def setup - I18n.stubs(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES + I18n.stubs(:translate).with(:'date.month_names', :locale => 'en-US').returns Date::MONTHNAMES end # select_month @@ -67,12 +67,12 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase end def test_select_month_translates_monthnames - I18n.expects(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES + I18n.expects(:translate).with(:'date.month_names', :locale => 'en-US').returns Date::MONTHNAMES select_month(8, :locale => 'en-US') end def test_select_month_given_use_short_month_option_translates_abbr_monthnames - I18n.expects(:translate).with(:'date.abbr_month_names', 'en-US').returns Date::ABBR_MONTHNAMES + I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en-US').returns Date::ABBR_MONTHNAMES select_month(8, :locale => 'en-US', :use_short_month => true) end @@ -84,7 +84,7 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase end def test_date_or_time_select_given_no_order_options_translates_order - I18n.expects(:translate).with(:'date.order', 'en-US').returns [:year, :month, :day] + I18n.expects(:translate).with(:'date.order', :locale => 'en-US').returns [:year, :month, :day] datetime_select('post', 'updated_at', :locale => 'en-US') end end diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index be40ddbc88..50c20c3627 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -11,7 +11,7 @@ class NumberHelperI18nTests < Test::Unit::TestCase end def test_number_to_currency_translates_currency_formats - I18n.expects(:translate).with(:'currency.format', 'en-US').returns @defaults + I18n.expects(:translate).with(:'currency.format', :locale => 'en-US').returns @defaults number_to_currency(1, :locale => 'en-US') end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 8ba09b3992..a328c4d927 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -70,7 +70,7 @@ module ActiveRecord msgs << options[:default] if options[:default] msgs << key - I18n.t options.merge(:default => msgs, :scope => [:active_record, :error_messages]) + I18n.t nil, options.merge(:default => msgs, :scope => [:active_record, :error_messages]) end # Returns true if the specified +attribute+ has errors associated with it. @@ -158,7 +158,6 @@ module ActiveRecord # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] def full_messages(options = {}) full_messages = [] - locale = options[:locale] @errors.each_key do |attr| @errors[attr].each do |message| @@ -168,7 +167,7 @@ module ActiveRecord full_messages << message else key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}" - attr_name = I18n.translate(key, locale, :default => @base.class.human_attribute_name(attr)) + attr_name = I18n.translate(key, :locale => options[:locale], :default => @base.class.human_attribute_name(attr)) full_messages << attr_name + " " + message end end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 5be518c547..86834fe920 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -46,14 +46,14 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase global_scope = [:active_record, :error_messages] custom_scope = global_scope + [:custom, 'topic', :title] - I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid] + I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid] @topic.errors.generate_message :title, :invalid, :default => 'default from class def' end def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti custom_scope = [:active_record, :error_messages, :custom, 'topic', :title] - I18n.expects(:t).with :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid] + I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid] Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def' end @@ -79,7 +79,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @topic.errors.instance_variable_set :@errors, { 'title' => 'empty' } - I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", 'en-US', :default => 'Title').returns('Title') + I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", :locale => 'en-US', :default => 'Title').returns('Title') @topic.errors.full_messages :locale => 'en-US' end end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 80bf1404de..59dc96754f 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -10,10 +10,7 @@ module ActiveSupport #:nodoc: def to_sentence(options = {}) options.assert_valid_keys(:connector, :skip_last_comma, :locale) - locale = options[:locale] - locale ||= self.locale if respond_to?(:locale) - - default = I18n.translate(:'support.array.sentence_connector', locale) + default = I18n.translate(:'support.array.sentence_connector', :locale => options[:locale]) options.reverse_merge! :connector => default, :skip_last_comma => false options[:connector] = "#{options[:connector]} " unless options[:connector].nil? || options[:connector].strip == '' diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1 b/activesupport/lib/active_support/vendor/i18n-0.0.1 index 970bc7ab5f..46aad28993 160000 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1 +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1 @@ -1 +1 @@ -Subproject commit 970bc7ab5faa94e41ee4a56bc8913c144c1cdd19 +Subproject commit 46aad289935eaf059c429acb5f3bfa0946f2d99f diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb index e6d90f09c1..17074b6cc6 100644 --- a/activesupport/test/i18n_test.rb +++ b/activesupport/test/i18n_test.rb @@ -18,15 +18,15 @@ class I18nTest < Test::Unit::TestCase end def test_date_localization_with_default_format - assert_equal "2008-07-02", I18n.localize(@date, nil, :default) + assert_equal "2008-07-02", I18n.localize(@date, :format => :default) end def test_date_localization_with_short_format - assert_equal "Jul 02", I18n.localize(@date, nil, :short) + assert_equal "Jul 02", I18n.localize(@date, :format => :short) end def test_date_localization_with_long_format - assert_equal "July 02, 2008", I18n.localize(@date, nil, :long) + assert_equal "July 02, 2008", I18n.localize(@date, :format => :long) end def test_time_localization_should_use_default_format @@ -34,15 +34,15 @@ class I18nTest < Test::Unit::TestCase end def test_time_localization_with_default_format - assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", I18n.localize(@time, nil, :default) + assert_equal "Wed, 02 Jul 2008 16:47:01 +0100", I18n.localize(@time, :format => :default) end def test_time_localization_with_short_format - assert_equal "02 Jul 16:47", I18n.localize(@time, nil, :short) + assert_equal "02 Jul 16:47", I18n.localize(@time, :format => :short) end def test_time_localization_with_long_format - assert_equal "July 02, 2008 16:47", I18n.localize(@time, nil, :long) + assert_equal "July 02, 2008 16:47", I18n.localize(@time, :format => :long) end def test_day_names -- cgit v1.2.3 From 826c3db42105518b3a88cf56e348b48c1660f850 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Mon, 7 Jul 2008 22:46:16 +0200 Subject: Updated ActiveRecord::Errors#default_error_messages deprecation warning according to i18n changes --- activerecord/lib/active_record/validations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index a328c4d927..83d55f23ea 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -21,7 +21,7 @@ module ActiveRecord class << self def default_error_messages - ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use 'active_record.error_messages'.t.") + ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('active_record.error_messages').") I18n.translate 'active_record.error_messages' end end -- cgit v1.2.3 From 2949918b4cea26435d1f0a076fe884c8113b40fa Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Tue, 8 Jul 2008 11:53:19 +0200 Subject: Make sure object name is translated in #error_messages_for --- actionpack/lib/action_view/helpers/active_record_helper.rb | 1 + actionpack/test/template/active_record_helper_i18n_test.rb | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index 4ff16cd70c..a2fee53fb6 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -176,6 +176,7 @@ module ActionView options[:header_message] else object_name = options[:object_name].to_s.gsub('_', ' ') + object_name = I18n.t(object_name, :default => object_name) || '' locale.t :header_message, :count => count, :object_name => object_name end message = options.include?(:message) ? options[:message] : locale.t(:message) diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb index d78b0e4c0c..d35e79b94a 100644 --- a/actionpack/test/template/active_record_helper_i18n_test.rb +++ b/actionpack/test/template/active_record_helper_i18n_test.rb @@ -7,6 +7,7 @@ class ActiveRecordHelperI18nTest < Test::Unit::TestCase uses_mocha 'active_record_helper_i18n_test' do def setup @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) + @object_name = 'book' stubs(:content_tag).returns 'content_tag' I18n.stubs(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns "1 error prohibited this from being saved" @@ -20,17 +21,26 @@ class ActiveRecordHelperI18nTest < Test::Unit::TestCase def test_error_messages_for_given_no_header_message_option_it_translates_header_message I18n.expects(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns 'header message' + I18n.expects(:t).with('', :default => '').once error_messages_for(:object => @object, :locale => 'en-US') end def test_error_messages_for_given_a_message_option_it_does_not_translate_message I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).never + I18n.expects(:t).with('', :default => '').once error_messages_for(:object => @object, :message => 'message', :locale => 'en-US') end def test_error_messages_for_given_no_message_option_it_translates_message I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' + I18n.expects(:t).with('', :default => '').once error_messages_for(:object => @object, :locale => 'en-US') end + + def test_error_messages_for_given_object_name_it_translates_object_name + I18n.expects(:t).with(:header_message, :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => @object_name).returns "1 error prohibited this #{@object_name} from being saved" + I18n.expects(:t).with(@object_name, :default => @object_name).once.returns @object_name + error_messages_for(:object => @object, :locale => 'en-US', :object_name => @object_name) + end end end \ No newline at end of file -- cgit v1.2.3 From cf5d6ab9a849d19ac683180cc7b603ca94b13ed7 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Tue, 8 Jul 2008 12:37:49 +0200 Subject: Added localize helper method --- actionpack/lib/action_view/helpers/translation_helper.rb | 4 ++++ actionpack/test/template/translation_helper_test.rb | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index c13c2dfc04..e1010ccf5f 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -11,6 +11,10 @@ module ActionView keys = I18n.send :normalize_translation_keys, e.locale, e.key, e.options[:scope] content_tag('span', keys.join(', '), :class => 'translation_missing') end + + def localize(*args) + I18n.l *args + end end end end \ No newline at end of file diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb index e97bcdb731..2263d48a65 100644 --- a/actionpack/test/template/translation_helper_test.rb +++ b/actionpack/test/template/translation_helper_test.rb @@ -39,4 +39,10 @@ class TranslationHelperTest < Test::Unit::TestCase # error_messages_for(:object => @object, :locale => 'en-US') # end end + + def test_delegates_localize_to_i18n + @time = Time.utc(2008, 7, 8, 12, 18, 38) + assert_equal "Tue, 08 Jul 2008 12:18:38 +0100", localize(@time) + assert_equal "08 Jul 12:18", localize(@time, :format => :short) + end end \ No newline at end of file -- cgit v1.2.3 From dc77359c16abc0f693e3847e677c0cad62d0df50 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Tue, 8 Jul 2008 17:41:18 +0200 Subject: Removed unnecessary or condition in #error_messages_for --- actionpack/lib/action_view/helpers/active_record_helper.rb | 2 +- actionpack/test/template/active_record_helper_i18n_test.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index a2fee53fb6..aa978a33bd 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -176,7 +176,7 @@ module ActionView options[:header_message] else object_name = options[:object_name].to_s.gsub('_', ' ') - object_name = I18n.t(object_name, :default => object_name) || '' + object_name = I18n.t(object_name, :default => object_name) locale.t :header_message, :count => count, :object_name => object_name end message = options.include?(:message) ? options[:message] : locale.t(:message) diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb index d35e79b94a..feec64aa30 100644 --- a/actionpack/test/template/active_record_helper_i18n_test.rb +++ b/actionpack/test/template/active_record_helper_i18n_test.rb @@ -21,19 +21,19 @@ class ActiveRecordHelperI18nTest < Test::Unit::TestCase def test_error_messages_for_given_no_header_message_option_it_translates_header_message I18n.expects(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns 'header message' - I18n.expects(:t).with('', :default => '').once + I18n.expects(:t).with('', :default => '').once.returns '' error_messages_for(:object => @object, :locale => 'en-US') end def test_error_messages_for_given_a_message_option_it_does_not_translate_message I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).never - I18n.expects(:t).with('', :default => '').once + I18n.expects(:t).with('', :default => '').once.returns '' error_messages_for(:object => @object, :message => 'message', :locale => 'en-US') end def test_error_messages_for_given_no_message_option_it_translates_message I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:' - I18n.expects(:t).with('', :default => '').once + I18n.expects(:t).with('', :default => '').once.returns '' error_messages_for(:object => @object, :locale => 'en-US') end -- cgit v1.2.3 From bb33432b0f5bf644713e696e4dafc7e7d3cc5808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=BE=E7=94=B0=20=E6=98=8E?= Date: Tue, 15 Jul 2008 21:30:20 +0900 Subject: Ruby 1.9 compat: call Proc#binding explicitly. [#623 state:resolved] --- actionpack/lib/action_view/helpers/tag_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index aeafd3906d..e9b6dd6e43 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -115,7 +115,7 @@ module ActionView # can't take an <% end %> later on, so we have to use <% ... %> # and implicitly concat. def block_called_from_erb?(block) - block && eval(BLOCK_CALLED_FROM_ERB, block) + block && eval(BLOCK_CALLED_FROM_ERB, block.binding) end def content_tag_string(name, content, options, escape = true) -- cgit v1.2.3 From 4d76bad387036d96a955eae7c260a9633e915d11 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Tue, 15 Jul 2008 10:40:33 -0700 Subject: Ruby 1.9 compat: account for different String#hash --- actionpack/test/template/asset_tag_helper_test.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 020e112fd0..3cfc8fa4ed 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -335,8 +335,9 @@ class AssetTagHelperTest < ActionView::TestCase ActionController::Base.asset_host = 'http://a%d.example.com' ActionController::Base.perform_caching = true + hash = '/javascripts/cache/money.js'.hash % 4 assert_dom_equal( - %(), + %(), javascript_include_tag(:all, :cache => "cache/money") ) -- cgit v1.2.3 From 3c282f3a0a7c1d5ab91241674251794ead5fa41d Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Tue, 15 Jul 2008 10:42:50 -0700 Subject: Ruby 1.9 compat: only eval with block.binding in 1.9, uses more memory than eval with block --- actionpack/lib/action_view/helpers/tag_helper.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index e9b6dd6e43..5a296da247 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -110,12 +110,18 @@ module ActionView private BLOCK_CALLED_FROM_ERB = 'defined? __in_erb_template' - # Check whether we're called from an erb template. - # We'd return a string in any other case, but erb <%= ... %> - # can't take an <% end %> later on, so we have to use <% ... %> - # and implicitly concat. - def block_called_from_erb?(block) - block && eval(BLOCK_CALLED_FROM_ERB, block.binding) + if RUBY_VERSION < '1.9.0' + # Check whether we're called from an erb template. + # We'd return a string in any other case, but erb <%= ... %> + # can't take an <% end %> later on, so we have to use <% ... %> + # and implicitly concat. + def block_called_from_erb?(block) + block && eval(BLOCK_CALLED_FROM_ERB, block) + end + else + def block_called_from_erb?(block) + block && eval(BLOCK_CALLED_FROM_ERB, block.binding) + end end def content_tag_string(name, content, options, escape = true) -- cgit v1.2.3 From 24a8ae4e08fcd15a8c3792990d1d0981d004d339 Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Tue, 15 Jul 2008 20:39:36 +0200 Subject: Try to get more useful errors out of the test_line_offset failures --- actionpack/test/controller/render_test.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index a857810b78..9a94db4b00 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -101,12 +101,7 @@ class TestController < ActionController::Base end def render_line_offset - begin - render :inline => '<% raise %>', :locals => {:foo => 'bar'} - rescue RuntimeError => exc - end - line = exc.backtrace.first - render :text => line + render :inline => '<% raise %>', :locals => {:foo => 'bar'} end def heading @@ -238,10 +233,15 @@ class RenderTest < Test::Unit::TestCase end def test_line_offset - get :render_line_offset - line = @response.body - assert(line =~ %r{:(\d+):}) - assert_equal "1", $1 + begin + get :render_line_offset + flunk "the action should have raised an exception" + rescue RuntimeError => exc + line = exc.backtrace.first + assert(line =~ %r{:(\d+):}) + assert_equal "1", $1, + "The line offset is wrong, perhaps the wrong exception has been raised, exception was: #{exc.inspect}" + end end def test_render_with_forward_slash -- cgit v1.2.3 From 4f72feb84c25b54f66c7192c788b7fd965f2d493 Mon Sep 17 00:00:00 2001 From: Jonathan Viney Date: Wed, 2 Jul 2008 16:01:26 +1200 Subject: Move the transaction counter to the connection object rather than maintaining it on the current Thread. Signed-off-by: Michael Koziarski [#533 state:resolved] --- .../connection_adapters/abstract_adapter.rb | 13 +++++++++++ activerecord/lib/active_record/fixtures.rb | 8 +++---- activerecord/lib/active_record/transactions.rb | 17 +++------------ activerecord/test/cases/fixtures_test.rb | 6 +++--- activerecord/test/cases/multiple_db_test.rb | 25 ++++++++++++++++++++++ 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index f48b107a2a..47dbf5a5f3 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -118,6 +118,19 @@ module ActiveRecord @connection end + def open_transactions + @open_transactions ||= 0 + end + + def increment_open_transactions + @open_transactions ||= 0 + @open_transactions += 1 + end + + def decrement_open_transactions + @open_transactions -= 1 + end + def log_info(sql, name, runtime) if @logger && @logger.debug? name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})" diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 17fb9355c4..622cfc3c3f 100755 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) all_loaded_fixtures.update(fixtures_map) - connection.transaction(Thread.current['open_transactions'].to_i == 0) do + connection.transaction(connection.open_transactions.zero?) do fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } fixtures.each { |fixture| fixture.insert_fixtures } @@ -930,7 +930,7 @@ module Test #:nodoc: load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end - ActiveRecord::Base.send :increment_open_transactions + ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.begin_db_transaction # Load fixtures for every test. else @@ -951,9 +951,9 @@ module Test #:nodoc: end # Rollback changes if a transaction is active. - if use_transactional_fixtures? && Thread.current['open_transactions'] != 0 + if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0 ActiveRecord::Base.connection.rollback_db_transaction - Thread.current['open_transactions'] = 0 + ActiveRecord::Base.connection.decrement_open_transactions end ActiveRecord::Base.verify_active_connections! end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 3b6835762c..354a6c83a2 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -73,25 +73,14 @@ module ActiveRecord # trigger a ROLLBACK when raised, but not be re-raised by the transaction block. module ClassMethods def transaction(&block) - increment_open_transactions + connection.increment_open_transactions begin - connection.transaction(Thread.current['start_db_transaction'], &block) + connection.transaction(connection.open_transactions == 1, &block) ensure - decrement_open_transactions + connection.decrement_open_transactions end end - - private - def increment_open_transactions #:nodoc: - open = Thread.current['open_transactions'] ||= 0 - Thread.current['start_db_transaction'] = open.zero? - Thread.current['open_transactions'] = open + 1 - end - - def decrement_open_transactions #:nodoc: - Thread.current['open_transactions'] -= 1 - end end def transaction(&block) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 0ea24868f1..6ba7597f56 100755 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -461,11 +461,11 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase alias_method :teardown, :blank_teardown def test_no_rollback_in_teardown_unless_transaction_active - assert_equal 0, Thread.current['open_transactions'] + assert_equal 0, ActiveRecord::Base.connection.open_transactions assert_raise(RuntimeError) { ar_setup_fixtures } - assert_equal 0, Thread.current['open_transactions'] + assert_equal 0, ActiveRecord::Base.connection.open_transactions assert_nothing_raised { ar_teardown_fixtures } - assert_equal 0, Thread.current['open_transactions'] + assert_equal 0, ActiveRecord::Base.connection.open_transactions end private diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index eb3e43c8ac..7c3e0f2ca6 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -57,4 +57,29 @@ class MultipleDbTest < ActiveRecord::TestCase assert Course.connection end + + def test_transactions_across_databases + c1 = Course.find(1) + e1 = Entrant.find(1) + + begin + Course.transaction do + Entrant.transaction do + c1.name = "Typo" + e1.name = "Typo" + c1.save + e1.save + raise "No I messed up." + end + end + rescue + # Yup caught it + end + + assert_equal "Typo", c1.name + assert_equal "Typo", e1.name + + assert_equal "Ruby Development", Course.find(1).name + assert_equal "Ruby Developer", Entrant.find(1).name + end end -- cgit v1.2.3 From aca246ab25497bb6787d2e18680e9f73ad13d223 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 15 Jul 2008 14:41:38 -0500 Subject: Get buffer for fragment cache from template's @output_buffer --- actionpack/CHANGELOG | 2 + .../lib/action_controller/caching/fragments.rb | 4 +- actionpack/lib/action_view/helpers/cache_helper.rb | 2 +- .../lib/action_view/helpers/prototype_helper.rb | 375 +++++++++++---------- actionpack/lib/action_view/template_handler.rb | 4 - .../lib/action_view/template_handlers/builder.rb | 7 +- .../lib/action_view/template_handlers/erb.rb | 6 - .../lib/action_view/template_handlers/rjs.rb | 11 - actionpack/test/controller/caching_test.rb | 43 +-- actionpack/test/template/javascript_helper_test.rb | 2 + actionpack/test/template/prototype_helper_test.rb | 2 +- 11 files changed, 198 insertions(+), 260 deletions(-) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 5b7bfe9c30..52d00a417c 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Get buffer for fragment cache from template's @output_buffer [Josh Peek] + * Set config.action_view.warn_cache_misses = true to receive a warning if you perform an action that results in an expensive disk operation that could be cached [Josh Peek] * Refactor template preloading. New abstractions include Renderable mixins and a refactored Template class [Josh Peek] diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 57b31ec9d1..b1f25fdf5c 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -60,10 +60,8 @@ module ActionController #:nodoc: ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) end - def fragment_for(block, name = {}, options = nil) #:nodoc: + def fragment_for(buffer, name = {}, options = nil, &block) #:nodoc: if perform_caching - buffer = yield - if cache = read_fragment(name, options) buffer.concat(cache) else diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index 2cdbae6e40..64d1ad2715 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -32,7 +32,7 @@ module ActionView # Topics listed alphabetically # <% end %> def cache(name = {}, options = nil, &block) - _last_render.handler.new(@controller).cache_fragment(block, name, options) + @controller.fragment_for(output_buffer, name, options, &block) end end end diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index d0c281c803..edb43844a4 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -3,25 +3,25 @@ require 'set' module ActionView module Helpers # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides - # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation, + # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation, # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php] - # functionality, and more traditional object-oriented facilities for JavaScript. + # functionality, and more traditional object-oriented facilities for JavaScript. # This module provides a set of helpers to make it more convenient to call - # functions from Prototype using Rails, including functionality to call remote - # Rails methods (that is, making a background request to a Rails action) using Ajax. - # This means that you can call actions in your controllers without - # reloading the page, but still update certain parts of it using + # functions from Prototype using Rails, including functionality to call remote + # Rails methods (that is, making a background request to a Rails action) using Ajax. + # This means that you can call actions in your controllers without + # reloading the page, but still update certain parts of it using # injections into the DOM. A common use case is having a form that adds # a new element to a list without reloading the page or updating a shopping # cart total when a new item is added. # # == Usage - # To be able to use these helpers, you must first include the Prototype - # JavaScript framework in your pages. + # To be able to use these helpers, you must first include the Prototype + # JavaScript framework in your pages. # # javascript_include_tag 'prototype' # - # (See the documentation for + # (See the documentation for # ActionView::Helpers::JavaScriptHelper for more information on including # this and other JavaScript files in your Rails templates.) # @@ -29,7 +29,7 @@ module ActionView # # link_to_remote "Add to cart", # :url => { :action => "add", :id => product.id }, - # :update => { :success => "cart", :failure => "error" } + # :update => { :success => "cart", :failure => "error" } # # ...through a form... # @@ -50,8 +50,8 @@ module ActionView # :update => :hits, # :with => 'query' # %> - # - # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than + # + # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than # are listed here); check out the documentation for each method to find out more about its usage and options. # # === Common Options @@ -63,7 +63,7 @@ module ActionView # When building your action handlers (that is, the Rails actions that receive your background requests), it's # important to remember a few things. First, whatever your action would normall return to the browser, it will # return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause - # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up. + # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up. # You can turn the layout off on particular actions by doing the following: # # class SiteController < ActionController::Base @@ -74,8 +74,8 @@ module ActionView # # render :layout => false # - # You can tell the type of request from within your action using the request.xhr? (XmlHttpRequest, the - # method that Ajax uses to make background requests) method. + # You can tell the type of request from within your action using the request.xhr? (XmlHttpRequest, the + # method that Ajax uses to make background requests) method. # def name # # Is this an XmlHttpRequest request? # if (request.xhr?) @@ -93,7 +93,7 @@ module ActionView # # Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request. # - # If you are just returning a little data or don't want to build a template for your output, you may opt to simply + # If you are just returning a little data or don't want to build a template for your output, you may opt to simply # render text output, like this: # # render :text => 'Return this from my method!' @@ -103,7 +103,7 @@ module ActionView # # == Updating multiple elements # See JavaScriptGenerator for information on updating multiple elements - # on the page in an Ajax response. + # on the page in an Ajax response. module PrototypeHelper unless const_defined? :CALLBACKS CALLBACKS = Set.new([ :uninitialized, :loading, :loaded, @@ -114,64 +114,64 @@ module ActionView :form, :with, :update, :script ]).merge(CALLBACKS) end - # Returns a link to a remote action defined by options[:url] - # (using the url_for format) that's called in the background using + # Returns a link to a remote action defined by options[:url] + # (using the url_for format) that's called in the background using # XMLHttpRequest. The result of that request can then be inserted into a - # DOM object whose id can be specified with options[:update]. + # DOM object whose id can be specified with options[:update]. # Usually, the result would be a partial prepared by the controller with - # render :partial. + # render :partial. # # Examples: - # # Generates: Delete this post - # link_to_remote "Delete this post", :update => "posts", + # link_to_remote "Delete this post", :update => "posts", # :url => { :action => "destroy", :id => post.id } # - # # Generates: Refresh - # link_to_remote(image_tag("refresh"), :update => "emails", + # link_to_remote(image_tag("refresh"), :update => "emails", # :url => { :action => "list_emails" }) - # + # # You can override the generated HTML options by specifying a hash in # options[:html]. - # + # # link_to_remote "Delete this post", :update => "posts", - # :url => post_url(@post), :method => :delete, - # :html => { :class => "destructive" } + # :url => post_url(@post), :method => :delete, + # :html => { :class => "destructive" } # # You can also specify a hash for options[:update] to allow for - # easy redirection of output to an other DOM element if a server-side + # easy redirection of output to an other DOM element if a server-side # error occurs: # # Example: - # # Generates: Delete this post # link_to_remote "Delete this post", # :url => { :action => "destroy", :id => post.id }, # :update => { :success => "posts", :failure => "error" } # - # Optionally, you can use the options[:position] parameter to - # influence how the target DOM element is updated. It must be one of + # Optionally, you can use the options[:position] parameter to + # influence how the target DOM element is updated. It must be one of # :before, :top, :bottom, or :after. # # The method used is by default POST. You can also specify GET or you # can simulate PUT or DELETE over POST. All specified with options[:method] # # Example: - # # Generates: Destroy # link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete # - # By default, these remote requests are processed asynchronous during - # which various JavaScript callbacks can be triggered (for progress - # indicators and the likes). All callbacks get access to the - # request object, which holds the underlying XMLHttpRequest. + # By default, these remote requests are processed asynchronous during + # which various JavaScript callbacks can be triggered (for progress + # indicators and the likes). All callbacks get access to the + # request object, which holds the underlying XMLHttpRequest. # # To access the server response, use request.responseText, to # find out the HTTP status, use request.status. # # Example: - # # Generates: hello # word = 'hello' # link_to_remote word, @@ -180,43 +180,43 @@ module ActionView # # The callbacks that may be specified are (in order): # - # :loading:: Called when the remote document is being + # :loading:: Called when the remote document is being # loaded with data by the browser. # :loaded:: Called when the browser has finished loading # the remote document. - # :interactive:: Called when the user can interact with the - # remote document, even though it has not + # :interactive:: Called when the user can interact with the + # remote document, even though it has not # finished loading. # :success:: Called when the XMLHttpRequest is completed, # and the HTTP status code is in the 2XX range. # :failure:: Called when the XMLHttpRequest is completed, # and the HTTP status code is not in the 2XX # range. - # :complete:: Called when the XMLHttpRequest is complete - # (fires after success/failure if they are + # :complete:: Called when the XMLHttpRequest is complete + # (fires after success/failure if they are # present). - # - # You can further refine :success and :failure by + # + # You can further refine :success and :failure by # adding additional callbacks for specific status codes. # # Example: - # # Generates: hello # link_to_remote word, # :url => { :action => "action" }, # 404 => "alert('Not found...? Wrong URL...?')", # :failure => "alert('HTTP Error ' + request.status + '!')" # - # A status code callback overrides the success/failure handlers if + # A status code callback overrides the success/failure handlers if # present. # # If you for some reason or another need synchronous processing (that'll - # block the browser while the request is happening), you can specify + # block the browser while the request is happening), you can specify # options[:type] = :synchronous. # # You can customize further browser side call logic by passing in - # JavaScript code snippets via some optional parameters. In their order + # JavaScript code snippets via some optional parameters. In their order # of use these are: # # :confirm:: Adds confirmation dialog. @@ -228,7 +228,7 @@ module ActionView # :after:: Called immediately after request was # initiated and before :loading. # :submit:: Specifies the DOM element ID that's used - # as the parent of the form elements. By + # as the parent of the form elements. By # default this is the current form, but # it could just as well be the ID of a # table row or any other DOM element. @@ -238,10 +238,10 @@ module ActionView # URL query string. # # Example: - # + # # :with => "'name=' + $('name').value" # - # You can generate a link that uses AJAX in the general case, while + # You can generate a link that uses AJAX in the general case, while # degrading gracefully to plain link behavior in the absence of # JavaScript by setting html_options[:href] to an alternate URL. # Note the extra curly braces around the options hash separate @@ -251,7 +251,7 @@ module ActionView # link_to_remote "Delete this post", # { :update => "posts", :url => { :action => "destroy", :id => post.id } }, # :href => url_for(:action => "destroy", :id => post.id) - def link_to_remote(name, options = {}, html_options = nil) + def link_to_remote(name, options = {}, html_options = nil) link_to_function(name, remote_function(options), html_options || options.delete(:html)) end @@ -262,15 +262,15 @@ module ActionView # and defining callbacks is the same as link_to_remote. # Examples: # # Call get_averages and put its results in 'avg' every 10 seconds - # # Generates: - # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages', + # # Generates: + # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages', # # {asynchronous:true, evalScripts:true})}, 10) # periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg') # # # Call invoice every 10 seconds with the id of the customer # # If it succeeds, update the invoice DIV; if it fails, update the error DIV # # Generates: - # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'}, + # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'}, # # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10) # periodically_call_remote(:url => { :action => 'invoice', :id => customer.id }, # :update => { :success => "invoice", :failure => "error" } @@ -286,11 +286,11 @@ module ActionView javascript_tag(code) end - # Returns a form tag that will submit using XMLHttpRequest in the - # background instead of the regular reloading POST arrangement. Even + # Returns a form tag that will submit using XMLHttpRequest in the + # background instead of the regular reloading POST arrangement. Even # though it's using JavaScript to serialize the form elements, the form # submission will work just like a regular submission as viewed by the - # receiving side (all elements available in params). The options for + # receiving side (all elements available in params). The options for # specifying the target with :url and defining callbacks is the same as # +link_to_remote+. # @@ -299,21 +299,21 @@ module ActionView # # Example: # # Generates: - # #
- # form_remote_tag :html => { :action => + # form_remote_tag :html => { :action => # url_for(:controller => "some", :action => "place") } # # The Hash passed to the :html key is equivalent to the options (2nd) # argument in the FormTagHelper.form_tag method. # - # By default the fall-through action is the same as the one specified in + # By default the fall-through action is the same as the one specified in # the :url (and the default method is :post). # # form_remote_tag also takes a block, like form_tag: # # Generates: - # #
# #
# <% form_remote_tag :url => '/posts' do -%> @@ -323,19 +323,19 @@ module ActionView options[:form] = true options[:html] ||= {} - options[:html][:onsubmit] = - (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") + + options[:html][:onsubmit] = + (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") + "#{remote_function(options)}; return false;" form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block) end - # Creates a form that will submit using XMLHttpRequest in the background - # instead of the regular reloading POST arrangement and a scope around a + # Creates a form that will submit using XMLHttpRequest in the background + # instead of the regular reloading POST arrangement and a scope around a # specific resource that is used as a base for questioning about - # values for the fields. + # values for the fields. # - # === Resource + # === Resource # # Example: # <% remote_form_for(@post) do |f| %> @@ -348,7 +348,7 @@ module ActionView # ... # <% end %> # - # === Nested Resource + # === Nested Resource # # Example: # <% remote_form_for([@post, @comment]) do |f| %> @@ -387,23 +387,23 @@ module ActionView concat('') end alias_method :form_remote_for, :remote_form_for - + # Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+ # that will submit form using XMLHttpRequest in the background instead of a regular POST request that - # reloads the page. + # reloads the page. # # # Create a button that submits to the create action - # # - # # Generates: # <%= button_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %> # # # Submit to the remote action update and update the DIV succeed or fail based # # on the success or failure of the request # # - # # Generates: # <%= button_to_remote 'update_btn', 'Update', :url => { :action => 'update' }, # :update => { :success => "succeed", :failure => "fail" } @@ -423,7 +423,7 @@ module ActionView tag("input", options[:html], false) end alias_method :submit_to_remote, :button_to_remote - + # Returns 'eval(request.responseText)' which is the JavaScript function # that +form_remote_tag+ can call in :complete to evaluate a multiple # update return document using +update_element_function+ calls. @@ -433,11 +433,11 @@ module ActionView # Returns the JavaScript needed for a remote function. # Takes the same arguments as link_to_remote. - # + # # Example: - # # Generates: { :action => :update_options }) %>"> # # @@ -455,7 +455,7 @@ module ActionView update << "'#{options[:update]}'" end - function = update.empty? ? + function = update.empty? ? "new Ajax.Request(" : "new Ajax.Updater(#{update}, " @@ -476,9 +476,9 @@ module ActionView # callback when its contents have changed. The default callback is an # Ajax call. By default the value of the observed field is sent as a # parameter with the Ajax call. - # + # # Example: - # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest', + # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest', # # '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})}) # <%= observe_field :suggest, :url => { :action => :find_suggestion }, # :frequency => 0.25, @@ -500,14 +500,14 @@ module ActionView # new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')}) # The element parameter is the DOM element being observed, and the value is its value at the # time the observer is triggered. - # + # # Additional options are: # :frequency:: The frequency (in seconds) at which changes to # this field will be detected. Not setting this # option at all or to a value equal to or less than # zero will use event based observation instead of # time based observation. - # :update:: Specifies the DOM ID of the element whose + # :update:: Specifies the DOM ID of the element whose # innerHTML should be updated with the # XMLHttpRequest response text. # :with:: A JavaScript expression specifying the parameters @@ -518,7 +518,7 @@ module ActionView # variable +value+. # # Examples - # + # # :with => "'my_custom_key=' + value" # :with => "'person[name]=' + prompt('New name')" # :with => "Form.Element.serialize('other-field')" @@ -544,7 +544,7 @@ module ActionView # observe_field 'book_title', # :url => 'http://example.com/books/edit/1', # :with => 'title' - # + # # # Sends params: {:book_title => 'Title of the book'} when the focus leaves # # the input field. # observe_field 'book_title', @@ -558,7 +558,7 @@ module ActionView build_observer('Form.Element.EventObserver', field_id, options) end end - + # Observes the form with the DOM ID specified by +form_id+ and calls a # callback when its contents have changed. The default callback is an # Ajax call. By default all fields of the observed field are sent as @@ -574,16 +574,17 @@ module ActionView build_observer('Form.EventObserver', form_id, options) end end - - # All the methods were moved to GeneratorMethods so that + + # All the methods were moved to GeneratorMethods so that # #include_helpers_from_context has nothing to overwrite. class JavaScriptGenerator #:nodoc: def initialize(context, &block) #:nodoc: @context, @lines = context, [] + @context.output_buffer = @lines if @context include_helpers_from_context @context.instance_exec(self, &block) end - + private def include_helpers_from_context @context.extended_by.each do |mod| @@ -591,17 +592,17 @@ module ActionView end extend GeneratorMethods end - - # JavaScriptGenerator generates blocks of JavaScript code that allow you - # to change the content and presentation of multiple DOM elements. Use + + # JavaScriptGenerator generates blocks of JavaScript code that allow you + # to change the content and presentation of multiple DOM elements. Use # this in your Ajax response bodies, either in a \n\n\n\n), javascript_include_tag(:defaults)) -- cgit v1.2.3 From 1e0f94a77c717dd06a86edda97de5a4c4ad919a8 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Thu, 17 Jul 2008 14:45:14 -0700 Subject: Introduce simple internationalization support --- actionpack/test/i18n_coverage | 9 --------- activerecord/test/i18n_coverage | 6 ------ railties/CHANGELOG | 2 ++ 3 files changed, 2 insertions(+), 15 deletions(-) delete mode 100755 actionpack/test/i18n_coverage delete mode 100755 activerecord/test/i18n_coverage diff --git a/actionpack/test/i18n_coverage b/actionpack/test/i18n_coverage deleted file mode 100755 index 57b54e9d47..0000000000 --- a/actionpack/test/i18n_coverage +++ /dev/null @@ -1,9 +0,0 @@ -rcov -x abstract_unit.rb \ --i action_view/helpers/number_helper.rb,action_view/helpers/date_helper.rb,action_view/helpers/active_record_helper.rb \ -template/number_helper_i18n_test.rb \ -template/date_helper_i18n_test.rb \ -template/active_record_helper_i18n_test.rb \ - -# template/number_helper_test.rb \ -# template/date_helper_test.rb \ -# template/active_record_helper_test.rb \ No newline at end of file diff --git a/activerecord/test/i18n_coverage b/activerecord/test/i18n_coverage deleted file mode 100755 index 1589a6c06f..0000000000 --- a/activerecord/test/i18n_coverage +++ /dev/null @@ -1,6 +0,0 @@ -rcov -I connections/native_mysql \ --x cases/helper,config,connection,models \ --i active_record/validations.rb \ -cases/validations_i18n_test.rb \ - -# cases/validations_test.rb \ No newline at end of file diff --git a/railties/CHANGELOG b/railties/CHANGELOG index b5c5aba460..9c5e5b59c6 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Introduce simple internationalization support. [Ruby i18n team] + * Make script/plugin install -r option work with git based plugins. #257. [Tim Pope Jakub Kuźma]. Example: script/plugin install git://github.com/mislav/will_paginate.git -r agnostic # Installs 'agnostic' branch -- cgit v1.2.3 From a1fcbd971d681e44de5ea33e6a8470ff8b8144c0 Mon Sep 17 00:00:00 2001 From: Joachim Garth Date: Fri, 27 Jun 2008 20:03:51 +0200 Subject: Make sure association preloading works with full STI class name [#465 state:Resolved] Signed-off-by: Pratik Naik --- .../lib/active_record/association_preload.rb | 2 +- .../eager_load_includes_full_sti_class_test.rb | 36 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 64888f9110..c7594809b7 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -252,7 +252,7 @@ module ActiveRecord table_name = reflection.klass.quoted_table_name if interface = reflection.options[:as] - conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'" + conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" else foreign_key = reflection.primary_key_name conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)" diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb new file mode 100644 index 0000000000..7c470616a5 --- /dev/null +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' +require 'models/post' +require 'models/tagging' + +module Namespaced + class Post < ActiveRecord::Base + set_table_name 'posts' + has_one :tagging, :as => :taggable, :class_name => 'Tagging' + end +end + +class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase + + def setup + generate_test_objects + end + + def generate_test_objects + post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 ) + tagging = Tagging.create( :taggable => post ) + end + + def test_class_names + old = ActiveRecord::Base.store_full_sti_class + + ActiveRecord::Base.store_full_sti_class = false + post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + assert_nil post.tagging + + ActiveRecord::Base.store_full_sti_class = true + post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + assert_equal 'Tagging', post.tagging.class.name + ensure + ActiveRecord::Base.store_full_sti_class = old + end +end -- cgit v1.2.3 From 57a2780f14447152ece1b1301fc6c25b2ec43da5 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Wed, 16 Jul 2008 04:32:15 -0700 Subject: etag! and last_modified! conditional GET helpers --- actionpack/CHANGELOG | 4 + actionpack/lib/action_controller/base.rb | 15 ++- actionpack/lib/action_controller/response.rb | 40 +++++-- actionpack/test/controller/render_test.rb | 152 +++++++++++++++++---------- 4 files changed, 150 insertions(+), 61 deletions(-) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index da9fdbfd9d..f6432fe0be 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,9 @@ *Edge* +* Conditional GET utility methods. [Jeremy Kemper] + * etag!([:admin, post, current_user]) sets the ETag response header and returns head(:not_modified) if it matches the If-None-Match request header. + * last_modified!(post.updated_at) sets Last-Modified and returns head(:not_modified) if it's no later than If-Modified-Since. + * All 2xx requests are considered successful [Josh Peek] * Fixed that AssetTagHelper#compute_public_path shouldn't cache the asset_host along with the source or per-request proc's won't run [DHH] diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index c56812c2d9..50727c67c4 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -519,6 +519,8 @@ module ActionController #:nodoc: public # Extracts the action_name from the request parameters and performs that action. def process(request, response, method = :perform_action, *arguments) #:nodoc: + response.request = request + initialize_template_class(response) assign_shortcuts(request, response) initialize_current_url @@ -529,8 +531,6 @@ module ActionController #:nodoc: send(method, *arguments) assign_default_content_type_and_charset - - response.request = request response.prepare! unless component_request? response ensure @@ -968,6 +968,17 @@ module ActionController #:nodoc: render :nothing => true, :status => status end + # Sets the Last-Modified response header. Returns 304 Not Modified if the + # If-Modified-Since request header is <= last modified. + def last_modified!(utc_time) + head(:not_modified) if response.last_modified!(utc_time) + end + + # Sets the ETag response header. Returns 304 Not Modified if the + # If-None-Match request header matches. + def etag!(etag) + head(:not_modified) if response.etag!(etag) + end # Clears the rendered results, allowing for another render to be performed. def erase_render_results #:nodoc: diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb index 1d9f6676ba..9955532844 100755 --- a/actionpack/lib/action_controller/response.rb +++ b/actionpack/lib/action_controller/response.rb @@ -41,20 +41,48 @@ module ActionController set_content_length! end + # Sets the Last-Modified response header. Returns whether it's older than + # the If-Modified-Since request header. + def last_modified!(utc_time) + headers['Last-Modified'] ||= utc_time.httpdate + if request && since = request.headers['HTTP_IF_MODIFIED_SINCE'] + utc_time <= Time.rfc2822(since) + end + end + + # Sets the ETag response header. Returns whether it matches the + # If-None-Match request header. + def etag!(tag) + headers['ETag'] ||= %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(tag))}") + if request && request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag'] + true + end + end private def handle_conditional_get! - if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty? - self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}") - self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control'] + if nonempty_ok_response? + set_conditional_cache_control! - if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag'] - self.headers['Status'] = '304 Not Modified' + if etag!(body) + headers['Status'] = '304 Not Modified' self.body = '' end end end + def nonempty_ok_response? + status = headers['Status'] + ok = !status || status[0..2] == '200' + ok && body.is_a?(String) && !body.empty? + end + + def set_conditional_cache_control! + if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control'] + headers['Cache-Control'] = 'private, max-age=0, must-revalidate' + end + end + def convert_content_type! if content_type = headers.delete("Content-Type") self.headers["type"] = content_type @@ -73,4 +101,4 @@ module ActionController self.headers["Content-Length"] = body.size unless body.respond_to?(:call) end end -end \ No newline at end of file +end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 9a94db4b00..041c54c7fd 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -8,14 +8,18 @@ module Fun end end - -# FIXME: crashes Ruby 1.9 class TestController < ActionController::Base layout :determine_layout def hello_world end + def conditional_hello + etag! [:foo, 123] + last_modified! Time.now.utc.beginning_of_day + render :action => 'hello_world' unless performed? + end + def render_hello_world render :template => "test/hello_world" end @@ -408,6 +412,72 @@ class RenderTest < Test::Unit::TestCase assert_equal "Goodbye, Local David", @response.body end + def test_should_render_formatted_template + get :formatted_html_erb + assert_equal 'formatted html erb', @response.body + end + + def test_should_render_formatted_xml_erb_template + get :formatted_xml_erb, :format => :xml + assert_equal 'passed formatted xml erb', @response.body + end + + def test_should_render_formatted_html_erb_template + get :formatted_xml_erb + assert_equal 'passed formatted html erb', @response.body + end + + def test_should_render_formatted_html_erb_template_with_faulty_accepts_header + @request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*" + get :formatted_xml_erb + assert_equal 'passed formatted html erb', @response.body + end + + def test_should_render_html_formatted_partial + get :partial + assert_equal 'partial html', @response.body + end + + def test_should_render_html_partial_with_dot + get :partial_dot_html + assert_equal 'partial html', @response.body + end + + def test_should_render_html_formatted_partial_with_rjs + xhr :get, :partial_as_rjs + assert_equal %(Element.replace("foo", "partial html");), @response.body + end + + def test_should_render_html_formatted_partial_with_rjs_and_js_format + xhr :get, :respond_to_partial_as_rjs + assert_equal %(Element.replace("foo", "partial html");), @response.body + end + + def test_should_render_js_partial + xhr :get, :partial, :format => 'js' + assert_equal 'partial js', @response.body + end + + def test_should_render_with_alternate_default_render + xhr :get, :render_alternate_default + assert_equal %(Element.replace("foo", "partial html");), @response.body + end + + def test_should_render_xml_but_keep_custom_content_type + get :render_xml_with_custom_content_type + assert_equal "application/atomsvc+xml", @response.content_type + end +end + +class EtagRenderTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = TestController.new + + @request.host = "www.nextangle.com" + end + def test_render_200_should_set_etag get :render_hello_world_from_variable assert_equal etag_for("hello david"), @response.headers['ETag'] @@ -460,64 +530,40 @@ class RenderTest < Test::Unit::TestCase assert_equal etag_for("\n\n

Hello

\n

This is grand!

\n\n
\n"), @response.headers['ETag'] end - def test_should_render_formatted_template - get :formatted_html_erb - assert_equal 'formatted html erb', @response.body - end - - def test_should_render_formatted_xml_erb_template - get :formatted_xml_erb, :format => :xml - assert_equal 'passed formatted xml erb', @response.body - end - - def test_should_render_formatted_html_erb_template - get :formatted_xml_erb - assert_equal 'passed formatted html erb', @response.body - end - - def test_should_render_formatted_html_erb_template_with_faulty_accepts_header - @request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*" - get :formatted_xml_erb - assert_equal 'passed formatted html erb', @response.body - end - - def test_should_render_html_formatted_partial - get :partial - assert_equal 'partial html', @response.body - end - - def test_should_render_html_partial_with_dot - get :partial_dot_html - assert_equal 'partial html', @response.body - end + protected + def etag_for(text) + %("#{Digest::MD5.hexdigest(text)}") + end +end - def test_should_render_html_formatted_partial_with_rjs - xhr :get, :partial_as_rjs - assert_equal %(Element.replace("foo", "partial html");), @response.body - end +class LastModifiedRenderTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = TestController.new - def test_should_render_html_formatted_partial_with_rjs_and_js_format - xhr :get, :respond_to_partial_as_rjs - assert_equal %(Element.replace("foo", "partial html");), @response.body + @request.host = "www.nextangle.com" + @last_modified = Time.now.utc.beginning_of_day.httpdate end - def test_should_render_js_partial - xhr :get, :partial, :format => 'js' - assert_equal 'partial js', @response.body + def test_responds_with_last_modified + get :conditional_hello + assert_equal @last_modified, @response.headers['Last-Modified'] end - def test_should_render_with_alternate_default_render - xhr :get, :render_alternate_default - assert_equal %(Element.replace("foo", "partial html");), @response.body + def test_request_not_modified + @request.headers["HTTP_IF_MODIFIED_SINCE"] = @last_modified + get :conditional_hello + assert_equal "304 Not Modified", @response.headers['Status'] + assert @response.body.blank?, @response.body + assert_equal @last_modified, @response.headers['Last-Modified'] end - def test_should_render_xml_but_keep_custom_content_type - get :render_xml_with_custom_content_type - assert_equal "application/atomsvc+xml", @response.content_type + def test_request_modified + @request.headers["HTTP_IF_MODIFIED_SINCE"] = 'Thu, 16 Jul 2008 00:00:00 GMT' + get :conditional_hello + assert_equal "200 OK", @response.headers['Status'] + assert !@response.body.blank? + assert_equal @last_modified, @response.headers['Last-Modified'] end - - protected - def etag_for(text) - %("#{Digest::MD5.hexdigest(text)}") - end end -- cgit v1.2.3 From 7430c4168fad07b480dbf80c8ac75ba7db8c634f Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Wed, 16 Jul 2008 22:27:04 -0700 Subject: Decrease default benchmark runs from 10 to 4 --- activesupport/lib/active_support/testing/performance.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb index 5f2027eb3b..71d6f4d9c6 100644 --- a/activesupport/lib/active_support/testing/performance.rb +++ b/activesupport/lib/active_support/testing/performance.rb @@ -11,7 +11,7 @@ module ActiveSupport DEFAULTS = if benchmark = ARGV.include?('--benchmark') # HAX for rake test { :benchmark => true, - :runs => 10, + :runs => 4, :metrics => [:process_time, :memory, :objects, :gc_runs, :gc_time], :output => 'tmp/performance' } else -- cgit v1.2.3 From e1f23da53cef20a60e4bf458d959fe2bfe7d52ea Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Fri, 18 Jul 2008 11:18:16 -0500 Subject: Allow memoized methods to be reloaded and allow multiple symbols --- activesupport/lib/active_support/memoizable.rb | 32 +++++++++++---------- activesupport/test/memoizable_test.rb | 39 +++++++++++++++++--------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index d06250171a..c41feef4c7 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -5,28 +5,30 @@ module ActiveSupport end module ClassMethods - def memoize(symbol) - original_method = "_unmemoized_#{symbol}" - memoized_ivar = "@_memoized_#{symbol}" - raise "Already memoized #{symbol}" if instance_methods.map(&:to_s).include?(original_method) + def memoize(*symbols) + symbols.each do |symbol| + original_method = "unmemoized_#{symbol}" + memoized_ivar = "@#{symbol}" + raise "Already memoized #{symbol}" if instance_methods.map(&:to_s).include?(original_method) - alias_method original_method, symbol - class_eval <<-EOS, __FILE__, __LINE__ - def #{symbol} - if defined? #{memoized_ivar} - #{memoized_ivar} - else - #{memoized_ivar} = #{original_method} + alias_method original_method, symbol + class_eval <<-EOS, __FILE__, __LINE__ + def #{symbol}(reload = false) + if !reload && defined? #{memoized_ivar} + #{memoized_ivar} + else + #{memoized_ivar} = #{original_method}.freeze + end end - end - EOS + EOS + end end end def freeze methods.each do |method| - if m = method.to_s.match(/\A_unmemoized_(.*)/) - send(m[1]).freeze + if m = method.to_s.match(/\Aunmemoized_(.*)/) + send(m[1]) end end super diff --git a/activesupport/test/memoizable_test.rb b/activesupport/test/memoizable_test.rb index fc24a2942d..b649b31455 100644 --- a/activesupport/test/memoizable_test.rb +++ b/activesupport/test/memoizable_test.rb @@ -8,12 +8,16 @@ uses_mocha 'Memoizable' do def name fetch_name_from_floppy end - memoize :name def age nil end - memoize :age + + def random + rand(0) + end + + memoize :name, :age, :random private def fetch_name_from_floppy @@ -21,25 +25,34 @@ uses_mocha 'Memoizable' do end end + def setup + @person = Person.new + end + def test_memoization - person = Person.new - assert_equal "Josh", person.name + assert_equal "Josh", @person.name + + @person.expects(:fetch_name_from_floppy).never + 2.times { assert_equal "Josh", @person.name } + end - person.expects(:fetch_name_from_floppy).never - 2.times { assert_equal "Josh", person.name } + def test_reloadable + random = @person.random + assert_equal random, @person.random + assert_not_equal random, @person.random(:reload) end def test_memoized_methods_are_frozen - person = Person.new - person.freeze - assert_equal "Josh", person.name - assert_equal true, person.name.frozen? + assert_equal true, @person.name.frozen? + + @person.freeze + assert_equal "Josh", @person.name + assert_equal true, @person.name.frozen? end def test_memoization_frozen_with_nil_value - person = Person.new - person.freeze - assert_equal nil, person.age + @person.freeze + assert_equal nil, @person.age end def test_double_memoization -- cgit v1.2.3 From ef6f6625c91ea789a033799f649e4388e4a71045 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Fri, 18 Jul 2008 15:32:28 -0500 Subject: Changed ActiveSupport::Memoizable API to extend since it mainly adds the memoize class method --- actionpack/lib/action_view/renderable.rb | 4 +- actionpack/lib/action_view/renderable_partial.rb | 2 +- actionpack/lib/action_view/template.rb | 2 +- activesupport/lib/active_support/memoizable.rb | 55 ++++++++++++------------ activesupport/test/memoizable_test.rb | 42 +++++++++++++++--- 5 files changed, 67 insertions(+), 38 deletions(-) diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 46193670f3..5090d0160a 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -3,12 +3,12 @@ module ActionView # NOTE: The template that this mixin is beening include into is frozen # So you can not set or modify any instance variables + extend ActiveSupport::Memoizable + def self.included(base) @@mutex = Mutex.new end - include ActiveSupport::Memoizable - def filename 'compiled-template' end diff --git a/actionpack/lib/action_view/renderable_partial.rb b/actionpack/lib/action_view/renderable_partial.rb index fdb1a5e6a7..342850f0f0 100644 --- a/actionpack/lib/action_view/renderable_partial.rb +++ b/actionpack/lib/action_view/renderable_partial.rb @@ -3,7 +3,7 @@ module ActionView # NOTE: The template that this mixin is beening include into is frozen # So you can not set or modify any instance variables - include ActiveSupport::Memoizable + extend ActiveSupport::Memoizable def variable_name name.sub(/\A_/, '').to_sym diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 304aec3a4c..eba42518d7 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -1,7 +1,7 @@ module ActionView #:nodoc: class Template extend TemplateHandlers - include ActiveSupport::Memoizable + extend ActiveSupport::Memoizable include Renderable attr_accessor :filename, :load_path, :base_path, :name, :format, :extension diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index c41feef4c7..59fecbecb1 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -1,37 +1,38 @@ module ActiveSupport - module Memoizable - def self.included(base) #:nodoc: - base.extend(ClassMethods) - end - - module ClassMethods - def memoize(*symbols) - symbols.each do |symbol| - original_method = "unmemoized_#{symbol}" - memoized_ivar = "@#{symbol}" - raise "Already memoized #{symbol}" if instance_methods.map(&:to_s).include?(original_method) - - alias_method original_method, symbol - class_eval <<-EOS, __FILE__, __LINE__ - def #{symbol}(reload = false) - if !reload && defined? #{memoized_ivar} - #{memoized_ivar} - else - #{memoized_ivar} = #{original_method}.freeze - end + module Memoizable #:nodoc: + def self.extended(obj) + klass = obj.respond_to?(:class_eval) ? obj : obj.metaclass + klass.class_eval do + def freeze + methods.each do |method| + if m = method.to_s.match(/^unmemoized_(.*)/) + send(m[1]) end - EOS + end + super end end end - def freeze - methods.each do |method| - if m = method.to_s.match(/\Aunmemoized_(.*)/) - send(m[1]) - end + def memoize(*symbols) + symbols.each do |symbol| + original_method = "unmemoized_#{symbol}" + memoized_ivar = "@#{symbol}" + + klass = respond_to?(:class_eval) ? self : self.metaclass + raise "Already memoized #{symbol}" if klass.instance_methods.map(&:to_s).include?(original_method) + + klass.class_eval <<-EOS, __FILE__, __LINE__ + alias_method :#{original_method}, :#{symbol} + def #{symbol}(reload = false) + if !reload && defined? #{memoized_ivar} + #{memoized_ivar} + else + #{memoized_ivar} = #{original_method}.freeze + end + end + EOS end - super end end end diff --git a/activesupport/test/memoizable_test.rb b/activesupport/test/memoizable_test.rb index b649b31455..79769631ad 100644 --- a/activesupport/test/memoizable_test.rb +++ b/activesupport/test/memoizable_test.rb @@ -3,21 +3,24 @@ require 'abstract_unit' uses_mocha 'Memoizable' do class MemoizableTest < Test::Unit::TestCase class Person - include ActiveSupport::Memoizable + extend ActiveSupport::Memoizable def name fetch_name_from_floppy end + memoize :name + def age nil end - def random - rand(0) + def counter + @counter ||= 0 + @counter += 1 end - memoize :name, :age, :random + memoize :age, :counter private def fetch_name_from_floppy @@ -37,9 +40,9 @@ uses_mocha 'Memoizable' do end def test_reloadable - random = @person.random - assert_equal random, @person.random - assert_not_equal random, @person.random(:reload) + counter = @person.counter + assert_equal 1, @person.counter + assert_equal 2, @person.counter(:reload) end def test_memoized_methods_are_frozen @@ -58,5 +61,30 @@ uses_mocha 'Memoizable' do def test_double_memoization assert_raise(RuntimeError) { Person.memoize :name } end + + class Company + def name + lookup_name + end + + def lookup_name + "37signals" + end + end + + def test_object_memoization + company = Company.new + company.extend ActiveSupport::Memoizable + company.memoize :name + + assert_equal "37signals", company.name + # Mocha doesn't play well with frozen objects + company.metaclass.instance_eval { define_method(:lookup_name) { b00m } } + assert_equal "37signals", company.name + + assert_equal true, company.name.frozen? + company.freeze + assert_equal true, company.name.frozen? + end end end -- cgit v1.2.3 From d2ccb852d4e1f6f1b01e43f32213053ae3bef408 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Fri, 18 Jul 2008 16:00:20 -0500 Subject: Removed lagacy TemplateHandler#render API. Left in a legacy TemplateHandler and Compilable stub so plugins will not have to change anything. --- actionpack/lib/action_view/base.rb | 4 +-- actionpack/lib/action_view/renderable.rb | 6 ++--- actionpack/lib/action_view/template_handler.rb | 23 ++++++----------- actionpack/lib/action_view/template_handlers.rb | 1 - .../action_view/template_handlers/compilable.rb | 20 --------------- actionpack/test/controller/layout_test.rb | 12 ++------- actionpack/test/template/render_test.rb | 30 ++++------------------ 7 files changed, 20 insertions(+), 76 deletions(-) delete mode 100644 actionpack/lib/action_view/template_handlers/compilable.rb diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index a6872b1a47..ae6b284854 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -379,8 +379,8 @@ module ActionView #:nodoc: @assigns.each { |key, value| instance_variable_set("@#{key}", value) } end - def execute(template, local_assigns = {}) - send(template.method(local_assigns), local_assigns) do |*names| + def execute(method, local_assigns = {}) + send(method, local_assigns) do |*names| instance_variable_get "@content_for_#{names.first || 'layout'}" end end diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 5090d0160a..4f865cbced 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -19,7 +19,7 @@ module ActionView memoize :handler def compiled_source - handler.new(nil).compile(self) if handler.compilable? + handler.call(self) end memoize :compiled_source @@ -27,8 +27,8 @@ module ActionView view._first_render ||= self view._last_render = self view.send(:evaluate_assigns) - compile(local_assigns) if handler.compilable? - handler.new(view).render(self, local_assigns) + compile(local_assigns) + view.send(:execute, method(local_assigns), local_assigns) end def method(local_assigns) diff --git a/actionpack/lib/action_view/template_handler.rb b/actionpack/lib/action_view/template_handler.rb index e2dd305f93..d7e7c9b199 100644 --- a/actionpack/lib/action_view/template_handler.rb +++ b/actionpack/lib/action_view/template_handler.rb @@ -1,21 +1,14 @@ -module ActionView - class TemplateHandler - def self.compilable? - false - end - - def initialize(view) - @view = view - end +# Legacy TemplateHandler stub - def render(template, local_assigns = {}) - end - - def compile(template) +module ActionView + module TemplateHandlers + module Compilable end + end - def compilable? - self.class.compilable? + class TemplateHandler + def self.call(template) + new.compile(template) end end end diff --git a/actionpack/lib/action_view/template_handlers.rb b/actionpack/lib/action_view/template_handlers.rb index 1471e99e01..6c8aa4c2a7 100644 --- a/actionpack/lib/action_view/template_handlers.rb +++ b/actionpack/lib/action_view/template_handlers.rb @@ -1,5 +1,4 @@ require 'action_view/template_handler' -require 'action_view/template_handlers/compilable' require 'action_view/template_handlers/builder' require 'action_view/template_handlers/erb' require 'action_view/template_handlers/rjs' diff --git a/actionpack/lib/action_view/template_handlers/compilable.rb b/actionpack/lib/action_view/template_handlers/compilable.rb deleted file mode 100644 index a0ebaefeef..0000000000 --- a/actionpack/lib/action_view/template_handlers/compilable.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActionView - module TemplateHandlers - module Compilable - def self.included(base) - base.extend ClassMethod - end - - module ClassMethod - # If a handler is mixin this module, set compilable to true - def compilable? - true - end - end - - def render(template, local_assigns = {}) - @view.send(:execute, template, local_assigns) - end - end - end -end diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 92b6aa4f2f..72c01a9102 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -31,16 +31,8 @@ end class MultipleExtensions < LayoutTest end -class MabView < ActionView::TemplateHandler - def initialize(view) - end - - def render(template, local_assigns) - template.source - end -end - -ActionView::Template::register_template_handler :mab, MabView +ActionView::Template::register_template_handler :mab, + lambda { |template| template.source.inspect } class LayoutAutoDiscoveryTest < Test::Unit::TestCase def setup diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index cc5b4900dc..b1af043099 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -94,38 +94,18 @@ class ViewRenderTest < Test::Unit::TestCase assert_equal "Hello, World!", @view.render(:inline => "Hello, World!", :type => :foo) end - class CustomHandler < ActionView::TemplateHandler - def render(template, local_assigns) - [template.source, local_assigns].inspect - end - end - - def test_render_inline_with_custom_type - ActionView::Template.register_template_handler :foo, CustomHandler - assert_equal '["Hello, World!", {}]', @view.render(:inline => "Hello, World!", :type => :foo) - end - - def test_render_inline_with_locals_and_custom_type - ActionView::Template.register_template_handler :foo, CustomHandler - assert_equal '["Hello, <%= name %>!", {:name=>"Josh"}]', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo) - end - - class CompilableCustomHandler < ActionView::TemplateHandler - include ActionView::TemplateHandlers::Compilable - - def compile(template) - "@output_buffer = ''\n" + - "@output_buffer << 'source: #{template.source.inspect}'\n" - end + CustomHandler = lambda do |template| + "@output_buffer = ''\n" + + "@output_buffer << 'source: #{template.source.inspect}'\n" end def test_render_inline_with_compilable_custom_type - ActionView::Template.register_template_handler :foo, CompilableCustomHandler + ActionView::Template.register_template_handler :foo, CustomHandler assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo) end def test_render_inline_with_locals_and_compilable_custom_type - ActionView::Template.register_template_handler :foo, CompilableCustomHandler + ActionView::Template.register_template_handler :foo, CustomHandler assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo) end end -- cgit v1.2.3 From c3d1fda555c4bd5f8821d830c685ae5d0e7e52d0 Mon Sep 17 00:00:00 2001 From: Tom Ward Date: Fri, 18 Jul 2008 20:14:12 -0500 Subject: Set the response content type to that of found template if not explicitly set elsewhere [#444 state:resolved] Signed-off-by: Joshua Peek --- actionpack/lib/action_view/base.rb | 6 ++++++ actionpack/lib/action_view/renderable.rb | 5 ++++- actionpack/lib/action_view/template.rb | 5 +++++ actionpack/lib/action_view/template_handlers/builder.rb | 3 +-- actionpack/test/controller/render_test.rb | 15 ++++++++++----- .../test/fixtures/test/implicit_content_type.atom.builder | 2 ++ 6 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 actionpack/test/fixtures/test/implicit_content_type.atom.builder diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index ae6b284854..fe51af62e6 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -379,6 +379,12 @@ module ActionView #:nodoc: @assigns.each { |key, value| instance_variable_set("@#{key}", value) } end + def set_controller_content_type(content_type) + if controller.respond_to?(:response) + controller.response.content_type ||= content_type + end + end + def execute(method, local_assigns = {}) send(method, local_assigns) do |*names| instance_variable_get "@content_for_#{names.first || 'layout'}" diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 4f865cbced..2b825ac4e9 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -24,10 +24,13 @@ module ActionView memoize :compiled_source def render(view, local_assigns = {}) + compile(local_assigns) + view._first_render ||= self view._last_render = self + view.send(:evaluate_assigns) - compile(local_assigns) + view.send(:set_controller_content_type, mime_type) if respond_to?(:mime_type) view.send(:execute, method(local_assigns), local_assigns) end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index eba42518d7..1f528dd900 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -22,6 +22,11 @@ module ActionView #:nodoc: end memoize :format_and_extension + def mime_type + Mime::Type.lookup_by_extension(format) if format + end + memoize :mime_type + def path [base_path, [name, format, extension].compact.join('.')].compact.join('/') end diff --git a/actionpack/lib/action_view/template_handlers/builder.rb b/actionpack/lib/action_view/template_handlers/builder.rb index 335ec1abb4..7d24a5c423 100644 --- a/actionpack/lib/action_view/template_handlers/builder.rb +++ b/actionpack/lib/action_view/template_handlers/builder.rb @@ -6,8 +6,7 @@ module ActionView include Compilable def compile(template) - # ActionMailer does not have a response - "controller.respond_to?(:response) && controller.response.content_type ||= Mime::XML;" + + "set_controller_content_type(Mime::XML);" + "xml = ::Builder::XmlMarkup.new(:indent => 2);" + "self.output_buffer = xml.target!;" + template.source + diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 041c54c7fd..76832f5713 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -197,11 +197,11 @@ class TestController < ActionController::Base def render_alternate_default # For this test, the method "default_render" is overridden: - @alternate_default_render = lambda { - render :update do |page| - page.replace :foo, :partial => 'partial' - end - } + @alternate_default_render = lambda do + render :update do |page| + page.replace :foo, :partial => 'partial' + end + end end def rescue_action(e) raise end @@ -467,6 +467,11 @@ class RenderTest < Test::Unit::TestCase get :render_xml_with_custom_content_type assert_equal "application/atomsvc+xml", @response.content_type end + + def test_should_use_implicit_content_type + get :implicit_content_type, :format => 'atom' + assert_equal Mime::ATOM, @response.content_type + end end class EtagRenderTest < Test::Unit::TestCase diff --git a/actionpack/test/fixtures/test/implicit_content_type.atom.builder b/actionpack/test/fixtures/test/implicit_content_type.atom.builder new file mode 100644 index 0000000000..2fcb32d247 --- /dev/null +++ b/actionpack/test/fixtures/test/implicit_content_type.atom.builder @@ -0,0 +1,2 @@ +xml.atom do +end -- cgit v1.2.3 From d39485078ec56e25a96e97d44b53498d8a1c7426 Mon Sep 17 00:00:00 2001 From: Tom Ward Date: Fri, 18 Jul 2008 20:19:03 -0500 Subject: Raise ArgumentError if an invalid method is specified as part of a route's conditions. Also raise an error if HEAD is specified as the method, as rails routes all HEAD requests through the equivalent GET, though doesn't return the response body [#182 state:resolved] Signed-off-by: Joshua Peek --- actionpack/lib/action_controller/resources.rb | 9 +++++---- actionpack/lib/action_controller/routing/builder.rb | 15 +++++++++++++++ actionpack/test/controller/resources_test.rb | 20 ++++++++++++++++++++ actionpack/test/controller/routing_test.rb | 16 ++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/actionpack/lib/action_controller/resources.rb b/actionpack/lib/action_controller/resources.rb index b11aa5625b..0614b9a4d9 100644 --- a/actionpack/lib/action_controller/resources.rb +++ b/actionpack/lib/action_controller/resources.rb @@ -307,13 +307,13 @@ module ActionController # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' # # You may also use :name_prefix to override the generic named routes in a nested resource: - # + # # map.resources :articles do |article| # article.resources :comments, :name_prefix => nil - # end - # + # end + # # This will yield named resources like so: - # + # # comments_url(@article) # comment_url(@article, @comment) # @@ -559,6 +559,7 @@ module ActionController def action_options_for(action, resource, method = nil) default_options = { :action => action.to_s } require_id = !resource.kind_of?(SingletonResource) + case default_options[:action] when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb index b8323847fd..912999d845 100644 --- a/actionpack/lib/action_controller/routing/builder.rb +++ b/actionpack/lib/action_controller/routing/builder.rb @@ -76,6 +76,8 @@ module ActionController defaults = (options.delete(:defaults) || {}).dup conditions = (options.delete(:conditions) || {}).dup + validate_route_conditions(conditions) + path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact options.each do |key, value| hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements @@ -198,6 +200,19 @@ module ActionController route end + + private + def validate_route_conditions(conditions) + if method = conditions[:method] + if method == :head + raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" + end + + unless HTTP_METHODS.include?(method.to_sym) + raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}" + end + end + end end end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 0f7924649a..e153b0cc98 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -516,6 +516,26 @@ class ResourcesTest < Test::Unit::TestCase end end + def test_should_not_allow_invalid_head_method_for_member_routes + with_routing do |set| + set.draw do |map| + assert_raises(ArgumentError) do + map.resources :messages, :member => {:something => :head} + end + end + end + end + + def test_should_not_allow_invalid_http_methods_for_member_routes + with_routing do |set| + set.draw do |map| + assert_raises(ArgumentError) do + map.resources :messages, :member => {:something => :invalid} + end + end + end + end + def test_resource_action_separator with_routing do |set| set.draw do |map| diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index c5ccb71582..079189d7b3 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1801,6 +1801,22 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end end + def test_route_requirements_with_invalid_http_method_is_invalid + assert_raises ArgumentError do + set.draw do |map| + map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :invalid} + end + end + end + + def test_route_requirements_with_head_method_condition_is_invalid + assert_raises ArgumentError do + set.draw do |map| + map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :head} + end + end + end + def test_non_path_route_requirements_match_all set.draw do |map| map.connect 'page/37s', :controller => 'pages', :action => 'show', :name => /(jamis|david)/ -- cgit v1.2.3 From c609be45966316bb107e0bad2b0935ac4a0d0c41 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Fri, 18 Jul 2008 23:30:36 -0500 Subject: Ruby 1.9: Ensure Memoizable#freeze is only overriden once to avoid an endless loop --- activesupport/lib/active_support/memoizable.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index 59fecbecb1..f7cd73d39c 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -1,19 +1,5 @@ module ActiveSupport module Memoizable #:nodoc: - def self.extended(obj) - klass = obj.respond_to?(:class_eval) ? obj : obj.metaclass - klass.class_eval do - def freeze - methods.each do |method| - if m = method.to_s.match(/^unmemoized_(.*)/) - send(m[1]) - end - end - super - end - end - end - def memoize(*symbols) symbols.each do |symbol| original_method = "unmemoized_#{symbol}" @@ -23,6 +9,18 @@ module ActiveSupport raise "Already memoized #{symbol}" if klass.instance_methods.map(&:to_s).include?(original_method) klass.class_eval <<-EOS, __FILE__, __LINE__ + unless instance_methods.map(&:to_s).include?("freeze_without_memoizable") + alias_method :freeze_without_memoizable, :freeze + def freeze + methods.each do |method| + if m = method.to_s.match(/^unmemoized_(.*)/) + send(m[1]) + end + end + freeze_without_memoizable + end + end + alias_method :#{original_method}, :#{symbol} def #{symbol}(reload = false) if !reload && defined? #{memoized_ivar} -- cgit v1.2.3 From 108ed4a5660d4ebdc52563250463c762e3f27988 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 00:24:02 -0500 Subject: Ruby 1.9: Strip encoding from ERB source since you can not change character encoding during a method --- actionpack/lib/action_view/template_handlers/erb.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/actionpack/lib/action_view/template_handlers/erb.rb b/actionpack/lib/action_view/template_handlers/erb.rb index 2f2febaa52..3def949f1e 100644 --- a/actionpack/lib/action_view/template_handlers/erb.rb +++ b/actionpack/lib/action_view/template_handlers/erb.rb @@ -48,8 +48,11 @@ module ActionView self.erb_trim_mode = '-' def compile(template) - src = ::ERB.new(template.source, nil, erb_trim_mode, '@output_buffer').src - "__in_erb_template=true;#{src}" + src = ::ERB.new("<% __in_erb_template=true %>#{template.source}", nil, erb_trim_mode, '@output_buffer').src + + # Ruby 1.9 prepends an encoding to the source. However this is + # useless because you can only set an encoding on the first line + RUBY_VERSION >= '1.9' ? src.sub(/\A#coding:.*\n/, '') : src end end end -- cgit v1.2.3 From cab168ac9bbe24e5842fb7677d3fac820ddbc18c Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 00:46:02 -0500 Subject: Ruby 1.9: Fixed regexp warning by replacing nested repeat operator + and ? with '*' --- actionpack/lib/action_view/helpers/text_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 3e3452b615..9342b38680 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -468,7 +468,7 @@ module ActionView [-\w]+ # subdomain or domain (?:\.[-\w]+)* # remaining subdomains or domain (?::\d+)? # port - (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))+)?)* # path + (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$])))*)* # path (?:\?[\w\+@%&=.;-]+)? # query string (?:\#[\w\-]*)? # trailing anchor ) -- cgit v1.2.3 From f2059393481ceb632abc7a9d92670e409020d5bd Mon Sep 17 00:00:00 2001 From: Tom Ward Date: Sat, 19 Jul 2008 09:58:09 +0100 Subject: Ensure checked value is a string when validating case-sensitive uniqueness [#361 state:resolved] Signed-off-by: Pratik Naik --- activerecord/lib/active_record/validations.rb | 2 +- activerecord/test/cases/validations_test.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 3eec1305e4..b957ee3b9e 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -664,7 +664,7 @@ module ActiveRecord # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate # column in ruby when case sensitive option if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? - found = results.any? { |a| a[attr_name.to_s] == value } + found = results.any? { |a| a[attr_name.to_s] == value.to_s } end if found diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 60b00b3e8f..4b2d28c80b 100755 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -477,6 +477,15 @@ class ValidationsTest < ActiveRecord::TestCase assert_not_equal "has already been taken", t3.errors.on(:title) end + def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer + Topic.validates_uniqueness_of(:title, :case_sensitve => true) + t = Topic.create!('title' => 101) + + t2 = Topic.new('title' => 101) + assert !t2.valid? + assert t2.errors.on(:title) + end + def test_validate_uniqueness_with_non_standard_table_names i1 = WarehouseThing.create(:value => 1000) assert !i1.valid?, "i1 should not be valid" -- cgit v1.2.3 From d84d99a8f7dc672b050a6ab891c1680a323a7c97 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 10:52:30 -0500 Subject: Undefine old run method --- activesupport/lib/active_support/testing/setup_and_teardown.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 21d71eb92a..a514b61fea 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -15,12 +15,15 @@ module ActiveSupport define_callbacks :setup, :teardown if defined?(::Mini) + undef_method :run alias_method :run, :run_with_callbacks_and_miniunit else begin require 'mocha' + undef_method :run alias_method :run, :run_with_callbacks_and_mocha rescue LoadError + undef_method :run alias_method :run, :run_with_callbacks_and_testunit end end -- cgit v1.2.3 From 8c2e839e5a0fb1662ae867c70114c3fc91850a55 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 11:04:23 -0500 Subject: Fix some warnings in i18n lib --- activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n.rb | 6 +++--- .../lib/active_support/vendor/i18n-0.0.1/lib/i18n/backend/simple.rb | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n.rb index 2185194da9..1bb65263a3 100755 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n.rb @@ -53,7 +53,7 @@ module I18n # storage. Decoupled for backends like a db backend that persist their # translations, so the backend can decide whether/when to yield or not. def populate(&block) - backend.populate &block + backend.populate(&block) end # Stores translations for the given locale in the backend. @@ -173,8 +173,8 @@ module I18n # keys are Symbols. def normalize_translation_keys(locale, key, scope) keys = [locale] + Array(scope) + [key] - keys = keys.map{|key| key.to_s.split(/\./) } - keys.flatten.map{|key| key.to_sym} + keys = keys.map{|k| k.to_s.split(/\./) } + keys.flatten.map{|k| k.to_sym} end end end diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n/backend/simple.rb index 284f2bfcbd..b8be1cecfb 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n/backend/simple.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n/backend/simple.rb @@ -23,7 +23,7 @@ module I18n def translate(locale, key, options = {}) raise InvalidLocale.new(locale) if locale.nil? - return key.map{|key| translate locale, key, options } if key.is_a? Array + return key.map{|k| translate locale, k, options } if key.is_a? Array reserved = :scope, :default count, scope, default = options.values_at(:count, *reserved) @@ -66,7 +66,7 @@ module I18n def lookup(locale, key, scope = []) return unless key keys = I18n.send :normalize_translation_keys, locale, key, scope - keys.inject(@@translations){|result, key| result[key.to_sym] or return nil } + keys.inject(@@translations){|result, k| result[k.to_sym] or return nil } end # Evaluates a default translation. -- cgit v1.2.3 From b74b97fef5d94f91d6fbf9aec20516c7fe4ce24d Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 11:14:12 -0500 Subject: Update uses_mocha in ActionMailer and ActiveResource --- actionmailer/test/abstract_unit.rb | 18 +++++++++++++----- activeresource/test/abstract_unit.rb | 22 +++++++++++++--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 11058a770d..107b2e8bbe 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -30,12 +30,20 @@ class Net::SMTP end end -# Wrap tests that use Mocha and skip if unavailable. -def uses_mocha(test_name) - gem 'mocha', ">=0.9.0" +def uses_gem(gem_name, test_name, version = '> 0') + require 'rubygems' + gem gem_name.to_s, version + require gem_name.to_s yield -rescue Gem::LoadError - $stderr.puts "Skipping #{test_name} tests (Mocha >= 0.5 is required). `gem install mocha` and try again." +rescue LoadError + $stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again." +end + +# Wrap tests that use Mocha and skip if unavailable. +unless defined? uses_mocha + def uses_mocha(test_name, &block) + uses_gem('mocha', test_name, '>= 0.5.5', &block) + end end def set_delivery_method(delivery_method) diff --git a/activeresource/test/abstract_unit.rb b/activeresource/test/abstract_unit.rb index 615a6d9222..e612412033 100644 --- a/activeresource/test/abstract_unit.rb +++ b/activeresource/test/abstract_unit.rb @@ -9,14 +9,18 @@ require 'setter_trap' ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log") +def uses_gem(gem_name, test_name, version = '> 0') + require 'rubygems' + gem gem_name.to_s, version + require gem_name.to_s + yield +rescue LoadError + $stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again." +end + # Wrap tests that use Mocha and skip if unavailable. -def uses_mocha(test_name) - unless Object.const_defined?(:Mocha) - require 'mocha' - require 'stubba' +unless defined? uses_mocha + def uses_mocha(test_name, &block) + uses_gem('mocha', test_name, '>= 0.5.5', &block) end - yield -rescue LoadError => load_error - raise unless load_error.message =~ /mocha/i - $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again." -end \ No newline at end of file +end -- cgit v1.2.3 From 3fdd1acab61a46ab4823d63ad0bab4879d2bb446 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 11:30:15 -0500 Subject: Dropped SQLite 2 from default test runner --- activerecord/Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 60b17e02b9..983528aff7 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -30,7 +30,7 @@ desc 'Run mysql, sqlite, and postgresql tests by default' task :default => :test desc 'Run mysql, sqlite, and postgresql tests' -task :test => %w(test_mysql test_sqlite test_sqlite3 test_postgresql) +task :test => %w(test_mysql test_sqlite3 test_postgresql) for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase ) Rake::TestTask.new("test_#{adapter}") { |t| -- cgit v1.2.3 From 576cae004342899b0506a7834edc524a02a7d9ef Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 11:34:32 -0500 Subject: Stub out timestamped_migrations in generator tests --- railties/test/generators/generator_test_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/railties/test/generators/generator_test_helper.rb b/railties/test/generators/generator_test_helper.rb index 80d5b145be..0901b215e4 100644 --- a/railties/test/generators/generator_test_helper.rb +++ b/railties/test/generators/generator_test_helper.rb @@ -5,9 +5,10 @@ require 'fileutils' module ActiveRecord class Base class << self - attr_accessor :pluralize_table_names + attr_accessor :pluralize_table_names, :timestamped_migrations end self.pluralize_table_names = true + self.timestamped_migrations = true end module ConnectionAdapters -- cgit v1.2.3 From e0d7bace4ecb9152fac112e809af521e36fbc6a5 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 11:42:27 -0500 Subject: Prefer Mongrel over Thin [#658 state:resolved] --- railties/lib/commands/server.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/railties/lib/commands/server.rb b/railties/lib/commands/server.rb index 7306c248fb..15f417b5be 100644 --- a/railties/lib/commands/server.rb +++ b/railties/lib/commands/server.rb @@ -23,10 +23,10 @@ server = case ARGV.first when "lighttpd", "mongrel", "new_mongrel", "webrick", "thin" ARGV.shift else - if defined?(Thin) - "thin" - elsif defined?(Mongrel) + if defined?(Mongrel) "mongrel" + elsif defined?(Thin) + "thin" elsif RUBY_PLATFORM !~ /(:?mswin|mingw)/ && !silence_stderr { `lighttpd -version` }.blank? && defined?(FCGI) "lighttpd" else -- cgit v1.2.3 From 746122735269b9077c7d5d99d88e8b22d88ad8d5 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 12:23:08 -0500 Subject: Ruby 1.9: Call join on template_root instead of to_s --- actionmailer/lib/action_mailer/base.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 5a71935009..e4920f0c86 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -529,7 +529,7 @@ module ActionMailer #:nodoc: end def render_message(method_name, body) - render :file => method_name, :body => body, :use_full_path => true + render :file => method_name, :body => body end def render(opts) @@ -537,12 +537,11 @@ module ActionMailer #:nodoc: if opts[:file] && opts[:file] !~ /\// opts[:file] = "#{mailer_name}/#{opts[:file]}" end - opts[:use_full_path] = true initialize_template_class(body).render(opts) end def template_path - "#{template_root}/#{mailer_name}" + "#{template_root.join}/#{mailer_name}" end def initialize_template_class(assigns) -- cgit v1.2.3 From e23156e87bd32206a5ea529fbecc04fdf37d7bc2 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 12:35:42 -0500 Subject: Only create a path for ActionMailer template root instead of a path set. Better fix than 7461227 --- actionmailer/lib/action_mailer/base.rb | 4 ++-- actionmailer/test/mail_service_test.rb | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index e4920f0c86..bf60e2f3d5 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -426,7 +426,7 @@ module ActionMailer #:nodoc: end def template_root=(root) - write_inheritable_attribute(:template_root, ActionView::PathSet.new(Array(root))) + write_inheritable_attribute(:template_root, ActionView::PathSet::Path.new(root)) end end @@ -541,7 +541,7 @@ module ActionMailer #:nodoc: end def template_path - "#{template_root.join}/#{mailer_name}" + "#{template_root}/#{mailer_name}" end def initialize_template_class(assigns) diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb index 7f4a8817ca..e5ecb0e254 100755 --- a/actionmailer/test/mail_service_test.rb +++ b/actionmailer/test/mail_service_test.rb @@ -942,13 +942,13 @@ end # uses_mocha class InheritableTemplateRootTest < Test::Unit::TestCase def test_attr expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots" - assert_equal [expected], FunkyPathMailer.template_root.map(&:to_s) + assert_equal expected, FunkyPathMailer.template_root sub = Class.new(FunkyPathMailer) sub.template_root = 'test/path' - assert_equal ['test/path'], sub.template_root.map(&:to_s) - assert_equal [expected], FunkyPathMailer.template_root.map(&:to_s) + assert_equal 'test/path', sub.template_root + assert_equal expected, FunkyPathMailer.template_root end end -- cgit v1.2.3 From 938caf4e6b2448b45939d36824794ea0aa5e1804 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Sat, 19 Jul 2008 12:40:30 -0500 Subject: Removed unused option from FormHelper#fields_for [#641 state:resolved] Signed-off-by: Joshua Peek --- actionpack/lib/action_view/helpers/form_helper.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index fa26aa4640..4fa46d9ee3 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -304,10 +304,6 @@ module ActionView when String, Symbol object_name = record_or_name_or_array object = args.first - when Array - object = record_or_name_or_array.last - object_name = ActionController::RecordIdentifier.singular_class_name(object) - apply_form_for_options!(record_or_name_or_array, options) else object = record_or_name_or_array object_name = ActionController::RecordIdentifier.singular_class_name(object) -- cgit v1.2.3 From c98692abcfd3576ee5fcde3910330d1eb39a18a5 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Sat, 19 Jul 2008 13:06:43 -0500 Subject: Removed handling of string parameter in link_to to have all URL generation done by url_for Signed-off-by: Joshua Peek --- actionpack/lib/action_view/helpers/url_helper.rb | 93 +++++++++++++----------- actionpack/test/template/url_helper_test.rb | 11 ++- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 94e1f1d33a..f31502d99d 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -3,8 +3,8 @@ require 'action_view/helpers/javascript_helper' module ActionView module Helpers #:nodoc: # Provides a set of methods for making links and getting URLs that - # depend on the routing subsystem (see ActionController::Routing). - # This allows you to use the same format for links in views + # depend on the routing subsystem (see ActionController::Routing). + # This allows you to use the same format for links in views # and controllers. module UrlHelper include JavaScriptHelper @@ -33,8 +33,8 @@ module ActionView # # If you instead of a hash pass a record (like an Active Record or Active Resource) as the options parameter, # you'll trigger the named route for that record. The lookup will happen on the name of the class. So passing - # a Workshop object will attempt to use the workshop_path route. If you have a nested route, such as - # admin_workshop_path you'll have to call that explicitly (it's impossible for url_for to guess that route). + # a Workshop object will attempt to use the workshop_path route. If you have a nested route, such as + # admin_workshop_path you'll have to call that explicitly (it's impossible for url_for to guess that route). # # ==== Examples # <%= url_for(:action => 'index') %> @@ -62,19 +62,33 @@ module ActionView # <%= url_for(@workshop) %> # # calls @workshop.to_s # # => /workshops/5 + # + # <%= url_for("http://www.example.com") %> + # # => http://www.example.com + # + # <%= url_for(:back) %> + # # if request.env["HTTP_REFERER"] is set to "http://www.example.com" + # # => http://www.example.com + # + # <%= url_for(:back) %> + # # if request.env["HTTP_REFERER"] is not set or is blank + # # => javascript:history.back() def url_for(options = {}) options ||= {} - case options + url = case options + when String + escape = true + options when Hash options = { :only_path => options[:host].nil? }.update(options.symbolize_keys) escape = options.key?(:escape) ? options.delete(:escape) : true - url = @controller.send(:url_for, options) - when String - escape = true - url = options + @controller.send(:url_for, options) + when :back + escape = false + @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()' else escape = false - url = polymorphic_path(options) + polymorphic_path(options) end escape ? escape_once(url) : url @@ -116,8 +130,8 @@ module ActionView # # Note that if the user has JavaScript disabled, the request will fall back # to using GET. If :href => '#' is used and the user has JavaScript disabled - # clicking the link will have no effect. If you are relying on the POST - # behavior, your should check for it in your controller's action by using the + # clicking the link will have no effect. If you are relying on the POST + # behavior, your should check for it in your controller's action by using the # request object's methods for post?, delete? or put?. # # You can mix and match the +html_options+ with the exception of @@ -141,8 +155,8 @@ module ActionView # # link_to "Profile", :controller => "profiles", :action => "show", :id => @profile # # => Profile - # - # Similarly, + # + # Similarly, # # link_to "Profiles", profiles_path # # => Profiles @@ -197,9 +211,9 @@ module ActionView # # => View Image # # link_to "Delete Image", @image, :confirm => "Are you sure?", :method => :delete - # # => Delete Image def link_to(*args, &block) if block_given? @@ -211,14 +225,7 @@ module ActionView options = args.second || {} html_options = args.third - url = case options - when String - options - when :back - @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()' - else - self.url_for(options) - end + url = url_for(options) if html_options html_options = html_options.stringify_keys @@ -228,7 +235,7 @@ module ActionView else tag_options = nil end - + href_attr = "href=\"#{url}\"" unless href "#{name || url}" end @@ -260,7 +267,7 @@ module ActionView # * :confirm - This will add a JavaScript confirm # prompt with the question specified. If the user accepts, the link is # processed normally, otherwise no action is taken. - # + # # ==== Examples # <%= button_to "New", :action => "new" %> # # => "
@@ -286,12 +293,12 @@ module ActionView end form_method = method.to_s == 'get' ? 'get' : 'post' - + request_token_tag = '' if form_method == 'post' && protect_against_forgery? request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) end - + if confirm = html_options.delete("confirm") html_options["onclick"] = "return #{confirm_javascript_function(confirm)};" end @@ -309,7 +316,7 @@ module ActionView # Creates a link tag of the given +name+ using a URL created by the set of # +options+ unless the current request URI is the same as the links, in # which case only the name is returned (or the given block is yielded, if - # one exists). You can give link_to_unless_current a block which will + # one exists). You can give link_to_unless_current a block which will # specialize the default behavior (e.g., show a "Start Here" link rather # than the link's text). # @@ -336,13 +343,13 @@ module ActionView # # # The implicit block given to link_to_unless_current is evaluated if the current - # action is the action given. So, if we had a comments page and wanted to render a + # action is the action given. So, if we had a comments page and wanted to render a # "Go Back" link instead of a link to the comments page, we could do something like this... - # - # <%= + # + # <%= # link_to_unless_current("Comment", { :controller => 'comments', :action => 'new}) do - # link_to("Go back", { :controller => 'posts', :action => 'index' }) - # end + # link_to("Go back", { :controller => 'posts', :action => 'index' }) + # end # %> def link_to_unless_current(name, options = {}, html_options = {}, &block) link_to_unless current_page?(options), name, options, html_options, &block @@ -359,10 +366,10 @@ module ActionView # # If the user is logged in... # # => Reply # - # <%= + # <%= # link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) do |name| # link_to(name, { :controller => "accounts", :action => "signup" }) - # end + # end # %> # # If the user is logged in... # # => Reply @@ -391,10 +398,10 @@ module ActionView # # If the user isn't logged in... # # => Login # - # <%= + # <%= # link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) do # link_to(@current_user.login, { :controller => "accounts", :action => "show", :id => @current_user }) - # end + # end # %> # # If the user isn't logged in... # # => Login @@ -431,20 +438,20 @@ module ActionView # * :bcc - Blind Carbon Copy additional recipients on the email. # # ==== Examples - # mail_to "me@domain.com" + # mail_to "me@domain.com" # # => me@domain.com # - # mail_to "me@domain.com", "My email", :encode => "javascript" + # mail_to "me@domain.com", "My email", :encode => "javascript" # # => # - # mail_to "me@domain.com", "My email", :encode => "hex" + # mail_to "me@domain.com", "My email", :encode => "hex" # # => My email # - # mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email" + # mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email" # # => # # mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com", - # :subject => "This is an example email" + # :subject => "This is an example email" # # => My email def mail_to(email_address, name = nil, html_options = {}) html_options = html_options.stringify_keys diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 91d5c6ffb5..867503fb29 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -28,6 +28,16 @@ class UrlHelperTest < ActionView::TestCase assert_equal "http://www.example.com?a=b&c=d", url_for("http://www.example.com?a=b&c=d") end + def test_url_for_with_back + @controller.request = RequestMock.new("http://www.example.com/weblog/show", nil, nil, {'HTTP_REFERER' => 'http://www.example.com/referer'}) + assert_equal 'http://www.example.com/referer', url_for(:back) + end + + def test_url_for_with_back_and_no_referer + @controller.request = RequestMock.new("http://www.example.com/weblog/show", nil, nil, {}) + assert_equal 'javascript:history.back()', url_for(:back) + end + # todo: missing test cases def test_button_to_with_straight_url assert_dom_equal "
", button_to("Hello", "http://www.example.com") @@ -419,7 +429,6 @@ class LinkToUnlessCurrentWithControllerTest < ActionView::TestCase end end - class Workshop attr_accessor :id, :new_record -- cgit v1.2.3 From 706425e154a2a2581195c98309f30a18a0002a58 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 19 Jul 2008 13:56:38 -0500 Subject: Update Prototype to 1.6.0.2 (Patrick Joyce) [#599 status:committed] --- actionpack/CHANGELOG | 2 + .../action_view/helpers/javascripts/prototype.js | 304 ++++++++++----------- railties/html/javascripts/prototype.js | 304 ++++++++++----------- 3 files changed, 302 insertions(+), 308 deletions(-) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index f6432fe0be..ebe4c047b8 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Update Prototype to 1.6.0.2 #599 [Patrick Joyce] + * Conditional GET utility methods. [Jeremy Kemper] * etag!([:admin, post, current_user]) sets the ETag response header and returns head(:not_modified) if it matches the If-None-Match request header. * last_modified!(post.updated_at) sets Last-Modified and returns head(:not_modified) if it's no later than If-Modified-Since. diff --git a/actionpack/lib/action_view/helpers/javascripts/prototype.js b/actionpack/lib/action_view/helpers/javascripts/prototype.js index 546f9fe449..2c70b8a7e8 100644 --- a/actionpack/lib/action_view/helpers/javascripts/prototype.js +++ b/actionpack/lib/action_view/helpers/javascripts/prototype.js @@ -1,5 +1,5 @@ -/* Prototype JavaScript framework, version 1.6.0.1 - * (c) 2005-2007 Sam Stephenson +/* Prototype JavaScript framework, version 1.6.0.2 + * (c) 2005-2008 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://www.prototypejs.org/ @@ -7,7 +7,7 @@ *--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.6.0.1', + Version: '1.6.0.2', Browser: { IE: !!(window.attachEvent && !window.opera), @@ -110,7 +110,7 @@ Object.extend(Object, { try { if (Object.isUndefined(object)) return 'undefined'; if (object === null) return 'null'; - return object.inspect ? object.inspect() : object.toString(); + return object.inspect ? object.inspect() : String(object); } catch (e) { if (e instanceof RangeError) return '...'; throw e; @@ -171,7 +171,8 @@ Object.extend(Object, { }, isArray: function(object) { - return object && object.constructor === Array; + return object != null && typeof object == "object" && + 'splice' in object && 'join' in object; }, isHash: function(object) { @@ -578,7 +579,7 @@ var Template = Class.create({ } return before + String.interpret(ctx); - }.bind(this)); + }); } }); Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; @@ -806,20 +807,20 @@ Object.extend(Enumerable, { function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); - var length = iterable.length, results = new Array(length); + var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } if (Prototype.Browser.WebKit) { - function $A(iterable) { + $A = function(iterable) { if (!iterable) return []; if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && iterable.toArray) return iterable.toArray(); - var length = iterable.length, results = new Array(length); + var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; - } + }; } Array.from = $A; @@ -1298,7 +1299,7 @@ Ajax.Request = Class.create(Ajax.Base, { var contentType = response.getHeader('Content-type'); if (this.options.evalJS == 'force' - || (this.options.evalJS && contentType + || (this.options.evalJS && this.isSameOrigin() && contentType && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) this.evalResponse(); } @@ -1316,9 +1317,18 @@ Ajax.Request = Class.create(Ajax.Base, { } }, + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + getHeader: function(name) { try { - return this.transport.getResponseHeader(name); + return this.transport.getResponseHeader(name) || null; } catch (e) { return null } }, @@ -1391,7 +1401,8 @@ Ajax.Response = Class.create({ if (!json) return null; json = decodeURIComponent(escape(json)); try { - return json.evalJSON(this.request.options.sanitizeJSON); + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } @@ -1404,7 +1415,8 @@ Ajax.Response = Class.create({ this.responseText.blank()) return null; try { - return this.responseText.evalJSON(options.sanitizeJSON); + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } @@ -1608,24 +1620,28 @@ Element.Methods = { Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) insertions = {bottom:insertions}; - var content, t, range; + var content, insert, tagName, childNodes; - for (position in insertions) { + for (var position in insertions) { content = insertions[position]; position = position.toLowerCase(); - t = Element._insertionTranslations[position]; + insert = Element._insertionTranslations[position]; if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { - t.insert(element, content); + insert(element, content); continue; } content = Object.toHTML(content); - range = element.ownerDocument.createRange(); - t.initializeRange(element, range); - t.insert(element, range.createContextualFragment(content.stripScripts())); + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); content.evalScripts.bind(content).defer(); } @@ -1670,7 +1686,7 @@ Element.Methods = { }, descendants: function(element) { - return $(element).getElementsBySelector("*"); + return $(element).select("*"); }, firstDescendant: function(element) { @@ -1709,32 +1725,31 @@ Element.Methods = { element = $(element); if (arguments.length == 1) return $(element.parentNode); var ancestors = element.ancestors(); - return expression ? Selector.findElement(ancestors, expression, index) : - ancestors[index || 0]; + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); }, down: function(element, expression, index) { element = $(element); if (arguments.length == 1) return element.firstDescendant(); - var descendants = element.descendants(); - return expression ? Selector.findElement(descendants, expression, index) : - descendants[index || 0]; + return Object.isNumber(expression) ? element.descendants()[expression] : + element.select(expression)[index || 0]; }, previous: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); var previousSiblings = element.previousSiblings(); - return expression ? Selector.findElement(previousSiblings, expression, index) : - previousSiblings[index || 0]; + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); }, next: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); var nextSiblings = element.nextSiblings(); - return expression ? Selector.findElement(nextSiblings, expression, index) : - nextSiblings[index || 0]; + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); }, select: function() { @@ -1860,7 +1875,8 @@ Element.Methods = { do { ancestor = ancestor.parentNode; } while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); } - if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); + if (nextAncestor && nextAncestor.sourceIndex) + return (e > a && e < nextAncestor.sourceIndex); } while (element = element.parentNode) @@ -2004,7 +2020,7 @@ Element.Methods = { if (element) { if (element.tagName == 'BODY') break; var p = Element.getStyle(element, 'position'); - if (p == 'relative' || p == 'absolute') break; + if (p !== 'static') break; } } while (element); return Element._returnOffset(valueL, valueT); @@ -2153,46 +2169,6 @@ Element._attributeTranslations = { } }; - -if (!document.createRange || Prototype.Browser.Opera) { - Element.Methods.insert = function(element, insertions) { - element = $(element); - - if (Object.isString(insertions) || Object.isNumber(insertions) || - Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) - insertions = { bottom: insertions }; - - var t = Element._insertionTranslations, content, position, pos, tagName; - - for (position in insertions) { - content = insertions[position]; - position = position.toLowerCase(); - pos = t[position]; - - if (content && content.toElement) content = content.toElement(); - if (Object.isElement(content)) { - pos.insert(element, content); - continue; - } - - content = Object.toHTML(content); - tagName = ((position == 'before' || position == 'after') - ? element.parentNode : element).tagName.toUpperCase(); - - if (t.tags[tagName]) { - var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); - if (position == 'top' || position == 'after') fragments.reverse(); - fragments.each(pos.insert.curry(element)); - } - else element.insertAdjacentHTML(pos.adjacency, content.stripScripts()); - - content.evalScripts.bind(content).defer(); - } - - return element; - }; -} - if (Prototype.Browser.Opera) { Element.Methods.getStyle = Element.Methods.getStyle.wrap( function(proceed, element, style) { @@ -2237,12 +2213,31 @@ if (Prototype.Browser.Opera) { } else if (Prototype.Browser.IE) { - $w('positionedOffset getOffsetParent viewportOffset').each(function(method) { + // IE doesn't report offsets correctly for static elements, so we change them + // to "relative" to get the values, then change them back. + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { Element.Methods[method] = Element.Methods[method].wrap( function(proceed, element) { element = $(element); var position = element.getStyle('position'); - if (position != 'static') return proceed(element); + if (position !== 'static') return proceed(element); + // Trigger hasLayout on the offset parent so that IE6 reports + // accurate offsetTop and offsetLeft values for position: fixed. + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); @@ -2324,7 +2319,10 @@ else if (Prototype.Browser.IE) { }; Element._attributeTranslations.write = { - names: Object.clone(Element._attributeTranslations.read.names), + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), values: { checked: function(element, value) { element.checked = !!value; @@ -2444,7 +2442,7 @@ if (Prototype.Browser.IE || Prototype.Browser.Opera) { }; } -if (document.createElement('div').outerHTML) { +if ('outerHTML' in document.createElement('div')) { Element.Methods.replace = function(element, content) { element = $(element); @@ -2482,45 +2480,25 @@ Element._returnOffset = function(l, t) { Element._getContentFromAnonymousElement = function(tagName, html) { var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; - div.innerHTML = t[0] + html + t[1]; - t[2].times(function() { div = div.firstChild }); + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; return $A(div.childNodes); }; Element._insertionTranslations = { - before: { - adjacency: 'beforeBegin', - insert: function(element, node) { - element.parentNode.insertBefore(node, element); - }, - initializeRange: function(element, range) { - range.setStartBefore(element); - } + before: function(element, node) { + element.parentNode.insertBefore(node, element); }, - top: { - adjacency: 'afterBegin', - insert: function(element, node) { - element.insertBefore(node, element.firstChild); - }, - initializeRange: function(element, range) { - range.selectNodeContents(element); - range.collapse(true); - } + top: function(element, node) { + element.insertBefore(node, element.firstChild); }, - bottom: { - adjacency: 'beforeEnd', - insert: function(element, node) { - element.appendChild(node); - } + bottom: function(element, node) { + element.appendChild(node); }, - after: { - adjacency: 'afterEnd', - insert: function(element, node) { - element.parentNode.insertBefore(node, element.nextSibling); - }, - initializeRange: function(element, range) { - range.setStartAfter(element); - } + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); }, tags: { TABLE: ['', '
', 1], @@ -2532,7 +2510,6 @@ Element._insertionTranslations = { }; (function() { - this.bottom.initializeRange = this.top.initializeRange; Object.extend(this.tags, { THEAD: this.tags.TBODY, TFOOT: this.tags.TBODY, @@ -2716,7 +2693,7 @@ document.viewport = { window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } }; -/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style * license. Please see http://www.yui-ext.com/ for more information. */ @@ -2959,13 +2936,13 @@ Object.extend(Selector, { }, criteria: { - tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', - className: 'n = h.className(n, r, "#{1}", c); c = false;', - id: 'n = h.id(n, r, "#{1}", c); c = false;', - attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', attr: function(m) { m[3] = (m[5] || m[6]); - return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); }, pseudo: function(m) { if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); @@ -2989,7 +2966,8 @@ Object.extend(Selector, { tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, id: /^#([\w\-\*]+)(\b|$)/, className: /^\.([\w\-\*]+)(\b|$)/, - pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, + pseudo: +/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, attrPresence: /^\[([\w]+)\]/, attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }, @@ -3014,7 +2992,7 @@ Object.extend(Selector, { attr: function(element, matches) { var nodeValue = Element.readAttribute(element, matches[1]); - return Selector.operators[matches[2]](nodeValue, matches[3]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); } }, @@ -3029,14 +3007,15 @@ Object.extend(Selector, { // marks an array of nodes for counting mark: function(nodes) { + var _true = Prototype.emptyFunction; for (var i = 0, node; node = nodes[i]; i++) - node._counted = true; + node._countedByPrototype = _true; return nodes; }, unmark: function(nodes) { for (var i = 0, node; node = nodes[i]; i++) - node._counted = undefined; + node._countedByPrototype = undefined; return nodes; }, @@ -3044,15 +3023,15 @@ Object.extend(Selector, { // "ofType" flag indicates whether we're indexing for nth-of-type // rather than nth-child index: function(parentNode, reverse, ofType) { - parentNode._counted = true; + parentNode._countedByPrototype = Prototype.emptyFunction; if (reverse) { for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { var node = nodes[i]; - if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } } else { for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) - if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } }, @@ -3061,8 +3040,8 @@ Object.extend(Selector, { if (nodes.length == 0) return nodes; var results = [], n; for (var i = 0, l = nodes.length; i < l; i++) - if (!(n = nodes[i])._counted) { - n._counted = true; + if (!(n = nodes[i])._countedByPrototype) { + n._countedByPrototype = Prototype.emptyFunction; results.push(Element.extend(n)); } return Selector.handlers.unmark(results); @@ -3114,7 +3093,7 @@ Object.extend(Selector, { // TOKEN FUNCTIONS tagName: function(nodes, root, tagName, combinator) { - tagName = tagName.toUpperCase(); + var uTagName = tagName.toUpperCase(); var results = [], h = Selector.handlers; if (nodes) { if (combinator) { @@ -3127,7 +3106,7 @@ Object.extend(Selector, { if (tagName == "*") return nodes; } for (var i = 0, node; node = nodes[i]; i++) - if (node.tagName.toUpperCase() == tagName) results.push(node); + if (node.tagName.toUpperCase() === uTagName) results.push(node); return results; } else return root.getElementsByTagName(tagName); }, @@ -3174,16 +3153,18 @@ Object.extend(Selector, { return results; }, - attrPresence: function(nodes, root, attr) { + attrPresence: function(nodes, root, attr, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); var results = []; for (var i = 0, node; node = nodes[i]; i++) if (Element.hasAttribute(node, attr)) results.push(node); return results; }, - attr: function(nodes, root, attr, value, operator) { + attr: function(nodes, root, attr, value, operator, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); var handler = Selector.operators[operator], results = []; for (var i = 0, node; node = nodes[i]; i++) { var nodeValue = Element.readAttribute(node, attr); @@ -3262,7 +3243,7 @@ Object.extend(Selector, { var h = Selector.handlers, results = [], indexed = [], m; h.mark(nodes); for (var i = 0, node; node = nodes[i]; i++) { - if (!node.parentNode._counted) { + if (!node.parentNode._countedByPrototype) { h.index(node.parentNode, reverse, ofType); indexed.push(node.parentNode); } @@ -3300,7 +3281,7 @@ Object.extend(Selector, { var exclusions = new Selector(selector).findElements(root); h.mark(exclusions); for (var i = 0, results = [], node; node = nodes[i]; i++) - if (!node._counted) results.push(node); + if (!node._countedByPrototype) results.push(node); h.unmark(exclusions); return results; }, @@ -3334,11 +3315,19 @@ Object.extend(Selector, { '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } }, + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + matchElements: function(elements, expression) { - var matches = new Selector(expression).findElements(), h = Selector.handlers; + var matches = $$(expression), h = Selector.handlers; h.mark(matches); for (var i = 0, results = [], element; element = elements[i]; i++) - if (element._counted) results.push(element); + if (element._countedByPrototype) results.push(element); h.unmark(matches); return results; }, @@ -3351,11 +3340,7 @@ Object.extend(Selector, { }, findChildElements: function(element, expressions) { - var exprs = expressions.join(','); - expressions = []; - exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { - expressions.push(m[1].strip()); - }); + expressions = Selector.split(expressions.join(',')); var results = [], h = Selector.handlers; for (var i = 0, l = expressions.length, selector; i < l; i++) { selector = new Selector(expressions[i].strip()); @@ -3366,13 +3351,22 @@ Object.extend(Selector, { }); if (Prototype.Browser.IE) { - // IE returns comment nodes on getElementsByTagName("*"). - // Filter them out. - Selector.handlers.concat = function(a, b) { - for (var i = 0, node; node = b[i]; i++) - if (node.tagName !== "!") a.push(node); - return a; - }; + Object.extend(Selector.handlers, { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }, + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML. + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } + }); } function $$() { @@ -3850,9 +3844,9 @@ Object.extend(Event, (function() { var cache = Event.cache; function getEventID(element) { - if (element._eventID) return element._eventID; + if (element._prototypeEventID) return element._prototypeEventID[0]; arguments.callee.id = arguments.callee.id || 1; - return element._eventID = ++arguments.callee.id; + return element._prototypeEventID = [++arguments.callee.id]; } function getDOMEventName(eventName) { @@ -3880,7 +3874,7 @@ Object.extend(Event, (function() { return false; Event.extend(event); - handler.call(element, event) + handler.call(element, event); }; wrapper.handler = handler; @@ -3962,11 +3956,12 @@ Object.extend(Event, (function() { if (element == document && document.createEvent && !element.dispatchEvent) element = document.documentElement; + var event; if (document.createEvent) { - var event = document.createEvent("HTMLEvents"); + event = document.createEvent("HTMLEvents"); event.initEvent("dataavailable", true, true); } else { - var event = document.createEventObject(); + event = document.createEventObject(); event.eventType = "ondataavailable"; } @@ -3995,20 +3990,21 @@ Element.addMethods({ Object.extend(document, { fire: Element.Methods.fire.methodize(), observe: Element.Methods.observe.methodize(), - stopObserving: Element.Methods.stopObserving.methodize() + stopObserving: Element.Methods.stopObserving.methodize(), + loaded: false }); (function() { /* Support for the DOMContentLoaded event is based on work by Dan Webb, Matthias Miller, Dean Edwards and John Resig. */ - var timer, fired = false; + var timer; function fireContentLoadedEvent() { - if (fired) return; + if (document.loaded) return; if (timer) window.clearInterval(timer); document.fire("dom:loaded"); - fired = true; + document.loaded = true; } if (document.addEventListener) { diff --git a/railties/html/javascripts/prototype.js b/railties/html/javascripts/prototype.js index 546f9fe449..2c70b8a7e8 100644 --- a/railties/html/javascripts/prototype.js +++ b/railties/html/javascripts/prototype.js @@ -1,5 +1,5 @@ -/* Prototype JavaScript framework, version 1.6.0.1 - * (c) 2005-2007 Sam Stephenson +/* Prototype JavaScript framework, version 1.6.0.2 + * (c) 2005-2008 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://www.prototypejs.org/ @@ -7,7 +7,7 @@ *--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.6.0.1', + Version: '1.6.0.2', Browser: { IE: !!(window.attachEvent && !window.opera), @@ -110,7 +110,7 @@ Object.extend(Object, { try { if (Object.isUndefined(object)) return 'undefined'; if (object === null) return 'null'; - return object.inspect ? object.inspect() : object.toString(); + return object.inspect ? object.inspect() : String(object); } catch (e) { if (e instanceof RangeError) return '...'; throw e; @@ -171,7 +171,8 @@ Object.extend(Object, { }, isArray: function(object) { - return object && object.constructor === Array; + return object != null && typeof object == "object" && + 'splice' in object && 'join' in object; }, isHash: function(object) { @@ -578,7 +579,7 @@ var Template = Class.create({ } return before + String.interpret(ctx); - }.bind(this)); + }); } }); Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; @@ -806,20 +807,20 @@ Object.extend(Enumerable, { function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); - var length = iterable.length, results = new Array(length); + var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } if (Prototype.Browser.WebKit) { - function $A(iterable) { + $A = function(iterable) { if (!iterable) return []; if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && iterable.toArray) return iterable.toArray(); - var length = iterable.length, results = new Array(length); + var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; - } + }; } Array.from = $A; @@ -1298,7 +1299,7 @@ Ajax.Request = Class.create(Ajax.Base, { var contentType = response.getHeader('Content-type'); if (this.options.evalJS == 'force' - || (this.options.evalJS && contentType + || (this.options.evalJS && this.isSameOrigin() && contentType && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) this.evalResponse(); } @@ -1316,9 +1317,18 @@ Ajax.Request = Class.create(Ajax.Base, { } }, + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + getHeader: function(name) { try { - return this.transport.getResponseHeader(name); + return this.transport.getResponseHeader(name) || null; } catch (e) { return null } }, @@ -1391,7 +1401,8 @@ Ajax.Response = Class.create({ if (!json) return null; json = decodeURIComponent(escape(json)); try { - return json.evalJSON(this.request.options.sanitizeJSON); + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } @@ -1404,7 +1415,8 @@ Ajax.Response = Class.create({ this.responseText.blank()) return null; try { - return this.responseText.evalJSON(options.sanitizeJSON); + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } @@ -1608,24 +1620,28 @@ Element.Methods = { Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) insertions = {bottom:insertions}; - var content, t, range; + var content, insert, tagName, childNodes; - for (position in insertions) { + for (var position in insertions) { content = insertions[position]; position = position.toLowerCase(); - t = Element._insertionTranslations[position]; + insert = Element._insertionTranslations[position]; if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { - t.insert(element, content); + insert(element, content); continue; } content = Object.toHTML(content); - range = element.ownerDocument.createRange(); - t.initializeRange(element, range); - t.insert(element, range.createContextualFragment(content.stripScripts())); + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); content.evalScripts.bind(content).defer(); } @@ -1670,7 +1686,7 @@ Element.Methods = { }, descendants: function(element) { - return $(element).getElementsBySelector("*"); + return $(element).select("*"); }, firstDescendant: function(element) { @@ -1709,32 +1725,31 @@ Element.Methods = { element = $(element); if (arguments.length == 1) return $(element.parentNode); var ancestors = element.ancestors(); - return expression ? Selector.findElement(ancestors, expression, index) : - ancestors[index || 0]; + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); }, down: function(element, expression, index) { element = $(element); if (arguments.length == 1) return element.firstDescendant(); - var descendants = element.descendants(); - return expression ? Selector.findElement(descendants, expression, index) : - descendants[index || 0]; + return Object.isNumber(expression) ? element.descendants()[expression] : + element.select(expression)[index || 0]; }, previous: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); var previousSiblings = element.previousSiblings(); - return expression ? Selector.findElement(previousSiblings, expression, index) : - previousSiblings[index || 0]; + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); }, next: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); var nextSiblings = element.nextSiblings(); - return expression ? Selector.findElement(nextSiblings, expression, index) : - nextSiblings[index || 0]; + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); }, select: function() { @@ -1860,7 +1875,8 @@ Element.Methods = { do { ancestor = ancestor.parentNode; } while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); } - if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); + if (nextAncestor && nextAncestor.sourceIndex) + return (e > a && e < nextAncestor.sourceIndex); } while (element = element.parentNode) @@ -2004,7 +2020,7 @@ Element.Methods = { if (element) { if (element.tagName == 'BODY') break; var p = Element.getStyle(element, 'position'); - if (p == 'relative' || p == 'absolute') break; + if (p !== 'static') break; } } while (element); return Element._returnOffset(valueL, valueT); @@ -2153,46 +2169,6 @@ Element._attributeTranslations = { } }; - -if (!document.createRange || Prototype.Browser.Opera) { - Element.Methods.insert = function(element, insertions) { - element = $(element); - - if (Object.isString(insertions) || Object.isNumber(insertions) || - Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) - insertions = { bottom: insertions }; - - var t = Element._insertionTranslations, content, position, pos, tagName; - - for (position in insertions) { - content = insertions[position]; - position = position.toLowerCase(); - pos = t[position]; - - if (content && content.toElement) content = content.toElement(); - if (Object.isElement(content)) { - pos.insert(element, content); - continue; - } - - content = Object.toHTML(content); - tagName = ((position == 'before' || position == 'after') - ? element.parentNode : element).tagName.toUpperCase(); - - if (t.tags[tagName]) { - var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); - if (position == 'top' || position == 'after') fragments.reverse(); - fragments.each(pos.insert.curry(element)); - } - else element.insertAdjacentHTML(pos.adjacency, content.stripScripts()); - - content.evalScripts.bind(content).defer(); - } - - return element; - }; -} - if (Prototype.Browser.Opera) { Element.Methods.getStyle = Element.Methods.getStyle.wrap( function(proceed, element, style) { @@ -2237,12 +2213,31 @@ if (Prototype.Browser.Opera) { } else if (Prototype.Browser.IE) { - $w('positionedOffset getOffsetParent viewportOffset').each(function(method) { + // IE doesn't report offsets correctly for static elements, so we change them + // to "relative" to get the values, then change them back. + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { Element.Methods[method] = Element.Methods[method].wrap( function(proceed, element) { element = $(element); var position = element.getStyle('position'); - if (position != 'static') return proceed(element); + if (position !== 'static') return proceed(element); + // Trigger hasLayout on the offset parent so that IE6 reports + // accurate offsetTop and offsetLeft values for position: fixed. + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); @@ -2324,7 +2319,10 @@ else if (Prototype.Browser.IE) { }; Element._attributeTranslations.write = { - names: Object.clone(Element._attributeTranslations.read.names), + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), values: { checked: function(element, value) { element.checked = !!value; @@ -2444,7 +2442,7 @@ if (Prototype.Browser.IE || Prototype.Browser.Opera) { }; } -if (document.createElement('div').outerHTML) { +if ('outerHTML' in document.createElement('div')) { Element.Methods.replace = function(element, content) { element = $(element); @@ -2482,45 +2480,25 @@ Element._returnOffset = function(l, t) { Element._getContentFromAnonymousElement = function(tagName, html) { var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; - div.innerHTML = t[0] + html + t[1]; - t[2].times(function() { div = div.firstChild }); + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; return $A(div.childNodes); }; Element._insertionTranslations = { - before: { - adjacency: 'beforeBegin', - insert: function(element, node) { - element.parentNode.insertBefore(node, element); - }, - initializeRange: function(element, range) { - range.setStartBefore(element); - } + before: function(element, node) { + element.parentNode.insertBefore(node, element); }, - top: { - adjacency: 'afterBegin', - insert: function(element, node) { - element.insertBefore(node, element.firstChild); - }, - initializeRange: function(element, range) { - range.selectNodeContents(element); - range.collapse(true); - } + top: function(element, node) { + element.insertBefore(node, element.firstChild); }, - bottom: { - adjacency: 'beforeEnd', - insert: function(element, node) { - element.appendChild(node); - } + bottom: function(element, node) { + element.appendChild(node); }, - after: { - adjacency: 'afterEnd', - insert: function(element, node) { - element.parentNode.insertBefore(node, element.nextSibling); - }, - initializeRange: function(element, range) { - range.setStartAfter(element); - } + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); }, tags: { TABLE: ['', '
', 1], @@ -2532,7 +2510,6 @@ Element._insertionTranslations = { }; (function() { - this.bottom.initializeRange = this.top.initializeRange; Object.extend(this.tags, { THEAD: this.tags.TBODY, TFOOT: this.tags.TBODY, @@ -2716,7 +2693,7 @@ document.viewport = { window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } }; -/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style * license. Please see http://www.yui-ext.com/ for more information. */ @@ -2959,13 +2936,13 @@ Object.extend(Selector, { }, criteria: { - tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', - className: 'n = h.className(n, r, "#{1}", c); c = false;', - id: 'n = h.id(n, r, "#{1}", c); c = false;', - attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', attr: function(m) { m[3] = (m[5] || m[6]); - return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); }, pseudo: function(m) { if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); @@ -2989,7 +2966,8 @@ Object.extend(Selector, { tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, id: /^#([\w\-\*]+)(\b|$)/, className: /^\.([\w\-\*]+)(\b|$)/, - pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, + pseudo: +/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, attrPresence: /^\[([\w]+)\]/, attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }, @@ -3014,7 +2992,7 @@ Object.extend(Selector, { attr: function(element, matches) { var nodeValue = Element.readAttribute(element, matches[1]); - return Selector.operators[matches[2]](nodeValue, matches[3]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); } }, @@ -3029,14 +3007,15 @@ Object.extend(Selector, { // marks an array of nodes for counting mark: function(nodes) { + var _true = Prototype.emptyFunction; for (var i = 0, node; node = nodes[i]; i++) - node._counted = true; + node._countedByPrototype = _true; return nodes; }, unmark: function(nodes) { for (var i = 0, node; node = nodes[i]; i++) - node._counted = undefined; + node._countedByPrototype = undefined; return nodes; }, @@ -3044,15 +3023,15 @@ Object.extend(Selector, { // "ofType" flag indicates whether we're indexing for nth-of-type // rather than nth-child index: function(parentNode, reverse, ofType) { - parentNode._counted = true; + parentNode._countedByPrototype = Prototype.emptyFunction; if (reverse) { for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { var node = nodes[i]; - if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } } else { for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) - if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } }, @@ -3061,8 +3040,8 @@ Object.extend(Selector, { if (nodes.length == 0) return nodes; var results = [], n; for (var i = 0, l = nodes.length; i < l; i++) - if (!(n = nodes[i])._counted) { - n._counted = true; + if (!(n = nodes[i])._countedByPrototype) { + n._countedByPrototype = Prototype.emptyFunction; results.push(Element.extend(n)); } return Selector.handlers.unmark(results); @@ -3114,7 +3093,7 @@ Object.extend(Selector, { // TOKEN FUNCTIONS tagName: function(nodes, root, tagName, combinator) { - tagName = tagName.toUpperCase(); + var uTagName = tagName.toUpperCase(); var results = [], h = Selector.handlers; if (nodes) { if (combinator) { @@ -3127,7 +3106,7 @@ Object.extend(Selector, { if (tagName == "*") return nodes; } for (var i = 0, node; node = nodes[i]; i++) - if (node.tagName.toUpperCase() == tagName) results.push(node); + if (node.tagName.toUpperCase() === uTagName) results.push(node); return results; } else return root.getElementsByTagName(tagName); }, @@ -3174,16 +3153,18 @@ Object.extend(Selector, { return results; }, - attrPresence: function(nodes, root, attr) { + attrPresence: function(nodes, root, attr, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); var results = []; for (var i = 0, node; node = nodes[i]; i++) if (Element.hasAttribute(node, attr)) results.push(node); return results; }, - attr: function(nodes, root, attr, value, operator) { + attr: function(nodes, root, attr, value, operator, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); var handler = Selector.operators[operator], results = []; for (var i = 0, node; node = nodes[i]; i++) { var nodeValue = Element.readAttribute(node, attr); @@ -3262,7 +3243,7 @@ Object.extend(Selector, { var h = Selector.handlers, results = [], indexed = [], m; h.mark(nodes); for (var i = 0, node; node = nodes[i]; i++) { - if (!node.parentNode._counted) { + if (!node.parentNode._countedByPrototype) { h.index(node.parentNode, reverse, ofType); indexed.push(node.parentNode); } @@ -3300,7 +3281,7 @@ Object.extend(Selector, { var exclusions = new Selector(selector).findElements(root); h.mark(exclusions); for (var i = 0, results = [], node; node = nodes[i]; i++) - if (!node._counted) results.push(node); + if (!node._countedByPrototype) results.push(node); h.unmark(exclusions); return results; }, @@ -3334,11 +3315,19 @@ Object.extend(Selector, { '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } }, + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + matchElements: function(elements, expression) { - var matches = new Selector(expression).findElements(), h = Selector.handlers; + var matches = $$(expression), h = Selector.handlers; h.mark(matches); for (var i = 0, results = [], element; element = elements[i]; i++) - if (element._counted) results.push(element); + if (element._countedByPrototype) results.push(element); h.unmark(matches); return results; }, @@ -3351,11 +3340,7 @@ Object.extend(Selector, { }, findChildElements: function(element, expressions) { - var exprs = expressions.join(','); - expressions = []; - exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { - expressions.push(m[1].strip()); - }); + expressions = Selector.split(expressions.join(',')); var results = [], h = Selector.handlers; for (var i = 0, l = expressions.length, selector; i < l; i++) { selector = new Selector(expressions[i].strip()); @@ -3366,13 +3351,22 @@ Object.extend(Selector, { }); if (Prototype.Browser.IE) { - // IE returns comment nodes on getElementsByTagName("*"). - // Filter them out. - Selector.handlers.concat = function(a, b) { - for (var i = 0, node; node = b[i]; i++) - if (node.tagName !== "!") a.push(node); - return a; - }; + Object.extend(Selector.handlers, { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }, + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML. + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } + }); } function $$() { @@ -3850,9 +3844,9 @@ Object.extend(Event, (function() { var cache = Event.cache; function getEventID(element) { - if (element._eventID) return element._eventID; + if (element._prototypeEventID) return element._prototypeEventID[0]; arguments.callee.id = arguments.callee.id || 1; - return element._eventID = ++arguments.callee.id; + return element._prototypeEventID = [++arguments.callee.id]; } function getDOMEventName(eventName) { @@ -3880,7 +3874,7 @@ Object.extend(Event, (function() { return false; Event.extend(event); - handler.call(element, event) + handler.call(element, event); }; wrapper.handler = handler; @@ -3962,11 +3956,12 @@ Object.extend(Event, (function() { if (element == document && document.createEvent && !element.dispatchEvent) element = document.documentElement; + var event; if (document.createEvent) { - var event = document.createEvent("HTMLEvents"); + event = document.createEvent("HTMLEvents"); event.initEvent("dataavailable", true, true); } else { - var event = document.createEventObject(); + event = document.createEventObject(); event.eventType = "ondataavailable"; } @@ -3995,20 +3990,21 @@ Element.addMethods({ Object.extend(document, { fire: Element.Methods.fire.methodize(), observe: Element.Methods.observe.methodize(), - stopObserving: Element.Methods.stopObserving.methodize() + stopObserving: Element.Methods.stopObserving.methodize(), + loaded: false }); (function() { /* Support for the DOMContentLoaded event is based on work by Dan Webb, Matthias Miller, Dean Edwards and John Resig. */ - var timer, fired = false; + var timer; function fireContentLoadedEvent() { - if (fired) return; + if (document.loaded) return; if (timer) window.clearInterval(timer); document.fire("dom:loaded"); - fired = true; + document.loaded = true; } if (document.addEventListener) { -- cgit v1.2.3 From 1b4b1aa725a4f44c3473ae99b36d7cededba2bea Mon Sep 17 00:00:00 2001 From: Kevin Glowacz Date: Sat, 19 Jul 2008 15:08:53 -0500 Subject: Fixed index and auto index for nested fields_for [#327 state:resolved] Signed-off-by: Joshua Peek --- actionpack/lib/action_view/helpers/form_helper.rb | 30 ++++-- actionpack/test/template/form_helper_test.rb | 116 +++++++++++++++++++++- 2 files changed, 136 insertions(+), 10 deletions(-) diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 4fa46d9ee3..ada6fa2ea8 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -528,10 +528,10 @@ module ActionView def initialize(object_name, method_name, template_object, object = nil) @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup - @template_object= template_object + @template_object = template_object @object = object - if @object_name.sub!(/\[\]$/,"") - if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) + if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") + if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param) @auto_index = object.to_param else raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" @@ -708,7 +708,7 @@ module ActionView end def sanitized_object_name - @sanitized_object_name ||= @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") + @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") end def sanitized_method_name @@ -726,6 +726,13 @@ module ActionView def initialize(object_name, object, template, options, proc) @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc @default_options = @options ? @options.slice(:index) : {} + if @object_name.to_s.match(/\[\]$/) + if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) + @auto_index = object.to_param + else + raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + end + end end (field_helpers - %w(label check_box radio_button fields_for)).each do |selector| @@ -738,16 +745,25 @@ module ActionView end def fields_for(record_or_name_or_array, *args, &block) + if options.has_key?(:index) + index = "[#{options[:index]}]" + elsif defined?(@auto_index) + self.object_name = @object_name.to_s.sub(/\[\]$/,"") + index = "[#{@auto_index}]" + else + index = "" + end + case record_or_name_or_array when String, Symbol - name = "#{object_name}[#{record_or_name_or_array}]" + name = "#{object_name}#{index}[#{record_or_name_or_array}]" when Array object = record_or_name_or_array.last - name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" + name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" args.unshift(object) else object = record_or_name_or_array - name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" + name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" args.unshift(object) end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 39649c3622..52e8bf376a 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -6,7 +6,7 @@ silence_warnings do alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast) alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast) alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast) - alias_method :secret?, :secret + alias_method :secret?, :secret def new_record=(boolean) @new_record = boolean @@ -22,6 +22,7 @@ silence_warnings do attr_reader :post_id def save; @id = 1; @post_id = 1 end def new_record?; @id.nil? end + def to_param; @id; end def name @id.nil? ? 'new comment' : "comment ##{@id}" end @@ -30,7 +31,6 @@ end class Comment::Nested < Comment; end - class FormHelperTest < ActionView::TestCase tests ActionView::Helpers::FormHelper @@ -447,6 +447,117 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_nested_fields_for_with_nested_collections + form_for('post[]', @post) do |f| + concat f.text_field(:title) + f.fields_for('comment[]', @comment) do |c| + concat c.text_field(:name) + end + end + + expected = "
" + + "" + + "" + + "
" + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index + form_for('post', @post, :index => 1) do |c| + concat c.text_field(:title) + c.fields_for('comment', @comment, :index => 1) do |r| + concat r.text_field(:name) + end + end + + expected = "
" + + "" + + "" + + "
" + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index + form_for(:post, @post, :index => 1) do |f| + f.fields_for(:comment, @post) do |c| + concat c.text_field(:title) + end + end + + expected = "
" + + "" + + "
" + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_on_both + form_for(:post, @post, :index => 1) do |f| + f.fields_for(:comment, @post, :index => 5) do |c| + concat c.text_field(:title) + end + end + + expected = "
" + + "" + + "
" + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_auto_index + form_for("post[]", @post) do |f| + f.fields_for(:comment, @post) do |c| + concat c.text_field(:title) + end + end + + expected = "
" + + "" + + "
" + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_auto_index_on_both + form_for("post[]", @post) do |f| + f.fields_for("comment[]", @post) do |c| + concat c.text_field(:title) + end + end + + expected = "
" + + "" + + "
" + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_and_auto_index + form_for("post[]", @post) do |f| + f.fields_for(:comment, @post, :index => 5) do |c| + concat c.text_field(:title) + end + end + + form_for(:post, @post, :index => 1) do |f| + f.fields_for("comment[]", @post) do |c| + concat c.text_field(:title) + end + end + + expected = "
" + + "" + + "
" + + "
" + + "" + + "
" + + assert_dom_equal expected, output_buffer + end + def test_fields_for fields_for(:post, @post) do |f| concat f.text_field(:title) @@ -831,7 +942,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - protected def comments_path(post) "/posts/#{post.id}/comments" -- cgit v1.2.3 From 55bfe6be52da1130abb1c148f10d8e9a11368ffd Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 15:27:55 -0500 Subject: Ensure ActionView::PathSet::Path is not initialized with a precompiled path --- actionmailer/lib/action_mailer/base.rb | 3 ++- actionpack/lib/action_view/paths.rb | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index bf60e2f3d5..a43296461b 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -426,7 +426,8 @@ module ActionMailer #:nodoc: end def template_root=(root) - write_inheritable_attribute(:template_root, ActionView::PathSet::Path.new(root)) + root = ActionView::PathSet::Path.new(root) if root.is_a?(String) + write_inheritable_attribute(:template_root, root) end end diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index c7a5df762f..78548d4aa2 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -28,6 +28,8 @@ module ActionView #:nodoc: delegate :to_s, :to_str, :inspect, :to => :path def initialize(path) + raise ArgumentError, "path already is a Path class" if path.is_a?(Path) + @path = path.freeze reload! end -- cgit v1.2.3 From 598b4546fd505efd98c4955f4881b4fd5b953588 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 15:43:39 -0500 Subject: Fixed spelling mistake in deprecation warning [#381 state:resolved] --- actionpack/lib/action_view/base.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index fe51af62e6..619a4270f8 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -172,13 +172,15 @@ module ActionView #:nodoc: end def self.cache_template_loading=(*args) - ActiveSupport::Deprecation.warn("config.action_view.cache_template_loading option has been deprecated and has no affect. " << - "Please remove it from your config files.", caller) + ActiveSupport::Deprecation.warn( + "config.action_view.cache_template_loading option has been deprecated" + + "and has no effect. Please remove it from your config files.", caller) end def self.cache_template_extensions=(*args) - ActiveSupport::Deprecation.warn("config.action_view.cache_template_extensions option has been deprecated and has no effect. " << - "Please remove it from your config files.", caller) + ActiveSupport::Deprecation.warn( + "config.action_view.cache_template_extensions option has been" + + "deprecated and has no effect. Please remove it from your config files.", caller) end # Specify whether RJS responses should be wrapped in a try/catch block -- cgit v1.2.3 From da85251f85420d3a906a1eb949b73bb286122ea9 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 19 Jul 2008 17:32:22 -0500 Subject: Refactor ActiveSupport::Dependencies injector so it would be possible to disable it --- activesupport/lib/active_support/dependencies.rb | 218 ++++++++++++----------- 1 file changed, 114 insertions(+), 104 deletions(-) diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 2f3fa72bb4..e3d4f3d7eb 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -1,8 +1,3 @@ -require 'set' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/load_error' -require 'active_support/core_ext/kernel' - module ActiveSupport #:nodoc: module Dependencies #:nodoc: extend self @@ -52,6 +47,119 @@ module ActiveSupport #:nodoc: mattr_accessor :constant_watch_stack self.constant_watch_stack = [] + # Module includes this module + module ModuleConstMissing #:nodoc: + def self.included(base) #:nodoc: + base.class_eval do + # Rename the original handler so we can chain it to the new one + alias_method :rails_original_const_missing, :const_missing + + # Use const_missing to autoload associations so we don't have to + # require_association when using single-table inheritance. + def const_missing(class_id) + ActiveSupport::Dependencies.load_missing_constant self, class_id + end + end + end + + def unloadable(const_desc = self) + super(const_desc) + end + end + + # Class includes this module + module ClassConstMissing #:nodoc: + def const_missing(const_name) + if [Object, Kernel].include?(self) || parent == self + super + else + begin + begin + Dependencies.load_missing_constant self, const_name + rescue NameError + parent.send :const_missing, const_name + end + rescue NameError => e + # Make sure that the name we are missing is the one that caused the error + parent_qualified_name = Dependencies.qualified_name_for parent, const_name + raise unless e.missing_name? parent_qualified_name + qualified_name = Dependencies.qualified_name_for self, const_name + raise NameError.new("uninitialized constant #{qualified_name}").copy_blame!(e) + end + end + end + end + + # Object includes this module + module Loadable #:nodoc: + def load(file, *extras) #:nodoc: + Dependencies.new_constants_in(Object) { super } + rescue Exception => exception # errors from loading file + exception.blame_file! file + raise + end + + def require(file, *extras) #:nodoc: + Dependencies.new_constants_in(Object) { super } + rescue Exception => exception # errors from required file + exception.blame_file! file + raise + end + + # Mark the given constant as unloadable. Unloadable constants are removed each + # time dependencies are cleared. + # + # Note that marking a constant for unloading need only be done once. Setup + # or init scripts may list each unloadable constant that may need unloading; + # each constant will be removed for every subsequent clear, as opposed to for + # the first clear. + # + # The provided constant descriptor may be a (non-anonymous) module or class, + # or a qualified constant name as a string or symbol. + # + # Returns true if the constant was not previously marked for unloading, false + # otherwise. + def unloadable(const_desc) + Dependencies.mark_for_unload const_desc + end + end + + # Exception file-blaming + module Blamable #:nodoc: + def blame_file!(file) + (@blamed_files ||= []).unshift file + end + + def blamed_files + @blamed_files ||= [] + end + + def describe_blame + return nil if blamed_files.empty? + "This error occurred while loading the following files:\n #{blamed_files.join "\n "}" + end + + def copy_blame!(exc) + @blamed_files = exc.blamed_files.clone + self + end + end + + def inject! + Object.instance_eval do + define_method(:require_or_load) { |file_name| Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load) + define_method(:require_dependency) { |file_name| Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency) + define_method(:require_association) { |file_name| Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association) + + alias_method :load_without_new_constant_marking, :load + include Loadable + end + + Module.instance_eval { include ModuleConstMissing } + Class.instance_eval { include ClassConstMissing } + Exception.instance_eval { include Blamable } + end + def load? mechanism == :load end @@ -452,102 +560,4 @@ module ActiveSupport #:nodoc: end end -Object.instance_eval do - define_method(:require_or_load) { |file_name| ActiveSupport::Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load) - define_method(:require_dependency) { |file_name| ActiveSupport::Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency) - define_method(:require_association) { |file_name| ActiveSupport::Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association) -end - -class Module #:nodoc: - # Rename the original handler so we can chain it to the new one - alias :rails_original_const_missing :const_missing - - # Use const_missing to autoload associations so we don't have to - # require_association when using single-table inheritance. - def const_missing(class_id) - ActiveSupport::Dependencies.load_missing_constant self, class_id - end - - def unloadable(const_desc = self) - super(const_desc) - end - -end - -class Class - def const_missing(const_name) - if [Object, Kernel].include?(self) || parent == self - super - else - begin - begin - ActiveSupport::Dependencies.load_missing_constant self, const_name - rescue NameError - parent.send :const_missing, const_name - end - rescue NameError => e - # Make sure that the name we are missing is the one that caused the error - parent_qualified_name = ActiveSupport::Dependencies.qualified_name_for parent, const_name - raise unless e.missing_name? parent_qualified_name - qualified_name = ActiveSupport::Dependencies.qualified_name_for self, const_name - raise NameError.new("uninitialized constant #{qualified_name}").copy_blame!(e) - end - end - end -end - -class Object - alias_method :load_without_new_constant_marking, :load - - def load(file, *extras) #:nodoc: - ActiveSupport::Dependencies.new_constants_in(Object) { super } - rescue Exception => exception # errors from loading file - exception.blame_file! file - raise - end - - def require(file, *extras) #:nodoc: - ActiveSupport::Dependencies.new_constants_in(Object) { super } - rescue Exception => exception # errors from required file - exception.blame_file! file - raise - end - - # Mark the given constant as unloadable. Unloadable constants are removed each - # time dependencies are cleared. - # - # Note that marking a constant for unloading need only be done once. Setup - # or init scripts may list each unloadable constant that may need unloading; - # each constant will be removed for every subsequent clear, as opposed to for - # the first clear. - # - # The provided constant descriptor may be a (non-anonymous) module or class, - # or a qualified constant name as a string or symbol. - # - # Returns true if the constant was not previously marked for unloading, false - # otherwise. - def unloadable(const_desc) - ActiveSupport::Dependencies.mark_for_unload const_desc - end -end - -# Add file-blaming to exceptions -class Exception #:nodoc: - def blame_file!(file) - (@blamed_files ||= []).unshift file - end - - def blamed_files - @blamed_files ||= [] - end - - def describe_blame - return nil if blamed_files.empty? - "This error occurred while loading the following files:\n #{blamed_files.join "\n "}" - end - - def copy_blame!(exc) - @blamed_files = exc.blamed_files.clone - self - end -end +ActiveSupport::Dependencies.inject! -- cgit v1.2.3 From 006cbb8fde3f20a684eabcfd11c53ae762cf8435 Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Sun, 20 Jul 2008 21:23:18 +0200 Subject: Handle the case where 64bit time_t won't overflow. --- activesupport/test/core_ext/time_ext_test.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 17a0968c0e..14309f4e2d 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -528,7 +528,12 @@ class TimeExtCalculationsTest < Test::Unit::TestCase assert_equal Time.time_with_datetime_fallback(:utc, 2005), Time.utc(2005) assert_equal Time.time_with_datetime_fallback(:utc, 2039), DateTime.civil(2039, 1, 1, 0, 0, 0, 0, 0) assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30, 1), Time.utc(2005, 2, 21, 17, 44, 30, 1) #with usec - assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1), DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0) + # This won't overflow on 64bit linux + expected_to_overflow = Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1) + unless expected_to_overflow.is_a?(Time) + assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1), + DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0) + end assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value end -- cgit v1.2.3 From 5bec5848b22527ee77c007565f7eea336e5c864f Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Sun, 20 Jul 2008 21:25:31 +0200 Subject: If it doesn't overflow, it won't have a .start method. So don't run the second assertion either --- activesupport/test/core_ext/time_ext_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 14309f4e2d..8740497b3d 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -533,8 +533,8 @@ class TimeExtCalculationsTest < Test::Unit::TestCase unless expected_to_overflow.is_a?(Time) assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1), DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0) + assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value end - assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value end def test_utc_time -- cgit v1.2.3 From ff9f6fcc75526d9fd89be834982dec8624c909c5 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Mon, 21 Jul 2008 12:57:15 -0500 Subject: Refactor DateHelper and improve test coverage [#665 state:resolved] Signed-off-by: Joshua Peek --- actionpack/lib/action_view/helpers/date_helper.rb | 298 +++++++++++----------- actionpack/test/template/date_helper_test.rb | 144 +++++++---- 2 files changed, 238 insertions(+), 204 deletions(-) diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 2cdb9a224e..c7a1d40ff2 100755 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -3,14 +3,15 @@ require 'action_view/helpers/tag_helper' module ActionView module Helpers - # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods - # share a number of common options that are as follows: + # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the + # select-type methods share a number of common options that are as follows: # - # * :prefix - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give - # birthday[month] instead of date[month] if passed to the select_month method. + # * :prefix - overwrites the default prefix of "date" used for the select names. So specifying "birthday" + # would give birthday[month] instead of date[month] if passed to the select_month method. # * :include_blank - set to true if it should be possible to set an empty date. - # * :discard_type - set to true if you want to discard the type part of the select name. If set to true, the select_month - # method would use simply "date" (which can be overwritten using :prefix) instead of "date[month]". + # * :discard_type - set to true if you want to discard the type part of the select name. If set to true, + # the select_month method would use simply "date" (which can be overwritten using :prefix) instead of + # "date[month]". module DateHelper include ActionView::Helpers::TagHelper DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') @@ -67,7 +68,7 @@ module ActionView I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale| case distance_in_minutes when 0..1 - return distance_in_minutes == 0 ? + return distance_in_minutes == 0 ? locale.t(:less_than_x_minutes, :count => 1) : locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds @@ -91,7 +92,7 @@ module ActionView else locale.t :over_x_years, :count => (distance_in_minutes / 525600).round end end - end + end # Like distance_of_time_in_words, but where to_time is fixed to Time.now. # @@ -107,15 +108,18 @@ module ActionView alias_method :distance_of_time_in_words_to_now, :time_ago_in_words - # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by - # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, - # which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of - # discard options. The discard options are :discard_year, :discard_month and :discard_day. Set to true, they'll - # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly - # set the order of the tags using the :order option with an array of symbols :year, :month and :day in - # the desired order. Symbols may be omitted and the respective select is not included. + # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). It's + # possible to tailor the selects through the +options+ hash, which accepts all the keys that each of the + # individual select builders do (like :use_month_numbers for select_month) as well as a range of discard + # options. The discard options are :discard_year, :discard_month and :discard_day. Set + # to true, they'll drop the respective select. Discarding the month select will also automatically discard the + # day select. It's also possible to explicitly set the order of the tags using the :order option with an + # array of symbols :year, :month and :day in the desired order. Symbols may be omitted + # and the respective select is not included. # - # Pass the :default option to set the default date. Use a Time object or a Hash of :year, :month, :day, :hour, :minute, and :second. + # Pass the :default option to set the default date. Use a Time object or a Hash of :year, + # :month, :day, :hour, :minute, and :second. # # Passing :disabled => true as part of the +options+ will make elements inaccessible for change. # @@ -133,7 +137,7 @@ module ActionView # # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute, # # with the year in the year drop down box starting at 1995, numbers used for months instead of words, - # # and without a day select box. + # # and without a day select box. # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, # :discard_day => true, :include_blank => true) # @@ -155,8 +159,8 @@ module ActionView # # The selects are prepared for multi-parameter assignment to an Active Record object. # - # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month - # choices are valid. + # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that + # all month choices are valid. def date_select(object_name, method, options = {}, html_options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options) end @@ -180,12 +184,12 @@ module ActionView # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute # time_select("mail", "sent_at") # - # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in - # # the sunrise attribute. + # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in + # # the sunrise attribute. # time_select("post", "start_time", :include_seconds => true) # - # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in - # # the submission_time attribute. + # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in + # # the submission_time attribute. # time_select("entry", "submission_time", :include_seconds => true) # # # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45. @@ -193,14 +197,15 @@ module ActionView # # The selects are prepared for multi-parameter assignment to an Active Record object. # - # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month - # choices are valid. + # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that + # all month choices are valid. def time_select(object_name, method, options = {}, html_options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_time_select_tag(options, html_options) end - # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based - # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: + # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a + # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified + # by +object+). Examples: # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -208,16 +213,16 @@ module ActionView # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute # datetime_select("post", "written_on") # - # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the + # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the # # post variable in the written_on attribute. # datetime_select("post", "written_on", :start_year => 1995) # - # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will be stored in the - # # trip variable in the departing attribute. + # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will + # # be stored in the trip variable in the departing attribute. # datetime_select("trip", "departing", :default => 3.days.from_now) # - # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable as the written_on - # # attribute. + # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable + # # as the written_on attribute. # datetime_select("post", "written_on", :discard_type => true) # # The selects are prepared for multi-parameter assignment to an Active Record object. @@ -227,9 +232,10 @@ module ActionView # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. # It's also possible to explicitly set the order of the tags using the :order option with an array of - # symbols :year, :month and :day in the desired order. If you do not supply a Symbol, it - # will be appended onto the :order passed in. You can also add :date_separator and :time_separator - # keys to the +options+ to control visual display of the elements. + # symbols :year, :month and :day in the desired order. If you do not supply a Symbol, + # it will be appended onto the :order passed in. You can also add :date_separator, + # :datetime_separator and :time_separator keys to the +options+ to control visual display of + # the elements. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -250,7 +256,12 @@ module ActionView # # with a '/' between each date field. # select_datetime(my_date_time, :date_separator => '/') # - # # Generates a datetime select that discards the type of the field and defaults to the datetime in + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # with a date fields separated by '/', time fields separated by '' and the date and time fields + # # separated by a comma (','). + # select_datetime(my_date_time, :date_separator => '/', :time_separator => '', :datetime_separator => ',') + # + # # Generates a datetime select that discards the type of the field and defaults to the datetime in # # my_date_time (four days after today) # select_datetime(my_date_time, :discard_type => true) # @@ -261,7 +272,7 @@ module ActionView def select_datetime(datetime = Time.current, options = {}, html_options = {}) separator = options[:datetime_separator] || '' select_date(datetime, options, html_options) + separator + select_time(datetime, options, html_options) - end + end # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. # It's possible to explicitly set the order of the tags using the :order option with an array of @@ -283,27 +294,29 @@ module ActionView # # with the fields ordered year, month, day rather than month, day, year. # select_date(my_date, :order => [:year, :month, :day]) # - # # Generates a date select that discards the type of the field and defaults to the date in + # # Generates a date select that discards the type of the field and defaults to the date in # # my_date (six days after today) # select_date(my_date, :discard_type => true) # + # # Generates a date select that defaults to the date in my_date, + # # which has fields separated by '/' + # select_date(my_date, :date_separator => '/') + # # # Generates a date select that defaults to the datetime in my_date (six days after today) # # prefixed with 'payday' rather than 'date' # select_date(my_date, :prefix => 'payday') # def select_date(date = Date.current, options = {}, html_options = {}) - options[:order] ||= [] + options.reverse_merge!(:order => [], :date_separator => '') [:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) } - select_date = '' - options[:order].each do |o| - select_date << self.send("select_#{o}", date, options, html_options) - end - select_date + options[:order].inject([]) { |s, o| + s << self.send("select_#{o}", date, options, html_options) + }.join(options[:date_separator]) end # Returns a set of html select-tags (one for hour and minute) - # You can set :time_separator key to format the output, and + # You can set :time_separator key to format the output, and # the :include_seconds option to include an input for seconds. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. @@ -318,7 +331,7 @@ module ActionView # select_time() # # # Generates a time select that defaults to the time in my_time, - # # which has fields separated by ':' + # # which has fields separated by ':' # select_time(my_time, :time_separator => ':') # # # Generates a time select that defaults to the time in my_time, @@ -331,7 +344,8 @@ module ActionView # def select_time(datetime = Time.current, options = {}, html_options = {}) separator = options[:time_separator] || '' - select_hour(datetime, options, html_options) + separator + select_minute(datetime, options, html_options) + (options[:include_seconds] ? separator + select_second(datetime, options, html_options) : '') + select_hour(datetime, options, html_options) + separator + select_minute(datetime, options, html_options) + + (options[:include_seconds] ? separator + select_second(datetime, options, html_options) : '') end # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. @@ -346,26 +360,16 @@ module ActionView # # # Generates a select field for seconds that defaults to the number given # select_second(33) - # + # # # Generates a select field for seconds that defaults to the seconds for the time in my_time # # that is named 'interval' rather than 'second' # select_second(my_time, :field_name => 'interval') # def select_second(datetime, options = {}, html_options = {}) val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : '' - if options[:use_hidden] - options[:include_seconds] ? hidden_html(options[:field_name] || 'second', val, options) : '' - else - second_options = [] - 0.upto(59) do |second| - second_options << ((val == second) ? - content_tag(:option, leading_zero_on_single_digits(second), :value => leading_zero_on_single_digits(second), :selected => "selected") : - content_tag(:option, leading_zero_on_single_digits(second), :value => leading_zero_on_single_digits(second)) - ) - second_options << "\n" - end - select_html(options[:field_name] || 'second', second_options.join, options, html_options) - end + options[:use_hidden] ? + (options[:include_seconds] ? _date_hidden_html(options[:field_name] || 'second', val, options) : '') : + _date_select_html(options[:field_name] || 'second', _date_build_options(val), options, html_options) end # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. @@ -381,26 +385,17 @@ module ActionView # # # Generates a select field for minutes that defaults to the number given # select_minute(14) - # + # # # Generates a select field for minutes that defaults to the minutes for the time in my_time # # that is named 'stride' rather than 'second' # select_minute(my_time, :field_name => 'stride') # def select_minute(datetime, options = {}, html_options = {}) val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : '' - if options[:use_hidden] - hidden_html(options[:field_name] || 'minute', val, options) - else - minute_options = [] - 0.step(59, options[:minute_step] || 1) do |minute| - minute_options << ((val == minute) ? - content_tag(:option, leading_zero_on_single_digits(minute), :value => leading_zero_on_single_digits(minute), :selected => "selected") : - content_tag(:option, leading_zero_on_single_digits(minute), :value => leading_zero_on_single_digits(minute)) - ) - minute_options << "\n" - end - select_html(options[:field_name] || 'minute', minute_options.join, options, html_options) - end + options[:use_hidden] ? + _date_hidden_html(options[:field_name] || 'minute', val, options) : + _date_select_html(options[:field_name] || 'minute', + _date_build_options(val, :step => options[:minute_step]), options, html_options) end # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. @@ -415,26 +410,15 @@ module ActionView # # # Generates a select field for minutes that defaults to the number given # select_minute(14) - # + # # # Generates a select field for minutes that defaults to the minutes for the time in my_time # # that is named 'stride' rather than 'second' # select_minute(my_time, :field_name => 'stride') # def select_hour(datetime, options = {}, html_options = {}) val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : '' - if options[:use_hidden] - hidden_html(options[:field_name] || 'hour', val, options) - else - hour_options = [] - 0.upto(23) do |hour| - hour_options << ((val == hour) ? - content_tag(:option, leading_zero_on_single_digits(hour), :value => leading_zero_on_single_digits(hour), :selected => "selected") : - content_tag(:option, leading_zero_on_single_digits(hour), :value => leading_zero_on_single_digits(hour)) - ) - hour_options << "\n" - end - select_html(options[:field_name] || 'hour', hour_options.join, options, html_options) - end + options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'hour', val, options) : + _date_select_html(options[:field_name] || 'hour', _date_build_options(val, :end => 23), options, html_options) end # Returns a select tag with options for each of the days 1 through 31 with the current day selected. @@ -449,36 +433,27 @@ module ActionView # # # Generates a select field for days that defaults to the number given # select_day(5) - # + # # # Generates a select field for days that defaults to the day for the date in my_date # # that is named 'due' rather than 'day' # select_day(my_time, :field_name => 'due') # def select_day(date, options = {}, html_options = {}) val = date ? (date.kind_of?(Fixnum) ? date : date.day) : '' - if options[:use_hidden] - hidden_html(options[:field_name] || 'day', val, options) - else - day_options = [] - 1.upto(31) do |day| - day_options << ((val == day) ? - content_tag(:option, day, :value => day, :selected => "selected") : - content_tag(:option, day, :value => day) - ) - day_options << "\n" - end - select_html(options[:field_name] || 'day', day_options.join, options, html_options) - end + options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'day', val, options) : + _date_select_html(options[:field_name] || 'day', + _date_build_options(val, :start => 1, :end => 31, :leading_zeros => false), + options, html_options) end - # Returns a select tag with options for each of the months January through December with the current month selected. - # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values - # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- - # set the :use_month_numbers key in +options+ to true for this to happen. If you want both numbers and names, - # set the :add_month_numbers key in +options+ to true. If you would prefer to show month names as abbreviations, - # set the :use_short_month key in +options+ to true. If you want to use your own month names, set the - # :use_month_names key in +options+ to an array of 12 month names. Override the field name using the - # :field_name option, 'month' by default. + # Returns a select tag with options for each of the months January through December with the current month + # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are + # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation + # instead of names -- set the :use_month_numbers key in +options+ to true for this to happen. If you + # want both numbers and names, set the :add_month_numbers key in +options+ to true. If you would prefer + # to show month names as abbreviations, set the :use_short_month key in +options+ to true. If you want + # to use your own month names, set the :use_month_names key in +options+ to an array of 12 month names. + # Override the field name using the :field_name option, 'month' by default. # # ==== Examples # # Generates a select field for months that defaults to the current month that @@ -490,7 +465,7 @@ module ActionView # select_month(Date.today, :field_name => 'start') # # # Generates a select field for months that defaults to the current month that - # # will use keys like "1", "3". + # # will use keys like "1", "3". # select_month(Date.today, :use_month_numbers => true) # # # Generates a select field for months that defaults to the current month that @@ -506,11 +481,11 @@ module ActionView # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # def select_month(date, options = {}, html_options = {}) - locale = options[:locale] + locale = options[:locale] val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' if options[:use_hidden] - hidden_html(options[:field_name] || 'month', val, options) + _date_hidden_html(options[:field_name] || 'month', val, options) else month_options = [] month_names = options[:use_month_names] || begin @@ -534,14 +509,15 @@ module ActionView ) month_options << "\n" end - select_html(options[:field_name] || 'month', month_options.join, options, html_options) + _date_select_html(options[:field_name] || 'month', month_options.join, options, html_options) end - end + end - # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius - # can be changed using the :start_year and :end_year keys in the +options+. Both ascending and descending year - # lists are supported by making :start_year less than or greater than :end_year. The date can also be - # substituted for a year given as a number. Override the field name using the :field_name option, 'year' by default. + # Returns a select tag with options for each of the five years on each side of the current, which is selected. + # The five year radius can be changed using the :start_year and :end_year keys in the + # +options+. Both ascending and descending year lists are supported by making :start_year less than or + # greater than :end_year. The date can also be substituted for a year given as a number. + # Override the field name using the :field_name option, 'year' by default. # # ==== Examples # # Generates a select field for years that defaults to the current year that @@ -562,38 +538,48 @@ module ActionView # def select_year(date, options = {}, html_options = {}) if !date || date == 0 - value = '' + val = '' middle_year = Date.today.year elsif date.kind_of?(Fixnum) - value = middle_year = date + val = middle_year = date else - value = middle_year = date.year + val = middle_year = date.year end if options[:use_hidden] - hidden_html(options[:field_name] || 'year', value, options) + _date_hidden_html(options[:field_name] || 'year', val, options) else - year_options = '' - start_year = options[:start_year] || middle_year - 5 - end_year = options[:end_year] || middle_year + 5 - step_val = start_year < end_year ? 1 : -1 - - start_year.step(end_year, step_val) do |year| - if value == year - year_options << content_tag(:option, year, :value => year, :selected => "selected") - else - year_options << content_tag(:option, year, :value => year) - end - year_options << "\n" - end - select_html(options[:field_name] || 'year', year_options, options, html_options) + options[:start_year] ||= middle_year - 5 + options[:end_year] ||= middle_year + 5 + step = options[:start_year] < options[:end_year] ? 1 : -1 + + _date_select_html(options[:field_name] || 'year', + _date_build_options(val, + :start => options[:start_year], + :end => options[:end_year], + :step => step, + :leading_zeros => false + ), options, html_options) end end private + def _date_build_options(selected, options={}) + options.reverse_merge!(:start => 0, :end => 59, :step => 1, :leading_zeros => true) + + select_options = [] + (options[:start] || 0).step((options[:end] || 59), options[:step] || 1) do |i| + value = options[:leading_zeros] ? sprintf("%02d", i) : i + tag_options = { :value => value } + tag_options[:selected] = "selected" if selected == i + + select_options << content_tag(:option, value, tag_options) + end + select_options.join("\n") + "\n" + end - def select_html(type, html_options, options, select_tag_options = {}) - name_and_id_from_options(options, type) + def _date_select_html(type, html_options, options, select_tag_options = {}) + _date_name_and_id_from_options(options, type) select_options = {:id => options[:id], :name => options[:name]} select_options.merge!(:disabled => 'disabled') if options[:disabled] select_options.merge!(select_tag_options) unless select_tag_options.empty? @@ -603,19 +589,15 @@ module ActionView content_tag(:select, select_html, select_options) + "\n" end - def hidden_html(type, value, options) - name_and_id_from_options(options, type) + def _date_hidden_html(type, value, options) + _date_name_and_id_from_options(options, type) hidden_html = tag(:input, :type => "hidden", :id => options[:id], :name => options[:name], :value => value) + "\n" end - def name_and_id_from_options(options, type) + def _date_name_and_id_from_options(options, type) options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]") options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') end - - def leading_zero_on_single_digits(number) - number > 9 ? number : "0#{number}" - end end class InstanceTag #:nodoc: @@ -641,11 +623,11 @@ module ActionView options = defaults.merge(options) datetime = value(object) datetime ||= default_time_from_options(options[:default]) unless options[:include_blank] - + position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } order = options[:order] ||= I18n.translate(:'date.order', :locale => locale) - + # Discard explicit and implicit by not being included in the :order discard = {} discard[:year] = true if options[:discard_year] or !order.include?(:year) @@ -654,26 +636,30 @@ module ActionView discard[:hour] = true if options[:discard_hour] discard[:minute] = true if options[:discard_minute] or discard[:hour] discard[:second] = true unless options[:include_seconds] && !discard[:minute] - + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid # (otherwise it could be 31 and february wouldn't be a valid date) if datetime && discard[:day] && !discard[:month] datetime = datetime.change(:day => 1) end - + # Maintain valid dates by including hidden fields for discarded elements [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } - + # Ensure proper ordering of :hour, :minute and :second [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) } - + date_or_time_select = '' order.reverse.each do |param| # Send hidden fields for discarded elements once output has started # This ensures AR can reconstruct valid dates using ParseDate next if discard[param] && (date_or_time_select.empty? || options[:ignore_date]) - date_or_time_select.insert(0, self.send("select_#{param}", datetime, options_with_prefix(position[param], options.merge(:use_hidden => discard[param])), html_options)) + date_or_time_select.insert(0, + self.send("select_#{param}", + datetime, + options_with_prefix(position[param], options.merge(:use_hidden => discard[param])), + html_options)) date_or_time_select.insert(0, case param when :hour then (discard[:year] && discard[:day] ? "" : " — ") @@ -682,7 +668,7 @@ module ActionView else "" end) end - + date_or_time_select end @@ -708,7 +694,7 @@ module ActionView default[:sec] ||= default[:second] time = Time.current - + [:year, :month, :day, :hour, :min, :sec].each do |key| default[key] ||= time.send(key) end diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index 8b4e94c67f..d8c07e731b 100755 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -17,7 +17,7 @@ class DateHelperTest < ActionView::TestCase end end end - + def assert_distance_of_time_in_words(from, to=nil) to ||= from @@ -86,13 +86,13 @@ class DateHelperTest < ActionView::TestCase from = Time.mktime(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) end - + def test_distance_in_words_with_time_zones from = Time.mktime(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from.in_time_zone('Alaska')) assert_distance_of_time_in_words(from.in_time_zone('Hawaii')) end - + def test_distance_in_words_with_different_time_zones from = Time.mktime(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words( @@ -100,13 +100,13 @@ class DateHelperTest < ActionView::TestCase from.in_time_zone('Hawaii') ) end - + def test_distance_in_words_with_dates start_date = Date.new 1975, 1, 31 end_date = Date.new 1977, 1, 31 assert_equal("over 2 years", distance_of_time_in_words(start_date, end_date)) end - + def test_distance_in_words_with_integers assert_equal "less than a minute", distance_of_time_in_words(59) assert_equal "about 1 hour", distance_of_time_in_words(60*60) @@ -757,6 +757,26 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => "selector") end + def test_select_date_with_separator + expected = %(\n" + + expected << " / " + + expected << %(\n" + + expected << " / " + + expected << %(\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) + end + def test_select_datetime expected = %(\n) + expected << %(\n\n\n) + expected << "\n" + + expected << "/" + + expected << %(\n" + + expected << "/" + + expected << %(\n" + + expected << "—" + + expected << %(\n" + + expected << ":" + + expected << %(\n" + + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "—", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector') + end + def test_select_time expected = %(\n} expected << %(\n" expected << " : " expected << %(\n" assert_dom_equal expected, time_select("post", "written_on") @@ -1203,11 +1255,11 @@ class DateHelperTest < ActionView::TestCase @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) expected = %(\n" expected << " : " expected << %(\n" assert_dom_equal expected, time_select("post", "written_on", :ignore_date => true) @@ -1222,15 +1274,15 @@ class DateHelperTest < ActionView::TestCase expected << %{\n} expected << %(\n" expected << " : " expected << %(\n" expected << " : " expected << %(\n" assert_dom_equal expected, time_select("post", "written_on", :include_seconds => true) @@ -1245,11 +1297,11 @@ class DateHelperTest < ActionView::TestCase expected << %{\n} expected << %(\n" expected << " : " expected << %(\n" assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector') @@ -1268,11 +1320,11 @@ class DateHelperTest < ActionView::TestCase expected << %{\n} expected << %(\n" expected << " : " expected << %(\n" assert_dom_equal expected, output_buffer @@ -1306,7 +1358,7 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, datetime_select("post", "updated_at") end - + uses_mocha 'TestDatetimeSelectDefaultsToTimeZoneNowWhenConfigTimeZoneIsSet' do def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0) @@ -1370,8 +1422,7 @@ class DateHelperTest < ActionView::TestCase expected << "\n" expected << %(\n" assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]") @@ -1388,8 +1439,7 @@ class DateHelperTest < ActionView::TestCase expected << "\n" expected << %(\n" assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]") @@ -1405,8 +1455,7 @@ class DateHelperTest < ActionView::TestCase expected << "\n" expected << %(\n" assert_dom_equal expected, select_date(0, :prefix => "date[first]") @@ -1422,8 +1471,7 @@ class DateHelperTest < ActionView::TestCase expected << "\n" expected << %(\n" assert_dom_equal expected, select_date(nil, :prefix => "date[first]") @@ -1530,15 +1578,15 @@ class DateHelperTest < ActionView::TestCase expected << " — " expected << %{\n" expected << " : " expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :include_seconds => true) @@ -1559,11 +1607,11 @@ class DateHelperTest < ActionView::TestCase expected << " — " expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true) @@ -1582,11 +1630,11 @@ class DateHelperTest < ActionView::TestCase expected << " — " expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :discard_month => true) @@ -1601,11 +1649,11 @@ class DateHelperTest < ActionView::TestCase expected << %{\n} expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true, :discard_month => true) @@ -1628,11 +1676,11 @@ class DateHelperTest < ActionView::TestCase expected << " — " expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:minute, :day, :hour, :month, :year, :second]) @@ -1653,11 +1701,11 @@ class DateHelperTest < ActionView::TestCase expected << " — " expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:day, :month]) @@ -1680,11 +1728,11 @@ class DateHelperTest < ActionView::TestCase expected << " — " expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35)) @@ -1727,11 +1775,11 @@ class DateHelperTest < ActionView::TestCase expected << " — " expected << %{\n" expected << " : " expected << %{\n" assert_dom_equal expected, datetime_select("post", "updated_at", :default => { :month => 10, :minute => 42, :hour => 9 }) @@ -1780,19 +1828,19 @@ class DateHelperTest < ActionView::TestCase assert_equal 2, dummy_instance_tag.send!(:default_time_from_options, :hour => 2).hour end end - + def test_instance_tag_default_time_from_options_handles_far_future_date dummy_instance_tag = ActionView::Helpers::InstanceTag.new(1,2,3) time = dummy_instance_tag.send!(:default_time_from_options, :year => 2050, :month => 2, :day => 10, :hour => 15, :min => 30, :sec => 45) assert_equal 2050, time.year end end - + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz yield ensure old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end + end end -- cgit v1.2.3 From 0f43de644ea48c1ad11d4bc73307af066bb52870 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Mon, 21 Jul 2008 13:05:27 -0500 Subject: Refactored NumberHelper API to accept arguments as an options hash [#666 state:resolved] Signed-off-by: Joshua Peek --- .../lib/action_view/helpers/number_helper.rb | 98 +++++++++++++++------- actionpack/test/template/number_helper_test.rb | 28 ++++++- 2 files changed, 93 insertions(+), 33 deletions(-) diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 6bb8263794..c4ba7ccc8e 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -22,7 +22,7 @@ module ActionView # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234 # # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") - # => +1.123.555.1234 x 1343 + # => +1.123.555.1234 x 1343 def number_to_phone(number, options = {}) number = number.to_s.strip unless number.nil? options = options.stringify_keys @@ -71,14 +71,14 @@ module ActionView def number_to_currency(number, options = {}) options = options.symbolize_keys defaults = I18n.translate(:'currency.format', :locale => options[:locale]) || {} - + precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] separator = options[:separator] || defaults[:separator] delimiter = options[:delimiter] || defaults[:delimiter] format = options[:format] || defaults[:format] separator = '' if precision == 0 - + begin parts = number_with_precision(number, precision).split('.') format.gsub(/%n/, number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s).gsub(/%u/, unit) @@ -118,49 +118,72 @@ module ActionView end end - # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You - # can customize the format using optional delimiter and separator parameters. + # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You can + # customize the format in the +options+ hash. # # ==== Options - # * delimiter - Sets the thousands delimiter (defaults to ","). - # * separator - Sets the separator between the units (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ","). + # * :separator - Sets the separator between the units (defaults to "."). # # ==== Examples - # number_with_delimiter(12345678) # => 12,345,678 - # number_with_delimiter(12345678.05) # => 12,345,678.05 - # number_with_delimiter(12345678, ".") # => 12.345.678 - # - # number_with_delimiter(98765432.98, " ", ",") + # number_with_delimiter(12345678) # => 12,345,678 + # number_with_delimiter(12345678.05) # => 12,345,678.05 + # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678 + # number_with_delimiter(12345678, :seperator => ",") # => 12,345,678 + # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",") # # => 98 765 432,98 - def number_with_delimiter(number, delimiter=",", separator=".") + # + # You can still use number_with_delimiter with the old API that accepts the + # +delimiter+ as its optional second and the +separator+ as its + # optional third parameter: + # number_with_delimiter(12345678, " ") # => 12 345.678 + # number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05 + def number_with_delimiter(number, *args) + options = args.extract_options! + unless args.empty? + options[:delimiter] = args[0] || "," + options[:separator] = args[1] || "." + end + options.reverse_merge!(:delimiter => ",", :separator => ".") + begin parts = number.to_s.split('.') - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") - parts.join separator + parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") + parts.join options[:separator] rescue number end end - # Formats a +number+ with the specified level of +precision+ (e.g., 112.32 has a precision of 2). The default - # level of precision is 3. + # Formats a +number+ with the specified level of :precision (e.g., 112.32 has a precision of 2). + # The default level of precision is 3. # # ==== Examples - # number_with_precision(111.2345) # => 111.235 - # number_with_precision(111.2345, 2) # => 111.23 - # number_with_precision(13, 5) # => 13.00000 - # number_with_precision(389.32314, 0) # => 389 - def number_with_precision(number, precision=3) - "%01.#{precision}f" % ((Float(number) * (10 ** precision)).round.to_f / 10 ** precision) + # number_with_precision(111.2345) # => 111.235 + # number_with_precision(111.2345, :precision => 2) # => 111.23 + # number_with_precision(13, :precision => 5) # => 13.00000 + # number_with_precision(389.32314, :precision => 0) # => 389 + # + # You can still use number_with_precision with the old API that accepts the + # +precision+ as its optional second parameter: + # number_with_precision(number_with_precision(111.2345, 2) # => 111.23 + def number_with_precision(number, *args) + options = args.extract_options! + unless args.empty? + options[:precision] = args[0] || 3 + end + options.reverse_merge!(:precision => 3) + "%01.#{options[:precision]}f" % + ((Float(number) * (10 ** options[:precision])).round.to_f / 10 ** options[:precision]) rescue number end # Formats the bytes in +size+ into a more understandable representation - # (e.g., giving it 1500 yields 1.5 KB). This method is useful for + # (e.g., giving it 1500 yields 1.5 KB). This method is useful for # reporting file sizes to users. This method returns nil if # +size+ cannot be converted into a number. You can change the default - # precision of 1 using the precision parameter +precision+. + # precision of 1 using the precision parameter :precision. # # ==== Examples # number_to_human_size(123) # => 123 Bytes @@ -169,17 +192,28 @@ module ActionView # number_to_human_size(1234567) # => 1.2 MB # number_to_human_size(1234567890) # => 1.1 GB # number_to_human_size(1234567890123) # => 1.1 TB + # number_to_human_size(1234567, :precision => 2) # => 1.18 MB + # number_to_human_size(483989, :precision => 0) # => 473 KB + # + # You can still use number_to_human_size with the old API that accepts the + # +precision+ as its optional second parameter: # number_to_human_size(1234567, 2) # => 1.18 MB - # number_to_human_size(483989, 0) # => 4 MB - def number_to_human_size(size, precision=1) - size = Kernel.Float(size) + # number_to_human_size(483989, 0) # => 473 KB + def number_to_human_size(size, *args) + options = args.extract_options! + unless args.empty? + options[:precision] = args[0] || 1 + end + options.reverse_merge!(:precision => 1) + + size = Float(size) case when size.to_i == 1; "1 Byte" when size < 1.kilobyte; "%d Bytes" % size - when size < 1.megabyte; "%.#{precision}f KB" % (size / 1.0.kilobyte) - when size < 1.gigabyte; "%.#{precision}f MB" % (size / 1.0.megabyte) - when size < 1.terabyte; "%.#{precision}f GB" % (size / 1.0.gigabyte) - else "%.#{precision}f TB" % (size / 1.0.terabyte) + when size < 1.megabyte; "%.#{options[:precision]}f KB" % (size / 1.0.kilobyte) + when size < 1.gigabyte; "%.#{options[:precision]}f MB" % (size / 1.0.megabyte) + when size < 1.terabyte; "%.#{options[:precision]}f GB" % (size / 1.0.gigabyte) + else "%.#{options[:precision]}f TB" % (size / 1.0.terabyte) end.sub(/([0-9]\.\d*?)0+ /, '\1 ' ).sub(/\. /,' ') rescue nil diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index 4a8d09b544..bff349a754 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -54,9 +54,16 @@ class NumberHelperTest < ActionView::TestCase assert_nil number_with_delimiter(nil) end + def test_number_with_delimiter_with_options_hash + assert_equal '12 345 678', number_with_delimiter(12345678, :delimiter => ' ') + assert_equal '12,345,678-05', number_with_delimiter(12345678.05, :separator => '-') + assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :separator => ',', :delimiter => '.') + assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :delimiter => '.', :separator => ',') + end + def test_number_with_precision assert_equal("111.235", number_with_precision(111.2346)) - assert_equal("31.83", number_with_precision(31.825, 2)) + assert_equal("31.83", number_with_precision(31.825, 2)) assert_equal("111.23", number_with_precision(111.2346, 2)) assert_equal("111.00", number_with_precision(111, 2)) assert_equal("111.235", number_with_precision("111.2346")) @@ -69,6 +76,17 @@ class NumberHelperTest < ActionView::TestCase assert_nil number_with_precision(nil) end + def test_number_with_precision_with_options_hash + assert_equal '111.235', number_with_precision(111.2346) + assert_equal '31.83', number_with_precision(31.825, :precision => 2) + assert_equal '111.23', number_with_precision(111.2346, :precision => 2) + assert_equal '111.00', number_with_precision(111, :precision => 2) + assert_equal '111.235', number_with_precision("111.2346") + assert_equal '31.83', number_with_precision("31.825", :precision => 2) + assert_equal '112', number_with_precision(111.50, :precision => 0) + assert_equal '1234567892', number_with_precision(1234567891.50, :precision => 0) + end + def test_number_to_human_size assert_equal '0 Bytes', number_to_human_size(0) assert_equal '1 Byte', number_to_human_size(1) @@ -94,4 +112,12 @@ class NumberHelperTest < ActionView::TestCase assert_nil number_to_human_size('x') assert_nil number_to_human_size(nil) end + + def test_number_to_human_size_with_options_hash + assert_equal '1.18 MB', number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4) + assert_equal '1.01 KB', number_to_human_size(1.0123.kilobytes, :precision => 2) + assert_equal '1.01 KB', number_to_human_size(1.0100.kilobytes, :precision => 4) + assert_equal '10 KB', number_to_human_size(10.000.kilobytes, :precision => 4) + end end -- cgit v1.2.3 From 3bd34b6ffe017dd81fd26743aab052fc4324eb0f Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 14 Jun 2008 16:24:23 -0500 Subject: Preload application classes. Uses same strategy as phusion passenger. --- railties/lib/initializer.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index b8b071d4c9..3be95de8d3 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -168,6 +168,9 @@ module Rails # Observers are loaded after plugins in case Observers or observed models are modified by plugins. load_observers + # load application classes + load_application_classes + # Flag initialized Rails.initialized = true end @@ -330,6 +333,14 @@ Run `rake gems:install` to install the missing gems. end end + def load_application_classes + require_dependency 'application' + + Dir.glob('app/{models,controllers,helpers}/*.rb').each do |file| + require_dependency file + end + end + # For Ruby 1.8, this initialization sets $KCODE to 'u' to enable the # multibyte safe operations. Plugin authors supporting other encodings # should override this behaviour and set the relevant +default_charset+ -- cgit v1.2.3 From 89ec72c2818a592323fe4ec3277638d379f1ac2a Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 21 Jul 2008 13:42:34 -0500 Subject: Added configurable eager load paths. Defaults to app/models, app/controllers, and app/helpers --- railties/CHANGELOG | 2 ++ railties/lib/initializer.rb | 25 +++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 9c5e5b59c6..5ff1867568 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Added configurable eager load paths. Defaults to app/models, app/controllers, and app/helpers [Josh Peek] + * Introduce simple internationalization support. [Ruby i18n team] * Make script/plugin install -r option work with git based plugins. #257. [Tim Pope Jakub Kuźma]. Example: diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 3be95de8d3..828d688475 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -333,11 +333,14 @@ Run `rake gems:install` to install the missing gems. end end + # Eager load application classes def load_application_classes - require_dependency 'application' - - Dir.glob('app/{models,controllers,helpers}/*.rb').each do |file| - require_dependency file + if configuration.cache_classes + configuration.eager_load_paths.each do |load_path| + Dir.glob("#{load_path}/*.rb").each do |file| + require_dependency file + end + end end end @@ -578,6 +581,11 @@ Run `rake gems:install` to install the missing gems. # All elements of this array must also be in +load_paths+. attr_accessor :load_once_paths + # An array of paths from which Rails will eager load on boot if cache + # classes is enabled. All elements of this array must also be in + # +load_paths+. + attr_accessor :eager_load_paths + # The log level to use for the default Rails logger. In production mode, # this defaults to :info. In development mode, it defaults to # :debug. @@ -686,6 +694,7 @@ Run `rake gems:install` to install the missing gems. self.frameworks = default_frameworks self.load_paths = default_load_paths self.load_once_paths = default_load_once_paths + self.eager_load_paths = default_eager_load_paths self.log_path = default_log_path self.log_level = default_log_level self.view_path = default_view_path @@ -826,6 +835,14 @@ Run `rake gems:install` to install the missing gems. [] end + def default_eager_load_paths + %w( + app/models + app/controllers + app/helpers + ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) } + end + def default_log_path File.join(root_path, 'log', "#{environment}.log") end -- cgit v1.2.3 From c67713a2fe78d6f2db49b09771841f5022995703 Mon Sep 17 00:00:00 2001 From: Daniel Guettler Date: Mon, 21 Jul 2008 15:21:13 -0400 Subject: Use klass.sti_name to make sure associations take store_full_sti_class into account. [#671 state:resolved] Signed-off-by: Pratik Naik --- activerecord/lib/active_record/associations.rb | 4 ++-- .../cases/associations/has_many_associations_test.rb | 18 ++++++++++++++++++ activerecord/test/models/company.rb | 7 +++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index fd9a443eb9..d916275ab9 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1884,7 +1884,7 @@ module ActiveRecord jt_sti_extra = " AND %s.%s = %s" % [ connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(through_reflection.active_record.inheritance_column), - through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)] + through_reflection.klass.quote_value(through_reflection.klass.sti_name)] end when :belongs_to first_key = primary_key @@ -1952,7 +1952,7 @@ module ActiveRecord join << %(AND %s.%s = %s ) % [ connection.quote_table_name(aliased_table_name), connection.quote_column_name(klass.inheritance_column), - klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record? + klass.quote_value(klass.sti_name)] unless klass.descends_from_active_record? [through_reflection, reflection].each do |ref| join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions] diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index b9c7ec6377..f8b8b1f96d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -999,4 +999,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.clients.loaded? end + def test_joins_with_namespaced_model_should_use_correct_type + old = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = true + + firm = Namespaced::Firm.create({ :name => 'Some Company' }) + firm.clients.create({ :name => 'Some Client' }) + + stats = Namespaced::Firm.find(firm.id, { + :select => "#{Namespaced::Firm.table_name}.*, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", + :joins => :clients, + :group => "#{Namespaced::Firm.table_name}.id" + }) + assert_equal 1, stats.num_clients.to_i + + ensure + ActiveRecord::Base.store_full_sti_class = old + end + end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index e6aa810146..cd435948a1 100755 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -18,6 +18,13 @@ end module Namespaced class Company < ::Company end + + class Firm < ::Company + has_many :clients, :class_name => 'Namespaced::Client' + end + + class Client < ::Company + end end class Firm < Company -- cgit v1.2.3 From 92f944818eece9fe4bc62ffb39accdb71ebc32be Mon Sep 17 00:00:00 2001 From: Miles Georgi Date: Sat, 19 Jul 2008 16:04:35 -0700 Subject: Make script/plugin work with svn+ssh urls. [#662 state:resolved] Signed-off-by: Pratik Naik --- railties/lib/commands/plugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/railties/lib/commands/plugin.rb b/railties/lib/commands/plugin.rb index 0256090d16..980244a71b 100644 --- a/railties/lib/commands/plugin.rb +++ b/railties/lib/commands/plugin.rb @@ -907,7 +907,7 @@ class RecursiveHTTPFetcher def ls @urls_to_fetch.collect do |url| - if url =~ /^svn:\/\/.*/ + if url =~ /^svn(\+ssh)?:\/\/.*/ `svn ls #{url}`.split("\n").map {|entry| "/#{entry}"} rescue nil else open(url) do |stream| -- cgit v1.2.3 From 8b858782fa693e89a47fc3dd5ae38d842ede6d04 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 21 Jul 2008 22:41:38 -0500 Subject: Ensure adapater specific code is loaded on ActiveRecord::Base.establish_connection --- .../connection_adapters/abstract/connection_specification.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 2a8807fb78..07b122efd1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -211,6 +211,7 @@ module ActiveRecord clear_active_connection_name @active_connection_name = name @@defined_connections[name] = spec + connection when Symbol, String if configuration = configurations[spec.to_s] establish_connection(configuration) -- cgit v1.2.3 From 8a87d8a6c2c6dfb423bcaf61c750010d80993b16 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 22 Jul 2008 10:26:44 -0500 Subject: Improved Memoizable test coverage and added support for multiple arguments --- .../active_support/core_ext/object/metaclass.rb | 5 + activesupport/lib/active_support/memoizable.rb | 55 ++++--- activesupport/test/memoizable_test.rb | 166 ++++++++++++++++----- 3 files changed, 165 insertions(+), 61 deletions(-) diff --git a/activesupport/lib/active_support/core_ext/object/metaclass.rb b/activesupport/lib/active_support/core_ext/object/metaclass.rb index 169a76dfb7..93fb0ad594 100644 --- a/activesupport/lib/active_support/core_ext/object/metaclass.rb +++ b/activesupport/lib/active_support/core_ext/object/metaclass.rb @@ -5,4 +5,9 @@ class Object self end end + + # If class_eval is called on an object, add those methods to its metaclass + def class_eval(*args, &block) + metaclass.class_eval(*args, &block) + end end diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index f7cd73d39c..21636b8af4 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -1,32 +1,43 @@ module ActiveSupport - module Memoizable #:nodoc: + module Memoizable + module Freezable + def self.included(base) + base.class_eval do + unless base.method_defined?(:freeze_without_memoizable) + alias_method_chain :freeze, :memoizable + end + end + end + + def freeze_with_memoizable + methods.each do |method| + if m = method.to_s.match(/^_unmemoized_(.*)/) + send(m[1]) + end + end + freeze_without_memoizable + end + end + def memoize(*symbols) symbols.each do |symbol| - original_method = "unmemoized_#{symbol}" - memoized_ivar = "@#{symbol}" + original_method = "_unmemoized_#{symbol}" + memoized_ivar = "@_memoized_#{symbol}" - klass = respond_to?(:class_eval) ? self : self.metaclass - raise "Already memoized #{symbol}" if klass.instance_methods.map(&:to_s).include?(original_method) + class_eval <<-EOS, __FILE__, __LINE__ + include Freezable - klass.class_eval <<-EOS, __FILE__, __LINE__ - unless instance_methods.map(&:to_s).include?("freeze_without_memoizable") - alias_method :freeze_without_memoizable, :freeze - def freeze - methods.each do |method| - if m = method.to_s.match(/^unmemoized_(.*)/) - send(m[1]) - end - end - freeze_without_memoizable - end - end + raise "Already memoized #{symbol}" if method_defined?(:#{original_method}) + alias #{original_method} #{symbol} + + def #{symbol}(*args) + #{memoized_ivar} ||= {} + reload = args.pop if args.last == true || args.last == :reload - alias_method :#{original_method}, :#{symbol} - def #{symbol}(reload = false) - if !reload && defined? #{memoized_ivar} - #{memoized_ivar} + if !reload && #{memoized_ivar} && #{memoized_ivar}.has_key?(args) + #{memoized_ivar}[args] else - #{memoized_ivar} = #{original_method}.freeze + #{memoized_ivar}[args] = #{original_method}(*args).freeze end end EOS diff --git a/activesupport/test/memoizable_test.rb b/activesupport/test/memoizable_test.rb index 79769631ad..cd84dcda53 100644 --- a/activesupport/test/memoizable_test.rb +++ b/activesupport/test/memoizable_test.rb @@ -5,86 +5,174 @@ uses_mocha 'Memoizable' do class Person extend ActiveSupport::Memoizable - def name - fetch_name_from_floppy + attr_reader :name_calls, :age_calls + def initialize + @name_calls = 0 + @age_calls = 0 end - memoize :name + def name + @name_calls += 1 + "Josh" + end def age + @age_calls += 1 nil end - def counter - @counter ||= 0 - @counter += 1 + memoize :name, :age + end + + class Company + attr_reader :name_calls + def initialize + @name_calls = 0 end - memoize :age, :counter + def name + @name_calls += 1 + "37signals" + end + end + + module Rates + extend ActiveSupport::Memoizable - private - def fetch_name_from_floppy - "Josh" + attr_reader :sales_tax_calls + def sales_tax(price) + @sales_tax_calls ||= 0 + @sales_tax_calls += 1 + price * 0.1025 + end + memoize :sales_tax + end + + class Calculator + extend ActiveSupport::Memoizable + include Rates + + attr_reader :fib_calls + def initialize + @fib_calls = 0 + end + + def fib(n) + @fib_calls += 1 + + if n == 0 || n == 1 + n + else + fib(n - 1) + fib(n - 2) end + end + memoize :fib + + def counter + @count ||= 0 + @count += 1 + end + memoize :counter end def setup @person = Person.new + @calculator = Calculator.new end def test_memoization assert_equal "Josh", @person.name + assert_equal 1, @person.name_calls - @person.expects(:fetch_name_from_floppy).never - 2.times { assert_equal "Josh", @person.name } + 3.times { assert_equal "Josh", @person.name } + assert_equal 1, @person.name_calls + end + + def test_memoization_with_nil_value + assert_equal nil, @person.age + assert_equal 1, @person.age_calls + + 3.times { assert_equal nil, @person.age } + assert_equal 1, @person.age_calls end def test_reloadable - counter = @person.counter - assert_equal 1, @person.counter - assert_equal 2, @person.counter(:reload) + counter = @calculator.counter + assert_equal 1, @calculator.counter + assert_equal 2, @calculator.counter(:reload) + assert_equal 2, @calculator.counter + assert_equal 3, @calculator.counter(true) + assert_equal 3, @calculator.counter end - def test_memoized_methods_are_frozen - assert_equal true, @person.name.frozen? + def test_memoization_cache_is_different_for_each_instance + assert_equal 1, @calculator.counter + assert_equal 2, @calculator.counter(:reload) + assert_equal 1, Calculator.new.counter + end + def test_memoized_is_not_affected_by_freeze @person.freeze assert_equal "Josh", @person.name - assert_equal true, @person.name.frozen? end - def test_memoization_frozen_with_nil_value - @person.freeze - assert_equal nil, @person.age + def test_memoization_with_args + assert_equal 55, @calculator.fib(10) + assert_equal 11, @calculator.fib_calls end - def test_double_memoization - assert_raise(RuntimeError) { Person.memoize :name } + def test_reloadable_with_args + assert_equal 55, @calculator.fib(10) + assert_equal 11, @calculator.fib_calls + assert_equal 55, @calculator.fib(10, :reload) + assert_equal 12, @calculator.fib_calls + assert_equal 55, @calculator.fib(10, true) + assert_equal 13, @calculator.fib_calls end - class Company - def name - lookup_name + def test_object_memoization + [Company.new, Company.new, Company.new].each do |company| + company.extend ActiveSupport::Memoizable + company.memoize :name + + assert_equal "37signals", company.name + assert_equal 1, company.name_calls + assert_equal "37signals", company.name + assert_equal 1, company.name_calls end + end - def lookup_name - "37signals" - end + def test_memoized_module_methods + assert_equal 1.025, @calculator.sales_tax(10) + assert_equal 1, @calculator.sales_tax_calls + assert_equal 1.025, @calculator.sales_tax(10) + assert_equal 1, @calculator.sales_tax_calls + assert_equal 2.5625, @calculator.sales_tax(25) + assert_equal 2, @calculator.sales_tax_calls end - def test_object_memoization + def test_object_memoized_module_methods company = Company.new - company.extend ActiveSupport::Memoizable - company.memoize :name + company.extend(Rates) + + assert_equal 1.025, company.sales_tax(10) + assert_equal 1, company.sales_tax_calls + assert_equal 1.025, company.sales_tax(10) + assert_equal 1, company.sales_tax_calls + assert_equal 2.5625, company.sales_tax(25) + assert_equal 2, company.sales_tax_calls + end - assert_equal "37signals", company.name - # Mocha doesn't play well with frozen objects - company.metaclass.instance_eval { define_method(:lookup_name) { b00m } } - assert_equal "37signals", company.name + def test_double_memoization + assert_raise(RuntimeError) { Person.memoize :name } + person = Person.new + person.extend ActiveSupport::Memoizable + assert_raise(RuntimeError) { person.memoize :name } - assert_equal true, company.name.frozen? - company.freeze - assert_equal true, company.name.frozen? + company = Company.new + company.extend ActiveSupport::Memoizable + company.memoize :name + assert_raise(RuntimeError) { company.memoize :name } end end end -- cgit v1.2.3 From bc5896e708bf8070835bebe61de03b701fa5e6f7 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 22 Jul 2008 10:27:32 -0500 Subject: Memoize ActionView::Base pick_template and find_partial_path for rendering duration --- actionpack/lib/action_controller/base.rb | 2 ++ actionpack/lib/action_view/base.rb | 7 +++++-- actionpack/lib/action_view/partials.rb | 9 +++++---- actionpack/lib/action_view/template.rb | 2 +- .../test/template/compiled_templates_test.rb | 23 ++++++++++++---------- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 50727c67c4..4dabff637b 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1261,6 +1261,8 @@ module ActionController #:nodoc: def template_exempt_from_layout?(template_name = default_template_name) template_name = @template.pick_template(template_name).to_s if @template @@exempt_from_layout.any? { |ext| template_name =~ ext } + rescue ActionView::MissingTemplate + false end def default_template_name(action_name = self.action_name) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 619a4270f8..bdcb1dc246 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -323,8 +323,8 @@ module ActionView #:nodoc: if self.class.warn_cache_misses && logger = ActionController::Base.logger logger.debug "[PERFORMANCE] Rendering a template that was " + "not found in view path. Templates outside the view path are " + - "not cached and result in expensive disk operations. Move this " + - "file into #{view_paths.join(':')} or add the folder to your " + + "not cached and result in expensive disk operations. Move this " + + "file into #{view_paths.join(':')} or add the folder to your " + "view path list" end @@ -332,6 +332,9 @@ module ActionView #:nodoc: end end + extend ActiveSupport::Memoizable + memoize :pick_template + private # Renders the template present at template_path. The hash in local_assigns # is made available as local variables. diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index 5aa4c83009..eb74d4a4c7 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -102,6 +102,8 @@ module ActionView # # As you can see, the :locals hash is shared between both the partial and its layout. module Partials + extend ActiveSupport::Memoizable + private def render_partial(partial_path, object_assigns = nil, local_assigns = {}) #:nodoc: local_assigns ||= {} @@ -129,14 +131,12 @@ module ActionView local_assigns = local_assigns ? local_assigns.clone : {} spacer = partial_spacer_template ? render(:partial => partial_spacer_template) : '' - _paths = {} - _templates = {} index = 0 collection.map do |object| _partial_path ||= partial_path || ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path) - path = _paths[_partial_path] ||= find_partial_path(_partial_path) - template = _templates[path] ||= pick_template(path) + path = find_partial_path(_partial_path) + template = pick_template(path) local_assigns[template.counter_name] = index result = template.render_partial(self, object, local_assigns, as) index += 1 @@ -153,5 +153,6 @@ module ActionView "_#{partial_path}" end end + memoize :find_partial_path end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 1f528dd900..3fcd9a2d01 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -75,7 +75,7 @@ module ActionView #:nodoc: load_paths = Array(load_paths) + [nil] load_paths.each do |load_path| file = [load_path, path].compact.join('/') - return load_path, file if File.exist?(file) + return load_path, file if File.exist?(file) && File.file?(file) end raise MissingTemplate.new(load_paths, path) end diff --git a/actionpack/test/template/compiled_templates_test.rb b/actionpack/test/template/compiled_templates_test.rb index 4b34827f91..52996c7fcb 100644 --- a/actionpack/test/template/compiled_templates_test.rb +++ b/actionpack/test/template/compiled_templates_test.rb @@ -4,7 +4,6 @@ require 'controller/fake_models' uses_mocha 'TestTemplateRecompilation' do class CompiledTemplatesTest < Test::Unit::TestCase def setup - @view = ActionView::Base.new(ActionController::Base.view_paths, {}) @compiled_templates = ActionView::Base::CompiledTemplates @compiled_templates.instance_methods.each do |m| @compiled_templates.send(:remove_method, m) if m =~ /^_run_/ @@ -13,29 +12,33 @@ uses_mocha 'TestTemplateRecompilation' do def test_template_gets_compiled assert_equal 0, @compiled_templates.instance_methods.size - assert_equal "Hello world!", @view.render("test/hello_world.erb") + assert_equal "Hello world!", render("test/hello_world.erb") assert_equal 1, @compiled_templates.instance_methods.size end def test_template_gets_recompiled_when_using_different_keys_in_local_assigns assert_equal 0, @compiled_templates.instance_methods.size - assert_equal "Hello world!", @view.render("test/hello_world.erb") - assert_equal "Hello world!", @view.render("test/hello_world.erb", {:foo => "bar"}) + assert_equal "Hello world!", render("test/hello_world.erb") + assert_equal "Hello world!", render("test/hello_world.erb", {:foo => "bar"}) assert_equal 2, @compiled_templates.instance_methods.size end def test_compiled_template_will_not_be_recompiled_when_rendered_with_identical_local_assigns assert_equal 0, @compiled_templates.instance_methods.size - assert_equal "Hello world!", @view.render("test/hello_world.erb") + assert_equal "Hello world!", render("test/hello_world.erb") ActionView::Template.any_instance.expects(:compile!).never - assert_equal "Hello world!", @view.render("test/hello_world.erb") + assert_equal "Hello world!", render("test/hello_world.erb") end - def test_compiled_template_will_always_be_recompiled_when_rendered_if_template_is_outside_cache + def test_compiled_template_will_be_recompiled_when_rendered_if_template_is_outside_cache assert_equal 0, @compiled_templates.instance_methods.size - assert_equal "Hello world!", @view.render("#{FIXTURE_LOAD_PATH}/test/hello_world.erb") - ActionView::Template.any_instance.expects(:compile!).times(3) - 3.times { assert_equal "Hello world!", @view.render("#{FIXTURE_LOAD_PATH}/test/hello_world.erb") } + assert_equal "Hello world!", render("#{FIXTURE_LOAD_PATH}/test/hello_world.erb") + assert_equal 1, @compiled_templates.instance_methods.size end + + private + def render(*args) + ActionView::Base.new(ActionController::Base.view_paths, {}).render(*args) + end end end -- cgit v1.2.3 From 2681685450631238511cfc3c2f0fa044c1f8033a Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 22 Jul 2008 11:12:16 -0500 Subject: Extract ActiveSupport::TypedArray class to ensure an array is all of the same type [#673 state:resolved] --- actionpack/lib/action_view/paths.rb | 31 +++------------ actionpack/test/controller/view_paths_test.rb | 10 +---- activesupport/lib/active_support.rb | 1 + activesupport/lib/active_support/typed_array.rb | 31 +++++++++++++++ activesupport/test/typed_array_test.rb | 51 +++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 activesupport/lib/active_support/typed_array.rb create mode 100644 activesupport/test/typed_array_test.rb diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index 78548d4aa2..9cb50ab4f8 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -1,5 +1,5 @@ module ActionView #:nodoc: - class PathSet < Array #:nodoc: + class PathSet < ActiveSupport::TypedArray #:nodoc: def self.type_cast(obj) if obj.is_a?(String) if Base.warn_cache_misses && defined?(Rails) && Rails.initialized? @@ -25,7 +25,7 @@ module ActionView #:nodoc: end attr_reader :path, :paths - delegate :to_s, :to_str, :inspect, :to => :path + delegate :to_s, :to_str, :hash, :inspect, :to => :path def initialize(path) raise ArgumentError, "path already is a Path class" if path.is_a?(Path) @@ -38,6 +38,10 @@ module ActionView #:nodoc: to_str == path.to_str end + def eql?(path) + to_str == path.to_str + end + def [](path) @paths[path] end @@ -67,28 +71,10 @@ module ActionView #:nodoc: end end - def initialize(*args) - super(*args).map! { |obj| self.class.type_cast(obj) } - end - def reload! each { |path| path.reload! } end - def <<(obj) - super(self.class.type_cast(obj)) - end - - def push(*objs) - delete_paths!(objs) - super(*objs.map { |obj| self.class.type_cast(obj) }) - end - - def unshift(*objs) - delete_paths!(objs) - super(*objs.map { |obj| self.class.type_cast(obj) }) - end - def [](template_path) each do |path| if template = path[template_path] @@ -97,10 +83,5 @@ module ActionView #:nodoc: end nil end - - private - def delete_paths!(paths) - paths.each { |p1| delete_if { |p2| p1.to_s == p2.to_s } } - end end end diff --git a/actionpack/test/controller/view_paths_test.rb b/actionpack/test/controller/view_paths_test.rb index 85fa58a45b..b859a92cbd 100644 --- a/actionpack/test/controller/view_paths_test.rb +++ b/actionpack/test/controller/view_paths_test.rb @@ -54,10 +54,7 @@ class ViewLoadPathsTest < Test::Unit::TestCase assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths @controller.append_view_path(FIXTURE_LOAD_PATH) - assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths - - @controller.append_view_path([FIXTURE_LOAD_PATH]) - assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths + assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths end def test_controller_prepends_view_path_correctly @@ -68,10 +65,7 @@ class ViewLoadPathsTest < Test::Unit::TestCase assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths @controller.prepend_view_path(FIXTURE_LOAD_PATH) - assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths - - @controller.prepend_view_path([FIXTURE_LOAD_PATH]) - assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths + assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths end def test_template_appends_view_path_correctly diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 1df911a3f2..51067e910e 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -39,6 +39,7 @@ require 'active_support/cache' require 'active_support/dependencies' require 'active_support/deprecation' +require 'active_support/typed_array' require 'active_support/ordered_hash' require 'active_support/ordered_options' require 'active_support/option_merger' diff --git a/activesupport/lib/active_support/typed_array.rb b/activesupport/lib/active_support/typed_array.rb new file mode 100644 index 0000000000..1a4d8a8faf --- /dev/null +++ b/activesupport/lib/active_support/typed_array.rb @@ -0,0 +1,31 @@ +module ActiveSupport + class TypedArray < Array + def self.type_cast(obj) + obj + end + + def initialize(*args) + super(*args).map! { |obj| self.class.type_cast(obj) } + end + + def <<(obj) + super(self.class.type_cast(obj)) + end + + def concat(array) + super(array.map! { |obj| self.class.type_cast(obj) }) + end + + def insert(index, obj) + super(index, self.class.type_cast(obj)) + end + + def push(*objs) + super(*objs.map { |obj| self.class.type_cast(obj) }) + end + + def unshift(*objs) + super(*objs.map { |obj| self.class.type_cast(obj) }) + end + end +end diff --git a/activesupport/test/typed_array_test.rb b/activesupport/test/typed_array_test.rb new file mode 100644 index 0000000000..023f3a1b84 --- /dev/null +++ b/activesupport/test/typed_array_test.rb @@ -0,0 +1,51 @@ +require 'abstract_unit' + +class TypedArrayTest < Test::Unit::TestCase + class StringArray < ActiveSupport::TypedArray + def self.type_cast(obj) + obj.to_s + end + end + + def setup + @array = StringArray.new + end + + def test_string_array_initialize + assert_equal ["1", "2", "3"], StringArray.new([1, "2", :"3"]) + end + + def test_string_array_append + @array << 1 + @array << "2" + @array << :"3" + assert_equal ["1", "2", "3"], @array + end + + def test_string_array_concat + @array.concat([1, "2"]) + @array.concat([:"3"]) + assert_equal ["1", "2", "3"], @array + end + + def test_string_array_insert + @array.insert(0, 1) + @array.insert(1, "2") + @array.insert(2, :"3") + assert_equal ["1", "2", "3"], @array + end + + def test_string_array_push + @array.push(1) + @array.push("2") + @array.push(:"3") + assert_equal ["1", "2", "3"], @array + end + + def test_string_array_unshift + @array.unshift(:"3") + @array.unshift("2") + @array.unshift(1) + assert_equal ["1", "2", "3"], @array + end +end -- cgit v1.2.3 From 93e10f9911fb2a096681ee0a0bc82487a9a06c44 Mon Sep 17 00:00:00 2001 From: Jan De Poorter Date: Wed, 23 Jul 2008 12:50:16 +0200 Subject: Ensure NamedScope#any? uses COUNT query wherever possible. [#680 state:resolved] Signed-off-by: Pratik Naik --- activerecord/lib/active_record/named_scope.rb | 10 +++++++++- activerecord/test/cases/named_scope_test.rb | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 080e3d0f5e..d5a1c5fe08 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -103,7 +103,7 @@ module ActiveRecord attr_reader :proxy_scope, :proxy_options [].methods.each do |m| - unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/ + unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?)/ delegate m, :to => :proxy_found end end @@ -140,6 +140,14 @@ module ActiveRecord @found ? @found.empty? : count.zero? end + def any? + if block_given? + proxy_found.any? { |*block_args| yield(*block_args) } + else + !empty? + end + end + protected def proxy_found @found || load_found diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 0c1eb23428..e21ffbbdba 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -184,6 +184,28 @@ class NamedScopeTest < ActiveRecord::TestCase end end + def test_any_should_not_load_results + topics = Topic.base + assert_queries(1) do + topics.expects(:empty?).returns(true) + assert !topics.any? + end + end + + def test_any_should_call_proxy_found_if_using_a_block + topics = Topic.base + assert_queries(1) do + topics.expects(:empty?).never + topics.any? { true } + end + end + + def test_any_should_not_fire_query_if_named_scope_loaded + topics = Topic.base + topics.collect # force load + assert_no_queries { assert topics.any? } + end + def test_should_build_with_proxy_options topic = Topic.approved.build({}) assert topic.approved -- cgit v1.2.3 From db1bac796e2d53fac4b51a3f560010b8f663fb54 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 23 Jul 2008 10:24:47 -0500 Subject: Just file? --- actionpack/lib/action_view/template.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 3fcd9a2d01..b281ff61f2 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -75,7 +75,7 @@ module ActionView #:nodoc: load_paths = Array(load_paths) + [nil] load_paths.each do |load_path| file = [load_path, path].compact.join('/') - return load_path, file if File.exist?(file) && File.file?(file) + return load_path, file if File.file?(file) end raise MissingTemplate.new(load_paths, path) end -- cgit v1.2.3 From 97a954bf1dd05e79a873bffc94fcf5420b807371 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 23 Jul 2008 10:41:28 -0500 Subject: Load view path cache after plugins and gems. --- actionpack/lib/action_view/paths.rb | 18 +++++++++++++++--- railties/lib/initializer.rb | 13 ++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index 9cb50ab4f8..a37706faee 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -27,11 +27,10 @@ module ActionView #:nodoc: attr_reader :path, :paths delegate :to_s, :to_str, :hash, :inspect, :to => :path - def initialize(path) + def initialize(path, load = true) raise ArgumentError, "path already is a Path class" if path.is_a?(Path) - @path = path.freeze - reload! + reload! if load end def ==(path) @@ -46,6 +45,14 @@ module ActionView #:nodoc: @paths[path] end + def loaded? + @loaded ? true : false + end + + def load + reload! unless loaded? + end + # Rebuild load path directory cache def reload! @paths = {} @@ -59,6 +66,7 @@ module ActionView #:nodoc: end @paths.freeze + @loaded = true end private @@ -71,6 +79,10 @@ module ActionView #:nodoc: end end + def load + each { |path| path.load } + end + def reload! each { |path| path.reload! } end diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 828d688475..97bb81a3c8 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -168,6 +168,9 @@ module Rails # Observers are loaded after plugins in case Observers or observed models are modified by plugins. load_observers + # Load view path cache + load_view_paths + # load application classes load_application_classes @@ -333,6 +336,12 @@ Run `rake gems:install` to install the missing gems. end end + def load_view_paths + ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes + ActionMailer::Base.template_root.load + ActionController::Base.view_paths.load + end + # Eager load application classes def load_application_classes if configuration.cache_classes @@ -428,9 +437,7 @@ Run `rake gems:install` to install the missing gems. # paths have already been set, it is not changed, otherwise it is # set to use Configuration#view_path. def initialize_framework_views - ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes - view_path = ActionView::PathSet::Path.new(configuration.view_path) - + view_path = ActionView::PathSet::Path.new(configuration.view_path, false) ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer) ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty? end -- cgit v1.2.3 From e0db925be04ab3e9c3db67dd0daa8caf3680dd21 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 23 Jul 2008 11:23:25 -0500 Subject: Revert 'bc5896e' --- actionpack/lib/action_view/base.rb | 3 --- actionpack/lib/action_view/partials.rb | 9 ++++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index bdcb1dc246..c769013d42 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -332,9 +332,6 @@ module ActionView #:nodoc: end end - extend ActiveSupport::Memoizable - memoize :pick_template - private # Renders the template present at template_path. The hash in local_assigns # is made available as local variables. diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index eb74d4a4c7..5aa4c83009 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -102,8 +102,6 @@ module ActionView # # As you can see, the :locals hash is shared between both the partial and its layout. module Partials - extend ActiveSupport::Memoizable - private def render_partial(partial_path, object_assigns = nil, local_assigns = {}) #:nodoc: local_assigns ||= {} @@ -131,12 +129,14 @@ module ActionView local_assigns = local_assigns ? local_assigns.clone : {} spacer = partial_spacer_template ? render(:partial => partial_spacer_template) : '' + _paths = {} + _templates = {} index = 0 collection.map do |object| _partial_path ||= partial_path || ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path) - path = find_partial_path(_partial_path) - template = pick_template(path) + path = _paths[_partial_path] ||= find_partial_path(_partial_path) + template = _templates[path] ||= pick_template(path) local_assigns[template.counter_name] = index result = template.render_partial(self, object, local_assigns, as) index += 1 @@ -153,6 +153,5 @@ module ActionView "_#{partial_path}" end end - memoize :find_partial_path end end -- cgit v1.2.3 From 55adaa2efc08c892bf7be55d79ac571848068256 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 23 Jul 2008 13:47:30 -0500 Subject: Fixed bc5896e, and added test case for the caching bug it originally introduced. --- actionpack/lib/action_view/base.rb | 3 +++ actionpack/lib/action_view/partials.rb | 9 +++++---- actionpack/lib/action_view/renderable.rb | 2 +- actionpack/test/template/compiled_templates_test.rb | 5 ++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index c769013d42..bdcb1dc246 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -332,6 +332,9 @@ module ActionView #:nodoc: end end + extend ActiveSupport::Memoizable + memoize :pick_template + private # Renders the template present at template_path. The hash in local_assigns # is made available as local variables. diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index 5aa4c83009..eb74d4a4c7 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -102,6 +102,8 @@ module ActionView # # As you can see, the :locals hash is shared between both the partial and its layout. module Partials + extend ActiveSupport::Memoizable + private def render_partial(partial_path, object_assigns = nil, local_assigns = {}) #:nodoc: local_assigns ||= {} @@ -129,14 +131,12 @@ module ActionView local_assigns = local_assigns ? local_assigns.clone : {} spacer = partial_spacer_template ? render(:partial => partial_spacer_template) : '' - _paths = {} - _templates = {} index = 0 collection.map do |object| _partial_path ||= partial_path || ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path) - path = _paths[_partial_path] ||= find_partial_path(_partial_path) - template = _templates[path] ||= pick_template(path) + path = find_partial_path(_partial_path) + template = pick_template(path) local_assigns[template.counter_name] = index result = template.render_partial(self, object, local_assigns, as) index += 1 @@ -153,5 +153,6 @@ module ActionView "_#{partial_path}" end end + memoize :find_partial_path end end diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 2b825ac4e9..5fe1ca86f3 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -84,7 +84,7 @@ module ActionView # The template will be compiled if the file has not been compiled yet, or # if local_assigns has a new key, which isn't supported by the compiled code yet. def recompile?(symbol) - !(frozen? && Base::CompiledTemplates.method_defined?(symbol)) + !(ActionView::PathSet::Path.eager_load_templates? && Base::CompiledTemplates.method_defined?(symbol)) end end end diff --git a/actionpack/test/template/compiled_templates_test.rb b/actionpack/test/template/compiled_templates_test.rb index 52996c7fcb..e005aa0f03 100644 --- a/actionpack/test/template/compiled_templates_test.rb +++ b/actionpack/test/template/compiled_templates_test.rb @@ -30,9 +30,12 @@ uses_mocha 'TestTemplateRecompilation' do assert_equal "Hello world!", render("test/hello_world.erb") end - def test_compiled_template_will_be_recompiled_when_rendered_if_template_is_outside_cache + def test_compiled_template_will_always_be_recompiled_when_eager_loaded_templates_is_off + ActionView::PathSet::Path.expects(:eager_load_templates?).times(4).returns(false) assert_equal 0, @compiled_templates.instance_methods.size assert_equal "Hello world!", render("#{FIXTURE_LOAD_PATH}/test/hello_world.erb") + ActionView::Template.any_instance.expects(:compile!).times(3) + 3.times { assert_equal "Hello world!", render("#{FIXTURE_LOAD_PATH}/test/hello_world.erb") } assert_equal 1, @compiled_templates.instance_methods.size end -- cgit v1.2.3 From 3fd9036fc554979e951422a79f0f77f061112bdc Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Thu, 24 Jul 2008 11:58:26 -0500 Subject: Added config.dependency_loading to enable or disable the dependency loader after initialization --- activesupport/lib/active_support/dependencies.rb | 78 ++++++++++++++++++------ activesupport/test/dependencies_test.rb | 12 ++++ railties/lib/initializer.rb | 27 +++++++- 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index e3d4f3d7eb..a3f5f799a2 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -51,17 +51,28 @@ module ActiveSupport #:nodoc: module ModuleConstMissing #:nodoc: def self.included(base) #:nodoc: base.class_eval do - # Rename the original handler so we can chain it to the new one - alias_method :rails_original_const_missing, :const_missing + unless defined? const_missing_without_dependencies + alias_method_chain :const_missing, :dependencies + end + end + end - # Use const_missing to autoload associations so we don't have to - # require_association when using single-table inheritance. - def const_missing(class_id) - ActiveSupport::Dependencies.load_missing_constant self, class_id + def self.excluded(base) #:nodoc: + base.class_eval do + if defined? const_missing_without_dependencies + undef_method :const_missing + alias_method :const_missing, :const_missing_without_dependencies + undef_method :const_missing_without_dependencies end end end + # Use const_missing to autoload associations so we don't have to + # require_association when using single-table inheritance. + def const_missing_with_dependencies(class_id) + ActiveSupport::Dependencies.load_missing_constant self, class_id + end + def unloadable(const_desc = self) super(const_desc) end @@ -92,8 +103,38 @@ module ActiveSupport #:nodoc: # Object includes this module module Loadable #:nodoc: - def load(file, *extras) #:nodoc: - Dependencies.new_constants_in(Object) { super } + def self.included(base) #:nodoc: + base.class_eval do + unless defined? load_without_new_constant_marking + alias_method_chain :load, :new_constant_marking + end + end + end + + def self.excluded(base) #:nodoc: + base.class_eval do + if defined? load_without_new_constant_marking + undef_method :load + alias_method :load, :load_without_new_constant_marking + undef_method :load_without_new_constant_marking + end + end + end + + def require_or_load(file_name) + Dependencies.require_or_load(file_name) + end + + def require_dependency(file_name) + Dependencies.depend_on(file_name) + end + + def require_association(file_name) + Dependencies.associate_with(file_name) + end + + def load_with_new_constant_marking(file, *extras) #:nodoc: + Dependencies.new_constants_in(Object) { load_without_new_constant_marking(file, *extras) } rescue Exception => exception # errors from loading file exception.blame_file! file raise @@ -145,19 +186,18 @@ module ActiveSupport #:nodoc: end end - def inject! - Object.instance_eval do - define_method(:require_or_load) { |file_name| Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load) - define_method(:require_dependency) { |file_name| Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency) - define_method(:require_association) { |file_name| Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association) - - alias_method :load_without_new_constant_marking, :load - include Loadable - end - + def hook! + Object.instance_eval { include Loadable } Module.instance_eval { include ModuleConstMissing } Class.instance_eval { include ClassConstMissing } Exception.instance_eval { include Blamable } + true + end + + def unhook! + ModuleConstMissing.excluded(Module) + Loadable.excluded(Object) + true end def load? @@ -560,4 +600,4 @@ module ActiveSupport #:nodoc: end end -ActiveSupport::Dependencies.inject! +ActiveSupport::Dependencies.hook! diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 038547a862..39c9c74c94 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -762,4 +762,16 @@ class DependenciesTest < Test::Unit::TestCase ensure ActiveSupport::Dependencies.load_once_paths = [] end + + def test_hook_called_multiple_times + assert_nothing_raised { ActiveSupport::Dependencies.hook! } + end + + def test_unhook + ActiveSupport::Dependencies.unhook! + assert !Module.new.respond_to?(:const_missing_without_dependencies) + assert !Module.new.respond_to?(:load_without_new_constant_marking) + ensure + ActiveSupport::Dependencies.hook! + end end diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 97bb81a3c8..44863ab026 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -171,9 +171,12 @@ module Rails # Load view path cache load_view_paths - # load application classes + # Load application classes load_application_classes + # Disable dependency loading during request cycle + disable_dependency_loading + # Flag initialized Rails.initialized = true end @@ -525,6 +528,12 @@ Run `rake gems:install` to install the missing gems. Dispatcher.define_dispatcher_callbacks(configuration.cache_classes) Dispatcher.new(RAILS_DEFAULT_LOGGER).send :run_callbacks, :prepare_dispatch end + + def disable_dependency_loading + if configuration.cache_classes && !configuration.dependency_loading + ActiveSupport::Dependencies.unhook! + end + end end # The Configuration class holds all the parameters for the Initializer and @@ -659,6 +668,17 @@ Run `rake gems:install` to install the missing gems. !!@reload_plugins end + # Enables or disables dependency loading during the request cycle. Setting + # dependency_loading to true will allow new classes to be loaded + # during a request. Setting it to false will disable this behavior. + # + # Those who want to run in a threaded environment should disable this + # option and eager load or require all there classes on initialization. + # + # If cache_classes is disabled, dependency loaded will always be + # on. + attr_accessor :dependency_loading + # An array of gems that this rails application depends on. Rails will automatically load # these gems during installation, and allow you to install any missing gems with: # @@ -707,6 +727,7 @@ Run `rake gems:install` to install the missing gems. self.view_path = default_view_path self.controller_paths = default_controller_paths self.cache_classes = default_cache_classes + self.dependency_loading = default_dependency_loading self.whiny_nils = default_whiny_nils self.plugins = default_plugins self.plugin_paths = default_plugin_paths @@ -876,8 +897,8 @@ Run `rake gems:install` to install the missing gems. paths end - def default_dependency_mechanism - :load + def default_dependency_loading + true end def default_cache_classes -- cgit v1.2.3 From a87462afcb6c642e59bfcd2e11e3351e2b718c38 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Thu, 24 Jul 2008 13:41:51 -0500 Subject: AbstractRequest.relative_url_root is no longer automatically configured by a HTTP header. It can now be set in your configuration environment with config.action_controller.relative_url_root --- actionpack/CHANGELOG | 2 + actionpack/lib/action_controller/base.rb | 4 + actionpack/lib/action_controller/request.rb | 41 +++----- .../lib/action_controller/routing/optimisations.rb | 2 +- actionpack/lib/action_controller/url_rewriter.rb | 6 +- .../lib/action_view/helpers/asset_tag_helper.rb | 6 +- actionpack/test/controller/redirect_test.rb | 109 ++++++++++----------- actionpack/test/controller/request_test.rb | 100 +++++-------------- actionpack/test/controller/routing_test.rb | 10 +- actionpack/test/controller/url_rewriter_test.rb | 55 +++++------ actionpack/test/template/asset_tag_helper_test.rb | 45 ++++----- 11 files changed, 161 insertions(+), 219 deletions(-) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index ebe4c047b8..03e011c75c 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* AbstractRequest.relative_url_root is no longer automatically configured by a HTTP header. It can now be set in your configuration environment with config.action_controller.relative_url_root [Josh Peek] + * Update Prototype to 1.6.0.2 #599 [Patrick Joyce] * Conditional GET utility methods. [Jeremy Kemper] diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 4dabff637b..bae7e8c12e 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -354,6 +354,10 @@ module ActionController #:nodoc: class_inheritable_accessor :allow_forgery_protection self.allow_forgery_protection = true + # If you are deploying to a subdirectory, you will need to set + # config.action_controller.relative_url_root + class_inheritable_accessor :relative_url_root + # Holds the request object that's primarily used to get environment variables through access like # request.env["REQUEST_URI"]. attr_internal :request diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index c42f113d2c..c55788a531 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -3,13 +3,16 @@ require 'stringio' require 'strscan' module ActionController - # HTTP methods which are accepted by default. + # HTTP methods which are accepted by default. ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options )) # CgiRequest and TestRequest provide concrete implementations. class AbstractRequest - cattr_accessor :relative_url_root - remove_method :relative_url_root + def self.relative_url_root=(*args) + ActiveSupport::Deprecation.warn( + "ActionController::AbstractRequest.relative_url_root= has been renamed." + + "You can now set it with config.action_controller.relative_url_root=", caller) + end # The hash of environment variables for this request, # such as { 'RAILS_ENV' => 'production' }. @@ -111,14 +114,14 @@ module ActionController end end end - - + + # Sets the format by string extension, which can be used to force custom formats that are not controlled by the extension. # Example: # # class ApplicationController < ActionController::Base # before_filter :adjust_format_for_iphone - # + # # private # def adjust_format_for_iphone # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] @@ -303,26 +306,10 @@ EOM path = (uri = request_uri) ? uri.split('?').first.to_s : '' # Cut off the path to the installation directory if given - path.sub!(%r/^#{relative_url_root}/, '') - path || '' - end - - # Returns the path minus the web server relative installation directory. - # This can be set with the environment variable RAILS_RELATIVE_URL_ROOT. - # It can be automatically extracted for Apache setups. If the server is not - # Apache, this method returns an empty string. - def relative_url_root - @@relative_url_root ||= case - when @env["RAILS_RELATIVE_URL_ROOT"] - @env["RAILS_RELATIVE_URL_ROOT"] - when server_software == 'apache' - @env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '') - else - '' - end + path.sub!(%r/^#{ActionController::Base.relative_url_root}/, '') + path || '' end - # Read the request body. This is useful for web services that need to # work with raw requests directly. def raw_post @@ -343,15 +330,15 @@ EOM @symbolized_path_parameters = @parameters = nil end - # The same as path_parameters with explicitly symbolized keys - def symbolized_path_parameters + # The same as path_parameters with explicitly symbolized keys + def symbolized_path_parameters @symbolized_path_parameters ||= path_parameters.symbolize_keys end # Returns a hash with the parameters used to form the path of the request. # Returned hash keys are strings. See symbolized_path_parameters for symbolized keys. # - # Example: + # Example: # # {'action' => 'my_action', 'controller' => 'my_controller'} def path_parameters diff --git a/actionpack/lib/action_controller/routing/optimisations.rb b/actionpack/lib/action_controller/routing/optimisations.rb index cd4a423e6b..4b70ea13f2 100644 --- a/actionpack/lib/action_controller/routing/optimisations.rb +++ b/actionpack/lib/action_controller/routing/optimisations.rb @@ -76,7 +76,7 @@ module ActionController elements << '#{request.host_with_port}' end - elements << '#{request.relative_url_root if request.relative_url_root}' + elements << '#{ActionController::Base.relative_url_root if ActionController::Base.relative_url_root}' # The last entry in route.segments appears to *always* be a # 'divider segment' for '/' but we have assertions to ensure that diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb index 3a38f23396..d0bf6c0bd4 100644 --- a/actionpack/lib/action_controller/url_rewriter.rb +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -37,7 +37,7 @@ module ActionController # * :port - Optionally specify the port to connect to. # * :anchor - An anchor name to be appended to the path. # * :skip_relative_url_root - If true, the url is not constructed using the - # +relative_url_root+ set in ActionController::AbstractRequest.relative_url_root. + # +relative_url_root+ set in ActionController::Base.relative_url_root. # * :trailing_slash - If true, adds a trailing slash, as in "/archive/2009/" # # Any other key (:controller, :action, etc.) given to @@ -67,7 +67,7 @@ module ActionController [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) } end trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash) - url << ActionController::AbstractRequest.relative_url_root.to_s unless options[:skip_relative_url_root] + url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor] generated = Routing::Routes.generate(options, {}) url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated) @@ -108,7 +108,7 @@ module ActionController end path = rewrite_path(options) - rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root] + rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) rewritten_url << "##{options[:anchor]}" if options[:anchor] diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 8d0ee81684..769eada120 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -476,7 +476,7 @@ module ActionView if has_request [ @controller.request.protocol, ActionController::Base.asset_host.to_s, - @controller.request.relative_url_root, + ActionController::Base.relative_url_root, dir, source, ext, include_host ].join else [ ActionController::Base.asset_host.to_s, @@ -492,8 +492,8 @@ module ActionView else source = "/#{dir}/#{source}" unless source[0] == ?/ if has_request - unless source =~ %r{^#{@controller.request.relative_url_root}/} - source = "#{@controller.request.relative_url_root}#{source}" + unless source =~ %r{^#{ActionController::Base.relative_url_root}/} + source = "#{ActionController::Base.relative_url_root}#{source}" end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 28da5c6163..2f8bf7b6ee 100755 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -9,11 +9,11 @@ class Workshop def initialize(id, new_record) @id, @new_record = id, new_record end - + def new_record? @new_record end - + def to_s id.to_s end @@ -24,32 +24,32 @@ class RedirectController < ActionController::Base redirect_to :action => "hello_world" end - def redirect_with_status + def redirect_with_status redirect_to({:action => "hello_world", :status => 301}) - end + end def redirect_with_status_hash redirect_to({:action => "hello_world"}, {:status => 301}) - end + end - def url_redirect_with_status + def url_redirect_with_status redirect_to("http://www.example.com", :status => :moved_permanently) - end - - def url_redirect_with_status_hash + end + + def url_redirect_with_status_hash redirect_to("http://www.example.com", {:status => 301}) - end + end - def relative_url_redirect_with_status + def relative_url_redirect_with_status redirect_to("/things/stuff", :status => :found) - end - + end + def relative_url_redirect_with_status_hash redirect_to("/things/stuff", {:status => 301}) - end - - def redirect_to_back_with_status - redirect_to :back, :status => 307 + end + + def redirect_to_back_with_status + redirect_to :back, :status => 307 end def host_redirect @@ -90,9 +90,9 @@ class RedirectController < ActionController::Base end def rescue_errors(e) raise e end - + def rescue_action(e) raise end - + protected def dashbord_url(id, message) url_for :action => "dashboard", :params => { "id" => id, "message" => message } @@ -118,48 +118,48 @@ class RedirectTest < Test::Unit::TestCase assert_equal "http://test.host/redirect/hello_world", redirect_to_url end - def test_redirect_with_status - get :redirect_with_status - assert_response 301 - assert_equal "http://test.host/redirect/hello_world", redirect_to_url - end + def test_redirect_with_status + get :redirect_with_status + assert_response 301 + assert_equal "http://test.host/redirect/hello_world", redirect_to_url + end - def test_redirect_with_status_hash + def test_redirect_with_status_hash get :redirect_with_status_hash - assert_response 301 - assert_equal "http://test.host/redirect/hello_world", redirect_to_url + assert_response 301 + assert_equal "http://test.host/redirect/hello_world", redirect_to_url + end + + def test_url_redirect_with_status + get :url_redirect_with_status + assert_response 301 + assert_equal "http://www.example.com", redirect_to_url end - - def test_url_redirect_with_status - get :url_redirect_with_status - assert_response 301 - assert_equal "http://www.example.com", redirect_to_url - end def test_url_redirect_with_status_hash get :url_redirect_with_status_hash - assert_response 301 - assert_equal "http://www.example.com", redirect_to_url - end + assert_response 301 + assert_equal "http://www.example.com", redirect_to_url + end - - def test_relative_url_redirect_with_status - get :relative_url_redirect_with_status + + def test_relative_url_redirect_with_status + get :relative_url_redirect_with_status assert_response 302 - assert_equal "http://test.host/things/stuff", redirect_to_url - end - + assert_equal "http://test.host/things/stuff", redirect_to_url + end + def test_relative_url_redirect_with_status_hash get :relative_url_redirect_with_status_hash - assert_response 301 - assert_equal "http://test.host/things/stuff", redirect_to_url - end - - def test_redirect_to_back_with_status - @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" - get :redirect_to_back_with_status - assert_response 307 - assert_equal "http://www.example.com/coming/from", redirect_to_url + assert_response 301 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + + def test_redirect_to_back_with_status + @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" + get :redirect_to_back_with_status + assert_response 307 + assert_equal "http://www.example.com/coming/from", redirect_to_url end def test_simple_redirect_using_options @@ -204,20 +204,20 @@ class RedirectTest < Test::Unit::TestCase assert_response :redirect assert_equal "http://www.example.com/coming/from", redirect_to_url end - + def test_redirect_to_back_with_no_referer assert_raises(ActionController::RedirectBackError) { @request.env["HTTP_REFERER"] = nil get :redirect_to_back } end - + def test_redirect_to_record ActionController::Routing::Routes.draw do |map| map.resources :workshops map.connect ':controller/:action/:id' end - + get :redirect_to_existing_record assert_equal "http://test.host/workshops/5", redirect_to_url assert_redirected_to Workshop.new(5, false) @@ -237,7 +237,6 @@ class RedirectTest < Test::Unit::TestCase get :redirect_to_nil end end - end module ModuleTest diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb index 932c0e21a1..7db5264840 100644 --- a/actionpack/test/controller/request_test.rb +++ b/actionpack/test/controller/request_test.rb @@ -3,9 +3,14 @@ require 'action_controller/integration' class RequestTest < Test::Unit::TestCase def setup + ActionController::Base.relative_url_root = nil @request = ActionController::TestRequest.new end + def teardown + ActionController::Base.relative_url_root = nil + end + def test_remote_ip assert_equal '0.0.0.0', @request.remote_ip @@ -38,7 +43,7 @@ class RequestTest < Test::Unit::TestCase @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,3.4.5.6' assert_equal '3.4.5.6', @request.remote_ip - + @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1, 10.0.0.1, 3.4.5.6' assert_equal '3.4.5.6', @request.remote_ip @@ -120,155 +125,105 @@ class RequestTest < Test::Unit::TestCase assert_equal ":8080", @request.port_string end - def test_relative_url_root - @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" - @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3' - assert_equal '', @request.relative_url_root, "relative_url_root should be disabled on lighttpd" - - @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text' - - @request.env['SCRIPT_NAME'] = nil - assert_equal "", @request.relative_url_root - - @request.env['SCRIPT_NAME'] = "/dispatch.cgi" - assert_equal "", @request.relative_url_root - - @request.env['SCRIPT_NAME'] = "/myapp.rb" - assert_equal "", @request.relative_url_root - - @request.relative_url_root = nil - @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" - assert_equal "/hieraki", @request.relative_url_root - - @request.relative_url_root = nil - @request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi" - assert_equal "/collaboration/hieraki", @request.relative_url_root - - # apache/scgi case - @request.relative_url_root = nil - @request.env['SCRIPT_NAME'] = "/collaboration/hieraki" - assert_equal "/collaboration/hieraki", @request.relative_url_root - - @request.relative_url_root = nil - @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" - @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3' - @request.env['RAILS_RELATIVE_URL_ROOT'] = "/hieraki" - assert_equal "/hieraki", @request.relative_url_root - - # @env overrides path guess - @request.relative_url_root = nil - @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" - @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text' - @request.env['RAILS_RELATIVE_URL_ROOT'] = "/real_url" - assert_equal "/real_url", @request.relative_url_root - end - def test_request_uri @request.env['SERVER_SOFTWARE'] = 'Apache 42.342.3432' - @request.relative_url_root = nil @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri?mapped=1" assert_equal "/path/of/some/uri?mapped=1", @request.request_uri assert_equal "/path/of/some/uri", @request.path - @request.relative_url_root = nil @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri" assert_equal "/path/of/some/uri", @request.request_uri assert_equal "/path/of/some/uri", @request.path - @request.relative_url_root = nil @request.set_REQUEST_URI "/path/of/some/uri" assert_equal "/path/of/some/uri", @request.request_uri assert_equal "/path/of/some/uri", @request.path - @request.relative_url_root = nil @request.set_REQUEST_URI "/" assert_equal "/", @request.request_uri assert_equal "/", @request.path - @request.relative_url_root = nil @request.set_REQUEST_URI "/?m=b" assert_equal "/?m=b", @request.request_uri assert_equal "/", @request.path - @request.relative_url_root = nil @request.set_REQUEST_URI "/" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" assert_equal "/", @request.request_uri assert_equal "/", @request.path - @request.relative_url_root = nil + ActionController::Base.relative_url_root = "/hieraki" @request.set_REQUEST_URI "/hieraki/" @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" assert_equal "/hieraki/", @request.request_uri assert_equal "/", @request.path + ActionController::Base.relative_url_root = nil - @request.relative_url_root = nil + ActionController::Base.relative_url_root = "/collaboration/hieraki" @request.set_REQUEST_URI "/collaboration/hieraki/books/edit/2" @request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi" assert_equal "/collaboration/hieraki/books/edit/2", @request.request_uri assert_equal "/books/edit/2", @request.path + ActionController::Base.relative_url_root = nil # The following tests are for when REQUEST_URI is not supplied (as in IIS) - @request.relative_url_root = nil @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" @request.env['SCRIPT_NAME'] = nil #"/path/dispatch.rb" assert_equal "/path/of/some/uri?mapped=1", @request.request_uri assert_equal "/path/of/some/uri", @request.path + ActionController::Base.relative_url_root = '/path' @request.set_REQUEST_URI nil - @request.relative_url_root = nil @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" @request.env['SCRIPT_NAME'] = "/path/dispatch.rb" assert_equal "/path/of/some/uri?mapped=1", @request.request_uri assert_equal "/of/some/uri", @request.path + ActionController::Base.relative_url_root = nil @request.set_REQUEST_URI nil - @request.relative_url_root = nil @request.env['PATH_INFO'] = "/path/of/some/uri" @request.env['SCRIPT_NAME'] = nil assert_equal "/path/of/some/uri", @request.request_uri assert_equal "/path/of/some/uri", @request.path @request.set_REQUEST_URI nil - @request.relative_url_root = nil @request.env['PATH_INFO'] = "/" assert_equal "/", @request.request_uri assert_equal "/", @request.path @request.set_REQUEST_URI nil - @request.relative_url_root = nil @request.env['PATH_INFO'] = "/?m=b" assert_equal "/?m=b", @request.request_uri assert_equal "/", @request.path @request.set_REQUEST_URI nil - @request.relative_url_root = nil @request.env['PATH_INFO'] = "/" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" assert_equal "/", @request.request_uri assert_equal "/", @request.path + ActionController::Base.relative_url_root = '/hieraki' @request.set_REQUEST_URI nil - @request.relative_url_root = nil @request.env['PATH_INFO'] = "/hieraki/" @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" assert_equal "/hieraki/", @request.request_uri assert_equal "/", @request.path + ActionController::Base.relative_url_root = nil @request.set_REQUEST_URI '/hieraki/dispatch.cgi' - @request.relative_url_root = '/hieraki' + ActionController::Base.relative_url_root = '/hieraki' assert_equal "/dispatch.cgi", @request.path - @request.relative_url_root = nil + ActionController::Base.relative_url_root = nil @request.set_REQUEST_URI '/hieraki/dispatch.cgi' - @request.relative_url_root = '/foo' + ActionController::Base.relative_url_root = '/foo' assert_equal "/hieraki/dispatch.cgi", @request.path - @request.relative_url_root = nil + ActionController::Base.relative_url_root = nil # This test ensures that Rails uses REQUEST_URI over PATH_INFO - @request.relative_url_root = nil + ActionController::Base.relative_url_root = nil @request.env['REQUEST_URI'] = "/some/path" @request.env['PATH_INFO'] = "/another/path" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" @@ -276,13 +231,12 @@ class RequestTest < Test::Unit::TestCase assert_equal "/some/path", @request.path end - def test_host_with_default_port @request.host = "rubyonrails.org" @request.port = 80 assert_equal "rubyonrails.org", @request.host_with_port end - + def test_host_with_non_default_port @request.host = "rubyonrails.org" @request.port = 81 @@ -415,15 +369,15 @@ class RequestTest < Test::Unit::TestCase @request.env["CONTENT_TYPE"] = "application/xml; charset=UTF-8" assert_equal Mime::XML, @request.content_type end - + def test_user_agent assert_not_nil @request.user_agent end - + def test_parameters @request.instance_eval { @request_parameters = { "foo" => 1 } } @request.instance_eval { @query_parameters = { "bar" => 2 } } - + assert_equal({"foo" => 1, "bar" => 2}, @request.parameters) assert_equal({"foo" => 1}, @request.request_parameters) assert_equal({"bar" => 2}, @request.query_parameters) @@ -774,19 +728,19 @@ class MultipartRequestParameterParsingTest < Test::Unit::TestCase file = params['file'] foo = params['foo'] - + if RUBY_VERSION > '1.9' assert_kind_of File, file else assert_kind_of Tempfile, file end - + assert_equal 'file.txt', file.original_filename assert_equal "text/plain", file.content_type - + assert_equal 'bar', foo end - + def test_large_text_file params = process('large_text_file') assert_equal %w(file foo), params.keys.sort diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 079189d7b3..84996fd6b1 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -731,15 +731,10 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def request @request ||= MockRequest.new(:host => "named.route.test", :method => :get) end - - def relative_url_root=(value) - request.relative_url_root=value - end end class MockRequest - attr_accessor :path, :path_parameters, :host, :subdomains, :domain, - :method, :relative_url_root + attr_accessor :path, :path_parameters, :host, :subdomains, :domain, :method def initialize(values={}) values.each { |key, value| send("#{key}=", value) } @@ -920,10 +915,11 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def test_basic_named_route_with_relative_url_root rs.add_named_route :home, '', :controller => 'content', :action => 'list' x = setup_for_named_route - x.relative_url_root="/foo" + ActionController::Base.relative_url_root = "/foo" assert_equal("http://named.route.test/foo/", x.send(:home_url)) assert_equal "/foo/", x.send(:home_path) + ActionController::Base.relative_url_root = nil end def test_named_route_with_option diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index a9974db5d5..64e9a085ca 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -7,7 +7,7 @@ class UrlRewriterTests < Test::Unit::TestCase @request = ActionController::TestRequest.new @params = {} @rewriter = ActionController::UrlRewriter.new(@request, @params) - end + end def test_port assert_equal('http://test.host:1271/c/a/i', @@ -24,7 +24,7 @@ class UrlRewriterTests < Test::Unit::TestCase @rewriter.rewrite(:protocol => 'https://', :controller => 'c', :action => 'a', :id => 'i') ) end - + def test_user_name_and_password assert_equal( 'http://david:secret@test.host/c/a/i', @@ -38,12 +38,12 @@ class UrlRewriterTests < Test::Unit::TestCase @rewriter.rewrite(:user => "openid.aol.com/nextangler", :password => "one two?", :controller => 'c', :action => 'a', :id => 'i') ) end - - def test_anchor - assert_equal( - 'http://test.host/c/a/i#anchor', - @rewriter.rewrite(:controller => 'c', :action => 'a', :id => 'i', :anchor => 'anchor') - ) + + def test_anchor + assert_equal( + 'http://test.host/c/a/i#anchor', + @rewriter.rewrite(:controller => 'c', :action => 'a', :id => 'i', :anchor => 'anchor') + ) end def test_overwrite_params @@ -55,12 +55,12 @@ class UrlRewriterTests < Test::Unit::TestCase u = @rewriter.rewrite(:only_path => false, :overwrite_params => {:action => 'hi'}) assert_match %r(/hi/hi/2$), u end - + def test_overwrite_removes_original @params[:controller] = 'search' @params[:action] = 'list' @params[:list_page] = 1 - + assert_equal '/search/list?list_page=2', @rewriter.rewrite(:only_path => true, :overwrite_params => {"list_page" => 2}) u = @rewriter.rewrite(:only_path => false, :overwrite_params => {:list_page => 2}) assert_equal 'http://test.host/search/list?list_page=2', u @@ -86,19 +86,19 @@ class UrlRewriterTests < Test::Unit::TestCase end class UrlWriterTests < Test::Unit::TestCase - + class W include ActionController::UrlWriter end - + def teardown W.default_url_options.clear end - + def add_host! W.default_url_options[:host] = 'www.basecamphq.com' end - + def test_exception_is_thrown_without_host assert_raises RuntimeError do W.new.url_for :controller => 'c', :action => 'a', :id => 'i' @@ -110,35 +110,35 @@ class UrlWriterTests < Test::Unit::TestCase W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => 'anchor') ) end - + def test_default_host add_host! assert_equal('http://www.basecamphq.com/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i') ) end - + def test_host_may_be_overridden add_host! assert_equal('http://37signals.basecamphq.com/c/a/i', W.new.url_for(:host => '37signals.basecamphq.com', :controller => 'c', :action => 'a', :id => 'i') ) end - + def test_port add_host! assert_equal('http://www.basecamphq.com:3000/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :port => 3000) ) end - + def test_protocol add_host! assert_equal('https://www.basecamphq.com/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') ) end - + def test_protocol_with_and_without_separator add_host! assert_equal('https://www.basecamphq.com/c/a/i', @@ -184,15 +184,15 @@ class UrlWriterTests < Test::Unit::TestCase end def test_relative_url_root_is_respected - orig_relative_url_root = ActionController::AbstractRequest.relative_url_root - ActionController::AbstractRequest.relative_url_root = '/subdir' + orig_relative_url_root = ActionController::Base.relative_url_root + ActionController::Base.relative_url_root = '/subdir' add_host! assert_equal('https://www.basecamphq.com/subdir/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') ) ensure - ActionController::AbstractRequest.relative_url_root = orig_relative_url_root + ActionController::Base.relative_url_root = orig_relative_url_root end def test_named_routes @@ -217,8 +217,8 @@ class UrlWriterTests < Test::Unit::TestCase end def test_relative_url_root_is_respected_for_named_routes - orig_relative_url_root = ActionController::AbstractRequest.relative_url_root - ActionController::AbstractRequest.relative_url_root = '/subdir' + orig_relative_url_root = ActionController::Base.relative_url_root + ActionController::Base.relative_url_root = '/subdir' ActionController::Routing::Routes.draw do |map| map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index' @@ -231,7 +231,7 @@ class UrlWriterTests < Test::Unit::TestCase controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again') ensure ActionController::Routing::Routes.load! - ActionController::AbstractRequest.relative_url_root = orig_relative_url_root + ActionController::Base.relative_url_root = orig_relative_url_root end def test_only_path @@ -239,14 +239,14 @@ class UrlWriterTests < Test::Unit::TestCase map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index' map.connect ':controller/:action/:id' end - + # We need to create a new class in order to install the new named route. kls = Class.new { include ActionController::UrlWriter } controller = kls.new assert controller.respond_to?(:home_url) assert_equal '/brave/new/world', controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true) - + assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true)) assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama')) ensure @@ -306,5 +306,4 @@ class UrlWriterTests < Test::Unit::TestCase def extract_params(url) url.split('?', 2).last.split('&') end - end diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 0e7f9a94b7..8410e82c3c 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -30,7 +30,6 @@ class AssetTagHelperTest < ActionView::TestCase end.new @request = Class.new do - def relative_url_root() "" end def protocol() 'http://' end def ssl?() false end def host_with_port() 'localhost' end @@ -118,7 +117,7 @@ class AssetTagHelperTest < ActionView::TestCase %(image_path("xml")) => %(/images/xml), %(image_path("xml.png")) => %(/images/xml.png), %(image_path("dir/xml.png")) => %(/images/dir/xml.png), - %(image_path("/dir/xml.png")) => %(/dir/xml.png) + %(image_path("/dir/xml.png")) => %(/dir/xml.png) } PathToImageToTag = { @@ -173,7 +172,7 @@ class AssetTagHelperTest < ActionView::TestCase ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2' assert_dom_equal %(\n\n\n\n\n\n\n), javascript_include_tag(:defaults) end - + def test_custom_javascript_expansions ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => ["head", "body", "tail"] assert_dom_equal %(\n\n\n\n), javascript_include_tag('first', :monkey, 'last') @@ -216,7 +215,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_image_path ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end - + def test_path_to_image_alias_for_image_path PathToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end @@ -224,7 +223,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_image_tag ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end - + def test_timebased_asset_id expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s assert_equal %(Rails), image_tag("rails.png") @@ -233,7 +232,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_should_skip_asset_id_on_complete_url assert_equal %(Rails), image_tag("http://www.example.com/rails.png") end - + def test_should_use_preset_asset_id ENV["RAILS_ASSET_ID"] = "4500" assert_equal %(Rails), image_tag("rails.png") @@ -255,14 +254,14 @@ class AssetTagHelperTest < ActionView::TestCase ENV["RAILS_ASSET_ID"] = "" ActionController::Base.asset_host = 'http://a0.example.com' ActionController::Base.perform_caching = true - + assert_dom_equal( %(), javascript_include_tag(:all, :cache => true) ) assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) - + assert_dom_equal( %(), javascript_include_tag(:all, :cache => "money") @@ -344,7 +343,7 @@ class AssetTagHelperTest < ActionView::TestCase ensure FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'cache', 'money.js')) end - + def test_caching_javascript_include_tag_with_all_and_recursive_puts_defaults_at_the_start_of_the_file ENV["RAILS_ASSET_ID"] = "" ActionController::Base.asset_host = 'http://a0.example.com' @@ -390,7 +389,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_caching_javascript_include_tag_when_caching_off ENV["RAILS_ASSET_ID"] = "" ActionController::Base.perform_caching = false - + assert_dom_equal( %(\n\n\n\n\n\n\n), javascript_include_tag(:all, :cache => true) @@ -402,7 +401,7 @@ class AssetTagHelperTest < ActionView::TestCase ) assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) - + assert_dom_equal( %(\n\n\n\n\n\n\n), javascript_include_tag(:all, :cache => "money") @@ -420,7 +419,7 @@ class AssetTagHelperTest < ActionView::TestCase ENV["RAILS_ASSET_ID"] = "" ActionController::Base.asset_host = 'http://a0.example.com' ActionController::Base.perform_caching = true - + assert_dom_equal( %(), stylesheet_link_tag(:all, :cache => true) @@ -459,7 +458,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_caching_stylesheet_include_tag_when_caching_off ENV["RAILS_ASSET_ID"] = "" ActionController::Base.perform_caching = false - + assert_dom_equal( %(\n\n), stylesheet_link_tag(:all, :cache => true) @@ -471,7 +470,7 @@ class AssetTagHelperTest < ActionView::TestCase ) assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) - + assert_dom_equal( %(\n\n), stylesheet_link_tag(:all, :cache => "money") @@ -490,6 +489,8 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase tests ActionView::Helpers::AssetTagHelper def setup + ActionController::Base.relative_url_root = "/collaboration/hieraki" + @controller = Class.new do attr_accessor :request @@ -497,22 +498,22 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase "http://www.example.com/collaboration/hieraki" end end.new - - @request = Class.new do - def relative_url_root - "/collaboration/hieraki" - end + @request = Class.new do def protocol 'gopher://' end end.new - + @controller.request = @request - + ActionView::Helpers::AssetTagHelper::reset_javascript_include_default end + def teardown + ActionController::Base.relative_url_root = nil + end + def test_should_compute_proper_path assert_dom_equal(%(), auto_discovery_link_tag) assert_dom_equal(%(/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr")) @@ -521,7 +522,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase assert_dom_equal(%(Mouse), image_tag("mouse.png", :mouseover => "/images/mouse_over.png")) assert_dom_equal(%(Mouse2), image_tag("mouse2.png", :mouseover => image_path("mouse_over2.png"))) end - + def test_should_ignore_relative_root_path_on_complete_url assert_dom_equal(%(http://www.example.com/images/xml.png), image_path("http://www.example.com/images/xml.png")) end -- cgit v1.2.3 From 11fdcf88c2aea72ec84c5d4ab05986f5d35a9a81 Mon Sep 17 00:00:00 2001 From: Sam Granieri Date: Thu, 24 Jul 2008 13:51:54 -0500 Subject: Check for ActionMailer and ActionController before attempting to eager load their view paths Signed-off-by: Joshua Peek --- railties/lib/initializer.rb | 4 ++-- railties/test/initializer_test.rb | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 44863ab026..32411e8928 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -341,8 +341,8 @@ Run `rake gems:install` to install the missing gems. def load_view_paths ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes - ActionMailer::Base.template_root.load - ActionController::Base.view_paths.load + ActionMailer::Base.template_root.load if configuration.frameworks.include?(:action_mailer) + ActionController::Base.view_paths.load if configuration.frameworks.include?(:action_controller) end # Eager load application classes diff --git a/railties/test/initializer_test.rb b/railties/test/initializer_test.rb index dee7abe05f..07303a510e 100644 --- a/railties/test/initializer_test.rb +++ b/railties/test/initializer_test.rb @@ -136,8 +136,27 @@ uses_mocha 'framework paths' do end end - protected + def test_action_mailer_load_paths_set_only_if_action_mailer_in_use + @config.frameworks = [:action_controller] + initializer = Rails::Initializer.new @config + initializer.send :require_frameworks + + assert_nothing_raised NameError do + initializer.send :load_view_paths + end + end + def test_action_controller_load_paths_set_only_if_action_controller_in_use + @config.frameworks = [] + initializer = Rails::Initializer.new @config + initializer.send :require_frameworks + + assert_nothing_raised NameError do + initializer.send :load_view_paths + end + end + + protected def assert_framework_path(path) assert @config.framework_paths.include?(path), "<#{path.inspect}> not found among <#{@config.framework_paths.inspect}>" -- cgit v1.2.3 From f48b9ab5c2741ddbdbc0a9f4cd06875a1e3c8b02 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Thu, 24 Jul 2008 14:06:22 -0500 Subject: ActionController::Base.relative_url_root falls back to ENV['RAILS_RELATIVE_URL_ROOT'] --- actionpack/lib/action_controller/base.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index bae7e8c12e..5f4a38dac0 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -356,7 +356,12 @@ module ActionController #:nodoc: # If you are deploying to a subdirectory, you will need to set # config.action_controller.relative_url_root - class_inheritable_accessor :relative_url_root + # This defaults to ENV['RAILS_RELATIVE_URL_ROOT'] + cattr_writer :relative_url_root + + def self.relative_url_root + @@relative_url_root || ENV['RAILS_RELATIVE_URL_ROOT'] + end # Holds the request object that's primarily used to get environment variables through access like # request.env["REQUEST_URI"]. -- cgit v1.2.3 From e8fc894f66daa7909d1790f2cd145844d256d282 Mon Sep 17 00:00:00 2001 From: George Ogata Date: Wed, 23 Jul 2008 06:38:26 +1000 Subject: Make observers define #after_find in the model only if needed. [#676 state:resolved] Signed-off-by: Michael Koziarski --- activerecord/lib/active_record/observer.rb | 3 +++ activerecord/test/cases/lifecycle_test.rb | 32 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index c96e5f9d51..b35e407cc1 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -189,6 +189,9 @@ module ActiveRecord def add_observer!(klass) klass.add_observer(self) + if respond_to?(:after_find) && !klass.method_defined?(:after_find) + klass.class_eval 'def after_find() end' + end end end end diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 3432abee31..ab005c6b00 100755 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -143,12 +143,12 @@ class LifecycleTest < ActiveRecord::TestCase assert_equal developer.name, multi_observer.record.name end - def test_after_find_cannot_be_observed_when_its_not_defined_on_the_model + def test_after_find_can_be_observed_when_its_not_defined_on_the_model observer = MinimalisticObserver.instance assert_equal Minimalistic, MinimalisticObserver.observed_class minimalistic = Minimalistic.find(1) - assert_nil observer.minimalistic + assert_equal minimalistic, observer.minimalistic end def test_after_find_can_be_observed_when_its_defined_on_the_model @@ -159,6 +159,34 @@ class LifecycleTest < ActiveRecord::TestCase assert_equal topic, observer.topic end + def test_after_find_is_not_created_if_its_not_used + # use a fresh class so an observer can't have defined an + # after_find on it + model_class = Class.new(ActiveRecord::Base) + observer_class = Class.new(ActiveRecord::Observer) + observer_class.observe(model_class) + + observer = observer_class.instance + + assert !model_class.method_defined?(:after_find) + end + + def test_after_find_is_not_clobbered_if_it_already_exists + # use a fresh observer class so we can instantiate it (Observer is + # a Singleton) + model_class = Class.new(ActiveRecord::Base) do + def after_find; end + end + original_method = model_class.instance_method(:after_find) + observer_class = Class.new(ActiveRecord::Observer) do + def after_find; end + end + observer_class.observe(model_class) + + observer = observer_class.instance + assert_equal original_method, model_class.instance_method(:after_find) + end + def test_invalid_observer assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers } end -- cgit v1.2.3 From 490178c93008c6fca20e3e2d6302a28d86aab94d Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sun, 27 Jul 2008 16:06:51 -0500 Subject: Revert "Ensure adapater specific code is loaded on ActiveRecord::Base.establish_connection" This reverts commit 8b858782fa693e89a47fc3dd5ae38d842ede6d04. --- .../connection_adapters/abstract/connection_specification.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 07b122efd1..2a8807fb78 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -211,7 +211,6 @@ module ActiveRecord clear_active_connection_name @active_connection_name = name @@defined_connections[name] = spec - connection when Symbol, String if configuration = configurations[spec.to_s] establish_connection(configuration) -- cgit v1.2.3 From f7abf0c9db61621b3f27061debd7983075cdca61 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Sun, 27 Jul 2008 16:34:20 -0500 Subject: error_message_on takes an options hash instead of ordered parameters [#704 state:resolved] Signed-off-by: Joshua Peek --- .../action_view/helpers/active_record_helper.rb | 46 ++++++++++++++------- actionpack/lib/action_view/helpers/form_helper.rb | 4 +- .../test/template/active_record_helper_test.rb | 48 +++++++++++----------- 3 files changed, 58 insertions(+), 40 deletions(-) diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index 959d8a8563..ff70ce9aae 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -25,7 +25,7 @@ module ActionView # Returns an entire form with all needed input tags for a specified Active Record object. For example, if @post # has attributes named +title+ of type +VARCHAR+ and +body+ of type +TEXT+ then # - # form("post") + # form("post") # # would yield a form like the following (modulus formatting): # @@ -90,23 +90,41 @@ module ActionView end # Returns a string containing the error message attached to the +method+ on the +object+ if one exists. - # This error message is wrapped in a DIV tag, which can be extended to include a +prepend_text+ and/or +append_text+ - # (to properly explain the error), and a +css_class+ to style it accordingly. +object+ should either be the name of an instance variable or - # the actual object. As an example, let's say you have a model @post that has an error message on the +title+ attribute: + # This error message is wrapped in a DIV tag, which can be extended to include a :prepend_text + # and/or :append_text (to properly explain the error), and a :css_class to style it + # accordingly. +object+ should either be the name of an instance variable or the actual object. The method can be + # passed in either as a string or a symbol. + # As an example, let's say you have a model @post that has an error message on the +title+ attribute: # # <%= error_message_on "post", "title" %> # # =>
can't be empty
# - # <%= error_message_on @post, "title" %> + # <%= error_message_on @post, :title %> # # =>
can't be empty
# - # <%= error_message_on "post", "title", "Title simply ", " (or it won't work).", "inputError" %> - # # =>
Title simply can't be empty (or it won't work).
- def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError") + # <%= error_message_on "post", "title", + # :prepend_text => "Title simply ", + # :append_text => " (or it won't work).", + # :css_class => "inputError" %> + def error_message_on(object, method, *args) + options = args.extract_options! + unless args.empty? + ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate' + + 'prepend_text, append_text, and css_class arguments', caller) + + options[:prepend_text] = args[0] || '' + options[:append_text] = args[1] || '' + options[:css_class] = args[2] || 'formError' + end + options.reverse_merge!(:prepend_text => '', :append_text => '', :css_class => 'formError') + if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) && (errors = obj.errors.on(method)) - content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class) - else + content_tag("div", + "#{options[:prepend_text]}#{errors.is_a?(Array) ? errors.first : errors}#{options[:append_text]}", + :class => options[:css_class] + ) + else '' end end @@ -133,7 +151,7 @@ module ActionView # # To specify the display for one object, you simply provide its name as a parameter. # For example, for the @user model: - # + # # error_messages_for 'user' # # To specify more than one object, you simply list them; optionally, you can add an extra :object_name parameter, which @@ -157,7 +175,7 @@ module ActionView else objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact end - + count = objects.inject(0) {|sum, object| sum + object.errors.count } unless count.zero? html = {} @@ -174,7 +192,7 @@ module ActionView I18n.with_options :locale => options[:locale], :scope => [:active_record, :error] do |locale| header_message = if options.include?(:header_message) options[:header_message] - else + else object_name = options[:object_name].to_s.gsub('_', ' ') object_name = I18n.t(object_name, :default => object_name) locale.t :header_message, :count => count, :object_name => object_name @@ -193,7 +211,7 @@ module ActionView '' end end - + private def all_input_tags(record, record_name, options) input_block = options[:input_block] || default_input_block diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index ada6fa2ea8..7bb82ba5bb 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -782,8 +782,8 @@ module ActionView @template.radio_button(@object_name, method, tag_value, objectify_options(options)) end - def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError") - @template.error_message_on(@object, method, prepend_text, append_text, css_class) + def error_message_on(method, *args) + @template.error_message_on(@object, method, *args) end def error_messages(options = {}) diff --git a/actionpack/test/template/active_record_helper_test.rb b/actionpack/test/template/active_record_helper_test.rb index dfc30e651a..e46f95d18b 100644 --- a/actionpack/test/template/active_record_helper_test.rb +++ b/actionpack/test/template/active_record_helper_test.rb @@ -10,17 +10,17 @@ class ActiveRecordHelperTest < ActionView::TestCase alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast) alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast) end - + User = Struct.new("User", :email) User.class_eval do alias_method :email_before_type_cast, :email unless respond_to?(:email_before_type_cast) end - + Column = Struct.new("Column", :type, :name, :human_name) end def setup_post - @post = Post.new + @post = Post.new def @post.errors Class.new { def on(field) @@ -33,12 +33,12 @@ class ActiveRecordHelperTest < ActionView::TestCase false end end - def empty?() false end - def count() 1 end + def empty?() false end + def count() 1 end def full_messages() [ "Author name can't be empty" ] end }.new end - + def @post.new_record?() true end def @post.to_param() nil end @@ -58,16 +58,16 @@ class ActiveRecordHelperTest < ActionView::TestCase end def setup_user - @user = User.new + @user = User.new def @user.errors Class.new { def on(field) field == "email" end - def empty?() false end - def count() 1 end + def empty?() false end + def count() 1 end def full_messages() [ "User email can't be empty" ] end }.new end - + def @user.new_record?() true end def @user.to_param() nil end @@ -81,7 +81,7 @@ class ActiveRecordHelperTest < ActionView::TestCase @user.email = "" end - + def protect_against_forgery? @protect_against_forgery ? true : false end @@ -92,7 +92,7 @@ class ActiveRecordHelperTest < ActionView::TestCase setup_user @response = ActionController::TestResponse.new - + @controller = Object.new def @controller.url_for(options) options = options.symbolize_keys @@ -111,7 +111,7 @@ class ActiveRecordHelperTest < ActionView::TestCase assert_dom_equal( %(
), text_area("post", "body") - ) + ) end def test_text_field_with_errors @@ -140,7 +140,7 @@ class ActiveRecordHelperTest < ActionView::TestCase form("post") ) end - + def test_form_with_protect_against_forgery @protect_against_forgery = true @request_forgery_protection_token = 'authenticity_token' @@ -150,7 +150,7 @@ class ActiveRecordHelperTest < ActionView::TestCase form("post") ) end - + def test_form_with_method_option assert_dom_equal( %(


\n


), @@ -211,9 +211,9 @@ class ActiveRecordHelperTest < ActionView::TestCase other_post = @post assert_dom_equal "
can't be empty
", error_message_on(other_post, :author_name) end - - def test_error_message_on_should_use_options - assert_dom_equal "
beforecan't be emptyafter
", error_message_on(:post, :author_name, "before", "after", "differentError") + + def test_error_message_on_with_options_hash + assert_dom_equal "
beforecan't be emptyafter
", error_message_on(:post, :author_name, :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after') end def test_error_messages_for_many_objects @@ -224,10 +224,10 @@ class ActiveRecordHelperTest < ActionView::TestCase # add the default to put post back in the title assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post", :object_name => "post") - + # symbols work as well assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => :post) - + # any default works too assert_dom_equal %(

2 errors prohibited this monkey from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => "monkey") @@ -242,7 +242,7 @@ class ActiveRecordHelperTest < ActionView::TestCase message = "Please fix the following fields and resubmit:" assert_dom_equal %(

#{header_message}

#{message}

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :header_message => header_message, :message => message) end - + def test_error_messages_for_non_instance_variable actual_user = @user actual_post = @post @@ -251,14 +251,14 @@ class ActiveRecordHelperTest < ActionView::TestCase #explicitly set object assert_dom_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :object => actual_post) - + #multiple objects assert_dom_equal %(

2 errors prohibited this user from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post", :object => [actual_user, actual_post]) - + #nil object assert_equal '', error_messages_for('user', :object => nil) end - + def test_form_with_string_multipart assert_dom_equal( %(


\n


), -- cgit v1.2.3 From 10d9fe4bf3110c1d5de0c6b509fe0cbb9d5eda1d Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Sun, 27 Jul 2008 16:49:19 -0500 Subject: Refactored TextHelper#truncate, highlight, excerpt, word_wrap and auto_link to accept options hash [#705 state:resolved] Signed-off-by: Joshua Peek --- .../action_view/helpers/active_record_helper.rb | 2 +- actionpack/lib/action_view/helpers/text_helper.rb | 206 +++++++++++++++------ actionpack/test/template/text_helper_test.rb | 51 ++++- 3 files changed, 191 insertions(+), 68 deletions(-) diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index ff70ce9aae..fce03ff605 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -109,7 +109,7 @@ module ActionView def error_message_on(object, method, *args) options = args.extract_options! unless args.empty? - ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate' + + ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate ' + 'prepend_text, append_text, and css_class arguments', caller) options[:prepend_text] = args[0] || '' diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 9342b38680..3c9f7230c3 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -34,40 +34,69 @@ module ActionView end if RUBY_VERSION < '1.9' - # If +text+ is longer than +length+, +text+ will be truncated to the length of - # +length+ (defaults to 30) and the last characters will be replaced with the +truncate_string+ - # (defaults to "..."). + # Truncates a given +text+ after a given :length if +text+ is longer than :length + # (defaults to 30). The last characters will be replaced with the :omission (defaults to "..."). # # ==== Examples - # truncate("Once upon a time in a world far far away", 14) - # # => Once upon a... # # truncate("Once upon a time in a world far far away") # # => Once upon a time in a world f... # - # truncate("And they found that many people were sleeping better.", 25, "(clipped)") + # truncate("Once upon a time in a world far far away", :length => 14) + # # => Once upon a... + # + # truncate("And they found that many people were sleeping better.", :length => 25, "(clipped)") # # => And they found that many (clipped) # + # truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 15) + # # => And they found... (continued) + # + # You can still use truncate with the old API that accepts the + # +length+ as its optional second and the +ellipsis+ as its + # optional third parameter: + # truncate("Once upon a time in a world far far away", 14) + # # => Once upon a time in a world f... + # # truncate("And they found that many people were sleeping better.", 15, "... (continued)") # # => And they found... (continued) - def truncate(text, length = 30, truncate_string = "...") + def truncate(text, *args) + options = args.extract_options! + unless args.empty? + ActiveSupport::Deprecation.warn('truncate takes an option hash instead of separate ' + + 'length and omission arguments', caller) + + options[:length] = args[0] || 30 + options[:omission] = args[1] || "..." + end + options.reverse_merge!(:length => 30, :omission => "...") + if text - l = length - truncate_string.chars.length + l = options[:length] - options[:omission].chars.length chars = text.chars - (chars.length > length ? chars[0...l] + truncate_string : text).to_s + (chars.length > options[:length] ? chars[0...l] + options[:omission] : text).to_s end end else - def truncate(text, length = 30, truncate_string = "...") #:nodoc: + def truncate(text, *args) #:nodoc: + options = args.extract_options! + unless args.empty? + ActiveSupport::Deprecation.warn('truncate takes an option hash instead of separate ' + + 'length and omission arguments', caller) + + options[:length] = args[0] || 30 + options[:omission] = args[1] || "..." + end + options.reverse_merge!(:length => 30, :omission => "...") + if text - l = length - truncate_string.length - (text.length > length ? text[0...l] + truncate_string : text).to_s + l = options[:length].to_i - options[:omission].length + (text.length > options[:length].to_i ? text[0...l] + options[:omission] : text).to_s end end end # Highlights one or more +phrases+ everywhere in +text+ by inserting it into - # a +highlighter+ string. The highlighter can be specialized by passing +highlighter+ + # a :highlighter string. The highlighter can be specialized by passing :highlighter # as a single-quoted string with \1 where the phrase is to be inserted (defaults to # '\1') # @@ -78,52 +107,75 @@ module ActionView # highlight('You searched for: ruby, rails, dhh', 'actionpack') # # => You searched for: ruby, rails, dhh # - # highlight('You searched for: rails', ['for', 'rails'], '\1') + # highlight('You searched for: rails', ['for', 'rails'], :highlighter => '\1') # # => You searched for: rails # - # highlight('You searched for: rails', 'rails', "\1") - # # => You searched for: \1') + # highlight('You searched for: rails', 'rails', :highlighter => '\1') + # # => You searched for: rails + # + # You can still use highlight with the old API that accepts the + # +highlighter+ as its optional third parameter: + # highlight('You searched for: rails', 'rails', '\1') # => You searched for: rails + def highlight(text, phrases, *args) + options = args.extract_options! + unless args.empty? + options[:highlighter] = args[0] || '\1' + end + options.reverse_merge!(:highlighter => '\1') + if text.blank? || phrases.blank? text else match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') - text.gsub(/(#{match})/i, highlighter) + text.gsub(/(#{match})/i, options[:highlighter]) end end if RUBY_VERSION < '1.9' # Extracts an excerpt from +text+ that matches the first instance of +phrase+. - # The +radius+ expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters - # defined in +radius+ (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, - # then the +excerpt_string+ will be prepended/appended accordingly. The resulting string will be stripped in any case. - # If the +phrase+ isn't found, nil is returned. + # The :radius option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters + # defined in :radius (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, + # then the :omission option (which defaults to "...") will be prepended/appended accordingly. The resulting string + # will be stripped in any case. If the +phrase+ isn't found, nil is returned. # # ==== Examples - # excerpt('This is an example', 'an', 5) - # # => "...s is an exam..." + # excerpt('This is an example', 'an', :radius => 5) + # # => ...s is an exam... # - # excerpt('This is an example', 'is', 5) - # # => "This is a..." + # excerpt('This is an example', 'is', :radius => 5) + # # => This is a... # # excerpt('This is an example', 'is') - # # => "This is an example" + # # => This is an example # - # excerpt('This next thing is an example', 'ex', 2) - # # => "...next..." + # excerpt('This next thing is an example', 'ex', :radius => 2) + # # => ...next... # - # excerpt('This is also an example', 'an', 8, ' ') - # # => " is also an example" - def excerpt(text, phrase, radius = 100, excerpt_string = "...") + # excerpt('This is also an example', 'an', :radius => 8, :omission => ' ') + # # => is also an example + # + # You can still use excerpt with the old API that accepts the + # +radius+ as its optional third and the +ellipsis+ as its + # optional forth parameter: + # excerpt('This is an example', 'an', 5) # => ...s is an exam... + # excerpt('This is also an example', 'an', 8, ' ') # => is also an example + def excerpt(text, phrase, *args) + options = args.extract_options! + unless args.empty? + options[:radius] = args[0] || 100 + options[:omission] = args[1] || "..." + end + options.reverse_merge!(:radius => 100, :omission => "...") + if text && phrase phrase = Regexp.escape(phrase) if found_pos = text.chars =~ /(#{phrase})/i - start_pos = [ found_pos - radius, 0 ].max - end_pos = [ [ found_pos + phrase.chars.length + radius - 1, 0].max, text.chars.length ].min + start_pos = [ found_pos - options[:radius], 0 ].max + end_pos = [ [ found_pos + phrase.chars.length + options[:radius] - 1, 0].max, text.chars.length ].min - prefix = start_pos > 0 ? excerpt_string : "" - postfix = end_pos < text.chars.length - 1 ? excerpt_string : "" + prefix = start_pos > 0 ? options[:omission] : "" + postfix = end_pos < text.chars.length - 1 ? options[:omission] : "" prefix + text.chars[start_pos..end_pos].strip + postfix else @@ -132,16 +184,23 @@ module ActionView end end else - def excerpt(text, phrase, radius = 100, excerpt_string = "...") #:nodoc: + def excerpt(text, phrase, *args) #:nodoc: + options = args.extract_options! + unless args.empty? + options[:radius] = args[0] || 100 + options[:omission] = args[1] || "..." + end + options.reverse_merge!(:radius => 100, :omission => "...") + if text && phrase phrase = Regexp.escape(phrase) if found_pos = text =~ /(#{phrase})/i - start_pos = [ found_pos - radius, 0 ].max - end_pos = [ [ found_pos + phrase.length + radius - 1, 0].max, text.length ].min + start_pos = [ found_pos - options[:radius], 0 ].max + end_pos = [ [ found_pos + phrase.length + options[:radius] - 1, 0].max, text.length ].min - prefix = start_pos > 0 ? excerpt_string : "" - postfix = end_pos < text.length - 1 ? excerpt_string : "" + prefix = start_pos > 0 ? options[:omission] : "" + postfix = end_pos < text.length - 1 ? options[:omission] : "" prefix + text[start_pos..end_pos].strip + postfix else @@ -176,20 +235,31 @@ module ActionView # (which is 80 by default). # # ==== Examples - # word_wrap('Once upon a time', 4) - # # => Once\nupon\na\ntime - # - # word_wrap('Once upon a time', 8) - # # => Once upon\na time # # word_wrap('Once upon a time') # # => Once upon a time # - # word_wrap('Once upon a time', 1) + # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...') + # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\n a successor to the throne turned out to be more trouble than anyone could have\n imagined... + # + # word_wrap('Once upon a time', :line_width => 8) + # # => Once upon\na time + # + # word_wrap('Once upon a time', :line_width => 1) # # => Once\nupon\na\ntime - def word_wrap(text, line_width = 80) + # + # You can still use word_wrap with the old API that accepts the + # +line_width+ as its optional second parameter: + # word_wrap('Once upon a time', 8) # => Once upon\na time + def word_wrap(text, *args) + options = args.extract_options! + unless args.blank? + options[:line_width] = args[0] || 80 + end + options.reverse_merge!(:line_width => 80) + text.split("\n").collect do |line| - line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line + line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line end * "\n" end @@ -336,12 +406,32 @@ module ActionView # # => "Welcome to my new blog at http://www.m.... # Please e-mail me at me@email.com." # - def auto_link(text, link = :all, href_options = {}, &block) + # + # You can still use auto_link with the old API that accepts the + # +link+ as its optional second parameter and the +html_options+ hash + # as its optional third parameter: + # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com." + # auto_link(post_body, :urls) # => Once upon\na time + # # => "Welcome to my new blog at http://www.myblog.com. + # Please e-mail me at me@email.com." + # + # auto_link(post_body, :all, :target => "_blank") # => Once upon\na time + # # => "Welcome to my new blog at http://www.myblog.com. + # Please e-mail me at me@email.com." + def auto_link(text, *args, &block)#link = :all, href_options = {}, &block) return '' if text.blank? - case link - when :all then auto_link_email_addresses(auto_link_urls(text, href_options, &block), &block) - when :email_addresses then auto_link_email_addresses(text, &block) - when :urls then auto_link_urls(text, href_options, &block) + + options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter + unless args.empty? + options[:link] = args[0] || :all + options[:html] = args[1] || {} + end + options.reverse_merge!(:link => :all, :html => {}) + + case options[:link].to_sym + when :all then auto_link_email_addresses(auto_link_urls(text, options[:html], &block), &block) + when :email_addresses then auto_link_email_addresses(text, &block) + when :urls then auto_link_urls(text, options[:html], &block) end end @@ -468,7 +558,7 @@ module ActionView [-\w]+ # subdomain or domain (?:\.[-\w]+)* # remaining subdomains or domain (?::\d+)? # port - (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$])))*)* # path + (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))+)?)* # path (?:\?[\w\+@%&=.;-]+)? # query string (?:\#[\w\-]*)? # trailing anchor ) @@ -477,8 +567,8 @@ module ActionView # Turns all urls into clickable links. If a block is given, each url # is yielded and the result is used as the link text. - def auto_link_urls(text, href_options = {}) - extra_options = tag_options(href_options.stringify_keys) || "" + def auto_link_urls(text, html_options = {}) + extra_options = tag_options(html_options.stringify_keys) || "" text.gsub(AUTO_LINK_RE) do all, a, b, c, d = $&, $1, $2, $3, $4 if a =~ / 12) + assert_equal "Hello Wor...", truncate("Hello World!!", :length => 12) end def test_truncate_should_use_default_length_of_30 @@ -44,23 +44,29 @@ class TextHelperTest < ActionView::TestCase assert_equal str[0...27] + "...", truncate(str) end + def test_truncate_with_options_hash + assert_equal "This is a string that wil[...]", truncate("This is a string that will go longer then the default truncate length of 30", :omission => "[...]") + assert_equal "Hello W...", truncate("Hello World!", :length => 10) + assert_equal "Hello[...]", truncate("Hello World!", :omission => "[...]", :length => 10) + end + if RUBY_VERSION < '1.9.0' def test_truncate_multibyte with_kcode 'none' do - assert_equal "\354\225\210\353\205\225\355...", truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", 10) + assert_equal "\354\225\210\353\205\225\355...", truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", :length => 10) end with_kcode 'u' do assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...", - truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244", 10) + truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244", :length => 10) end end else def test_truncate_multibyte assert_equal "\354\225\210\353\205\225\355...", - truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", 10) + truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", :length => 10) assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding('UTF-8'), - truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8'), 10) + truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8'), :length => 10) end end @@ -88,7 +94,7 @@ class TextHelperTest < ActionView::TestCase assert_equal ' ', highlight(' ', 'blank text is returned verbatim') end - def test_highlighter_with_regexp + def test_highlight_with_regexp assert_equal( "This is a beautiful! morning", highlight("This is a beautiful! morning", "beautiful!") @@ -105,10 +111,17 @@ class TextHelperTest < ActionView::TestCase ) end - def test_highlighting_multiple_phrases_in_one_pass + def test_highlight_with_multiple_phrases_in_one_pass assert_equal %(wow em), highlight('wow em', %w(wow em), '\1') end + def test_highlight_with_options_hash + assert_equal( + "This is a beautiful morning, but also a beautiful day", + highlight("This is a beautiful morning, but also a beautiful day", "beautiful", :highlighter => '\1') + ) + end + def test_excerpt assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", 5)) assert_equal("This is a...", excerpt("This is a beautiful morning", "this", 5)) @@ -138,6 +151,16 @@ class TextHelperTest < ActionView::TestCase assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', 5)) end + def test_excerpt_with_options_hash + assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5)) + assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", :omission => "[...]",:radius => 5)) + assert_equal( + "This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]", + excerpt("This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?", "very", + :omission => "[...]") + ) + end + if RUBY_VERSION < '1.9' def test_excerpt_with_utf8 with_kcode('u') do @@ -162,6 +185,10 @@ class TextHelperTest < ActionView::TestCase assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", 15)) end + def test_word_wrap_with_options_hash + assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", :line_width => 15)) + end + def test_pluralization assert_equal("1 count", pluralize(1, "count")) assert_equal("2 counts", pluralize(2, "count")) @@ -285,7 +312,13 @@ class TextHelperTest < ActionView::TestCase url = "http://api.rubyonrails.com/Foo.html" email = "fantabulous@shiznadel.ic" - assert_equal %(

#{url[0...7]}...
#{email[0...7]}...

), auto_link("

#{url}
#{email}

") { |url| truncate(url, 10) } + assert_equal %(

#{url[0...7]}...
#{email[0...7]}...

), auto_link("

#{url}
#{email}

") { |url| truncate(url, :length => 10) } + end + + def test_auto_link_with_options_hash + assert_equal 'Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com.', + auto_link("Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com.", + :link => :all, :html => { :class => "menu", :target => "_blank" }) end def test_cycle_class -- cgit v1.2.3 From 6e754551254a8cc64e034163f5d0dc155b450388 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Jul 2008 12:26:59 +0100 Subject: Merge docrails changes --- actionpack/lib/action_controller/integration.rb | 18 +- actionpack/lib/action_controller/response.rb | 48 +- actionpack/lib/action_controller/test_case.rb | 62 +- actionpack/lib/action_controller/test_process.rb | 9 +- actionpack/lib/action_controller/url_rewriter.rb | 97 +- activerecord/lib/active_record/associations.rb | 418 ++++-- activerecord/lib/active_record/base.rb | 29 +- activerecord/lib/active_record/transactions.rb | 9 +- .../lib/active_support/core_ext/array/access.rb | 20 +- .../active_support/core_ext/time/conversions.rb | 1 + railties/Rakefile | 15 + .../doc/guides/creating_plugins/acts_as_yaffle.txt | 193 +++ railties/doc/guides/creating_plugins/appendix.txt | 46 + .../guides/creating_plugins/creating_plugins.txt | 84 ++ .../guides/creating_plugins/custom_generator.txt | 69 + .../doc/guides/creating_plugins/custom_route.txt | 69 + .../creating_plugins/migration_generator.txt | 89 ++ .../doc/guides/creating_plugins/odds_and_ends.txt | 122 ++ .../doc/guides/creating_plugins/preparation.txt | 169 +++ .../guides/creating_plugins/string_to_squawk.txt | 103 ++ .../doc/guides/creating_plugins/view_helper.txt | 61 + railties/doc/guides/icons/README | 5 + railties/doc/guides/icons/callouts/1.png | Bin 0 -> 329 bytes railties/doc/guides/icons/callouts/10.png | Bin 0 -> 361 bytes railties/doc/guides/icons/callouts/11.png | Bin 0 -> 565 bytes railties/doc/guides/icons/callouts/12.png | Bin 0 -> 617 bytes railties/doc/guides/icons/callouts/13.png | Bin 0 -> 623 bytes railties/doc/guides/icons/callouts/14.png | Bin 0 -> 411 bytes railties/doc/guides/icons/callouts/15.png | Bin 0 -> 640 bytes railties/doc/guides/icons/callouts/2.png | Bin 0 -> 353 bytes railties/doc/guides/icons/callouts/3.png | Bin 0 -> 350 bytes railties/doc/guides/icons/callouts/4.png | Bin 0 -> 345 bytes railties/doc/guides/icons/callouts/5.png | Bin 0 -> 348 bytes railties/doc/guides/icons/callouts/6.png | Bin 0 -> 355 bytes railties/doc/guides/icons/callouts/7.png | Bin 0 -> 344 bytes railties/doc/guides/icons/callouts/8.png | Bin 0 -> 357 bytes railties/doc/guides/icons/callouts/9.png | Bin 0 -> 357 bytes railties/doc/guides/icons/caution.png | Bin 0 -> 2554 bytes railties/doc/guides/icons/example.png | Bin 0 -> 2354 bytes railties/doc/guides/icons/home.png | Bin 0 -> 1340 bytes railties/doc/guides/icons/important.png | Bin 0 -> 2657 bytes railties/doc/guides/icons/next.png | Bin 0 -> 1302 bytes railties/doc/guides/icons/note.png | Bin 0 -> 2730 bytes railties/doc/guides/icons/prev.png | Bin 0 -> 1348 bytes railties/doc/guides/icons/tip.png | Bin 0 -> 2602 bytes railties/doc/guides/icons/up.png | Bin 0 -> 1320 bytes railties/doc/guides/icons/warning.png | Bin 0 -> 2828 bytes ...ating_records_directly_from_form_parameters.txt | 86 ++ .../cross_site_scripting.txt | 64 + .../securing_rails_applications.txt | 14 + .../securing_rails_applications/sql_injection.txt | 87 ++ .../testing_rails_applications.txt | 1371 ++++++++++++++++++++ 52 files changed, 3179 insertions(+), 179 deletions(-) create mode 100644 railties/doc/guides/creating_plugins/acts_as_yaffle.txt create mode 100644 railties/doc/guides/creating_plugins/appendix.txt create mode 100644 railties/doc/guides/creating_plugins/creating_plugins.txt create mode 100644 railties/doc/guides/creating_plugins/custom_generator.txt create mode 100644 railties/doc/guides/creating_plugins/custom_route.txt create mode 100644 railties/doc/guides/creating_plugins/migration_generator.txt create mode 100644 railties/doc/guides/creating_plugins/odds_and_ends.txt create mode 100644 railties/doc/guides/creating_plugins/preparation.txt create mode 100644 railties/doc/guides/creating_plugins/string_to_squawk.txt create mode 100644 railties/doc/guides/creating_plugins/view_helper.txt create mode 100644 railties/doc/guides/icons/README create mode 100644 railties/doc/guides/icons/callouts/1.png create mode 100644 railties/doc/guides/icons/callouts/10.png create mode 100644 railties/doc/guides/icons/callouts/11.png create mode 100644 railties/doc/guides/icons/callouts/12.png create mode 100644 railties/doc/guides/icons/callouts/13.png create mode 100644 railties/doc/guides/icons/callouts/14.png create mode 100644 railties/doc/guides/icons/callouts/15.png create mode 100644 railties/doc/guides/icons/callouts/2.png create mode 100644 railties/doc/guides/icons/callouts/3.png create mode 100644 railties/doc/guides/icons/callouts/4.png create mode 100644 railties/doc/guides/icons/callouts/5.png create mode 100644 railties/doc/guides/icons/callouts/6.png create mode 100644 railties/doc/guides/icons/callouts/7.png create mode 100644 railties/doc/guides/icons/callouts/8.png create mode 100644 railties/doc/guides/icons/callouts/9.png create mode 100644 railties/doc/guides/icons/caution.png create mode 100644 railties/doc/guides/icons/example.png create mode 100644 railties/doc/guides/icons/home.png create mode 100644 railties/doc/guides/icons/important.png create mode 100644 railties/doc/guides/icons/next.png create mode 100644 railties/doc/guides/icons/note.png create mode 100644 railties/doc/guides/icons/prev.png create mode 100644 railties/doc/guides/icons/tip.png create mode 100644 railties/doc/guides/icons/up.png create mode 100644 railties/doc/guides/icons/warning.png create mode 100644 railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt create mode 100644 railties/doc/guides/securing_rails_applications/cross_site_scripting.txt create mode 100644 railties/doc/guides/securing_rails_applications/securing_rails_applications.txt create mode 100644 railties/doc/guides/securing_rails_applications/sql_injection.txt create mode 100644 railties/doc/guides/testing_rails_applications/testing_rails_applications.txt diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb index 2a732448f2..43158ea412 100644 --- a/actionpack/lib/action_controller/integration.rb +++ b/actionpack/lib/action_controller/integration.rb @@ -165,11 +165,19 @@ module ActionController status/100 == 3 end - # Performs a GET request with the given parameters. The parameters may - # be +nil+, a Hash, or a string that is appropriately encoded - # (application/x-www-form-urlencoded or multipart/form-data). - # The headers should be a hash. The keys will automatically be upcased, with the - # prefix 'HTTP_' added if needed. + # Performs a GET request with the given parameters. + # + # - +path+: The URI (as a String) on which you want to perform a GET request. + # - +parameters+: The HTTP parameters that you want to pass. This may be +nil+, + # a Hash, or a String that is appropriately encoded + # (application/x-www-form-urlencoded or multipart/form-data). + # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will + # automatically be upcased, with the prefix 'HTTP_' added if needed. + # + # This method returns an AbstractResponse object, which one can use to inspect + # the details of the response. Furthermore, if this method was called from an + # ActionController::IntegrationTest object, then that object's @response + # instance variable will point to the same response object. # # You can also perform POST, PUT, DELETE, and HEAD requests with +post+, # +put+, +delete+, and +head+. diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb index 9955532844..da352b6993 100755 --- a/actionpack/lib/action_controller/response.rb +++ b/actionpack/lib/action_controller/response.rb @@ -1,19 +1,61 @@ require 'digest/md5' -module ActionController - class AbstractResponse #:nodoc: +module ActionController # :nodoc: + # Represents an HTTP response generated by a controller action. One can use an + # ActionController::AbstractResponse object to retrieve the current state of the + # response, or customize the response. An AbstractResponse object can either + # represent a "real" HTTP response (i.e. one that is meant to be sent back to the + # web browser) or a test response (i.e. one that is generated from integration + # tests). See CgiResponse and TestResponse, respectively. + # + # AbstractResponse is mostly a Ruby on Rails framework implement detail, and should + # never be used directly in controllers. Controllers should use the methods defined + # in ActionController::Base instead. For example, if you want to set the HTTP + # response's content MIME type, then use ActionControllerBase#headers instead of + # AbstractResponse#headers. + # + # Nevertheless, integration tests may want to inspect controller responses in more + # detail, and that's when AbstractResponse can be useful for application developers. + # Integration test methods such as ActionController::Integration::Session#get and + # ActionController::Integration::Session#post return objects of type TestResponse + # (which are of course also of type AbstractResponse). + # + # For example, the following demo integration "test" prints the body of the + # controller response to the console: + # + # class DemoControllerTest < ActionController::IntegrationTest + # def test_print_root_path_to_console + # get('/') + # puts @response.body + # end + # end + class AbstractResponse DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } attr_accessor :request - attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout + + # The body content (e.g. HTML) of the response, as a String. + attr_accessor :body + # The headers of the response, as a Hash. It maps header names to header values. + attr_accessor :headers + attr_accessor :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout def initialize @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] end + # Sets the HTTP response's content MIME type. For example, in the controller + # you could write this: + # + # response.content_type = "text/plain" + # + # If a character set has been defined for this response (see charset=) then + # the character set information will also be included in the content type + # information. def content_type=(mime_type) self.headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type end + # Returns the response's content MIME type, or nil if content type has been set. def content_type content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0] content_type.blank? ? nil : content_type diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index c09050c390..3e66947d5f 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -15,23 +15,61 @@ module ActionController end end - # Superclass for Action Controller functional tests. Infers the controller under test from the test class name, - # and creates @controller, @request, @response instance variables. + # Superclass for ActionController functional tests. Functional tests allow you to + # test a single controller action per test method. This should not be confused with + # integration tests (see ActionController::IntegrationTest), which are more like + # "stories" that can involve multiple controllers and mutliple actions (i.e. multiple + # different HTTP requests). # - # class WidgetsControllerTest < ActionController::TestCase - # def test_index - # get :index + # == Basic example + # + # Functional tests are written as follows: + # 1. First, one uses the +get+, +post+, +put+, +delete+ or +head+ method to simulate + # an HTTP request. + # 2. Then, one asserts whether the current state is as expected. "State" can be anything: + # the controller's HTTP response, the database contents, etc. + # + # For example: + # + # class BooksControllerTest < ActionController::TestCase + # def test_create + # # Simulate a POST response with the given HTTP parameters. + # post(:create, :book => { :title => "Love Hina" }) + # + # # Assert that the controller tried to redirect us to + # # the created book's URI. + # assert_response :found + # + # # Assert that the controller really put the book in the database. + # assert_not_nil Book.find_by_title("Love Hina") # end # end # - # * @controller - WidgetController.new - # * @request - ActionController::TestRequest.new - # * @response - ActionController::TestResponse.new + # == Special instance variables + # + # ActionController::TestCase will also automatically provide the following instance + # variables for use in the tests: + # + # @controller:: + # The controller instance that will be tested. + # @request:: + # An ActionController::TestRequest, representing the current HTTP + # request. You can modify this object before sending the HTTP request. For example, + # you might want to set some session properties before sending a GET request. + # @response:: + # An ActionController::TestResponse object, representing the response + # of the last HTTP response. In the above example, @response becomes valid + # after calling +post+. If the various assert methods are not sufficient, then you + # may use this object to inspect the HTTP response in detail. + # + # (Earlier versions of Rails required each functional test to subclass + # Test::Unit::TestCase and define @controller, @request, @response in +setup+.) # - # (Earlier versions of Rails required each functional test to subclass Test::Unit::TestCase and define - # @controller, @request, @response in +setup+.) + # == Controller is automatically inferred # - # If the controller cannot be inferred from the test class name, you can explicity set it with +tests+. + # ActionController::TestCase will automatically infer the controller under test + # from the test class name. If the controller cannot be inferred from the test + # class name, you can explicity set it with +tests+. # # class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase # tests WidgetController @@ -103,4 +141,4 @@ module ActionController @request.remote_addr = '208.77.188.166' # example.com end end -end \ No newline at end of file +end diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index 721592b81f..66675aaa13 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -266,7 +266,13 @@ module ActionController #:nodoc: end end - class TestResponse < AbstractResponse #:nodoc: + # Integration test methods such as ActionController::Integration::Session#get + # and ActionController::Integration::Session#post return objects of class + # TestResponse, which represent the HTTP response results of the requested + # controller actions. + # + # See AbstractResponse for more information on controller response objects. + class TestResponse < AbstractResponse include TestResponseBehavior end @@ -348,6 +354,7 @@ module ActionController #:nodoc: module TestProcess def self.included(base) # execute the request simulating a specific HTTP method and set/volley the response + # TODO: this should be un-DRY'ed for the sake of API documentation. %w( get post put delete head ).each do |method| base.class_eval <<-EOV, __FILE__, __LINE__ def #{method}(action, parameters = nil, session = nil, flash = nil) diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb index d0bf6c0bd4..d86e2db67d 100644 --- a/actionpack/lib/action_controller/url_rewriter.rb +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -1,19 +1,96 @@ module ActionController - # Write URLs from arbitrary places in your codebase, such as your mailers. + # In routes.rb one defines URL-to-controller mappings, but the reverse + # is also possible: an URL can be generated from one of your routing definitions. + # URL generation functionality is centralized in this module. # - # Example: + # See ActionController::Routing and ActionController::Resources for general + # information about routing and routes.rb. # - # class MyMailer - # include ActionController::UrlWriter - # default_url_options[:host] = 'www.basecamphq.com' + # Tip: If you need to generate URLs from your models or some other place, + # then ActionController::UrlWriter is what you're looking for. Read on for + # an introduction. # - # def signup_url(token) - # url_for(:controller => 'signup', action => 'index', :token => token) + # == URL generation from parameters + # + # As you may know, some functions - such as ActionController::Base#url_for + # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set + # of parameters. For example, you've probably had the chance to write code + # like this in one of your views: + # + # <%= link_to('Click here', :controller => 'users', + # :action => 'new', :message => 'Welcome!') %> + # + # #=> Generates a link to: /users/new?message=Welcome%21 + # + # link_to, and all other functions that require URL generation functionality, + # actually use ActionController::UrlWriter under the hood. And in particular, + # they use the ActionController::UrlWriter#url_for method. One can generate + # the same path as the above example by using the following code: + # + # include UrlWriter + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :only_path => true) + # # => "/users/new?message=Welcome%21" + # + # Notice the :only_path => true part. This is because UrlWriter has no + # information about the website hostname that your Rails app is serving. So if you + # want to include the hostname as well, then you must also pass the :host + # argument: + # + # include UrlWriter + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :host => 'www.example.com') # Changed this. + # # => "http://www.example.com/users/new?message=Welcome%21" + # + # By default, all controllers and views have access to a special version of url_for, + # that already knows what the current hostname is. So if you use url_for in your + # controllers or your views, then you don't need to explicitly pass the :host + # argument. + # + # For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for. + # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for' + # in full. However, mailers don't have hostname information, and what's why you'll still + # have to specify the :host argument when generating URLs in mailers. + # + # + # == URL generation for named routes + # + # UrlWriter also allows one to access methods that have been auto-generated from + # named routes. For example, suppose that you have a 'users' resource in your + # routes.rb: + # + # map.resources :users + # + # This generates, among other things, the method users_path. By default, + # this method is accessible from your controllers, views and mailers. If you need + # to access this auto-generated method from other places (such as a model), then + # you can do that in two ways. + # + # The first way is to include ActionController::UrlWriter in your class: + # + # class User < ActiveRecord::Base + # include ActionController::UrlWriter # !!! + # + # def name=(value) + # write_attribute('name', value) + # write_attribute('base_uri', users_path) # !!! # end - # end + # end # - # In addition to providing +url_for+, named routes are also accessible after - # including UrlWriter. + # The second way is to access them through ActionController::UrlWriter. + # The autogenerated named routes methods are available as class methods: + # + # class User < ActiveRecord::Base + # def name=(value) + # write_attribute('name', value) + # path = ActionController::UrlWriter.users_path # !!! + # write_attribute('base_uri', path) # !!! + # end + # end module UrlWriter # The default options for urls written by this writer. Typically a :host # pair is provided. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d916275ab9..4e33dfe69f 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -73,6 +73,7 @@ module ActiveRecord end end + # See ActiveRecord::Associations::ClassMethods for documentation. module Associations # :nodoc: def self.included(base) base.extend(ClassMethods) @@ -150,6 +151,7 @@ module ActiveRecord # #others.destroy_all | X | X | X # #others.find(*args) | X | X | X # #others.find_first | X | | + # #others.exist? | X | X | X # #others.uniq | X | X | X # #others.reset | X | X | X # @@ -612,31 +614,53 @@ module ActiveRecord # All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones # possible. module ClassMethods - # Adds the following methods for retrieval and query of collections of associated objects: - # +collection+ is replaced with the symbol passed as the first argument, so - # has_many :clients would add among others clients.empty?. - # * collection(force_reload = false) - Returns an array of all the associated objects. + # Specifies a one-to-many association. The following methods for retrieval and query of + # collections of associated objects will be added: + # + # [collection(force_reload = false)] + # Returns an array of all the associated objects. # An empty array is returned if none are found. - # * collection<<(object, ...) - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. - # * collection.delete(object, ...) - Removes one or more objects from the collection by setting their foreign keys to +NULL+. + # [collection<<(object, ...)] + # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. + # [collection.delete(object, ...)] + # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model. - # * collection=objects - Replaces the collections content by deleting and adding objects as appropriate. - # * collection_singular_ids - Returns an array of the associated objects' ids - # * collection_singular_ids=ids - Replace the collection with the objects identified by the primary keys in +ids+ - # * collection.clear - Removes every object from the collection. This destroys the associated objects if they - # are associated with :dependent => :destroy, deletes them directly from the database if :dependent => :delete_all, - # otherwise sets their foreign keys to +NULL+. - # * collection.empty? - Returns +true+ if there are no associated objects. - # * collection.size - Returns the number of associated objects. - # * collection.find - Finds an associated object according to the same rules as Base.find. - # * collection.build(attributes = {}, ...) - Returns one or more new objects of the collection type that have been instantiated - # with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an - # associated object already exists, not if it's +nil+! - # * collection.create(attributes = {}) - Returns a new object of the collection type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation). - # *Note:* This only works if an associated object already exists, not if it's +nil+! + # [collection=objects] + # Replaces the collections content by deleting and adding objects as appropriate. + # [collection_singular_ids] + # Returns an array of the associated objects' ids + # [collection_singular_ids=ids] + # Replace the collection with the objects identified by the primary keys in +ids+ + # [collection.clear] + # Removes every object from the collection. This destroys the associated objects if they + # are associated with :dependent => :destroy, deletes them directly from the + # database if :dependent => :delete_all, otherwise sets their foreign keys to +NULL+. + # [collection.empty?] + # Returns +true+ if there are no associated objects. + # [collection.size] + # Returns the number of associated objects. + # [collection.find(...)] + # Finds an associated object according to the same rules as ActiveRecord::Base.find. + # [collection.exist?(...)] + # Checks whether an associated object with the given conditions exists. + # Uses the same rules as ActiveRecord::Base.exists?. + # [collection.build(attributes = {}, ...)] + # Returns one or more new objects of the collection type that have been instantiated + # with +attributes+ and linked to this object through a foreign key, but have not yet + # been saved. Note: This only works if an associated object already exists, not if + # it's +nil+! + # [collection.create(attributes = {})] + # Returns a new object of the collection type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that has already + # been saved (if it passed the validation). Note: This only works if an associated + # object already exists, not if it's +nil+! + # + # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so + # has_many :clients would add among others clients.empty?.) + # + # === Example # - # Example: A Firm class declares has_many :clients, which will add: + # A Firm class declares has_many :clients, which will add: # * Firm#clients (similar to Clients.find :all, :conditions => "firm_id = #{id}") # * Firm#clients<< # * Firm#clients.delete @@ -647,54 +671,77 @@ module ActiveRecord # * Firm#clients.empty? (similar to firm.clients.size == 0) # * Firm#clients.size (similar to Client.count "firm_id = #{id}") # * Firm#clients.find (similar to Client.find(id, :conditions => "firm_id = #{id}")) + # * Firm#clients.exist?(:name => 'ACME') (similar to Client.exist?(:name => 'ACME', :firm_id => firm.id)) # * Firm#clients.build (similar to Client.new("firm_id" => id)) # * Firm#clients.create (similar to c = Client.new("firm_id" => id); c.save; c) # The declaration can also include an options hash to specialize the behavior of the association. # - # Options are: - # * :class_name - Specify the class name of the association. Use it only if that name can't be inferred + # === Supported options + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So has_many :products will by default be linked to the Product class, but # if the real class name is SpecialProduct, you'll have to specify it with this option. - # * :conditions - Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ + # [:conditions] + # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ # SQL fragment, such as price > 5 AND name LIKE 'B%'. Record creations from the association are scoped if a hash # is used. has_many :posts, :conditions => {:published => true} will create published posts with @blog.posts.create # or @blog.posts.build. - # * :order - Specify the order in which the associated objects are returned as an ORDER BY SQL fragment, + # [:order] + # Specify the order in which the associated objects are returned as an ORDER BY SQL fragment, # such as last_name, first_name DESC. - # * :foreign_key - Specify the foreign key used for the association. By default this is guessed to be the name + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id" # as the default :foreign_key. - # * :primary_key - Specify the method that returns the primary key used for the association. By default this is +id+. - # * :dependent - If set to :destroy all the associated objects are destroyed + # [:primary_key] + # Specify the method that returns the primary key used for the association. By default this is +id+. + # [:dependent] + # If set to :destroy all the associated objects are destroyed # alongside this object by calling their +destroy+ method. If set to :delete_all all associated # objects are deleted *without* calling their +destroy+ method. If set to :nullify all associated # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using # the :through option. - # * :finder_sql - Specify a complete SQL statement to fetch the association. This is a good way to go for complex + # [:finder_sql] + # Specify a complete SQL statement to fetch the association. This is a good way to go for complex # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added. - # * :counter_sql - Specify a complete SQL statement to fetch the size of the association. If :finder_sql is + # [:counter_sql] + # Specify a complete SQL statement to fetch the size of the association. If :finder_sql is # specified but not :counter_sql, :counter_sql will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM. - # * :extend - Specify a named module for extending the proxy. See "Association extensions". - # * :include - Specify second-order associations that should be eager loaded when the collection is loaded. - # * :group - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * :limit - An integer determining the limit on the number of rows that should be returned. - # * :offset - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. - # * :select - By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join + # [:extend] + # Specify a named module for extending the proxy. See "Association extensions". + # [:include] + # Specify second-order associations that should be eager loaded when the collection is loaded. + # [:group] + # An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # [:limit] + # An integer determining the limit on the number of rows that should be returned. + # [:offset] + # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # [:select] + # By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * :as - Specifies a polymorphic interface (See belongs_to). - # * :through - Specifies a Join Model through which to perform the query. Options for :class_name and :foreign_key + # [:as] + # Specifies a polymorphic interface (See belongs_to). + # [:through] + # Specifies a Join Model through which to perform the query. Options for :class_name and :foreign_key # are ignored, as the association uses the source reflection. You can only use a :through query through a belongs_to # or has_many association on the join model. - # * :source - Specifies the source association name used by has_many :through queries. Only use it if the name cannot be + # [:source] + # Specifies the source association name used by has_many :through queries. Only use it if the name cannot be # inferred from the association. has_many :subscribers, :through => :subscriptions will look for either :subscribers or # :subscriber on Subscription, unless a :source is given. - # * :source_type - Specifies type of the source association used by has_many :through queries where the source + # [:source_type] + # Specifies type of the source association used by has_many :through queries where the source # association is a polymorphic +belongs_to+. - # * :uniq - If true, duplicates will be omitted from the collection. Useful in conjunction with :through. - # * :readonly - If true, all the associated objects are readonly through the association. - # * :validate - If false, don't validate the associated objects when saving the parent object. true by default. - # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). - # + # [:uniq] + # If true, duplicates will be omitted from the collection. Useful in conjunction with :through. + # [:readonly] + # If true, all the associated objects are readonly through the association. + # [:validate] + # If false, don't validate the associated objects when saving the parent object. true by default. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). + # Option examples: # has_many :comments, :order => "posted_on" # has_many :comments, :include => :author @@ -725,58 +772,91 @@ module ActiveRecord end end - # Adds the following methods for retrieval and query of a single associated object: - # +association+ is replaced with the symbol passed as the first argument, so - # has_one :manager would add among others manager.nil?. - # * association(force_reload = false) - Returns the associated object. +nil+ is returned if none is found. - # * association=(associate) - Assigns the associate object, extracts the primary key, sets it as the foreign key, + # Specifies a one-to-one association with another class. This method should only be used + # if the other class contains the foreign key. If the current class contains the foreign key, + # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use has_one and when to use belongs_to. + # + # The following methods for retrieval and query of a single associated object will be added: + # + # [association(force_reload = false)] + # Returns the associated object. +nil+ is returned if none is found. + # [association=(associate)] + # Assigns the associate object, extracts the primary key, sets it as the foreign key, # and saves the associate object. - # * association.nil? - Returns +true+ if there is no associated object. - # * build_association(attributes = {}) - Returns a new object of the associated type that has been instantiated - # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. Note: This ONLY works if - # an association already exists. It will NOT work if the association is +nil+. - # * create_association(attributes = {}) - Returns a new object of the associated type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation). + # [association.nil?] + # Returns +true+ if there is no associated object. + # [build_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+ and linked to this object through a foreign key, but has not + # yet been saved. Note: This ONLY works if an association already exists. + # It will NOT work if the association is +nil+. + # [create_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that + # has already been saved (if it passed the validation). + # + # (+association+ is replaced with the symbol passed as the first argument, so + # has_one :manager would add among others manager.nil?.) # - # Example: An Account class declares has_one :beneficiary, which will add: + # === Example + # + # An Account class declares has_one :beneficiary, which will add: # * Account#beneficiary (similar to Beneficiary.find(:first, :conditions => "account_id = #{id}")) # * Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save) # * Account#beneficiary.nil? # * Account#build_beneficiary (similar to Beneficiary.new("account_id" => id)) # * Account#create_beneficiary (similar to b = Beneficiary.new("account_id" => id); b.save; b) # + # === Options + # # The declaration can also include an options hash to specialize the behavior of the association. # # Options are: - # * :class_name - Specify the class name of the association. Use it only if that name can't be inferred + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So has_one :manager will by default be linked to the Manager class, but # if the real class name is Person, you'll have to specify it with this option. - # * :conditions - Specify the conditions that the associated object must meet in order to be included as a +WHERE+ + # [:conditions] + # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as rank = 5. - # * :order - Specify the order in which the associated objects are returned as an ORDER BY SQL fragment, + # [:order] + # Specify the order in which the associated objects are returned as an ORDER BY SQL fragment, # such as last_name, first_name DESC. - # * :dependent - If set to :destroy, the associated object is destroyed when this object is. If set to + # [:dependent] + # If set to :destroy, the associated object is destroyed when this object is. If set to # :delete, the associated object is deleted *without* calling its destroy method. If set to :nullify, the associated # object's foreign key is set to +NULL+. Also, association is assigned. - # * :foreign_key - Specify the foreign key used for the association. By default this is guessed to be the name + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association will use "person_id" # as the default :foreign_key. - # * :primary_key - Specify the method that returns the primary key used for the association. By default this is +id+. - # * :include - Specify second-order associations that should be eager loaded when this object is loaded. - # * :as - Specifies a polymorphic interface (See belongs_to). - # * :select - By default, this is * as in SELECT * FROM, but can be changed if, for example, you want to do a join + # [:primary_key] + # Specify the method that returns the primary key used for the association. By default this is +id+. + # [:include] + # Specify second-order associations that should be eager loaded when this object is loaded. + # [:as] + # Specifies a polymorphic interface (See belongs_to). + # [:select] + # By default, this is * as in SELECT * FROM, but can be changed if, for example, you want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * :through: Specifies a Join Model through which to perform the query. Options for :class_name and :foreign_key + # [:through] + # Specifies a Join Model through which to perform the query. Options for :class_name and :foreign_key # are ignored, as the association uses the source reflection. You can only use a :through query through a # has_one or belongs_to association on the join model. - # * :source - Specifies the source association name used by has_one :through queries. Only use it if the name cannot be + # [:source] + # Specifies the source association name used by has_one :through queries. Only use it if the name cannot be # inferred from the association. has_one :favorite, :through => :favorites will look for a # :favorite on Favorite, unless a :source is given. - # * :source_type - Specifies type of the source association used by has_one :through queries where the source + # [:source_type] + # Specifies type of the source association used by has_one :through queries where the source # association is a polymorphic +belongs_to+. - # * :readonly - If true, the associated object is readonly through the association. - # * :validate - If false, don't validate the associated object when saving the parent object. +false+ by default. - # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). + # [:readonly] + # If true, the associated object is readonly through the association. + # [:validate] + # If false, don't validate the associated object when saving the parent object. +false+ by default. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -816,18 +896,34 @@ module ActiveRecord end end - # Adds the following methods for retrieval and query for a single associated object for which this object holds an id: - # +association+ is replaced with the symbol passed as the first argument, so - # belongs_to :author would add among others author.nil?. - # * association(force_reload = false) - Returns the associated object. +nil+ is returned if none is found. - # * association=(associate) - Assigns the associate object, extracts the primary key, and sets it as the foreign key. - # * association.nil? - Returns +true+ if there is no associated object. - # * build_association(attributes = {}) - Returns a new object of the associated type that has been instantiated + # Specifies a one-to-one association with another class. This method should only be used + # if this class contains the foreign key. If the other class contains the foreign key, + # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use +has_one+ and when to use +belongs_to+. + # + # Methods will be added for retrieval and query for a single associated object, for which + # this object holds an id: + # + # [association(force_reload = false)] + # Returns the associated object. +nil+ is returned if none is found. + # [association=(associate)] + # Assigns the associate object, extracts the primary key, and sets it as the foreign key. + # [association.nil?] + # Returns +true+ if there is no associated object. + # [build_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. - # * create_association(attributes = {}) - Returns a new object of the associated type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation). + # [create_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that + # has already been saved (if it passed the validation). + # + # (+association+ is replaced with the symbol passed as the first argument, so + # belongs_to :author would add among others author.nil?.) # - # Example: A Post class declares belongs_to :author, which will add: + # === Example + # + # A Post class declares belongs_to :author, which will add: # * Post#author (similar to Author.find(author_id)) # * Post#author=(author) (similar to post.author_id = author.id) # * Post#author? (similar to post.author == some_author) @@ -836,23 +932,30 @@ module ActiveRecord # * Post#create_author (similar to post.author = Author.new; post.author.save; post.author) # The declaration can also include an options hash to specialize the behavior of the association. # - # Options are: - # * :class_name - Specify the class name of the association. Use it only if that name can't be inferred + # === Options + # + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So has_one :author will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. - # * :conditions - Specify the conditions that the associated object must meet in order to be included as a +WHERE+ + # [:conditions] + # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as authorized = 1. - # * :select - By default, this is * as in SELECT * FROM, but can be changed if, for example, you want to do a join + # [:select] + # By default, this is * as in SELECT * FROM, but can be changed if, for example, you want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * :foreign_key - Specify the foreign key used for the association. By default this is guessed to be the name + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name # of the association with an "_id" suffix. So a class that defines a belongs_to :person association will use # "person_id" as the default :foreign_key. Similarly, belongs_to :favorite_person, :class_name => "Person" # will use a foreign key of "favorite_person_id". - # * :dependent - If set to :destroy, the associated object is destroyed when this object is. If set to + # [:dependent] + # If set to :destroy, the associated object is destroyed when this object is. If set to # :delete, the associated object is deleted *without* calling its destroy method. This option should not be specified when # belongs_to is used in conjunction with a has_many relationship on another class because of the potential to leave # orphaned records behind. - # * :counter_cache - Caches the number of belonging objects on the associate class through the use of +increment_counter+ + # [:counter_cache] + # Caches the number of belonging objects on the associate class through the use of +increment_counter+ # and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's # destroyed. This requires that a column named #{table_name}_count (such as +comments_count+ for a belonging Comment class) # is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing @@ -860,13 +963,18 @@ module ActiveRecord # When creating a counter cache column, the database statement or migration must specify a default value of 0, failing to do # this results in a counter with +NULL+ value, which will never increment. # Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+. - # * :include - Specify second-order associations that should be eager loaded when this object is loaded. - # * :polymorphic - Specify this association is a polymorphic association by passing +true+. + # [:include] + # Specify second-order associations that should be eager loaded when this object is loaded. + # [:polymorphic] + # Specify this association is a polymorphic association by passing +true+. # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the +attr_readonly+ list in the associated classes (e.g. class Post; attr_readonly :comments_count; end). - # * :readonly - If true, the associated object is readonly through the association. - # * :validate - If false, don't validate the associated objects when saving the parent object. +false+ by default. - # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). + # [:readonly] + # If true, the associated object is readonly through the association. + # [:validate] + # If false, don't validate the associated objects when saving the parent object. +false+ by default. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -952,8 +1060,9 @@ module ActiveRecord configure_dependency_for_belongs_to(reflection) end - # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as - # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project + # Specifies a many-to-many relationship with another class. This associates two classes via an + # intermediate join table. Unless the join table is explicitly specified as an option, it is + # guessed using the lexical order of the class names. So a join between Developer and Project # will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence # is calculated using the < operator for String. This means that if the strings are of different lengths, # and the strings are equal when compared up to the shortest length, then the longer string is considered of higher @@ -968,28 +1077,48 @@ module ActiveRecord # associations with attributes to a real join model (see introduction). # # Adds the following methods for retrieval and query: - # +collection+ is replaced with the symbol passed as the first argument, so - # has_and_belongs_to_many :categories would add among others categories.empty?. - # * collection(force_reload = false) - Returns an array of all the associated objects. + # + # [collection(force_reload = false)] + # Returns an array of all the associated objects. # An empty array is returned if none are found. - # * collection<<(object, ...) - Adds one or more objects to the collection by creating associations in the join table + # [collection<<(object, ...)] + # Adds one or more objects to the collection by creating associations in the join table # (collection.push and collection.concat are aliases to this method). - # * collection.delete(object, ...) - Removes one or more objects from the collection by removing their associations from the join table. + # [collection.delete(object, ...)] + # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. - # * collection=objects - Replaces the collection's content by deleting and adding objects as appropriate. - # * collection_singular_ids - Returns an array of the associated objects' ids. - # * collection_singular_ids=ids - Replace the collection by the objects identified by the primary keys in +ids+. - # * collection.clear - Removes every object from the collection. This does not destroy the objects. - # * collection.empty? - Returns +true+ if there are no associated objects. - # * collection.size - Returns the number of associated objects. - # * collection.find(id) - Finds an associated object responding to the +id+ and that + # [collection=objects] + # Replaces the collection's content by deleting and adding objects as appropriate. + # [collection_singular_ids] + # Returns an array of the associated objects' ids. + # [collection_singular_ids=ids] + # Replace the collection by the objects identified by the primary keys in +ids+. + # [collection.clear] + # Removes every object from the collection. This does not destroy the objects. + # [collection.empty?] + # Returns +true+ if there are no associated objects. + # [collection.size] + # Returns the number of associated objects. + # [collection.find(id)] + # Finds an associated object responding to the +id+ and that # meets the condition that it has to be associated with this object. - # * collection.build(attributes = {}) - Returns a new object of the collection type that has been instantiated + # Uses the same rules as ActiveRecord::Base.find. + # [collection.exist?(...)] + # Checks whether an associated object with the given conditions exists. + # Uses the same rules as ActiveRecord::Base.exists?. + # [collection.build(attributes = {})] + # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object through the join table, but has not yet been saved. - # * collection.create(attributes = {}) - Returns a new object of the collection type that has been instantiated + # [collection.create(attributes = {})] + # Returns a new object of the collection type that has been instantiated # with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation). # - # Example: A Developer class declares has_and_belongs_to_many :projects, which will add: + # (+collection+ is replaced with the symbol passed as the first argument, so + # has_and_belongs_to_many :categories would add among others categories.empty?.) + # + # === Example + # + # A Developer class declares has_and_belongs_to_many :projects, which will add: # * Developer#projects # * Developer#projects<< # * Developer#projects.delete @@ -1000,45 +1129,66 @@ module ActiveRecord # * Developer#projects.empty? # * Developer#projects.size # * Developer#projects.find(id) + # * Developer#clients.exist?(...) # * Developer#projects.build (similar to Project.new("project_id" => id)) # * Developer#projects.create (similar to c = Project.new("project_id" => id); c.save; c) # The declaration may include an options hash to specialize the behavior of the association. # - # Options are: - # * :class_name - Specify the class name of the association. Use it only if that name can't be inferred + # === Options + # + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So has_and_belongs_to_many :projects will by default be linked to the # Project class, but if the real class name is SuperProject, you'll have to specify it with this option. - # * :join_table - Specify the name of the join table if the default based on lexical order isn't what you want. - # WARNING: If you're overwriting the table name of either class, the +table_name+ method MUST be declared underneath any - # +has_and_belongs_to_many+ declaration in order to work. - # * :foreign_key - Specify the foreign key used for the association. By default this is guessed to be the name + # [:join_table] + # Specify the name of the join table if the default based on lexical order isn't what you want. + # WARNING: If you're overwriting the table name of either class, the +table_name+ method + # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work. + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association # will use "person_id" as the default :foreign_key. - # * :association_foreign_key - Specify the association foreign key used for the association. By default this is + # [:association_foreign_key] + # Specify the association foreign key used for the association. By default this is # guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project, # the +has_and_belongs_to_many+ association will use "project_id" as the default :association_foreign_key. - # * :conditions - Specify the conditions that the associated object must meet in order to be included as a +WHERE+ + # [:conditions] + # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as authorized = 1. Record creations from the association are scoped if a hash is used. # has_many :posts, :conditions => {:published => true} will create published posts with @blog.posts.create # or @blog.posts.build. - # * :order - Specify the order in which the associated objects are returned as an ORDER BY SQL fragment, + # [:order] + # Specify the order in which the associated objects are returned as an ORDER BY SQL fragment, # such as last_name, first_name DESC - # * :uniq - If true, duplicate associated objects will be ignored by accessors and query methods. - # * :finder_sql - Overwrite the default generated SQL statement used to fetch the association with a manual statement - # * :delete_sql - Overwrite the default generated SQL statement used to remove links between the associated + # [:uniq] + # If true, duplicate associated objects will be ignored by accessors and query methods. + # [:finder_sql] + # Overwrite the default generated SQL statement used to fetch the association with a manual statement + # [:delete_sql] + # Overwrite the default generated SQL statement used to remove links between the associated # classes with a manual statement. - # * :insert_sql - Overwrite the default generated SQL statement used to add links between the associated classes + # [:insert_sql] + # Overwrite the default generated SQL statement used to add links between the associated classes # with a manual statement. - # * :extend - Anonymous module for extending the proxy, see "Association extensions". - # * :include - Specify second-order associations that should be eager loaded when the collection is loaded. - # * :group - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * :limit - An integer determining the limit on the number of rows that should be returned. - # * :offset - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. - # * :select - By default, this is * as in SELECT * FROM, but can be changed if, for example, you want to do a join + # [:extend] + # Anonymous module for extending the proxy, see "Association extensions". + # [:include] + # Specify second-order associations that should be eager loaded when the collection is loaded. + # [:group] + # An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # [:limit] + # An integer determining the limit on the number of rows that should be returned. + # [:offset] + # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # [:select] + # By default, this is * as in SELECT * FROM, but can be changed if, for example, you want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * :readonly - If true, all the associated objects are readonly through the association. - # * :validate - If false, don't validate the associated objects when saving the parent object. +true+ by default. - # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). + # [:readonly] + # If true, all the associated objects are readonly through the association. + # [:validate] + # If false, don't validate the associated objects when saving the parent object. +true+ by default. + # [:accessible<] + # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_and_belongs_to_many :projects diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 4f5d72a0be..9cb64223e2 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -83,8 +83,33 @@ module ActiveRecord #:nodoc: class ReadOnlyRecord < ActiveRecordError end - # Used by Active Record transaction mechanism to distinguish rollback from other exceptional situations. - # You can use it to roll your transaction back explicitly in the block passed to +transaction+ method. + # ActiveRecord::Transactions::ClassMethods.transaction uses this exception + # to distinguish a deliberate rollback from other exceptional situations. + # Normally, raising an exception will cause the +transaction+ method to rollback + # the database transaction *and* pass on the exception. But if you raise an + # ActiveRecord::Rollback exception, then the database transaction will be rolled back, + # without passing on the exception. + # + # For example, you could do this in your controller to rollback a transaction: + # + # class BooksController < ActionController::Base + # def create + # Book.transaction do + # book = Book.new(params[:book]) + # book.save! + # if today_is_friday? + # # The system must fail on Friday so that our support department + # # won't be out of job. We silently rollback this transaction + # # without telling the user. + # raise ActiveRecord::Rollback, "Call tech support!" + # end + # end + # # ActiveRecord::Rollback is the only exception that won't be passed on + # # by ActiveRecord::Base.transaction, so this line will still be reached + # # even on Friday. + # redirect_to root_url + # end + # end class Rollback < ActiveRecordError end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 354a6c83a2..0531afbb52 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -66,12 +66,15 @@ module ActiveRecord # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction # depends on or you can raise exceptions in the callbacks to rollback. # - # == Exception handling + # == Exception handling and rolling back # # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you - # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will - # trigger a ROLLBACK when raised, but not be re-raised by the transaction block. + # should be ready to catch those in your application code. + # + # One exception is the ActiveRecord::Rollback exception, which will trigger a ROLLBACK when raised, + # but not be re-raised by the transaction block. module ClassMethods + # See ActiveRecord::Transactions::ClassMethods for detailed documentation. def transaction(&block) connection.increment_open_transactions diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 4ac95efdc7..779ca40aea 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -8,6 +8,7 @@ module ActiveSupport #:nodoc: # %w( a b c d ).from(0) # => %w( a b c d ) # %w( a b c d ).from(2) # => %w( c d ) # %w( a b c d ).from(10) # => nil + # %w().from(0) # => nil def from(position) self[position..-1] end @@ -17,51 +18,52 @@ module ActiveSupport #:nodoc: # %w( a b c d ).to(0) # => %w( a ) # %w( a b c d ).to(2) # => %w( a b c ) # %w( a b c d ).to(10) # => %w( a b c d ) + # %w().to(0) # => %w() def to(position) self[0..position] end - # Equal to self[1] + # Equals to self[1]. def second self[1] end - # Equal to self[2] + # Equals to self[2]. def third self[2] end - # Equal to self[3] + # Equals to self[3]. def fourth self[3] end - # Equal to self[4] + # Equals to self[4]. def fifth self[4] end - # Equal to self[5] + # Equals to self[5]. def sixth self[5] end - # Equal to self[6] + # Equals to self[6]. def seventh self[6] end - # Equal to self[7] + # Equals to self[7]. def eighth self[7] end - # Equal to self[8] + # Equals to self[8]. def ninth self[8] end - # Equal to self[9] + # Equals to self[9]. def tenth self[9] end diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 9054008309..f42be46770 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -30,6 +30,7 @@ module ActiveSupport #:nodoc: # time.to_s(:time) # => "06:10:17" # # time.to_formatted_s(:db) # => "2007-01-18 06:10:17" + # time.to_formatted_s(:number) # => "20070118061017" # time.to_formatted_s(:short) # => "18 Jan 06:10" # time.to_formatted_s(:long) # => "January 18, 2007 06:10" # time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10" diff --git a/railties/Rakefile b/railties/Rakefile index f64b60b0dd..174c85b59a 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -272,6 +272,21 @@ Rake::RDocTask.new { |rdoc| rdoc.rdoc_files.include('lib/commands/**/*.rb') } +guides = ['securing_rails_applications', 'testing_rails_applications', 'creating_plugins'] +guides_html_files = [] +guides.each do |guide_name| + input = "doc/guides/#{guide_name}/#{guide_name}.txt" + output = "doc/guides/#{guide_name}/#{guide_name}.html" + guides_html_files << output + file output => Dir["doc/guides/#{guide_name}/*.txt"] do + sh "mizuho", input, "--template", "manualsonrails", "--multi-page", + "--icons-dir", "../icons" + end +end + +desc "Generate HTML output for the guides" +task :generate_guides => guides_html_files + # Generate GEM ---------------------------------------------------------------------------- task :copy_gem_environment do diff --git a/railties/doc/guides/creating_plugins/acts_as_yaffle.txt b/railties/doc/guides/creating_plugins/acts_as_yaffle.txt new file mode 100644 index 0000000000..12d40deb18 --- /dev/null +++ b/railties/doc/guides/creating_plugins/acts_as_yaffle.txt @@ -0,0 +1,193 @@ +== Add an `acts_as_yaffle` method to ActiveRecord == + +A common pattern in plugins is to add a method called `acts_as_something` to models. In this case, you want to write a method called `acts_as_yaffle` that adds a `squawk` method to your models. + +To keep things clean, create a new test file called 'acts_as_yaffle_test.rb' in your plugin's test directory and require your test helper. + +[source, ruby] +------------------------------------------------------ +# File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb + +require File.dirname(__FILE__) + '/test_helper.rb' + +class Hickwall < ActiveRecord::Base + acts_as_yaffle +end + +class ActsAsYaffleTest < Test::Unit::TestCase +end +------------------------------------------------------ + +[source, ruby] +------------------------------------------------------ +# File: vendor/plugins/lib/acts_as_yaffle.rb + +module Yaffle +end +------------------------------------------------------ + +One of the most common plugin patterns for `acts_as_yaffle` plugins is to structure your file like so: + +[source, ruby] +------------------------------------------------------ +module Yaffle + def self.included(base) + base.send :extend, ClassMethods + end + + module ClassMethods + # any method placed here will apply to classes, like Hickwall + def acts_as_something + send :include, InstanceMethods + end + end + + module InstanceMethods + # any method placed here will apply to instaces, like @hickwall + end +end +------------------------------------------------------ + +With structure you can easily separate the methods that will be used for the class (like `Hickwall.some_method`) and the instance (like `@hickwell.some_method`). + +Let's add class method named `acts_as_yaffle` - testing it out first. You already defined the ActiveRecord models in your test helper, so if you run tests now they will fail. + +Back in your `acts_as_yaffle` file, update ClassMethods like so: + +[source, ruby] +------------------------------------------------------ +module ClassMethods + def acts_as_yaffle(options = {}) + send :include, InstanceMethods + end +end +------------------------------------------------------ + +Now that test should pass. Since your plugin is going to work with field names, you need to allow people to define the field names, in case there is a naming conflict. You can write a few simple tests for this: + +[source, ruby] +------------------------------------------------------ +# File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb + +require File.dirname(__FILE__) + '/test_helper.rb' + +class ActsAsYaffleTest < Test::Unit::TestCase + def test_a_hickwalls_yaffle_text_field_should_be_last_squawk + assert_equal "last_squawk", Hickwall.yaffle_text_field + end + + def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at + assert_equal "last_squawked_at", Hickwall.yaffle_date_field + end + + def test_a_wickwalls_yaffle_text_field_should_be_last_tweet + assert_equal "last_tweet", Wickwall.yaffle_text_field + end + + def test_a_wickwalls_yaffle_date_field_should_be_last_tweeted_at + assert_equal "last_tweeted_at", Wickwall.yaffle_date_field + end +end +------------------------------------------------------ + +To make these tests pass, you could modify your `acts_as_yaffle` file like so: + +[source, ruby] +------------------------------------------------------ +# File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb + +module Yaffle + def self.included(base) + base.send :extend, ClassMethods + end + + module ClassMethods + def acts_as_yaffle(options = {}) + cattr_accessor :yaffle_text_field, :yaffle_date_field + self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s + self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s + send :include, InstanceMethods + end + end + + module InstanceMethods + end +end +------------------------------------------------------ + +Now you can add tests for the instance methods, and the instance method itself: + +[source, ruby] +------------------------------------------------------ +# File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb + +require File.dirname(__FILE__) + '/test_helper.rb' + +class ActsAsYaffleTest < Test::Unit::TestCase + + def test_a_hickwalls_yaffle_text_field_should_be_last_squawk + assert_equal "last_squawk", Hickwall.yaffle_text_field + end + def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at + assert_equal "last_squawked_at", Hickwall.yaffle_date_field + end + + def test_a_wickwalls_yaffle_text_field_should_be_last_squawk + assert_equal "last_tweet", Wickwall.yaffle_text_field + end + def test_a_wickwalls_yaffle_date_field_should_be_last_squawked_at + assert_equal "last_tweeted_at", Wickwall.yaffle_date_field + end + + def test_hickwalls_squawk_should_populate_last_squawk + hickwall = Hickwall.new + hickwall.squawk("Hello World") + assert_equal "squawk! Hello World", hickwall.last_squawk + end + def test_hickwalls_squawk_should_populate_last_squawked_at + hickwall = Hickwall.new + hickwall.squawk("Hello World") + assert_equal Date.today, hickwall.last_squawked_at + end + + def test_wickwalls_squawk_should_populate_last_tweet + wickwall = Wickwall.new + wickwall.squawk("Hello World") + assert_equal "squawk! Hello World", wickwall.last_tweet + end + def test_wickwalls_squawk_should_populate_last_tweeted_at + wickwall = Wickwall.new + wickwall.squawk("Hello World") + assert_equal Date.today, wickwall.last_tweeted_at + end +end +------------------------------------------------------ + +[source, ruby] +------------------------------------------------------ +# File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb + +module Yaffle + def self.included(base) + base.send :extend, ClassMethods + end + + module ClassMethods + def acts_as_yaffle(options = {}) + cattr_accessor :yaffle_text_field, :yaffle_date_field + self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s + self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s + send :include, InstanceMethods + end + end + + module InstanceMethods + def squawk(string) + write_attribute(self.class.yaffle_text_field, string.to_squawk) + write_attribute(self.class.yaffle_date_field, Date.today) + end + end +end +------------------------------------------------------ + +Note the use of `write_attribute` to write to the field in model. diff --git a/railties/doc/guides/creating_plugins/appendix.txt b/railties/doc/guides/creating_plugins/appendix.txt new file mode 100644 index 0000000000..a78890ccd5 --- /dev/null +++ b/railties/doc/guides/creating_plugins/appendix.txt @@ -0,0 +1,46 @@ +== Appendix == + +=== References === + + * http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-i + * http://nubyonrails.com/articles/2006/05/09/the-complete-guide-to-rails-plugins-part-ii + * http://github.com/technoweenie/attachment_fu/tree/master + * http://daddy.platte.name/2007/05/rails-plugins-keep-initrb-thin.html + +=== Final plugin directory structure === + +The final plugin should have a directory structure that looks something like this: + +------------------------------------------------ + |-- MIT-LICENSE + |-- README + |-- Rakefile + |-- generators + | `-- yaffle + | |-- USAGE + | |-- templates + | | `-- definition.txt + | `-- yaffle_generator.rb + |-- init.rb + |-- install.rb + |-- lib + | |-- acts_as_yaffle.rb + | |-- commands.rb + | |-- core_ext.rb + | |-- routing.rb + | `-- view_helpers.rb + |-- tasks + | `-- yaffle_tasks.rake + |-- test + | |-- acts_as_yaffle_test.rb + | |-- core_ext_test.rb + | |-- database.yml + | |-- debug.log + | |-- routing_test.rb + | |-- schema.rb + | |-- test_helper.rb + | `-- view_helpers_test.rb + |-- uninstall.rb + `-- yaffle_plugin.sqlite3.db +------------------------------------------------ + diff --git a/railties/doc/guides/creating_plugins/creating_plugins.txt b/railties/doc/guides/creating_plugins/creating_plugins.txt new file mode 100644 index 0000000000..f2ed6ed8bb --- /dev/null +++ b/railties/doc/guides/creating_plugins/creating_plugins.txt @@ -0,0 +1,84 @@ +The Basics of Creating Rails Plugins +==================================== + +Pretend for a moment that you are an avid bird watcher. Your favorite bird is the Yaffle, and you want to create a plugin that allows other developers to share in the Yaffle goodness. + +In this tutorial you will learn how to create a plugin that includes: + + * Core Extensions - extending String with a `to_squawk` method: ++ +[source, ruby] +------------------------------------------- +# Anywhere +"hello!".to_squawk # => "squawk! hello!" +------------------------------------------- + +* An `acts_as_yaffle` method for ActiveRecord models that adds a `squawk` method: ++ +[source, ruby] +------------------------------------------- +class Hickwall < ActiveRecord::Base + acts_as_yaffle :yaffle_text_field => :last_sang_at +end + +Hickwall.new.squawk("Hello World") +------------------------------------------- + +* A view helper that will print out squawking info: ++ +[source, ruby] +------------------------------------------- +squawk_info_for(@hickwall) +------------------------------------------- + +* A generator that creates a migration to add squawk columns to a model: ++ +------------------------------------------- +script/generate yaffle hickwall +------------------------------------------- + +* A custom generator command: ++ +[source, ruby] +------------------------------------------- +class YaffleGenerator < Rails::Generator::NamedBase + def manifest + m.yaffle_definition + end +end +------------------------------------------- + +* A custom route method: ++ +[source, ruby] +------------------------------------------- +ActionController::Routing::Routes.draw do |map| + map.yaffles +end +------------------------------------------- + +In addition you'll learn how to: + + * test your plugins. + * work with 'init.rb', how to store model, views, controllers, helpers and even other plugins in your plugins. + * create documentation for your plugin. + * write custom Rake tasks in your plugin. + + +include::preparation.txt[] + +include::string_to_squawk.txt[] + +include::acts_as_yaffle.txt[] + +include::view_helper.txt[] + +include::migration_generator.txt[] + +include::custom_generator.txt[] + +include::custom_route.txt[] + +include::odds_and_ends.txt[] + +include::appendix.txt[] diff --git a/railties/doc/guides/creating_plugins/custom_generator.txt b/railties/doc/guides/creating_plugins/custom_generator.txt new file mode 100644 index 0000000000..6d9613ea01 --- /dev/null +++ b/railties/doc/guides/creating_plugins/custom_generator.txt @@ -0,0 +1,69 @@ +== Add a custom generator command == + +You may have noticed above that you can used one of the built-in rails migration commands `m.migration_template`. You can create your own commands for these, using the following steps: + + 1. Add the require and hook statements to init.rb. + 2. Create the commands - creating 3 sets, Create, Destroy, List. + 3. Add the method to your generator. + +Working with the internals of generators is beyond the scope of this tutorial, but here is a basic example: + +[source, ruby] +----------------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb +require "commands" +Rails::Generator::Commands::Create.send :include, Yaffle::Generator::Commands::Create +Rails::Generator::Commands::Destroy.send :include, Yaffle::Generator::Commands::Destroy +Rails::Generator::Commands::List.send :include, Yaffle::Generator::Commands::List +----------------------------------------------------------- + +[source, ruby] +----------------------------------------------------------- +# File: vendor/plugins/yaffle/lib/commands.rb + +require 'rails_generator' +require 'rails_generator/commands' + +module Yaffle #:nodoc: + module Generator #:nodoc: + module Commands #:nodoc: + module Create + def yaffle_definition + file("definition.txt", "definition.txt") + end + end + + module Destroy + def yaffle_definition + file("definition.txt", "definition.txt") + end + end + + module List + def yaffle_definition + file("definition.txt", "definition.txt") + end + end + end + end +end +----------------------------------------------------------- + +----------------------------------------------------------- +# File: vendor/plugins/yaffle/generators/yaffle/templates/definition.txt + +Yaffle is a bird +----------------------------------------------------------- + +[source, ruby] +----------------------------------------------------------- +# File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb + +class YaffleGenerator < Rails::Generator::NamedBase + def manifest + m.yaffle_definition + end +end +----------------------------------------------------------- + +This example just uses the built-in "file" method, but you could do anything that Ruby allows. diff --git a/railties/doc/guides/creating_plugins/custom_route.txt b/railties/doc/guides/creating_plugins/custom_route.txt new file mode 100644 index 0000000000..7e399247ee --- /dev/null +++ b/railties/doc/guides/creating_plugins/custom_route.txt @@ -0,0 +1,69 @@ +== Add a Custom Route == + +Testing routes in plugins can be complex, especially if the controllers are also in the plugin itself. Jamis Buck showed a great example of this in http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2. + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/test/routing_test.rb + +require "#{File.dirname(__FILE__)}/test_helper" + +class RoutingTest < Test::Unit::TestCase + + def setup + ActionController::Routing::Routes.draw do |map| + map.yaffles + end + end + + def test_yaffles_route + assert_recognition :get, "/yaffles", :controller => "yaffles_controller", :action => "index" + end + + private + + # yes, I know about assert_recognizes, but it has proven problematic to + # use in these tests, since it uses RouteSet#recognize (which actually + # tries to instantiate the controller) and because it uses an awkward + # parameter order. + def assert_recognition(method, path, options) + result = ActionController::Routing::Routes.recognize_path(path, :method => method) + assert_equal options, result + end +end +-------------------------------------------------------- + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb + +require "routing" +ActionController::Routing::RouteSet::Mapper.send :include, Yaffle::Routing::MapperExtensions +-------------------------------------------------------- + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/lib/routing.rb + +module Yaffle #:nodoc: + module Routing #:nodoc: + module MapperExtensions + def yaffles + @set.add_route("/yaffles", {:controller => "yaffles_controller", :action => "index"}) + end + end + end +end +-------------------------------------------------------- + +[source, ruby] +-------------------------------------------------------- +# File: config/routes.rb + +ActionController::Routing::Routes.draw do |map| + ... + map.yaffles +end +-------------------------------------------------------- + +You can also see if your routes work by running `rake routes` from your app directory. diff --git a/railties/doc/guides/creating_plugins/migration_generator.txt b/railties/doc/guides/creating_plugins/migration_generator.txt new file mode 100644 index 0000000000..598a0c8437 --- /dev/null +++ b/railties/doc/guides/creating_plugins/migration_generator.txt @@ -0,0 +1,89 @@ +== Create a migration generator == + +When you created the plugin above, you specified the --with-generator option, so you already have the generator stubs in your plugin. + +We'll be relying on the built-in rails generate template for this tutorial. Going into the details of generators is beyond the scope of this tutorial. + +Type: + + script/generate + +You should see the line: + + Plugins (vendor/plugins): yaffle + +When you run `script/generate yaffle` you should see the contents of your USAGE file. For this plugin, the USAGE file looks like this: + +------------------------------------------------------------------ +Description: + Creates a migration that adds yaffle squawk fields to the given model + +Example: + ./script/generate yaffle hickwall + + This will create: + db/migrate/TIMESTAMP_add_yaffle_fields_to_hickwall +------------------------------------------------------------------ + +Now you can add code to your generator: + +[source, ruby] +------------------------------------------------------------------ +# File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb + +class YaffleGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + m.migration_template 'migration:migration.rb', "db/migrate", {:assigns => yaffle_local_assigns, + :migration_file_name => "add_yaffle_fields_to_#{custom_file_name}" + } + end + end + + private + def custom_file_name + custom_name = class_name.underscore.downcase + custom_name = custom_name.pluralize if ActiveRecord::Base.pluralize_table_names + end + + def yaffle_local_assigns + returning(assigns = {}) do + assigns[:migration_action] = "add" + assigns[:class_name] = "add_yaffle_fields_to_#{custom_file_name}" + assigns[:table_name] = custom_file_name + assigns[:attributes] = [Rails::Generator::GeneratedAttribute.new("last_squawk", "string")] + assigns[:attributes] << Rails::Generator::GeneratedAttribute.new("last_squawked_at", "datetime") + end + end +end +------------------------------------------------------------------ + +Note that you need to be aware of whether or not table names are pluralized. + +This does a few things: + + * Reuses the built in rails `migration_template` method. + * Reuses the built-in rails migration template. + +When you run the generator like + + script/generate yaffle bird + +You will see a new file: + +[source, ruby] +------------------------------------------------------------------ +# File: db/migrate/20080529225649_add_yaffle_fields_to_birds.rb + +class AddYaffleFieldsToBirds < ActiveRecord::Migration + def self.up + add_column :birds, :last_squawk, :string + add_column :birds, :last_squawked_at, :datetime + end + + def self.down + remove_column :birds, :last_squawked_at + remove_column :birds, :last_squawk + end +end +------------------------------------------------------------------ diff --git a/railties/doc/guides/creating_plugins/odds_and_ends.txt b/railties/doc/guides/creating_plugins/odds_and_ends.txt new file mode 100644 index 0000000000..eb127f73ca --- /dev/null +++ b/railties/doc/guides/creating_plugins/odds_and_ends.txt @@ -0,0 +1,122 @@ +== Odds and ends == + +=== Work with init.rb === + +The plugin initializer script 'init.rb' is invoked via `eval` (not `require`) so it has slightly different behavior. + +If you reopen any classes in init.rb itself your changes will potentially be made to the wrong module. There are 2 ways around this: + +The first way is to explicitly define the top-level module space for all modules and classes, like `::Hash`: + +[source, ruby] +--------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb + +class ::Hash + def is_a_special_hash? + true + end +end +--------------------------------------------------- + +OR you can use `module_eval` or `class_eval`: + +--------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb + +Hash.class_eval do + def is_a_special_hash? + true + end +end +--------------------------------------------------- + +=== Generate RDoc Documentation === + +Once your plugin is stable, the tests pass on all database and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy. + +The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are: + + * Your name. + * How to install. + * How to add the functionality to the app (several examples of common use cases). + * Warning, gotchas or tips that might help save users time. + +Once your README is solid, go through and add rdoc comments to all of the methods that developers will use. + +Before you generate your documentation, be sure to go through and add nodoc comments to those modules and methods that are not important to your users. + +Once your comments are good to go, navigate to your plugin directory and run: + + rake rdoc + + +=== Store models, views, helpers, and controllers in your plugins === + +You can easily store models, views, helpers and controllers in plugins. Just create a folder for each in the lib folder, add them to the load path and remove them from the load once path: + +[source, ruby] +--------------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb + +%w{ models controllers helpers }.each do |dir| + path = File.join(directory, 'lib', dir) + $LOAD_PATH << path + Dependencies.load_paths << path + Dependencies.load_once_paths.delete(path) +end +--------------------------------------------------------- + +Adding directories to the load path makes them appear just like files in the the main app directory - except that they are only loaded once, so you have to restart the web server to see the changes in the browser. + +Adding directories to the load once paths allow those changes to picked up as soon as you save the file - without having to restart the web server. + + +=== Write custom Rake tasks in your plugin === + +When you created the plugin with the built-in rails generator, it generated a rake file for you in 'vendor/plugins/yaffle/tasks/yaffle.rake'. Any rake task you add here will be available to the app. + +Many plugin authors put all of their rake tasks into a common namespace that is the same as the plugin, like so: + +[source, ruby] +--------------------------------------------------------- +# File: vendor/plugins/yaffle/tasks/yaffle.rake + +namespace :yaffle do + desc "Prints out the word 'Yaffle'" + task :squawk => :environment do + puts "squawk!" + end +end +--------------------------------------------------------- + +When you run `rake -T` from your plugin you will see: + +--------------------------------------------------------- +yaffle:squawk # Prints out the word 'Yaffle' +--------------------------------------------------------- + +You can add as many files as you want in the tasks directory, and if they end in .rake Rails will pick them up. + +=== Store plugins in alternate locations === + +You can store plugins wherever you want - you just have to add those plugins to the plugins path in 'environment.rb'. + +Since the plugin is only loaded after the plugin paths are defined, you can't redefine this in your plugins - but it may be good to now. + +You can even store plugins inside of other plugins for complete plugin madness! + +[source, ruby] +--------------------------------------------------------- +config.plugin_paths << File.join(RAILS_ROOT,"vendor","plugins","yaffle","lib","plugins") +--------------------------------------------------------- + +=== Create your own Plugin Loaders and Plugin Locators === + +If the built-in plugin behavior is inadequate, you can change almost every aspect of the location and loading process. You can write your own plugin locators and plugin loaders, but that's beyond the scope of this tutorial. + + +=== Use Custom Plugin Generators === + +If you are an RSpec fan, you can install the `rspec_plugin_generator` gem, which will generate the spec folder and database for you. See http://github.com/pat-maddox/rspec-plugin-generator/tree/master. + diff --git a/railties/doc/guides/creating_plugins/preparation.txt b/railties/doc/guides/creating_plugins/preparation.txt new file mode 100644 index 0000000000..77e3a3561f --- /dev/null +++ b/railties/doc/guides/creating_plugins/preparation.txt @@ -0,0 +1,169 @@ +== Preparation == + +=== Create the basic app === + +In this tutorial we will create a basic rails application with 1 resource: bird. Start out by building the basic rails app: + +------------------------------------------------ +rails plugin_demo +cd plugin_demo +script/generate scaffold bird name:string +rake db:migrate +script/server +------------------------------------------------ + +Then navigate to http://localhost:3000/birds. Make sure you have a functioning rails app before continuing. + +NOTE: The aforementioned instructions will work for sqlite3. For more detailed instructions on how to create a rails app for other databases see the API docs. + + +=== Create the plugin === + +The built-in Rails plugin generator stubs out a new plugin. Pass the plugin name, either 'CamelCased' or 'under_scored', as an argument. Pass `\--with-generator` to add an example generator also. + +This creates a plugin in 'vendor/plugins' including an 'init.rb' and 'README' as well as standard 'lib', 'task', and 'test' directories. + +Examples: +---------------------------------------------- +./script/generate plugin BrowserFilters +./script/generate plugin BrowserFilters --with-generator +---------------------------------------------- + +Later in the plugin we will create a generator, so go ahead and add the `\--with-generator` option now: + +---------------------------------------------- +script/generate plugin yaffle --with-generator +---------------------------------------------- + +You should see the following output: + +---------------------------------------------- +create vendor/plugins/yaffle/lib +create vendor/plugins/yaffle/tasks +create vendor/plugins/yaffle/test +create vendor/plugins/yaffle/README +create vendor/plugins/yaffle/MIT-LICENSE +create vendor/plugins/yaffle/Rakefile +create vendor/plugins/yaffle/init.rb +create vendor/plugins/yaffle/install.rb +create vendor/plugins/yaffle/uninstall.rb +create vendor/plugins/yaffle/lib/yaffle.rb +create vendor/plugins/yaffle/tasks/yaffle_tasks.rake +create vendor/plugins/yaffle/test/core_ext_test.rb +create vendor/plugins/yaffle/generators +create vendor/plugins/yaffle/generators/yaffle +create vendor/plugins/yaffle/generators/yaffle/templates +create vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb +create vendor/plugins/yaffle/generators/yaffle/USAGE +---------------------------------------------- + +For this plugin you won't need the file 'vendor/plugins/yaffle/lib/yaffle.rb' so you can delete that. + +---------------------------------------------- +rm vendor/plugins/yaffle/lib/yaffle.rb +---------------------------------------------- + +.Editor's note: +NOTE: Many plugin authors prefer to keep this file, and add all of the require statements in it. That way, they only line in init.rb would be `require "yaffle"`. If you are developing a plugin that has a lot of files in the lib directory, you may want to create a subdirectory like lib/yaffle and store your files in there. That way your init.rb file stays clean + + +=== Setup the plugin for testing === + +Testing plugins that use the entire Rails stack can be complex, and the generator doesn't offer any help. In this tutorial you will learn how to test your plugin against multiple different adapters using ActiveRecord. This tutorial will not cover how to use fixtures in plugin tests. + +To setup your plugin to allow for easy testing you'll need to add 3 files: + + * A 'database.yml' file with all of your connection strings. + * A 'schema.rb' file with your table definitions. + * A test helper that sets up the database before your tests. + +For this plugin you'll need 2 tables/models, Hickwalls and Wickwalls, so add the following files: + +*vendor/plugins/yaffle/test/database.yml:* + +---------------------------------------------- +sqlite: + :adapter: sqlite + :dbfile: yaffle_plugin.sqlite.db + +sqlite3: + :adapter: sqlite3 + :dbfile: yaffle_plugin.sqlite3.db + +postgresql: + :adapter: postgresql + :username: postgres + :password: postgres + :database: yaffle_plugin_test + :min_messages: ERROR + +mysql: + :adapter: mysql + :host: localhost + :username: rails + :password: + :database: yaffle_plugin_test +---------------------------------------------- + +*vendor/plugins/yaffle/test/test_helper.rb:* + +[source, ruby] +---------------------------------------------- +ActiveRecord::Schema.define(:version => 0) do + create_table :hickwalls, :force => true do |t| + t.string :name + t.string :last_squawk + t.datetime :last_squawked_at + end + create_table :wickwalls, :force => true do |t| + t.string :name + t.string :last_tweet + t.datetime :last_tweeted_at + end +end + +# File: vendor/plugins/yaffle/test/test_helper.rb + +ENV['RAILS_ENV'] = 'test' +ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..' + +require 'test/unit' +require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb')) + +config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") + +db_adapter = ENV['DB'] + +# no db passed, try one of these fine config-free DBs before bombing. +db_adapter ||= + begin + require 'rubygems' + require 'sqlite' + 'sqlite' + rescue MissingSourceFile + begin + require 'sqlite3' + 'sqlite3' + rescue MissingSourceFile + end + end + +if db_adapter.nil? + raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3." +end + +ActiveRecord::Base.establish_connection(config[db_adapter]) + +load(File.dirname(__FILE__) + "/schema.rb") + +require File.dirname(__FILE__) + '/../init.rb' + +class Hickwall < ActiveRecord::Base + acts_as_yaffle +end + +class Wickwall < ActiveRecord::Base + acts_as_yaffle :yaffle_text_field => :last_tweet, :yaffle_date_field => :last_tweeted_at +end +---------------------------------------------- diff --git a/railties/doc/guides/creating_plugins/string_to_squawk.txt b/railties/doc/guides/creating_plugins/string_to_squawk.txt new file mode 100644 index 0000000000..50516cef69 --- /dev/null +++ b/railties/doc/guides/creating_plugins/string_to_squawk.txt @@ -0,0 +1,103 @@ +== Add a `to_squawk` method to String == + +To update a core class you will have to: + + * Write tests for the desired functionality. + * Create a file for the code you wish to use. + * Require that file from your 'init.rb'. + +Most plugins store their code classes in the plugin's lib directory. When you add a file to the lib directory, you must also require that file from 'init.rb'. The file you are going to add for this tutorial is 'lib/core_ext.rb'. + +First, you need to write the tests. Testing plugins is very similar to testing rails apps. The generated test file should look something like this: + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/test/core_ext_test.rb + +require 'test/unit' + +class CoreExtTest < Test::Unit::TestCase + # Replace this with your real tests. + def test_this_plugin + flunk + end +end +-------------------------------------------------------- + +Start off by removing the default test, and adding a require statement for your test helper. + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/test/core_ext_test.rb + +require 'test/unit' +require File.dirname(__FILE__) + '/test_helper.rb' + +class CoreExtTest < Test::Unit::TestCase +end +-------------------------------------------------------- + +Navigate to your plugin directory and run `rake test`: + +-------------------------------------------------------- +cd vendor/plugins/yaffle +rake test +-------------------------------------------------------- + +Your test should fail with `no such file to load -- ./test/../lib/core_ext.rb (LoadError)` because we haven't created any file yet. Create the file 'lib/core_ext.rb' and re-run the tests. You should see a different error message: + +-------------------------------------------------------- +1.) Failure ... +No tests were specified +-------------------------------------------------------- + +Great - now you are ready to start development. The first thing we'll do is to add a method to String called `to_squawk` which will prefix the string with the word ``squawk!''. The test will look something like this: + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb + +class CoreExtTest < Test::Unit::TestCase + def test_string_should_respond_to_squawk + assert_equal true, "".respond_to?(:to_squawk) + end + + def test_string_prepend_empty_strings_with_the_word_squawk + assert_equal "squawk!", "".to_squawk + end + + def test_string_prepend_non_empty_strings_with_the_word_squawk + assert_equal "squawk! Hello World", "Hello World".to_squawk + end +end +-------------------------------------------------------- + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb + +require "core_ext" +-------------------------------------------------------- + +[source, ruby] +-------------------------------------------------------- +# File: vendor/plugins/yaffle/lib/core_ext.rb + +String.class_eval do + def to_squawk + "squawk! #{self}".strip + end +end +-------------------------------------------------------- + +When monkey-patching existing classes it's often better to use `class_eval` instead of opening the class directly. + +To test that your method does what it says it does, run the unit tests. To test this manually, fire up a console and start squawking: + +-------------------------------------------------------- +$ ./script/console +>> "Hello World".to_squawk +=> "squawk! Hello World" +-------------------------------------------------------- + +If that worked, congratulations! You just created your first test-driven plugin that extends a core ruby class. diff --git a/railties/doc/guides/creating_plugins/view_helper.txt b/railties/doc/guides/creating_plugins/view_helper.txt new file mode 100644 index 0000000000..b03a190e1a --- /dev/null +++ b/railties/doc/guides/creating_plugins/view_helper.txt @@ -0,0 +1,61 @@ +== Create a `squawk_info_for` view helper == + +Creating a view helper is a 3-step process: + + * Add an appropriately named file to the 'lib' directory. + * Require the file and hooks in 'init.rb'. + * Write the tests. + +First, create the test to define the functionality you want: + +[source, ruby] +--------------------------------------------------------------- +# File: vendor/plugins/yaffle/test/view_helpers_test.rb + +require File.dirname(__FILE__) + '/test_helper.rb' +include YaffleViewHelper + +class ViewHelpersTest < Test::Unit::TestCase + def test_squawk_info_for_should_return_the_text_and_date + time = Time.now + hickwall = Hickwall.new + hickwall.last_squawk = "Hello World" + hickwall.last_squawked_at = time + assert_equal "Hello World, #{time.to_s}", squawk_info_for(hickwall) + end +end +--------------------------------------------------------------- + +Then add the following statements to init.rb: + +[source, ruby] +--------------------------------------------------------------- +# File: vendor/plugins/yaffle/init.rb + +require "view_helpers" +ActionView::Base.send :include, YaffleViewHelper +--------------------------------------------------------------- + +Then add the view helpers file and + +[source, ruby] +--------------------------------------------------------------- +# File: vendor/plugins/yaffle/lib/view_helpers.rb + +module YaffleViewHelper + def squawk_info_for(yaffle) + returning "" do |result| + result << yaffle.read_attribute(yaffle.class.yaffle_text_field) + result << ", " + result << yaffle.read_attribute(yaffle.class.yaffle_date_field).to_s + end + end +end +--------------------------------------------------------------- + +You can also test this in script/console by using the `helper` method: + +--------------------------------------------------------------- +$ ./script/console +>> helper.squawk_info_for(@some_yaffle_instance) +--------------------------------------------------------------- diff --git a/railties/doc/guides/icons/README b/railties/doc/guides/icons/README new file mode 100644 index 0000000000..f12b2a730c --- /dev/null +++ b/railties/doc/guides/icons/README @@ -0,0 +1,5 @@ +Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook +icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency +from the Jimmac icons to get round MS IE and FOP PNG incompatibilies. + +Stuart Rackham diff --git a/railties/doc/guides/icons/callouts/1.png b/railties/doc/guides/icons/callouts/1.png new file mode 100644 index 0000000000..7d473430b7 Binary files /dev/null and b/railties/doc/guides/icons/callouts/1.png differ diff --git a/railties/doc/guides/icons/callouts/10.png b/railties/doc/guides/icons/callouts/10.png new file mode 100644 index 0000000000..997bbc8246 Binary files /dev/null and b/railties/doc/guides/icons/callouts/10.png differ diff --git a/railties/doc/guides/icons/callouts/11.png b/railties/doc/guides/icons/callouts/11.png new file mode 100644 index 0000000000..ce47dac3f5 Binary files /dev/null and b/railties/doc/guides/icons/callouts/11.png differ diff --git a/railties/doc/guides/icons/callouts/12.png b/railties/doc/guides/icons/callouts/12.png new file mode 100644 index 0000000000..31daf4e2f2 Binary files /dev/null and b/railties/doc/guides/icons/callouts/12.png differ diff --git a/railties/doc/guides/icons/callouts/13.png b/railties/doc/guides/icons/callouts/13.png new file mode 100644 index 0000000000..14021a89c2 Binary files /dev/null and b/railties/doc/guides/icons/callouts/13.png differ diff --git a/railties/doc/guides/icons/callouts/14.png b/railties/doc/guides/icons/callouts/14.png new file mode 100644 index 0000000000..64014b75fe Binary files /dev/null and b/railties/doc/guides/icons/callouts/14.png differ diff --git a/railties/doc/guides/icons/callouts/15.png b/railties/doc/guides/icons/callouts/15.png new file mode 100644 index 0000000000..0d65765fcf Binary files /dev/null and b/railties/doc/guides/icons/callouts/15.png differ diff --git a/railties/doc/guides/icons/callouts/2.png b/railties/doc/guides/icons/callouts/2.png new file mode 100644 index 0000000000..5d09341b2f Binary files /dev/null and b/railties/doc/guides/icons/callouts/2.png differ diff --git a/railties/doc/guides/icons/callouts/3.png b/railties/doc/guides/icons/callouts/3.png new file mode 100644 index 0000000000..ef7b700471 Binary files /dev/null and b/railties/doc/guides/icons/callouts/3.png differ diff --git a/railties/doc/guides/icons/callouts/4.png b/railties/doc/guides/icons/callouts/4.png new file mode 100644 index 0000000000..adb8364eb5 Binary files /dev/null and b/railties/doc/guides/icons/callouts/4.png differ diff --git a/railties/doc/guides/icons/callouts/5.png b/railties/doc/guides/icons/callouts/5.png new file mode 100644 index 0000000000..4d7eb46002 Binary files /dev/null and b/railties/doc/guides/icons/callouts/5.png differ diff --git a/railties/doc/guides/icons/callouts/6.png b/railties/doc/guides/icons/callouts/6.png new file mode 100644 index 0000000000..0ba694af6c Binary files /dev/null and b/railties/doc/guides/icons/callouts/6.png differ diff --git a/railties/doc/guides/icons/callouts/7.png b/railties/doc/guides/icons/callouts/7.png new file mode 100644 index 0000000000..472e96f8ac Binary files /dev/null and b/railties/doc/guides/icons/callouts/7.png differ diff --git a/railties/doc/guides/icons/callouts/8.png b/railties/doc/guides/icons/callouts/8.png new file mode 100644 index 0000000000..5e60973c21 Binary files /dev/null and b/railties/doc/guides/icons/callouts/8.png differ diff --git a/railties/doc/guides/icons/callouts/9.png b/railties/doc/guides/icons/callouts/9.png new file mode 100644 index 0000000000..a0676d26cc Binary files /dev/null and b/railties/doc/guides/icons/callouts/9.png differ diff --git a/railties/doc/guides/icons/caution.png b/railties/doc/guides/icons/caution.png new file mode 100644 index 0000000000..cb9d5ea0df Binary files /dev/null and b/railties/doc/guides/icons/caution.png differ diff --git a/railties/doc/guides/icons/example.png b/railties/doc/guides/icons/example.png new file mode 100644 index 0000000000..bba1c0010d Binary files /dev/null and b/railties/doc/guides/icons/example.png differ diff --git a/railties/doc/guides/icons/home.png b/railties/doc/guides/icons/home.png new file mode 100644 index 0000000000..37a5231bac Binary files /dev/null and b/railties/doc/guides/icons/home.png differ diff --git a/railties/doc/guides/icons/important.png b/railties/doc/guides/icons/important.png new file mode 100644 index 0000000000..1096c23295 Binary files /dev/null and b/railties/doc/guides/icons/important.png differ diff --git a/railties/doc/guides/icons/next.png b/railties/doc/guides/icons/next.png new file mode 100644 index 0000000000..64e126bdda Binary files /dev/null and b/railties/doc/guides/icons/next.png differ diff --git a/railties/doc/guides/icons/note.png b/railties/doc/guides/icons/note.png new file mode 100644 index 0000000000..841820f7c4 Binary files /dev/null and b/railties/doc/guides/icons/note.png differ diff --git a/railties/doc/guides/icons/prev.png b/railties/doc/guides/icons/prev.png new file mode 100644 index 0000000000..3e8f12fe24 Binary files /dev/null and b/railties/doc/guides/icons/prev.png differ diff --git a/railties/doc/guides/icons/tip.png b/railties/doc/guides/icons/tip.png new file mode 100644 index 0000000000..a3a029d898 Binary files /dev/null and b/railties/doc/guides/icons/tip.png differ diff --git a/railties/doc/guides/icons/up.png b/railties/doc/guides/icons/up.png new file mode 100644 index 0000000000..2db1ce62fa Binary files /dev/null and b/railties/doc/guides/icons/up.png differ diff --git a/railties/doc/guides/icons/warning.png b/railties/doc/guides/icons/warning.png new file mode 100644 index 0000000000..0b0c419df2 Binary files /dev/null and b/railties/doc/guides/icons/warning.png differ diff --git a/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt b/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt new file mode 100644 index 0000000000..b237c4a17f --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt @@ -0,0 +1,86 @@ +== Creating records directly from form parameters == + +=== The problem === + +Let's say you want to make a user registration system. Your users table looks like this: + +------------------------------------------------------- +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name VARCHAR(20) NOT NULL, -- the login name + password VARCHAR(20) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT "User", -- e.g. "Admin", "Moderator, "User" + approved INTEGER NOT NULL DEFAULT 0 -- is the registered accound approved by the administrator? +); + +CREATE UNIQUE INDEX users_name_unique ON users(name); +------------------------------------------------------- + +The registration form: + +[source, xhtml] +------------------------------------------------------- +
+ + +
+------------------------------------------------------- + +The easiest way to create a user object from the form data in the controller is: + +[source, ruby] +------------------------------------------------------- +User.create(params[:user]) +------------------------------------------------------- + +But what happens if someone decides to save the registration form to his disk and play around with adding a few fields? + +[source, xhtml] +------------------------------------------------------- +
+ + + + +
+------------------------------------------------------- + +He can create an account, make himself admin and approve his own account with one click. + +=== The solution === + +Active Record provides two ways of securing sensitive attributes from being overwritten by malicious users that change the form. The first is `attr_protected` that denies mass-assignment the right to change the named parameters. + +Using `attr_protected`, we can secure the User models like this: + +[source, ruby] +------------------------------------------------------- +class User < ActiveRecord::Base + attr_protected :approved, :role +end +------------------------------------------------------- + +This will ensure that on doing `User.create(params[:user])` both `params[:user][:approved]` and `params[:user][:role]` will be ignored. You'll have to manually set them like this: + +[source, ruby] +------------------------------------------------------- +user = User.new(params[:user]) +user.approved = sanitize_properly(params[:user][:approved]) +user.role = sanitize_properly(params[:user][:role]) +------------------------------------------------------- + +=== Allowing instead of protecting === + +If you're afraid you might forget to apply `attr_protected` to the right attributes before making your model available to the cruel world, you can also specify the protection in reverse. You simply allow access instead of deny it, so only the attributes named in `attr_accessible` are available for mass-assignment. + +Using `attr_accessible`, we can secure the User models like this: + +[source, ruby] +------------------------------------------------------- +class User < ActiveRecord::Base + attr_accessible :name, :password +end +------------------------------------------------------- + +This description has exactly the same affect as doing `attr_protected :approved, :role`, but when you add new attrbutes to the User model, they'll be protected by default. + diff --git a/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt b/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt new file mode 100644 index 0000000000..56b3940511 --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt @@ -0,0 +1,64 @@ +== Cross Site Scripting (CSS/XSS) == + +=== The problem === + +Many web applications use session cookies to track the requests of a user. The cookie is used to identify the request and connect it to the session data (the `session` method in Rails). Usually the session contains a reference to the user that is currently logged in, e.g. the id of a User object. + +Cross Site Scripting is a technique for ``stealing'' the cookie from another visitor of the website, and thus stealing the login. Cookies are only available from the domain where the were originally created. The easiest way to get access to the cookie is to place a specially crafted piece of JavaScript code on the website; the script can read the cookie of a visitor and send it to the attacker, e.g. by transmitting the data as an URL parameter to another website. +2.2 Example of an attack + +A site with the following Eruby template is vulnerable to XSS: + +------------------------------------ +<%= params[:text] %> +------------------------------------ + +The 'text' parameter is directly included in the page, so it is possible to insert JavaScript code by setting the parameter to something like + +[source, xhtml] +------------------------------------ + +------------------------------------ + +After url-encoding the script (e.g. with `CGI.encode`) the attacker can put it into an URL and make his victim open the URL in the browser. + +If you open the URL + +------------------------------------ +http://website.domain/controller/action?text=%3Cscript%3Ealert%28document.cookie%29%3C%2Fscript%3E +------------------------------------ + +you will get a popup window with your session cookie. Instead of opening the popup the script could as well send the session id to another server. + +=== How to protect your application === + +Obviously the key to a successful attack is the possibility to place JavaScript on a website that can receive the session cookie. This can be a blog with user comments, a web forum or a Wiki. How can you protect against it? Strictly convert HTML meta characters (`<` and `>`) to the equivalent HTML entities (`<` and `>`) in every string that is rendered in the website. This will ensure that, no matter what kind of text an attacker enters in a form or attaches to an URL, the browser will always render it as plain text and never interpret any HTML tags. This is a good idea anyway, as a user can easily mess up your layout by leaving tags open. If you want the user to be able to format his texts, it is usually better to use a markup language like Textile, Markdown or RDoc. + +Rails provides the helper method `h` for HTML meta character conversion in Views. + +Example: + +------------------------------------ +<%=h post.subject %> +<%=h post.text %> +------------------------------------ + +You should accustom to using `h` for any variable that is rendered in the view, even if you think you can trust it to be from a reliable source. + +=== XSS attacks using an echo service === + +The echo service is a service running on TCP port 7 that returns back everything you send to it. On Debian it is active by default. Now how can this be a security problem for a web application? + +If the server of the target website 'target.domain' is running an echo service, the attacker could create a form like the following on his own website: + +[source, xhtml] +------------------------------------ +
+ + +
+------------------------------------ + +If he makes someone who has a session cookie on the target website submit this form, the content of the hidden field is sent to the echo server at port 7 – and returned. If the browser decides to display the returned data as HTML (some versions of IE do), it will execute the JavaScript code; and because the domain is 'target.domain' the session cookie is available for the script. + +This is rather a client-side issue, but to reduce the probability of a successful attack you should deactivate any echo services on the web server. However this does not provide full security, because there are also some other services (e.g. FTP, POP3) that can be used instead of the echo server. diff --git a/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt b/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt new file mode 100644 index 0000000000..b2cebbd311 --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt @@ -0,0 +1,14 @@ +Securing Rails applications +=========================== + +This manual describes common security problems in web applications and how +to avoid them with Rails. If you have any questions or suggestions, please +mail me at ror(at)andreas-s.net. + + +include::sql_injection.txt[] + +include::cross_site_scripting.txt[] + +include::creating_records_directly_from_form_parameters.txt[] + diff --git a/railties/doc/guides/securing_rails_applications/sql_injection.txt b/railties/doc/guides/securing_rails_applications/sql_injection.txt new file mode 100644 index 0000000000..51a0520b39 --- /dev/null +++ b/railties/doc/guides/securing_rails_applications/sql_injection.txt @@ -0,0 +1,87 @@ +== SQL Injection == + +=== The problem === + +SQL injection is the #1 security problem in many web applications. How does it work? If the web application includes strings from unreliable sources (usually form parameters) in SQL statements and doesn't correctly quote any SQL meta characters like backslashes or single quotes, an attacker can change `WHERE` conditions in SQL statements, create records with invalid data or even execute arbitrary SQL statements. + +=== How to protect your application === + +If you only use the predefined ActiveRecord functions (attributes, save, find) without writing any conditions, limits or SQL queries yourself, ActiveRecord takes care of quoting any dangerous characters in the data for you. + +If you don't, you have to make sure that strings for arguments that are directly used to build SQL queries (like the condition, limit and sort arguments for `find :all`) do not contain any SQL meta characters. + +=== Using arbitrary strings in conditions and SQL statements === + +==== Wrong ==== + +Imagine a webmail system where a user can select a list of all the emails with a certain subject. A query could look like this: + +[source, ruby] +--------------------------------------------------------------------------- +Email.find(:all, "owner_id = 123 AND subject = '#{params[:subject]}'") +--------------------------------------------------------------------------- + +This is dangerous. Imagine a user sending the string `\' OR 1 \--` in the parameter 'subject'; the resulting statement will look like this: + +[source, ruby] +--------------------------------------------------------------------------- +Email.find(:all, "owner_id = 123 AND subject = '' OR 1 --'") +--------------------------------------------------------------------------- + +Because of ``OR 1'' the condition is always true. The part ``\--'' starts a SQL comment; everything after it will be ignored. The result: the user will get a list of all the emails in the database. + +(Of course the 'owner_id' would have to be inserted dynamically in a real application; this was omitted to keep the examples as simple as possible.) + +==== Correct ==== + +[source, ruby] +--------------------------------------------------------------------------- +Email.find(:all, ["owner_id = 123 AND subject = ?", params[:subject]]) +--------------------------------------------------------------------------- + +If the argument for find_all is an array instead of a string, ActiveRecord will insert the elements 2..n of the array for the `?` placeholders in the element 1, add quotation marks if the elements are strings, and quote all characters that have a special meaning for the database adapter used by the `Email` model. + +If you don't like the syntax of the array, you can take care of the quoting yourself by calling the `quote_value` method of the model class. You have to do this when you use `find_by_sql`, as the Array argument doesn't work there: + +[source, ruby] +--------------------------------------------------------------------------- +Email.find_by_sql("SELECT * FROM email WHERE owner_id = 123 AND subject = #{Email.quote_value(subject)}") +--------------------------------------------------------------------------- + +The quotation marks are added automatically by `Email.quote_value` if the argument is a string. + +=== Extracting queries into model methods === + +If you need to execute a query with the similar options in several places in your code, you should create a model method for that query. Instead of + +[source, ruby] +--------------------------------------------------------------------------- +emails = Email.find(:all, ["subject = ?", subject]) +--------------------------------------------------------------------------- + +you could define the following class method in the model: + +[source, ruby] +--------------------------------------------------------------------------- +class Email < ActiveRecord::Base + def self.find_with_subject(subject) + Email.find(:all, ["subject = ?", subject]) + end +end +--------------------------------------------------------------------------- + +and call it like this: + +[source, ruby] +--------------------------------------------------------------------------- +emails = Email.find_with_subject(subject) +--------------------------------------------------------------------------- + +This has the advantage that you don't have to care about meta characters when using the function `find_with_subject`. Generally you should always make sure that this kind of model method can not break anything, even if it is called with untrusted arguments. + +.ActiveRecord automatically generates finder methods +NOTE: The `find_with_subject` method given in the above example is actually redundant. +ActiveRecord automatically generates finder methods for columns. In this case, ActiveRecord +automatically generates the method `find_by_subject` (which works exactly like the `find_with_subject` +method given in the example). `find_with_subject` is only given here as an example. In general, +you should use ActiveRecord's auto-generated finder methods instead of writing your own. diff --git a/railties/doc/guides/testing_rails_applications/testing_rails_applications.txt b/railties/doc/guides/testing_rails_applications/testing_rails_applications.txt new file mode 100644 index 0000000000..a775ed2f09 --- /dev/null +++ b/railties/doc/guides/testing_rails_applications/testing_rails_applications.txt @@ -0,0 +1,1371 @@ +A Guide to Testing Rails Applications +===================================== + +This article is for fellow Rubyists looking for more information on test writing and how that fits into Ruby On Rails. If you're new to test writing or experienced with test writing, but not in Rails, hopefully you'll gain some practical tips and insight on successful testing. +Assumptions + +Just so we're all on the same page here, I'm making a few assumptions about you. + + * You've got Ruby installed and know how to run a Ruby script. + * You've got Rails installed + * You've created a basic Rails application with 1 controller and 1 model. + +If you haven't accomplished all of the above, you might be jumping ahead of yourself. Check out www.rubyonrails.org for some great beginner's tutorials. + +Make sure you have Ruby 1.8.6 or greater and Rails 2.0 or greater. + +Also, hats off to xal (and others) for creating this site. + + +== Testing 1-2-3... is this thing on? + +=== What are These Strange Creatures? + +I have to say that tests are such a straight-forward concept that it's actually harder to describe what they are than it is to show you how to do them. + +So what are tests? + +The Official Answer:: + Tests are collections of questions and scenarios that provide us (developers) with consistent results which prove that our application is doing what we expect it to do. + +The Unofficial Answer:: + We write tests to interrogate, taunt, bully, punish, and otherwise kick the crap out of our application code until it consistently gives us the correct answer. Think of it as ``tough love''. ;) + +Need some examples? + + * ensure user names have at least 4 characters + * ensure that when we save this record, the administrator is e-mailed + * ensure all replies are deleted when we delete the parent message + * ensure that Canadians have GST of 7% applied to their purchases + +The idea of tests is that you write them at the same time as you write your application. In essence, you're writing two application. As your application grows and becomes more complex, you create many tests. Re-running these tests at any point will help you discover if you've ``broke'' anything along the way. + +Some developers actually take this a step further and create the tests before they create the application. This is called Test Driven Development (TDD), and although I personally don't subscribe to it, it's a totally legit way of coding. + +There's volumes written about Test Driven Development and testing in general. For more information, talk to Mr. Google. + +=== So Why Test? + +As mentioned before, tests offer proof that you've done a good job. Your tests become your own personal QA assistant. If you code your tests correctly, at any point in development, you will know: + + * what processes work + * what processes fail + * the effect of adding new pieces onto your application + +With your tests as your safety net, you can do wild refactoring of your code and be able to tell what needs to be addressed simply by seeing which tests fail. + +In addition to normal "did it pass?" testing, you can go the opposite route. You can explicitly try to break your application such as feeding it unexpected and crazy input, shutting down critical resources like a database to see what happens, and tampering with things such as session values just to see what happens. By testing your application where the weak points are, you can fix it *before* it ever becomes an issue. + +A strong suite of tests ensures your application has a high level of quality. It's worth the extra effort. + +Another positive side-effect of writing tests is that you have basic usage documentation of how things work. Simply by looking at your testing code, you can see how you need to work with an object to make it do it's thing. + +But enough about theory. + + +== Introducing Test/Unit + +=== The Small Picture + +==== Hello World + +As we all know, Ruby ships with a boat load of great libraries. If you're like me, you've only used a quarter of them or so. One little gem of a library is 'test/unit'. It comes packaged with Ruby 1.8.1, so there's nothing to download. + +By using one line of code `require \'test/unit'`, you arm your Ruby script with the capability to write tests. + +Let's show you a really basic test, and then we'll step through it and explain some details. + +[source,ruby] +------------------------------------------------------ +# hello_test.rb + +require 'test/unit' + +class HelloTestCase < Test::Unit::TestCase + def test_hello + assert true + end +end +------------------------------------------------------ + +Quite possibly the simplest and least useful test ever invented, but it shows you the bare bones of writing a test case. That script can be run from the command line the same way your run any other Ruby script. + +Simply run `ruby hello_test.rb` and you will see the following: + +------------------------------------------------------ +Started +. +Finished in 0.0 seconds. + +1 tests, 1 assertions, 0 failures, 0 errors +------------------------------------------------------ + +Congratulations, your first test. + + +==== What Does It All Mean? + +By looking at the output of a test, you will be able to tell if the tests pass or not. In our example, not surprisingly, we've passed. The summary shows *1 test, 1 assertion, 0 failures, and 0 errors*. + +So, let's break our test source code down. + +First, line 1. + +[source,ruby] +------------------------------------------------------ +require 'test/unit' +------------------------------------------------------ + +You'll always have this when writing unit tests. It contains the classes and functionality to write and run unit tests. + +Next, we have a class `HelloTestCase` which derives from `Test::Unit::TestCase`. + +[source,ruby] +------------------------------------------------------ +class HelloTestCase < Test::Unit::TestCase +------------------------------------------------------ + +All of the tests that you create will directly subclass `Test::Unit::TestCase`. The TestCase provides the ``housing'' of where your tests will live. More on this in a bit. + +Finally, we have a method called `test_hello`. + +[source,ruby] +------------------------------------------------------ +def test_hello +------------------------------------------------------ + +All tests must follow this naming convention: *their names start with the first 4 characters test*, as in `test_hello`, `testme`, and `testarosa`. If you create a method that doesn't start with test, the testing framework won't recognize it as a test, hence, won't run it automatically, hence it is a normal Ruby method. + +Inside our `test_hello` method, we have an assertion. + +[source,ruby] +------------------------------------------------------ +assert true +------------------------------------------------------ + +Assertions are where the real work gets done. There are a whole army of different types of assertions that you'll use to make sure your code is doing the right thing. + + +=== The Big Picture + +Grab a cup of coffee and dunk your head in some ice water, because here's some more theory. + +There are 4 major players in the testing game. + +==== A: The Assertion + +An 'Assertion' is 1 line of code that evaluates an object (or expression) for expected results. + +For example, is this value = that value? is this object nil? does this line of code throw an exception? is the user's password greater than 5 characters? + +==== B: The Test ==== + +A 'Test' is method that contains assertions which represent a particular testing scenario. + +For example, we may have a test method called `test_valid_password`. In order for this test to pass, we might need to check a few things: + + * password is 4 or more characters + * password isn't the word ‘password' + * password isn't all spaces + +If all of these assertions are successful, the test will pass. + +==== C: The Test Case ==== +A 'Test Case' is a class inherited from `Test::Unit::TestCase` containing a testing ``strategy'' comprised of contextually related tests. + +For example, I may have a test case called UserTestCase which has a bunch of tests that check that: + + * the password is valid (`test_password`) + * the username doesn't have any forbidden words (`test_username_cussin`) + * a user is under the age of 150 (`test_shriveled_raisin`) + +==== D: The Test Suite ==== +A 'Test Suite' is a collection of test cases. When you run a test suite, it will, in turn, execute each test that belongs to it. + +For example, instead of running each test unit individually, you can run them all by creating a suite and including them. This is good for stuff like continuous-build integration. + +We won't get into test suites in this article. + + +==== The Hierarchy ==== + +The relationship of these objects to one-another looks like this: + + * a test suite + * has many test cases + * which have many tests + * which have many assertions + + +== Hello World on Steroids == + +=== The Victim === + +In the last episode, we learned that we write tests to prove our code works properly. Let's create a really simple class for us to test. + +[source,ruby] +-------------------------------------------------- +# secret_agent.rb +class SecretAgent + + # simple public properties + attr_accessor :username + attr_accessor :password + + # our "constructor" + def initialize(username,password) + @username = username + @password = password + end + + # the logic to determine if the password is + # good enough for the user to use + def is_password_secure? + return false if @password.nil? + return false if @password.empty? + return false if @password.size < 4 + return false if @password 'stirred' + return false if @password ‘password' + return false if @password == @username + true + end +end +-------------------------------------------------- + +Ok. So, we've got a class which represents a secret agent. What we're about to do is test a user to make sure that their password is secure enough to use in our top-secret database. + +The `is_password_secure?` method will answer that for us. If the user's password passes our stringent set of rules, then the function will return a true value and the secret agent granted access. + + +=== The Assault === + +Let's build upon the last test we wrote and add in a few more things, specifically, let's test out this brand new SecretAgent. + +[source,ruby] +----------------------------------------------- +require 'test/unit' +require 'secret_agent' + +class HelloTestCase < Test::Unit::TestCase + + def test_hello + assert true + end + + # our new test will exercise the new SecretAgent class + def test_these_passwords + # first, let's try a few passwords that should fail + assert !SecretAgent.new("bond","abc").is_password_secure? + assert !SecretAgent.new("bond","007").is_password_secure? + assert !SecretAgent.new("bond","stirred").is_password_secure? + assert !SecretAgent.new("bond","password").is_password_secure? + assert !SecretAgent.new("bond","bond").is_password_secure? + assert !SecretAgent.new("bond","").is_password_secure? + assert !SecretAgent.new("bond",nil).is_password_secure? + + # now, let's try passwords that should succeed + assert SecretAgent.new("bond","goldfinger").is_password_secure? + assert SecretAgent.new("bond","1234").is_password_secure? + assert SecretAgent.new("bond","shaken").is_password_secure? + end +end +----------------------------------------------- + +In this example, we've expanded our test case by adding another test called `test_these_passwords` with 10 assertions. Let's run it, cross our fingers, and hope it passes. + +----------------------------------------------- +Started +.. +Finished in 0.01 seconds. + +2 tests, 11 assertions, 0 failures, 0 errors +----------------------------------------------- + +Sweet! It passed! + +A couple of new things to notice about the results. See the line right underneath the word Started? Notice it has 2 dots instead of only 1 before? Each . represents a test. You can have 1 of 3 values where the dots are. + + * *.* means successful test (pass) + * *F* means failed test + * *E* means an error has occurred + +=== Failure, Error and General Discomfort === + +Let's add another in 2 more tests. This time, we'll make one of them fail and the other throw an error. Yes, on purpose. Yes, I'm a trained professional. + +[source,ruby] +--------------------------------------------------------------- +... +# this will result in a failure because the assertion fails... plus everyone +# knows batman really exists +def test_bam_zap_sock_pow + batman = nil + assert_not_nil batman +end + +# this will result in an error because it contains an undefined symbol +def test_uh_oh_hotdog + assert_not_nil does_this_var_speak_korean +end +... +--------------------------------------------------------------- + +Ok... Let's run this puppy. + +--------------------------------------------------------------- +Started +F..E +Finished in 0.07 seconds. + +1) Failure: +test_bam_zap_sock_pow(HelloTestCase) [/example/test.rb:62]: + expected to not be nil. + +2) Error: +test_uh_oh_hotdog(HelloTestCase): +NameError: undefined local variable or method 'does_this_var_speak_korean' for +# /example/test.rb:68:in 'test_uh_oh_hotdog' + +4 tests, 12 assertions, 1 failures, 1 errors +--------------------------------------------------------------- + +Wow. Nasty. + +Take a look at the 2nd line. This time we have 'F..E' which means failure, pass, pass, error. In the details underneath the total elapsed time, you see what exactly went wrong and where. + +The incredibly observant will notice that even though the two new methods were added as the 3rd and 4th tests, they showed up in the results as 1st and 4th. That's because the tests are sorted alphabetically. + +So, that's what errors and failures look like. The difference is, a failure represents an assertion attempt that gave us the wrong results whereas an error is a Ruby problem. + + +=== This Side Up ^ === + +Another thing about assertions. They're fragile. If the test finds an assertion that fails, it will stop execution of that method and move on to the next test. + +If I had a test with 4 assertions, of which numbers 2, 3, and 4 were all going to fail, you'd only see #2 as the cause of the failed test. If you were to correct that failure, then rerun the test, it would be #3 thats causes grief. + +Got it? Good, there will be a pop quiz on Monday. + + +== The Test Case Life Cycle == + +=== 4.1 A Quick Recap === + +You already saw a simple test case in action. It looked something like this: + +[source,ruby] +--------------------------------------------------------- +require 'test/unit' + +class MyTestCase < Test::Unit::TestCase + + def test_1 + assert true + end + + def test_2 + end + + def test_3 + end + +end +--------------------------------------------------------- + +You saw that: + + * we need to use `require ‘test/unit'` + * we need to inherit from `Test::Unit::TestCase` + * we need to define methods that start with `test` + * we need assertions to prove our code works + +Next, we're going to look at the flow of how test cases are run. + + +=== The Flow === + +When a test case is run, the testing framework creates a ‘fresh' object before running each test. That allows the test to not have to worry about what state the other test methods leave the object in. So for the above example, the testing flow looks like this when run: + + * an object of class `MyTestCase` is created + * method `test_1` is run + * the test case object is destroyed + +then... + + * a brand new object of class `MyTestCase` is created + * method `test_2` is run + * the test case object is destroyed + +then... + + * one last object of class `MyTestCase` is created + * method `test_3` is run + * the test case object is destroyed + + +=== Setup and Teardown Exposed === + +Now, there are 2 special methods that you can use to hook into this process. One is called setup and the other is called teardown. + +Let's rewrite our test. + +[source,ruby] +------------------------------------------ +require 'test/unit' + +class MyTestCase < Test::Unit::TestCase + + # called before every single test + def setup + @name = 'jimmy' + @age = 150 + end + + # called after every single test + def teardown + end + + # our tests + def test_1 + assert true + end + + def test_2 + end + + def test_3 + end +end +------------------------------------------ + +The `setup` method will always be called just before the test method. Comparatively, the `teardown` method will always be called always be called just after the test method. So now, the flow looks like this: + + * an object of class `MyTestCase` is created + * method `setup` is run + * method `test_1` is run + * method `teardown` is run + * the test case object is destroyed + +then... + + * a brand new object of class `MyTestCase` is created + * method `setup` is run + * method `test_2` is run + * method `teardown` is run + * the test case object is destroyed + +then... + + * one last object of class `MyTestCase` is created + * method `setup` is run + * method `test_3` is run + * method `teardown` is run + * the test case object is destroyed + +What can you do with this? Well, the `setup` method is good for stuff like creating objects that each method uses. For example, maybe we need to create a user and populate her with sample data before running each of the tests? + +As you'll see later, Rails uses the `setup` and `teardown` methods extensively. + + +== Hey Test/Unit. Assert This! == + +=== The Assertion Lineup === + +By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure things are going as planned. + +There are a bunch of different types of assertions you can use. Here's the complete list of assertions that ship with test/unit. The [msg] is an optional string message you can specify to make your test failure messages clearer. It's not required. + +`assert( boolean, [msg] )`:: + Ensures that the object/expression is true. + +`assert_equal( obj1, obj2, [msg] )`:: + Ensures that `obj1 == obj2` is true. + +`assert_not_equal( obj1, obj2, [msg] )`:: + Ensures that `obj1 == obj2` is false. + +`assert_same( obj1, obj2, [msg] )`:: + Ensures that `obj1.equal?(obj2)` is true. + +`assert_not_same( obj1, obj2, [msg] )`:: + Ensures that `obj1.equal?(obj2)` is false. + +`assert_nil( obj, [msg] )`:: + Ensures that `obj.nil?` is true. + +`assert_not_nil( obj, [msg] )`:: + Ensures that `obj.nil?` is false. + +`assert_match( regexp, string, [msg] )`:: + Ensures that a string matches the regular expression. + +`assert_no_match( regexp, string, [msg] )`:: + Ensures that a string doesn't matches the regular expression. + +`assert_in_delta( expecting, actual, delta, [msg] )`:: + Ensures that the numbers `expecting` and `actual` are within `delta` of each other. + +`assert_throws( symbol, [msg] ) { block }`:: + Ensures that the given block throws the symbol. + +`assert_raises( exception1, exception2, ... ) { block }`:: + Ensures that the given block raises one of the given exceptions. + +`assert_nothing_raised( exception1, exception2, ... ) { block }`:: + Ensures that the given block doesn't raise one of the given exceptions. + +`assert_instance_of( class, obj, [msg] )`:: + Ensures that `obj` is of the `class` type. + +`assert_kind_of( class, obj, [msg] )`:: + Ensures that `obj` is or descends from `class`. + +`assert_respond_to( obj, symbol, [msg] )`:: + Ensures that obj has a method called symbol. + +`assert_operator( obj1, operator, obj2, [msg] )`:: + Ensures that `obj1.operator(obj2)` is true. + +`assert_send( array, [msg] )`:: + Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh? + +`flunk( [msg] )`:: + Ensures failure... like me and high school chemistry exams. + +Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It has some specialized assertions to make your life easier. + +Creating your own assertions is more of an advanced topic we won't cover in this tutorial. + + +== The Rails Fly-By == + +=== It's About Frikkin' Time === + +Finally we get to testing in with Ruby on Rails! By this point, I'm going to assume a few things. + + * you're familiar with the basic building blocks of 'test/unit' + * you know that there are a bunch of assertions you can use + * you know what the special methods `setup` and `teardown` are + * you're conscious + +Rails promotes testing. The Ruby on Rails framework itself was built using these testing methodologies. + +In fact, the features built-in to Rails makes it exceptionally easy to do testing. For every model and controller you create using the `script/generate model` and `script/generate controller` scripts, corresponding test stubs are created. + +=== Where They Live === + +Your tests, quite surprisingly, go in your 'test' directory under your rails application. In the test directory, you'll see 4 sub-directories; one for controller tests ('functional'), one for model tests ('unit'), one for mock objects ('mocks') and one that only holds sample data ('fixtures'). + + * test + - /unit + - /functional + - /fixtures + - /mocks + +=== How to Turn Them On === + +To run these tests, you simply run the test script directly: + +--------------------------------------------- +ruby test/unit/my_good_old_test_unit.rb +--------------------------------------------- + +Another way to run your tests is to have the main rakefile run it for you. `rake test_functional` will run all your controller tests and `rake test_units` will run all your model tests. + +=== The 3 Environments === + +Testing support was woven into the Rails fabric from the beginning. It wasn't a ``oh! let's bolt on support for running tests because they're new and cool'' epiphany. + +Each Rails application you build has 3 sides. A side for production, a side for development and a side for testing. + +Let's take a closer look at the Rails 'config/database.yml' file. This YAML configuration file has 3 different sections defining 3 unique database setups: + + * production + * development + * test + +Why 3 different databases? Well, there's one for testing where you can use sample data, there's one for development where you'll be most of the time as you develop your application, and then production for the ``real deal'', or when it goes live. + +Every new Rails application should have these 3 sections filled out. They should point to different databases. You may end up not having a local database for your production environment, but development and test should both exist and be different. + +=== Why Make This Distinction? === + +If you stop and think about it for a second, it makes sense. + +By segregating your development database and your testing database, you're not in any danger of losing any data where it matters. + +For example, you need to test your new `delete_this_user_and_every_everything_associated_with_it` function. Wouldn't you want to run this in an environment which makes no difference if you destroy data or not? + +When you do end up destroying your testing database (and it will happen, trust me), simply run a task in your rakefile to rebuild it from scratch according to the specs defined in the development database. You can do this by running `rake db:test:prepare`. + + +== The Lo-Down on Fixtures == + +=== What They Are === + +The structure is one thing, but what about when I want to automatically create sample data? + +Enter fixtures. Fixtures is a fancy word for ‘sample data'. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and assume one of two formats: *YAML* or *CSV*. + +You'll find fixtures under your 'test/fixtures' directory. When you run `script/generate model` to create a new model, fixture stubs will be automatically created and placed in this directory. + +=== YAML the Camel is a Mammal with Enamel === + +YAML-formatted fixtures are a very human-friendly way to describe your sample data. These types of fixtures have the *.yml* file extension (as in 'users.yml'). + +On any given Sunday, a YAML fixture file may look like this: + +--------------------------------------------- +# low & behold! I am a YAML comment! +david: + id: 1 + name: David Heinemeier Hansson + birthday: 1979-10-15 + profession: Systems development + +steve: + id: 2 + name: Steve Ross Kellock + birthday: 1974-09-27 + profession: guy with keyboard +--------------------------------------------- + +Each fixture is given a 'name' followed by an indented list of colon-separated key/value pairs. Records are separated by a blank space. You can place comments by using the # character in the first column. + +=== Comma Seperated === + +Fixtures can also be described using the all-too-familiar comma-separated value file format. These files, just like YAML fixtures are placed in the 'test/fixtures directory', but these end with the *.csv* file extension (as in 'celebrity_holiday_figures.csv'). + +A CSV fixture looks like this: + +-------------------------------------------------------------- +id, username, password, stretchable, comments +1, sclaus, ihatekids, false, I like to say ""Ho! Ho! Ho!"" +2, ebunny, ihateeggs, true, Hoppity hop y'all +3, tfairy, ilovecavities, true, "Pull your teeth, I will" +-------------------------------------------------------------- + +The first line is the header. It is a comma-separated list of fields. The rest of the file is the payload: 1 record per line. A few notes about this format: + + * each cell is stripped of outward facing spaces + * if you use a comma as data, the cell must be encased in quotes + * if you use a quote as data, you must escape it with a 2nd quote + * don't use blank lines + * nulls can be achived by just placing a comma, for example, (1,sclaus,,false,) minus the parenthesis of course. + +Unlike the YAML format where you give each fixture a name, CSV fixture names are automatically generated. They follow a pattern of ``model-name''-''counter''. In the above example, you would have: + +-------------------------------------------------------------- +celebrity-holiday-figures-1 +celebrity-holiday-figures-2 +celebrity-holiday-figures-3 +-------------------------------------------------------------- + +The CSV format is great to use if you have existing data in a spreadsheet or database and you are able to save it (or export it) as a CSV. + +=== ERb'in It Up === + +ERb allows you embed ruby code within templates. Both the YAML and CSV fixture formats are pre-processed with ERb. This allows you to use Ruby to help you generate some sample data. + +I'll demonstrate with a YAML file: + +-------------------------------------------------------------- +<% earth_size = 20 -%> +mercury: + id: 1 + size: <%= earth_size / 50 %> + +venus: + id: 2 + size: <%= earth_size / 2 %> + +mars: + id: 3 + size: <%= earth_size - 69 %> +-------------------------------------------------------------- + +Anything encased within the + +------------- +<% -%> +------------- + +tag is considered Ruby code. To actually print something out, you must use the + +------------- +<%= %> +------------- + +tag. + +=== Fixtures in Action === + +Rails makes no assumptions when it comes to fixtures. You must explicitly load them yourself by using the fixtures method within your TestCase. For example, a users model unit test might look like this: + +[source, ruby] +-------------------------------------------------------------- +# Allow this test to hook into the Rails framework. +require File.dirname(__FILE__) + '/../test_helper' + +class UserTest < Test::Unit::TestCase + fixtures :users + + # Count the fixtures. + def test_count_my_fixtures + assert_equal 5, User.count + end +end +-------------------------------------------------------------- + +Using the fixtures method and placing the symbol name of the model, Rails will automatically load up the fixtures for you at the start of each test method. + +[source, ruby] +-------------------------------------------------------------- +fixtures :users +-------------------------------------------------------------- + +What exactly does this line of code do? It does 3 things: + + * it nukes any existing data living in the users table +* it loads the fixture data (if any) into the users table + * it dumps the data into a variable in case you want to access it directly + +So, in the above example, if we had another test method, we wouldn't have 10 users on the 2nd test because they would be wiped out before being created. + +You can load multiple fixtures by including them on the same line separated by commas. + +[source, ruby] +-------------------------------------------------------------- +fixtures :users, :losers, :bruisers, :cruisers +-------------------------------------------------------------- + +=== Hashes with Special Powers === + +Fixtures are basically Hash objects. As mentioned in point #3 above, you can access the hash object directly because it is automatically setup as a local variable of the test case. + +[source, ruby] +-------------------------------------------------------------- +... + fixtures :users + + def test_user + # this will return the Hash for the fixture named david + users(:david) + + # this will return the property for david called id + users(:david).id + end +... +-------------------------------------------------------------- + +But, by there's another side to fixtures... at night, if the moon is full and the wind completely still, fixtures can also transform themselves into the form of the original class! + +Now you can get at the methods only available to that class. + +[source, ruby] +-------------------------------------------------------------- +... + fixtures :users + + def test_user + # using the find method, we grab the "real" david as a User + david = users(:david).find + + # an now we have access to methods only available to a User class + email( david.girlfriend.email, david.illegitimate_children ) + end +... +-------------------------------------------------------------- + + +== Testing Your Models == + +=== A Unit Test === + +Unit tests are what we use to test our models. Generally, there is one test for each model. The test stubs are created automatically when you use `script/generate model SecretAgent` (for example). + +Let's take a look at what a unit test might look like. + +[source, ruby] +------------------------------------------------------- +# hook into the Rails environment +require File.dirname(__FILE__) + '/../test_helper' + +class SecretAgent < ActiveSupport::TestCase + fixtures :secret_agents + + # ensure the SecretAgent plays well with the database + def test_create_read_update_delete + # create a brand new secret agent + jimmy = SecretAgent.new("jagent", "unbelievablysecretpassword") + + # save him + assert jimmy.save + + # read him back + agent = SecretAgent.find(jimmy.id) + + # compare the usernames + assert_equal jimmy.username, agent.username + + # change the password by using hi-tech encryption + agent.username = agent.username.reverse + + # save the changes + assert agent.save + + # the agent gets killed + assert agent.destroy + end +end +------------------------------------------------------- + +In this basic unit test, we're exercising the 'SecretAgent' model. In our solitary test, we're proving a bunch of things about our 'SecretAgent' model. We try 4 different assertions to test that we can do the basics with the database such as create, read, update and delete (creatively known as CRUD). + +It is up to you to decide just how much you want to test of your model. Ideally you test anything that could possibly break, however, it is really trial and error. Only you know what's best to test. + +You most certainly want to test the validation code, and additionally, you probably want a least 1 test for every method in your model. + + +== Testing Your Controllers == + +=== What Is It? === + +The goal of functional testing is to test your controllers. When you get into the realm of testing controllers, we're operating at a higher level than the model. At this level, we test for things such as: + + * was the web request successful? + * were we redirected to the right page? + * were we successfully authenticated? + * was the correct object stored in the response template? + +Just as there is a one-to-one ratio between unit tests and models, so there is between functional tests and controllers. For a controller named `HomeController`, you would have a test case named `HomeControllerTest`. + +=== An Anatomy Lesson === + +So let's take a look at an example of a functional test. + +[source, ruby] +---------------------------------------------------- +require File.dirname(__FILE__) + '/../test_helper' + +class HomeControllerTest < ActionController::TestCase + # let's test our main index page + def test_index + get :index + assert_response :success + end +end +---------------------------------------------------- + +==== Making the moves ==== + +In the one test we have called `test_index`, we are simulating a request on the action called index and making sure the request was successful. + +The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments. + + * The action of the controller you are requesting. It can be in the form of a string or a symbol. Cool people use symbols. ;) + * An optional hash of request parameters to pass into the action (eg. query string parameters or post variables). + * An optional hash of session variables to pass along with the request. + * An optional hash of flash to stash your goulash. + +*Example:* Calling the `:show` action, passing an `id` of 12 as the params and setting `user_id` of 5 in the session. + +[source, ruby] +---------------------------------------------------- +get(:show, {'id' => "12"}, {'user_id' => 5}) +---------------------------------------------------- + +*Another example:* Calling the `:view` action, passing an `id` of 12 as the params, this time with no session, but with a flash message. + +[source, ruby] +---------------------------------------------------- +get(:view, {'id' => '12'}, nil, {'message' => 'booya!'}) +---------------------------------------------------- + +==== Available at your disposal ==== + +For those of you familiar with HTTP protocol, you'll know that get is a type of request. There are 5 request types supported in Rails: + + * get + * post + * put + * head + * delete + +All of request types are methods that you can use, however, you'll probably end up using the first two more ofter than the others. + +=== The 4 Hashes of the Apocolypse === + +After the request has been made by using one of the 5 methods (get, post, etc...), you will have 4 Hash objects ready for use. + +They are (starring in alphabetical order): + +`assigns`:: + Any objects that are stored as instance variables in actions for use in views. +`cookies`:: + Any objects cookies that are set. +`flash`:: + Any objects living in the flash. +`session`:: + Any object living in session variables. + +For example, let's say we have a `MoviesController` with an action called `movie`. The code for that action might look something like: + +[source, ruby] +---------------------------------------------------- +def movie + @movie = Movie.find(params[:id]) + if @movie.nil? + flash['message'] = "That movie has been burned." + redirect_to :controller => 'error', :action => 'missing' + end +end +---------------------------------------------------- + +Now, to test out if the proper movie is being set, we could have a series of tests that look like this: + +[source, ruby] +---------------------------------------------------- +# this test proves that fetching a movie works +def test_successfully_finding_a_movie + get :movie, "id" => "1" + assert_not_nil assigns["movie"] + assert_equal 1, assigns["movie"].id + assert flash.empty? +end + +# and when we can't find a movie... +def test_movie_not_found + get :movie, "id" => "666999" + assert_nil assigns["movie"] + assert flash.has_key?("message") + assert assigns.empty? +end +---------------------------------------------------- + +As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name... except assigns. Check it out: + +[source, ruby] +---------------------------------------------------- +flash["gordon"] flash[:gordon] +session["shmession"] session[:shmession] +cookies["are_good_for_u"] cookies[:are_good_for_u] + +# Because you can't use assigns[:something] for historical reasons: +assigns["something"] assigns(:something) +---------------------------------------------------- + +Keep an eye out for that. mmmm kay? + +=== Response-Related Assertions === + +There are 3 assertions that deal with the overall response to a request. They are: + +`assert_template( expected_template, [msg] )`:: +Ensures the expected template was responsible for rendering. For example: ++ +[source, ruby] +-------------------------------------- +assert_template "user/profile" +-------------------------------------- ++ +This code will fail unless the template located at app/views/user/profile.rhtml was rendered. + +`assert_response( type_or_code, [msg] )`:: +Ensures the response type/status code is as expected. For example: ++ +[source, ruby] +-------------------------------------- +assert_response :success # page rendered ok +assert_response :redirect # we've been redirected +assert_response :missing # not found +assert_response 505 # status code was 505 +-------------------------------------- ++ +The possible options are: ++ + * `:success` (status code is 200) + * `:redirect` (status code is within 300..399) + * `:missing` (status code is 404) + * `:error` (status code is within 500..599) + * any number (to specifically reference a particular status code) + +`assert_redirected_to ( options={}, [msg] )`:: +Ensures we've been redirected to a specific place within our application. ++ +[source, ruby] +-------------------------- +assert_redirected_to :controller => 'widget', :action => 'view', :id => 555 +-------------------------- + +=== Tag-Related Assertions === + +The assert_tag and assert_no_tag assertions are for analysing the html returned from a request. + +==== assert_tag( options ) ==== +Ensures that a tag or text exists. There are a whole whack o' options you can use to discover what you are looking for. Some of the conditions are like XPATH in concept, but this is sexier. In fact, let's call it SEXPATH. + +The following description is lifted verbatim from the rails assertion docs. + +Asserts that there is a tag/node/element in the body of the response that meets all of the given conditions. The conditions parameter must be a hash of any of the following keys (all are optional): + + * `:tag` : the node type must match the corresponding value + * `:attributes` : a hash. The node's attributes must match the corresponding values in the hash. + * `:parent` : a hash. The node's parent must match the corresponding hash. + * `:child` : a hash. At least one of the node's immediate children must meet the criteria described by the hash. + * `:ancestor` : a hash. At least one of the node's ancestors must meet the criteria described by the hash. + * `:descendant` : a hash. At least one of the node's descendants must meet the criteria described by the hash. + * `:children` : a hash, for counting children of a node. Accepts the keys: + - `:count` : either a number or a range which must equal (or include) the number of children that match. + - `:less_than` : the number of matching children must be less than this number. + - `:greater_than` : the number of matching children must be greater than this number. + - `:only` : another hash consisting of the keys to use to match on the children, and only matching children will be counted. + - `:content` : (text nodes only). The content of the node must match the given value. + +Conditions are matched using the following algorithm: + + * if the condition is a *string*, it must be a substring of the value. + * if the condition is a *regexp*, it must match the value. + * if the condition is a *number*, the value must match `number.to_s`. + * if the condition is *true*, the value must not be `nil`. + * if the condition is *false* or *nil*, the value must be `nil`. + +These examples are taken from the same docs too: + +[source, ruby] +------------------------------------------------------------- + # assert that there is a "span" tag + assert_tag :tag => "span" + + # assert that there is a "span" inside of a "div" + assert_tag :tag => "span", :parent => { :tag => "div" } + + # assert that there is a "span" somewhere inside a table + assert_tag :tag => "span", :ancestor => { :tag => "table" } + + # assert that there is a "span" with at least one "em" child + assert_tag :tag => "span", :child => { :tag => "em" } + + # assert that there is a "span" containing a (possibly nested) + # "strong" tag. + assert_tag :tag => "span", :descendant => { :tag => "strong" } + + # assert that there is a "span" containing between 2 and 4 "em" tags + # as immediate children + assert_tag :tag => "span", + :children => { :count => 2..4, :only => { :tag => "em" } } + + # get funky: assert that there is a "div", with an "ul" ancestor + # and an "li" parent (with "class" = "enum"), and containing a + # "span" descendant that contains text matching /hello world/ + assert_tag :tag => "div", + :ancestor => { :tag => "ul" }, + :parent => { :tag => "li", + :attributes => { :class => "enum" } }, + :descendant => { :tag => "span", + :child => /hello world/ } +------------------------------------------------------------- + +==== assert_no_tag( options ) ==== +This is the exact opposite of assert_tag. It ensures that the tag does not exist. + +=== Routing-Related Assertions === + +==== assert_generates( expected_path, options, defaults={}, extras = {}, [msg] ) ==== +Ensures that the options map to the expected_path. + +[source, ruby] +------------------------------------------------------------- +opts = {:controller => "movies", :action => "movie", :id => "69"} +assert_generates "movies/movie/69", opts +------------------------------------------------------------- + +==== assert_recognizes( expected_options, path, extras={}, [msg] ) ==== +Ensures that when the path is chopped up into pieces, it is equal to expected_options. Essentially, the opposite of assert_generates. + +[source, ruby] +------------------------------------------------------------- +opts = {:controller => "movies", :action => "movie", :id => "69"} +assert_recognizes opts, "/movies/movie/69 + +# also, let's say i had a line in my config/routes.rb +# that looked like: +# +# map.connect ( +# 'calendar/:year/:month', +# :controller => 'content', +# :action => 'calendar', +# :year => nil, +# :month => nil, +# :requirements => {:year => /\d{4}/, :month => /\d{1,2}/} +# } +# +# Then, this would work too: +opts = { + :controller => 'content', + :action => 'calendar', + :year => '2005', + :month => '5' +} +assert_recognizes opts, 'calendar/2005/5' +------------------------------------------------------------- + +==== assert_routing( path, options, defaults={}, extras={}, [msg] ) ==== +Ensures that the path resolves into options, and the options, resolves into path. It's a two-way check to make sure your routing maps work as expected. + +This assertion is simply a wrapper around `assert_generates` and `assert_recognizes`. + +If you're going to test your routes, this assertion might be your best bet for robustness (yes, the overused buzzword of the 90's). + +[source, ruby] +------------------------------------------------------------- +opts = {:controller => "movies", :action => "movie", :id => "69"} +assert_routing "movies/movie/69", opts +------------------------------------------------------------- + +=== Testing File Uploads === +So your web app supports file uploads eh? Here's what you can do to test your uploads. + +This tip is brought to you by Chris Brinker, the letter R and the number 12. + +Chris says, '``In order to test a file being uploaded you have to mirror what cgi.rb is doing with a multipart post. Unfortunately what it does is quite long and complex, this code takes a file on your system, and turns it into what normally comes out of cgi.rb.''' + +Here are some helper methods based on Chris' work that you'll need to squirrel away either in a new unit, or cut ‘n' pasted right into your test. Any errors with this are my fault. + +[source, ruby] +------------------------------------------------------------- +# get us an object that represents an uploaded file +def uploaded_file(path, content_type="application/octet-stream", filename=nil) + filename ||= File.basename(path) + t = Tempfile.new(filename) + FileUtils.copy_file(path, t.path) + (class << t; self; end;).class_eval do + alias local_path path + define_method(:original_filename) { filename } + define_method(:content_type) { content_type } + end + return t +end + +# a JPEG helper +def uploaded_jpeg(path, filename=nil) + uploaded_file(path, 'image/jpeg', filename) +end + +# a GIF helper +def uploaded_gif(path, filename=nil) + uploaded_file(path, 'image/gif', filename) +end +------------------------------------------------------------- + +And to use this code, you'd have a test that would looks something like this: + +[source, ruby] +------------------------------------------------------------- +def test_a_file_upload + assert_equal 0, GalleryImage.count + heman = uploaded_jpeg("#{File.expand_path(RAILS_ROOT)}/text/fixtures/heman.jpg") + post :imageupload, 'imagefile' => heman + assert_redirected_to :controller => 'gallery', :action => 'view' + assert_equal 1, GalleryImage.count +end +------------------------------------------------------------- + + +== Testing Your Mailers == + +=== Keeping the postman in check === + +Your ActionMailer -- like every other part of your Rails application -- should be tested to ensure that it is working as expected. + +The goal of testing your ActionMailer is to ensure that: + + * emails are being processed (created and sent) + * the email content is correct (subject, sender, body, etc) + * the right emails are being sent at the righ times + +==== From all sides ==== + +There are two aspects of testing your mailer, the unit tests and the functional tests. +Unit tests + +In the unit tests, we run the mailer in isolation with tightly controlled inputs and compare the output to a known-value -- a fixture -- yay! more fixtures! + +==== Functional tests ==== + +In the functional tests we don't so much test the minute details produced by the mailer, instead we test that our controllers and models are using the mailer in the right way. We test to prove that the right email was sent at the right time. + +=== Unit Testing === + +In order to test that your mailer is working as expected, we can use unit tests to compare the actual results of the mailer with pre-writen examples of what should be produced. + +==== Revenge of the fixtures ==== + +For the purposes of unit testing a mailer, fixtures are used to provide an example of how output ``should'' look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory from the other fixtures. Don't tease them about it though, they hate that. + +When you generated your mailer (you did that right?) the generator created stub fixtures for each of the mailers actions. If you didn't use the generator you'll have to make those files yourself. + +==== The basic test case ==== + +Here is an example of what you start with. + +[source, ruby] +------------------------------------------------- +require File.dirname(__FILE__) + '/. ./test_helper' + +class MyMailerTest < Test::Unit::TestCase + FIXTURES_PATH = File.dirname(__FILE__) + '/. ./fixtures' + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @expected = TMail::Mail.new + end + + def test_signup + @expected.subject = 'MyMailer#signup' + @expected.body = read_fixture('signup') + @expected.date = Time.now + + assert_equal @expected.encoded, MyMailer.create_signup(@expected.date).encoded + end + + private + def read_fixture(action) + IO.readlines("#{FIXTURES_PATH}/my_mailer/#{action}") + end +end +------------------------------------------------- + +The `setup` method is mostly concerned with setting up a blank slate for the next test. However it is worth describing what each statement does + +[source, ruby] +------------------------------------------------- +ActionMailer::Base.delivery_method = :test +------------------------------------------------- + +sets the delivery method to test mode so that email will not actually be delivered (useful to avoid spamming your users while testing) but instead it will be appended to an array (ActionMailer::Base.deliveries). + +[source, ruby] +------------------------------------------------- +ActionMailer::Base.perform_deliveries = true +------------------------------------------------- + +Ensures the mail will be sent using the method specified by ActionMailer::Base.delivery_method, and finally + +[source, ruby] +------------------------------------------------- +ActionMailer::Base.deliveries = [] +------------------------------------------------- + +sets the array of sent messages to an empty array so we can be sure that anything we find there was sent as part of our current test. + +However often in unit tests, mails will not actually be sent, simply constructed, as in the example above, where the precise content of the email is checked against what it should be. Dave Thomas suggests an alternative approach, which is just to check the part of the email that is likely to break, i.e. the dynamic content. The following example assumes we have some kind of user table, and we might want to mail those users new passwords: + +[source, ruby] +------------------------------------------------- +require File.dirname(__FILE__) + '/../test_helper' +require 'my_mailer' + +class MyMailerTest < Test::Unit::TestCase + fixtures :users + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' + CHARSET = "utf-8" + + include ActionMailer::Quoting + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @expected = TMail::Mail.new + @expected.set_content_type "text", "plain", { "charset" => CHARSET } + end + + def test_reset_password + user = User.find(:first) + newpass = 'newpass' + response = MyMailer.create_reset_password(user,newpass) + assert_equal 'Your New Password', response.subject + assert_match /Dear #{user.full_name},/, response.body + assert_match /New Password: #{newpass}/, response.body + assert_equal user.email, response.to[0] + end + + private + def read_fixture(action) + IO.readlines("#{FIXTURES_PATH}/community_mailer/#{action}") + end + + def encode(subject) + quoted_printable(subject, CHARSET) + end +end +------------------------------------------------- + +and here we check the dynamic parts of the mail, specifically that we use the users' correct full name and that we give them the correct password. + +=== Functional Testing === + +Functional testing involves more than just checking that the email body, recipients and so forth are correct. In functional mail tests we call the mail deliver methods and check that the appropriate emails have been appended to the delivery list. It is fairly safe to assume that the deliver methods themselves do their job -- what we are probably more interested in is whether our own business logic is sending emails when we expect them to. For example the password reset operation we used an example in the previous section will probably be called in response to a user requesting a password reset through some sort of controller. + +[source, ruby] +---------------------------------------------------------------- +require File.dirname(__FILE__) + '/../test_helper' +require 'my_controller' + +# Raise errors beyond the default web-based presentation +class MyController; def rescue_action(e) raise e end; end + +class MyControllerTest < Test::Unit::TestCase + + def setup + @controller = MyController.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + end + + def test_reset_password + num_deliveries = ActionMailer::Base.deliveries.size + post :reset_password, :email => 'bob@test.com' + + assert_equal num_deliveries+1, ActionMailer::Base.deliveries.size + end + +end +---------------------------------------------------------------- + +=== Filtering emails in development === + +Sometimes you want to be somewhere inbetween the `:test` and `:smtp` settings. Say you're working on your development site, and you have a few testers working with you. The site isn't in production yet, but you'd like the testers to be able to receive emails from the site, but no one else. Here's a handy way to handle that situation, add this to your 'environment.rb' or 'development.rb' file + +[source, ruby] +---------------------------------------------------------------- +class ActionMailer::Base + + def perform_delivery_fixed_email(mail) + destinations = mail.destinations + if destinations.nil? + destinations = ["mymail@me.com"] + mail.subject = '[TEST-FAILURE]:'+mail.subject + else + mail.subject = '[TEST]:'+mail.subject + end + approved = ["testerone@me.com","testertwo@me.com"] + destinations = destinations.collect{|x| approved.collect{|y| (x==y ? x : nil)}}.flatten.compact + mail.to = destinations + if destinations.size > 0 + mail.ready_to_send + Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], + server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp| + smtp.sendmail(mail.encoded, mail.from, destinations) + end + end + + end + +end +---------------------------------------------------------------- + -- cgit v1.2.3 From e0513e33c4da60255e7c1aa71babcc9414f26858 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 28 Jul 2008 13:38:20 -0500 Subject: Routing whitespace cleanup --- .../assertions/routing_assertions.rb | 44 ++++++++-------- actionpack/lib/action_controller/routing.rb | 58 +++++++++++----------- .../lib/action_controller/routing/optimisations.rb | 22 ++++---- .../routing/recognition_optimisation.rb | 2 - actionpack/lib/action_controller/routing/route.rb | 15 +++--- .../lib/action_controller/routing/route_set.rb | 4 +- .../lib/action_controller/routing/routing_ext.rb | 1 - .../lib/action_controller/routing/segments.rb | 5 +- actionpack/test/controller/routing_test.rb | 1 + actionpack/test/controller/test_test.rb | 21 ++++---- 10 files changed, 84 insertions(+), 89 deletions(-) diff --git a/actionpack/lib/action_controller/assertions/routing_assertions.rb b/actionpack/lib/action_controller/assertions/routing_assertions.rb index 491b72d586..312b4e228b 100644 --- a/actionpack/lib/action_controller/assertions/routing_assertions.rb +++ b/actionpack/lib/action_controller/assertions/routing_assertions.rb @@ -2,7 +2,7 @@ module ActionController module Assertions # Suite of assertions to test routes generated by Rails and the handling of requests made to them. module RoutingAssertions - # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) + # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+. # # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes @@ -14,16 +14,16 @@ module ActionController # # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the - # extras argument, appending the query string on the path directly will not work. For example: + # extras argument, appending the query string on the path directly will not work. For example: # # # assert that a path of '/items/list/1?view=print' returns the correct options - # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }) + # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }) # - # The +message+ parameter allows you to pass in an error message that is displayed upon failure. + # The +message+ parameter allows you to pass in an error message that is displayed upon failure. # # ==== Examples # # Check the default route (i.e., the index action) - # assert_recognizes({:controller => 'items', :action => 'index'}, 'items') + # assert_recognizes({:controller => 'items', :action => 'index'}, 'items') # # # Test a specific action # assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list') @@ -44,16 +44,16 @@ module ActionController request_method = nil end - clean_backtrace do - ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + clean_backtrace do + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? request = recognized_request_for(path, request_method) - + expected_options = expected_options.clone extras.each_key { |key| expected_options.delete key } unless extras.nil? - + expected_options.stringify_keys! routing_diff = expected_options.diff(request.path_parameters) - msg = build_message(message, "The recognized options did not match , difference: ", + msg = build_message(message, "The recognized options did not match , difference: ", request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) assert_block(msg) { request.path_parameters == expected_options } end @@ -64,7 +64,7 @@ module ActionController # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures. # # The +defaults+ parameter is unused. - # + # # ==== Examples # # Asserts that the default action is generated for a route with no action # assert_generates("/items", :controller => "items", :action => "index") @@ -73,34 +73,34 @@ module ActionController # assert_generates("/items/list", :controller => "items", :action => "list") # # # Tests the generation of a route with a parameter - # assert_generates("/items/list/1", { :controller => "items", :action => "list", :id => "1" }) + # assert_generates("/items/list/1", { :controller => "items", :action => "list", :id => "1" }) # # # Asserts that the generated route gives us our custom route # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" } def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) - clean_backtrace do + clean_backtrace do expected_path = "/#{expected_path}" unless expected_path[0] == ?/ # Load routes.rb if it hasn't been loaded. - ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? - + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, defaults) found_extras = options.reject {|k, v| ! extra_keys.include? k} msg = build_message(message, "found extras , not ", found_extras, extras) assert_block(msg) { found_extras == extras } - - msg = build_message(message, "The generated path did not match ", generated_path, + + msg = build_message(message, "The generated path did not match ", generated_path, expected_path) assert_block(msg) { expected_path == generated_path } end end - # Asserts that path and options match both ways; in other words, it verifies that path generates + # Asserts that path and options match both ways; in other words, it verifies that path generates # options and then that options generates path. This essentially combines +assert_recognizes+ # and +assert_generates+ into one step. # # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The - # +message+ parameter allows you to specify a custom error message to display upon failure. + # +message+ parameter allows you to specify a custom error message to display upon failure. # # ==== Examples # # Assert a basic route: a controller with the default action (index) @@ -119,12 +119,12 @@ module ActionController # assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }) def assert_routing(path, options, defaults={}, extras={}, message=nil) assert_recognizes(options, path, extras, message) - - controller, default_controller = options[:controller], defaults[:controller] + + controller, default_controller = options[:controller], defaults[:controller] if controller && controller.include?(?/) && default_controller && default_controller.include?(?/) options[:controller] = "/#{controller}" end - + assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message) end diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb index dfbaa53b7c..8d51e823a6 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_controller/routing.rb @@ -201,7 +201,7 @@ module ActionController # With conditions you can define restrictions on routes. Currently the only valid condition is :method. # # * :method - Allows you to specify which method can access the route. Possible values are :post, - # :get, :put, :delete and :any. The default value is :any, + # :get, :put, :delete and :any. The default value is :any, # :any means that any method can access the route. # # Example: @@ -213,7 +213,7 @@ module ActionController # # Now, if you POST to /posts/:id, it will route to the create_comment action. A GET on the same # URL will route to the show action. - # + # # == Reloading routes # # You can reload routes if you feel you must: @@ -281,9 +281,9 @@ module ActionController end class << self - # Expects an array of controller names as the first argument. - # Executes the passed block with only the named controllers named available. - # This method is used in internal Rails testing. + # Expects an array of controller names as the first argument. + # Executes the passed block with only the named controllers named available. + # This method is used in internal Rails testing. def with_controllers(names) prior_controllers = @possible_controllers use_controllers! names @@ -292,10 +292,10 @@ module ActionController use_controllers! prior_controllers end - # Returns an array of paths, cleaned of double-slashes and relative path references. - # * "\\\" and "//" become "\\" or "/". - # * "/foo/bar/../config" becomes "/foo/config". - # The returned array is sorted by length, descending. + # Returns an array of paths, cleaned of double-slashes and relative path references. + # * "\\\" and "//" become "\\" or "/". + # * "/foo/bar/../config" becomes "/foo/config". + # The returned array is sorted by length, descending. def normalize_paths(paths) # do the hokey-pokey of path normalization... paths = paths.collect do |path| @@ -314,7 +314,7 @@ module ActionController paths = paths.uniq.sort_by { |path| - path.length } end - # Returns the array of controller names currently available to ActionController::Routing. + # Returns the array of controller names currently available to ActionController::Routing. def possible_controllers unless @possible_controllers @possible_controllers = [] @@ -339,28 +339,27 @@ module ActionController @possible_controllers end - # Replaces the internal list of controllers available to ActionController::Routing with the passed argument. - # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ]) + # Replaces the internal list of controllers available to ActionController::Routing with the passed argument. + # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ]) def use_controllers!(controller_names) @possible_controllers = controller_names end - # Returns a controller path for a new +controller+ based on a +previous+ controller path. - # Handles 4 scenarios: - # - # * stay in the previous controller: - # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion" - # - # * stay in the previous namespace: - # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts" - # - # * forced move to the root namespace: - # controller_relative_to( "/posts", "groups/discussion" ) # => "posts" - # - # * previous namespace is root: - # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts" - # - + # Returns a controller path for a new +controller+ based on a +previous+ controller path. + # Handles 4 scenarios: + # + # * stay in the previous controller: + # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion" + # + # * stay in the previous namespace: + # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts" + # + # * forced move to the root namespace: + # controller_relative_to( "/posts", "groups/discussion" ) # => "posts" + # + # * previous namespace is root: + # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts" + # def controller_relative_to(controller, previous) if controller.nil? then previous elsif controller[0] == ?/ then controller[1..-1] @@ -369,12 +368,11 @@ module ActionController end end end - Routes = RouteSet.new ActiveSupport::Inflector.module_eval do - # Ensures that routes are reloaded when Rails inflections are updated. + # Ensures that routes are reloaded when Rails inflections are updated. def inflections_with_route_reloading(&block) returning(inflections_without_route_reloading(&block)) { ActionController::Routing::Routes.reload! if block_given? diff --git a/actionpack/lib/action_controller/routing/optimisations.rb b/actionpack/lib/action_controller/routing/optimisations.rb index 4b70ea13f2..b44ebd5ca2 100644 --- a/actionpack/lib/action_controller/routing/optimisations.rb +++ b/actionpack/lib/action_controller/routing/optimisations.rb @@ -1,14 +1,14 @@ module ActionController module Routing - # Much of the slow performance from routes comes from the + # Much of the slow performance from routes comes from the # complexity of expiry, :requirements matching, defaults providing - # and figuring out which url pattern to use. With named routes - # we can avoid the expense of finding the right route. So if + # and figuring out which url pattern to use. With named routes + # we can avoid the expense of finding the right route. So if # they've provided the right number of arguments, and have no # :requirements, we can just build up a string and return it. - # - # To support building optimisations for other common cases, the - # generation code is separated into several classes + # + # To support building optimisations for other common cases, the + # generation code is separated into several classes module Optimisation def generate_optimisation_block(route, kind) return "" unless route.optimise? @@ -53,12 +53,12 @@ module ActionController # map.person '/people/:id' # # If the user calls person_url(@person), we can simply - # return a string like "/people/#{@person.to_param}" + # return a string like "/people/#{@person.to_param}" # rather than triggering the expensive logic in +url_for+. class PositionalArguments < Optimiser def guard_condition number_of_arguments = route.segment_keys.size - # if they're using foo_url(:id=>2) it's one + # if they're using foo_url(:id=>2) it's one # argument, but we don't want to generate /foos/id2 if number_of_arguments == 1 "(!defined?(default_url_options) || default_url_options.blank?) && defined?(request) && request && args.size == 1 && !args.first.is_a?(Hash)" @@ -94,14 +94,14 @@ module ActionController end # This case is mostly the same as the positional arguments case - # above, but it supports additional query parameters as the last + # above, but it supports additional query parameters as the last # argument class PositionalArgumentsWithAdditionalParams < PositionalArguments def guard_condition "(!defined?(default_url_options) || default_url_options.blank?) && defined?(request) && request && args.size == #{route.segment_keys.size + 1} && !args.last.has_key?(:anchor) && !args.last.has_key?(:port) && !args.last.has_key?(:host)" end - # This case uses almost the same code as positional arguments, + # This case uses almost the same code as positional arguments, # but add an args.last.to_query on the end def generation_code super.insert(-2, '?#{args.last.to_query}') @@ -110,7 +110,7 @@ module ActionController # To avoid generating "http://localhost/?host=foo.example.com" we # can't use this optimisation on routes without any segments def applicable? - super && route.segment_keys.size > 0 + super && route.segment_keys.size > 0 end end diff --git a/actionpack/lib/action_controller/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb index cf8f5232c1..67d354a943 100644 --- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb +++ b/actionpack/lib/action_controller/routing/recognition_optimisation.rb @@ -51,7 +51,6 @@ module ActionController # 3) segm test for /users/:id # (jump to list index = 5) # 4) full test for /users/:id => here we are! - class RouteSet def recognize_path(path, environment={}) result = recognize_optimized(path, environment) and return result @@ -152,7 +151,6 @@ module ActionController segments << nil segments end - end end end diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb index a0d108ba03..a05c8c10ac 100644 --- a/actionpack/lib/action_controller/routing/route.rb +++ b/actionpack/lib/action_controller/routing/route.rb @@ -226,15 +226,14 @@ module ActionController end end - protected - def requirement_for(key) - return requirements[key] if requirements.key? key - segments.each do |segment| - return segment.regexp if segment.respond_to?(:key) && segment.key == key + protected + def requirement_for(key) + return requirements[key] if requirements.key? key + segments.each do |segment| + return segment.regexp if segment.respond_to?(:key) && segment.key == key + end + nil end - nil - end - end end end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb index 5bc13cf268..4a9bc259f3 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_controller/routing/route_set.rb @@ -1,6 +1,6 @@ module ActionController module Routing - class RouteSet #:nodoc: + class RouteSet #:nodoc: # Mapper instances are used to build routes. The object passed to the draw # block in config/routes.rb is a Mapper instance. # @@ -432,4 +432,4 @@ module ActionController end end end -end \ No newline at end of file +end diff --git a/actionpack/lib/action_controller/routing/routing_ext.rb b/actionpack/lib/action_controller/routing/routing_ext.rb index 2ad20ee699..5f4ba90d0c 100644 --- a/actionpack/lib/action_controller/routing/routing_ext.rb +++ b/actionpack/lib/action_controller/routing/routing_ext.rb @@ -1,4 +1,3 @@ - class Object def to_param to_s diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index f0ad066bad..18e76b6b82 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -130,6 +130,7 @@ module ActionController def extract_value "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" end + def value_check if default # Then we know it won't be nil "#{value_regexp.inspect} =~ #{local_name}" if regexp @@ -141,6 +142,7 @@ module ActionController "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" end end + def expiry_statement "expired, hash = true, options if !expired && expire_on[:#{key}]" end @@ -175,7 +177,7 @@ module ActionController end def regexp_chunk - if regexp + if regexp if regexp_has_modifiers? "(#{regexp.to_s})" else @@ -214,7 +216,6 @@ module ActionController def regexp_has_modifiers? regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0 end - end class ControllerSegment < DynamicSegment #:nodoc: diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 84996fd6b1..22e394dc95 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -834,6 +834,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do puts "#{1 / per_url} url/s\n\n" end end + def test_time_generation n = 5000 if RunTimeTests diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index b624005a57..61b8c83ee0 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -64,7 +64,7 @@ class TestTest < Test::Unit::TestCase HTML end - + def test_xml_output response.content_type = "application/xml" render :text => < { :count => 1, :only => { :tag => "img" } } } } end - + def test_should_not_impose_childless_html_tags_in_xml process :test_xml_output @@ -486,7 +486,7 @@ XML assert_nil @request.env['HTTP_X_REQUESTED_WITH'] end - def test_header_properly_reset_after_get_request + def test_header_properly_reset_after_get_request get :test_params @request.recycle! assert_nil @request.instance_variable_get("@request_method") @@ -532,15 +532,15 @@ XML assert_equal file.path, file.local_path assert_equal expected, file.read end - + def test_test_uploaded_file_with_binary filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" content_type = 'image/png' - + binary_uploaded_file = ActionController::TestUploadedFile.new(path, content_type, :binary) assert_equal File.open(path, READ_BINARY).read, binary_uploaded_file.read - + plain_uploaded_file = ActionController::TestUploadedFile.new(path, content_type) assert_equal File.open(path, READ_PLAIN).read, plain_uploaded_file.read end @@ -549,10 +549,10 @@ XML filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" content_type = 'image/jpg' - + binary_file_upload = fixture_file_upload(path, content_type, :binary) assert_equal File.open(path, READ_BINARY).read, binary_file_upload.read - + plain_file_upload = fixture_file_upload(path, content_type) assert_equal File.open(path, READ_PLAIN).read, plain_file_upload.read end @@ -584,7 +584,7 @@ XML get :test_send_file assert_nothing_raised(NoMethodError) { @response.binary_content } end - + protected def with_foo_routing with_routing do |set| @@ -597,7 +597,6 @@ XML end end - class CleanBacktraceTest < Test::Unit::TestCase def test_should_reraise_the_same_object exception = Test::Unit::AssertionFailedError.new('message') @@ -658,7 +657,7 @@ end class NamedRoutesControllerTest < ActionController::TestCase tests ContentController - + def test_should_be_able_to_use_named_routes_before_a_request_is_done with_routing do |set| set.draw { |map| map.resources :contents } -- cgit v1.2.3 From a5db1488251304ec93256654859b430148f0c506 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 28 Jul 2008 13:41:42 -0500 Subject: Prepare Route#generate and Route#recognize early. Also refactor segments a bit to try to make immutable. --- .../lib/action_controller/routing/builder.rb | 29 +- .../lib/action_controller/routing/optimisations.rb | 1 + .../routing/recognition_optimisation.rb | 40 ++- actionpack/lib/action_controller/routing/route.rb | 300 +++++++++++---------- .../lib/action_controller/routing/route_set.rb | 3 +- .../lib/action_controller/routing/segments.rb | 29 +- actionpack/test/controller/routing_test.rb | 151 ++++------- actionpack/test/controller/test_test.rb | 4 +- 8 files changed, 259 insertions(+), 298 deletions(-) diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb index 912999d845..03427e41de 100644 --- a/actionpack/lib/action_controller/routing/builder.rb +++ b/actionpack/lib/action_controller/routing/builder.rb @@ -48,14 +48,10 @@ module ActionController end when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) when /\A\?(.*?)\?/ - returning segment = StaticSegment.new($1) do - segment.is_optional = true - end + StaticSegment.new($1, :optional => true) when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) when Regexp.new(separator_pattern) then - returning segment = DividerSegment.new($&) do - segment.is_optional = (optional_separators.include? $&) - end + DividerSegment.new($&, :optional => (optional_separators.include? $&)) end [segment, $~.post_match] end @@ -176,29 +172,16 @@ module ActionController defaults, requirements, conditions = divide_route_options(segments, options) requirements = assign_route_options(segments, defaults, requirements) - route = Route.new - - route.segments = segments - route.requirements = requirements - route.conditions = conditions + # TODO: Segments should be frozen on initialize + segments.each { |segment| segment.freeze } - if !route.significant_keys.include?(:action) && !route.requirements[:action] - route.requirements[:action] = "index" - route.significant_keys << :action - end - - # Routes cannot use the current string interpolation method - # if there are user-supplied :requirements as the interpolation - # code won't raise RoutingErrors when generating - if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION - route.optimise = false - end + route = Route.new(segments, requirements, conditions) if !route.significant_keys.include?(:controller) raise ArgumentError, "Illegal route: the :controller must be specified!" end - route + route.freeze end private diff --git a/actionpack/lib/action_controller/routing/optimisations.rb b/actionpack/lib/action_controller/routing/optimisations.rb index b44ebd5ca2..0fe836606c 100644 --- a/actionpack/lib/action_controller/routing/optimisations.rb +++ b/actionpack/lib/action_controller/routing/optimisations.rb @@ -20,6 +20,7 @@ module ActionController class Optimiser attr_reader :route, :kind + def initialize(route, kind) @route = route @kind = kind diff --git a/actionpack/lib/action_controller/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb index 67d354a943..6d54d0334c 100644 --- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb +++ b/actionpack/lib/action_controller/routing/recognition_optimisation.rb @@ -67,28 +67,6 @@ module ActionController end end - def recognize_optimized(path, env) - write_recognize_optimized - recognize_optimized(path, env) - end - - def write_recognize_optimized - tree = segment_tree(routes) - body = generate_code(tree) - instance_eval %{ - def recognize_optimized(path, env) - segments = to_plain_segments(path) - index = #{body} - return nil unless index - while index < routes.size - result = routes[index].recognize(path, env) and return result - index += 1 - end - nil - end - }, __FILE__, __LINE__ - end - def segment_tree(routes) tree = [0] @@ -151,6 +129,24 @@ module ActionController segments << nil segments end + + private + def write_recognize_optimized! + tree = segment_tree(routes) + body = generate_code(tree) + instance_eval %{ + def recognize_optimized(path, env) + segments = to_plain_segments(path) + index = #{body} + return nil unless index + while index < routes.size + result = routes[index].recognize(path, env) and return result + index += 1 + end + nil + end + }, __FILE__, __LINE__ + end end end end diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb index a05c8c10ac..2106ac09e0 100644 --- a/actionpack/lib/action_controller/routing/route.rb +++ b/actionpack/lib/action_controller/routing/route.rb @@ -3,11 +3,25 @@ module ActionController class Route #:nodoc: attr_accessor :segments, :requirements, :conditions, :optimise - def initialize - @segments = [] - @requirements = {} - @conditions = {} - @optimise = true + def initialize(segments = [], requirements = {}, conditions = {}) + @segments = segments + @requirements = requirements + @conditions = conditions + + if !significant_keys.include?(:action) && !requirements[:action] + @requirements[:action] = "index" + @significant_keys << :action + end + + # Routes cannot use the current string interpolation method + # if there are user-supplied :requirements as the interpolation + # code won't raise RoutingErrors when generating + has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp } + if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION + @optimise = false + else + @optimise = true + end end # Indicates whether the routes should be optimised with the string interpolation @@ -22,129 +36,6 @@ module ActionController end.compact end - # Write and compile a +generate+ method for this Route. - def write_generation - # Build the main body of the generation - body = "expired = false\n#{generation_extraction}\n#{generation_structure}" - - # If we have conditions that must be tested first, nest the body inside an if - body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements - args = "options, hash, expire_on = {}" - - # Nest the body inside of a def block, and then compile it. - raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - - # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash - # are the same as the keys that were recalled from the previous request. Thus, - # we can use the expire_on.keys to determine which keys ought to be used to build - # the query string. (Never use keys from the recalled request when building the - # query string.) - - method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - - method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - raw_method - end - - # Build several lines of code that extract values from the options hash. If any - # of the values are missing or rejected then a return will be executed. - def generation_extraction - segments.collect do |segment| - segment.extraction_code - end.compact * "\n" - end - - # Produce a condition expression that will check the requirements of this route - # upon generation. - def generation_requirements - requirement_conditions = requirements.collect do |key, req| - if req.is_a? Regexp - value_regexp = Regexp.new "\\A#{req.to_s}\\Z" - "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" - else - "hash[:#{key}] == #{req.inspect}" - end - end - requirement_conditions * ' && ' unless requirement_conditions.empty? - end - - def generation_structure - segments.last.string_structure segments[0..-2] - end - - # Write and compile a +recognize+ method for this Route. - def write_recognition - # Create an if structure to extract the params from a match if it occurs. - body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" - body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" - - # Build the method declaration and compile it - method_decl = "def recognize(path, env={})\n#{body}\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - method_decl - end - - # Plugins may override this method to add other conditions, like checks on - # host, subdomain, and so forth. Note that changes here only affect route - # recognition, not generation. - def recognition_conditions - result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] - result << "conditions[:method] === env[:method]" if conditions[:method] - result - end - - # Build the regular expression pattern that will match this route. - def recognition_pattern(wrap = true) - pattern = '' - segments.reverse_each do |segment| - pattern = segment.build_pattern pattern - end - wrap ? ("\\A" + pattern + "\\Z") : pattern - end - - # Write the code to extract the parameters from a matched route. - def recognition_extraction - next_capture = 1 - extraction = segments.collect do |segment| - x = segment.match_extraction(next_capture) - next_capture += Regexp.new(segment.regexp_chunk).number_of_captures - x - end - extraction.compact - end - - # Write the real generation implementation and then resend the message. - def generate(options, hash, expire_on = {}) - write_generation - generate options, hash, expire_on - end - - def generate_extras(options, hash, expire_on = {}) - write_generation - generate_extras options, hash, expire_on - end - - # Generate the query string with any extra keys in the hash and append - # it to the given path, returning the new path. - def append_query_string(path, hash, query_keys=nil) - return nil unless path - query_keys ||= extra_keys(hash) - "#{path}#{build_query_string(hash, query_keys)}" - end - - # Determine which keys in the given hash are "extra". Extra keys are - # those that were not used to generate a particular route. The extra - # keys also do not include those recalled from the prior request, nor - # do they include any keys that were implied in the route (like a - # :controller that is required, but not explicitly used in the - # text of the route.) - def extra_keys(hash, recall={}) - (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys - end - # Build a query string from the keys of the given hash. If +only_keys+ # is given (as an array), only the keys indicated will be used to build # the query string. The query string will correctly build array parameter @@ -161,12 +52,6 @@ module ActionController elements.empty? ? '' : "?#{elements.sort * '&'}" end - # Write the real recognition implementation and then resend the message. - def recognize(path, environment={}) - write_recognition - recognize path, environment - end - # A route's parameter shell contains parameter values that are not in the # route's path, but should be placed in the recognized hash. # @@ -186,7 +71,7 @@ module ActionController # includes keys that appear inside the path, and keys that have requirements # placed upon them. def significant_keys - @significant_keys ||= returning [] do |sk| + @significant_keys ||= returning([]) do |sk| segments.each { |segment| sk << segment.key if segment.respond_to? :key } sk.concat requirements.keys sk.uniq! @@ -209,12 +94,7 @@ module ActionController end def matches_controller_and_action?(controller, action) - unless defined? @matching_prepared - @controller_requirement = requirement_for(:controller) - @action_requirement = requirement_for(:action) - @matching_prepared = true - end - + prepare_matching! (@controller_requirement.nil? || @controller_requirement === controller) && (@action_requirement.nil? || @action_requirement === action) end @@ -226,7 +106,23 @@ module ActionController end end - protected + # TODO: Route should be prepared and frozen on initialize + def freeze + unless frozen? + write_generation! + write_recognition! + prepare_matching! + + parameter_shell + significant_keys + defaults + to_s + end + + super + end + + private def requirement_for(key) return requirements[key] if requirements.key? key segments.each do |segment| @@ -234,6 +130,126 @@ module ActionController end nil end + + # Write and compile a +generate+ method for this Route. + def write_generation! + # Build the main body of the generation + body = "expired = false\n#{generation_extraction}\n#{generation_structure}" + + # If we have conditions that must be tested first, nest the body inside an if + body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements + args = "options, hash, expire_on = {}" + + # Nest the body inside of a def block, and then compile it. + raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + + # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash + # are the same as the keys that were recalled from the previous request. Thus, + # we can use the expire_on.keys to determine which keys ought to be used to build + # the query string. (Never use keys from the recalled request when building the + # query string.) + + method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + + method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + raw_method + end + + # Build several lines of code that extract values from the options hash. If any + # of the values are missing or rejected then a return will be executed. + def generation_extraction + segments.collect do |segment| + segment.extraction_code + end.compact * "\n" + end + + # Produce a condition expression that will check the requirements of this route + # upon generation. + def generation_requirements + requirement_conditions = requirements.collect do |key, req| + if req.is_a? Regexp + value_regexp = Regexp.new "\\A#{req.to_s}\\Z" + "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" + else + "hash[:#{key}] == #{req.inspect}" + end + end + requirement_conditions * ' && ' unless requirement_conditions.empty? + end + + def generation_structure + segments.last.string_structure segments[0..-2] + end + + # Write and compile a +recognize+ method for this Route. + def write_recognition! + # Create an if structure to extract the params from a match if it occurs. + body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" + body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" + + # Build the method declaration and compile it + method_decl = "def recognize(path, env = {})\n#{body}\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + method_decl + end + + # Plugins may override this method to add other conditions, like checks on + # host, subdomain, and so forth. Note that changes here only affect route + # recognition, not generation. + def recognition_conditions + result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] + result << "conditions[:method] === env[:method]" if conditions[:method] + result + end + + # Build the regular expression pattern that will match this route. + def recognition_pattern(wrap = true) + pattern = '' + segments.reverse_each do |segment| + pattern = segment.build_pattern pattern + end + wrap ? ("\\A" + pattern + "\\Z") : pattern + end + + # Write the code to extract the parameters from a matched route. + def recognition_extraction + next_capture = 1 + extraction = segments.collect do |segment| + x = segment.match_extraction(next_capture) + next_capture += Regexp.new(segment.regexp_chunk).number_of_captures + x + end + extraction.compact + end + + # Generate the query string with any extra keys in the hash and append + # it to the given path, returning the new path. + def append_query_string(path, hash, query_keys = nil) + return nil unless path + query_keys ||= extra_keys(hash) + "#{path}#{build_query_string(hash, query_keys)}" + end + + # Determine which keys in the given hash are "extra". Extra keys are + # those that were not used to generate a particular route. The extra + # keys also do not include those recalled from the prior request, nor + # do they include any keys that were implied in the route (like a + # :controller that is required, but not explicitly used in the + # text of the route.) + def extra_keys(hash, recall = {}) + (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys + end + + def prepare_matching! + unless defined? @matching_prepared + @controller_requirement = requirement_for(:controller) + @action_requirement = requirement_for(:action) + @matching_prepared = true + end + end end end end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb index 4a9bc259f3..8dfc22f94f 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_controller/routing/route_set.rb @@ -194,6 +194,8 @@ module ActionController def initialize self.routes = [] self.named_routes = NamedRouteCollection.new + + write_recognize_optimized! end # Subclasses and plugins may override this method to specify a different @@ -231,7 +233,6 @@ module ActionController Routing.use_controllers! nil # Clear the controller cache so we may discover new ones clear! load_routes! - install_helpers end # reload! will always force a reload whereas load checks the timestamp first diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index 18e76b6b82..75784c3b78 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -4,11 +4,12 @@ module ActionController RESERVED_PCHAR = ':@&=+$,;' UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze + # TODO: Convert :is_optional accessor to read only attr_accessor :is_optional alias_method :optional?, :is_optional def initialize - self.is_optional = false + @is_optional = false end def extraction_code @@ -63,12 +64,14 @@ module ActionController end class StaticSegment < Segment #:nodoc: - attr_accessor :value, :raw + attr_reader :value, :raw alias_method :raw?, :raw - def initialize(value = nil) + def initialize(value = nil, options = {}) super() - self.value = value + @value = value + @raw = options[:raw] if options.key?(:raw) + @is_optional = options[:optional] if options.key?(:optional) end def interpolation_chunk @@ -97,10 +100,8 @@ module ActionController end class DividerSegment < StaticSegment #:nodoc: - def initialize(value = nil) - super(value) - self.raw = true - self.is_optional = true + def initialize(value = nil, options = {}) + super(value, {:raw => true, :optional => true}.merge(options)) end def optionality_implied? @@ -109,13 +110,17 @@ module ActionController end class DynamicSegment < Segment #:nodoc: - attr_accessor :key, :default, :regexp + attr_reader :key + + # TODO: Convert these accessors to read only + attr_accessor :default, :regexp def initialize(key = nil, options = {}) super() - self.key = key - self.default = options[:default] if options.key? :default - self.is_optional = true if options[:optional] || options.key?(:default) + @key = key + @default = options[:default] if options.key?(:default) + @regexp = options[:regexp] if options.key?(:regexp) + @is_optional = true if options[:optional] || options.key?(:default) end def to_s diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 22e394dc95..6cf134c26f 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -67,66 +67,56 @@ class SegmentTest < Test::Unit::TestCase end def test_interpolation_statement - s = ROUTING::StaticSegment.new - s.value = "Hello" + s = ROUTING::StaticSegment.new("Hello") assert_equal "Hello", eval(s.interpolation_statement([])) assert_equal "HelloHello", eval(s.interpolation_statement([s])) - s2 = ROUTING::StaticSegment.new - s2.value = "-" + s2 = ROUTING::StaticSegment.new("-") assert_equal "Hello-Hello", eval(s.interpolation_statement([s, s2])) - s3 = ROUTING::StaticSegment.new - s3.value = "World" + s3 = ROUTING::StaticSegment.new("World") assert_equal "Hello-World", eval(s3.interpolation_statement([s, s2])) end end class StaticSegmentTest < Test::Unit::TestCase def test_interpolation_chunk_should_respect_raw - s = ROUTING::StaticSegment.new - s.value = 'Hello World' - assert ! s.raw? + s = ROUTING::StaticSegment.new('Hello World') + assert !s.raw? assert_equal 'Hello%20World', s.interpolation_chunk - s.raw = true + s = ROUTING::StaticSegment.new('Hello World', :raw => true) assert s.raw? assert_equal 'Hello World', s.interpolation_chunk end def test_regexp_chunk_should_escape_specials - s = ROUTING::StaticSegment.new - - s.value = 'Hello*World' + s = ROUTING::StaticSegment.new('Hello*World') assert_equal 'Hello\*World', s.regexp_chunk - s.value = 'HelloWorld' + s = ROUTING::StaticSegment.new('HelloWorld') assert_equal 'HelloWorld', s.regexp_chunk end def test_regexp_chunk_should_add_question_mark_for_optionals - s = ROUTING::StaticSegment.new - s.value = "/" - s.is_optional = true + s = ROUTING::StaticSegment.new("/", :optional => true) assert_equal "/?", s.regexp_chunk - s.value = "hello" + s = ROUTING::StaticSegment.new("hello", :optional => true) assert_equal "(?:hello)?", s.regexp_chunk end end class DynamicSegmentTest < Test::Unit::TestCase - def segment + def segment(options = {}) unless @segment - @segment = ROUTING::DynamicSegment.new - @segment.key = :a + @segment = ROUTING::DynamicSegment.new(:a, options) end @segment end def test_extract_value - s = ROUTING::DynamicSegment.new - s.key = :a + s = ROUTING::DynamicSegment.new(:a) hash = {:a => '10', :b => '20'} assert_equal '10', eval(s.extract_value) @@ -149,31 +139,31 @@ class DynamicSegmentTest < Test::Unit::TestCase end def test_regexp_value_check_rejects_nil - segment.regexp = /\d+/ + segment = segment(:regexp => /\d+/) + a_value = nil - assert ! eval(segment.value_check) + assert !eval(segment.value_check) end def test_optional_regexp_value_check_should_accept_nil - segment.regexp = /\d+/ - segment.is_optional = true + segment = segment(:regexp => /\d+/, :optional => true) + a_value = nil assert eval(segment.value_check) end def test_regexp_value_check_rejects_no_match - segment.regexp = /\d+/ + segment = segment(:regexp => /\d+/) a_value = "Hello20World" - assert ! eval(segment.value_check) + assert !eval(segment.value_check) a_value = "20Hi" - assert ! eval(segment.value_check) + assert !eval(segment.value_check) end def test_regexp_value_check_accepts_match - segment.regexp = /\d+/ - + segment = segment(:regexp => /\d+/) a_value = "30" assert eval(segment.value_check) end @@ -184,14 +174,14 @@ class DynamicSegmentTest < Test::Unit::TestCase end def test_optional_value_needs_no_check - segment.is_optional = true + segment = segment(:optional => true) + a_value = nil assert_equal nil, segment.value_check end def test_regexp_value_check_should_accept_match_with_default - segment.regexp = /\d+/ - segment.default = '200' + segment = segment(:regexp => /\d+/, :default => '200') a_value = '100' assert eval(segment.value_check) @@ -234,7 +224,7 @@ class DynamicSegmentTest < Test::Unit::TestCase end def test_extraction_code_should_return_on_mismatch - segment.regexp = /\d+/ + segment = segment(:regexp => /\d+/) hash = merged = {:a => 'Hi', :b => '3'} options = {:b => '3'} a_value = nil @@ -292,7 +282,7 @@ class DynamicSegmentTest < Test::Unit::TestCase end def test_value_regexp_should_match_exacly - segment.regexp = /\d+/ + segment = segment(:regexp => /\d+/) assert_no_match segment.value_regexp, "Hello 10 World" assert_no_match segment.value_regexp, "Hello 10" assert_no_match segment.value_regexp, "10 World" @@ -300,40 +290,36 @@ class DynamicSegmentTest < Test::Unit::TestCase end def test_regexp_chunk_should_return_string - segment.regexp = /\d+/ + segment = segment(:regexp => /\d+/) assert_kind_of String, segment.regexp_chunk end def test_build_pattern_non_optional_with_no_captures # Non optional - a_segment = ROUTING::DynamicSegment.new - a_segment.regexp = /\d+/ #number_of_captures is 0 + a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /\d+/) assert_equal "(\\d+)stuff", a_segment.build_pattern('stuff') end def test_build_pattern_non_optional_with_captures # Non optional - a_segment = ROUTING::DynamicSegment.new - a_segment.regexp = /(\d+)(.*?)/ #number_of_captures is 2 + a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /(\d+)(.*?)/) assert_equal "((\\d+)(.*?))stuff", a_segment.build_pattern('stuff') end def test_optionality_implied - a_segment = ROUTING::DynamicSegment.new - a_segment.key = :id + a_segment = ROUTING::DynamicSegment.new(:id) assert a_segment.optionality_implied? - a_segment.key = :action + a_segment = ROUTING::DynamicSegment.new(:action) assert a_segment.optionality_implied? end def test_modifiers_must_be_handled_sensibly - a_segment = ROUTING::DynamicSegment.new - a_segment.regexp = /david|jamis/i + a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/i) assert_equal "((?i-mx:david|jamis))stuff", a_segment.build_pattern('stuff') - a_segment.regexp = /david|jamis/x + a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/x) assert_equal "((?x-mi:david|jamis))stuff", a_segment.build_pattern('stuff') - a_segment.regexp = /david|jamis/ + a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/) assert_equal "(david|jamis)stuff", a_segment.build_pattern('stuff') end end @@ -560,7 +546,7 @@ class RouteBuilderTest < Test::Unit::TestCase action = segments[-4] assert_equal :action, action.key - action.regexp = /show|in/ # Use 'in' to check partial matches + segments[-4] = ROUTING::DynamicSegment.new(:action, :regexp => /show|in/) builder.assign_default_route_options(segments) @@ -661,10 +647,10 @@ class RoutingTest < Test::Unit::TestCase ActionController::Routing.controller_paths = [] assert_equal [], ActionController::Routing.possible_controllers - ActionController::Routing::Routes.load! ActionController::Routing.controller_paths = [ root, root + '/app/controllers', root + '/vendor/plugins/bad_plugin/lib' ] + ActionController::Routing::Routes.load! assert_equal ["admin/user", "plugin", "user"], ActionController::Routing.possible_controllers.sort ensure @@ -1374,34 +1360,20 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end def slash_segment(is_optional = false) - returning ROUTING::DividerSegment.new('/') do |s| - s.is_optional = is_optional - end + ROUTING::DividerSegment.new('/', :optional => is_optional) end def default_route unless defined?(@default_route) - @default_route = ROUTING::Route.new - - @default_route.segments << (s = ROUTING::StaticSegment.new) - s.value = '/' - s.raw = true - - @default_route.segments << (s = ROUTING::DynamicSegment.new) - s.key = :controller - - @default_route.segments << slash_segment(:optional) - @default_route.segments << (s = ROUTING::DynamicSegment.new) - s.key = :action - s.default = 'index' - s.is_optional = true - - @default_route.segments << slash_segment(:optional) - @default_route.segments << (s = ROUTING::DynamicSegment.new) - s.key = :id - s.is_optional = true - - @default_route.segments << slash_segment(:optional) + segments = [] + segments << ROUTING::StaticSegment.new('/', :raw => true) + segments << ROUTING::DynamicSegment.new(:controller) + segments << slash_segment(:optional) + segments << ROUTING::DynamicSegment.new(:action, :default => 'index', :optional => true) + segments << slash_segment(:optional) + segments << ROUTING::DynamicSegment.new(:id, :optional => true) + segments << slash_segment(:optional) + @default_route = ROUTING::Route.new(segments).freeze end @default_route end @@ -1489,29 +1461,16 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end def test_significant_keys - user_url = ROUTING::Route.new - user_url.segments << (s = ROUTING::StaticSegment.new) - s.value = '/' - s.raw = true - - user_url.segments << (s = ROUTING::StaticSegment.new) - s.value = 'user' - - user_url.segments << (s = ROUTING::StaticSegment.new) - s.value = '/' - s.raw = true - s.is_optional = true - - user_url.segments << (s = ROUTING::DynamicSegment.new) - s.key = :user - - user_url.segments << (s = ROUTING::StaticSegment.new) - s.value = '/' - s.raw = true - s.is_optional = true + segments = [] + segments << ROUTING::StaticSegment.new('/', :raw => true) + segments << ROUTING::StaticSegment.new('user') + segments << ROUTING::StaticSegment.new('/', :raw => true, :optional => true) + segments << ROUTING::DynamicSegment.new(:user) + segments << ROUTING::StaticSegment.new('/', :raw => true, :optional => true) - user_url.requirements = {:controller => 'users', :action => 'show'} + requirements = {:controller => 'users', :action => 'show'} + user_url = ROUTING::Route.new(segments, requirements) keys = user_url.significant_keys.sort_by { |k| k.to_s } assert_equal [:action, :controller, :user], keys end diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index 61b8c83ee0..58d9ca537f 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -117,8 +117,8 @@ XML @controller = TestController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new - ActionController::Routing::Routes.reload ActionController::Routing.use_controllers! %w(content admin/user test_test/test) + ActionController::Routing::Routes.load_routes! end def teardown @@ -412,7 +412,7 @@ XML def test_assert_routing_with_method with_routing do |set| - set.draw { |map| map.resources(:content) } + set.draw { |map| map.resources(:content) } assert_routing({ :method => 'post', :path => 'content' }, { :controller => 'content', :action => 'create' }) end end -- cgit v1.2.3 From 19db0b732458347b5237ac90865d62b3fd2157f1 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 28 Jul 2008 14:31:40 -0500 Subject: Added back ActionController::Base.allow_concurrency flag and moved lock down to controller processing. --- actionpack/CHANGELOG | 2 ++ actionpack/lib/action_controller/base.rb | 15 ++++++++++++++- actionpack/lib/action_controller/dispatcher.rb | 18 +++++++----------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 03e011c75c..177c6a354e 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Added back ActionController::Base.allow_concurrency flag [Josh Peek] + * AbstractRequest.relative_url_root is no longer automatically configured by a HTTP header. It can now be set in your configuration environment with config.action_controller.relative_url_root [Josh Peek] * Update Prototype to 1.6.0.2 #599 [Patrick Joyce] diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5f4a38dac0..ac10a956f3 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -283,6 +283,14 @@ module ActionController #:nodoc: @@debug_routes = true cattr_accessor :debug_routes + # Indicates whether to allow concurrent action processing. Your + # controller actions and any other code they call must also behave well + # when called from concurrent threads. Turned off by default. + @@allow_concurrency = false + cattr_accessor :allow_concurrency + + @@guard = Monitor.new + # Modern REST web services often need to submit complex data to the web application. # The @@param_parsers hash lets you register handlers which will process the HTTP body and add parameters to the # params hash. These handlers are invoked for POST and PUT requests. @@ -537,7 +545,12 @@ module ActionController #:nodoc: forget_variables_added_to_assigns log_processing - send(method, *arguments) + + if @@allow_concurrency + send(method, *arguments) + else + @@guard.synchronize { send(method, *arguments) } + end assign_default_content_type_and_charset response.prepare! unless component_request? diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb index 7df987d525..835d8e834e 100644 --- a/actionpack/lib/action_controller/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatcher.rb @@ -2,8 +2,6 @@ module ActionController # Dispatches requests to the appropriate controller and takes care of # reloading the app after each request when Dependencies.load? is true. class Dispatcher - @@guard = Mutex.new - class << self def define_dispatcher_callbacks(cache_classes) unless cache_classes @@ -101,15 +99,13 @@ module ActionController end def dispatch - @@guard.synchronize do - begin - run_callbacks :before_dispatch - handle_request - rescue Exception => exception - failsafe_rescue exception - ensure - run_callbacks :after_dispatch, :enumerator => :reverse_each - end + begin + run_callbacks :before_dispatch + handle_request + rescue Exception => exception + failsafe_rescue exception + ensure + run_callbacks :after_dispatch, :enumerator => :reverse_each end end -- cgit v1.2.3 From 50bbc87f85e817a2926c56ccd81d3dc498a05e21 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Sun, 27 Jul 2008 21:44:53 -0700 Subject: MacRuby: BasicObject unavailable --- activesupport/lib/active_support/basic_object.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/basic_object.rb b/activesupport/lib/active_support/basic_object.rb index e06da79d26..1f77209e7f 100644 --- a/activesupport/lib/active_support/basic_object.rb +++ b/activesupport/lib/active_support/basic_object.rb @@ -7,7 +7,7 @@ # barebones base class that emulates Builder::BlankSlate while still relying on # Ruby 1.9's BasicObject in Ruby 1.9. module ActiveSupport - if RUBY_VERSION >= '1.9' + if defined? ::BasicObject class BasicObject < ::BasicObject undef_method :== undef_method :equal? -- cgit v1.2.3 From eb256718c3c1a0640f69861587239f1e6cde2820 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Sun, 27 Jul 2008 21:45:33 -0700 Subject: Remove send! usage, relic of reverted 1.9 behavior --- actionpack/lib/action_controller/integration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb index 43158ea412..1d2b81355c 100644 --- a/actionpack/lib/action_controller/integration.rb +++ b/actionpack/lib/action_controller/integration.rb @@ -507,7 +507,7 @@ EOF # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) reset! unless @integration_session - returning @integration_session.send!(sym, *args, &block) do + returning @integration_session.__send__(sym, *args, &block) do copy_session_variables! end end -- cgit v1.2.3 From ae6105ef01b2a767afa2bf5b64c90d288c752995 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Sun, 27 Jul 2008 21:45:55 -0700 Subject: Don't rememoize if already frozen --- activesupport/lib/active_support/memoizable.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index 21636b8af4..23dd96e4df 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -11,10 +11,9 @@ module ActiveSupport def freeze_with_memoizable methods.each do |method| - if m = method.to_s.match(/^_unmemoized_(.*)/) - send(m[1]) - end - end + __send__($1) if method.to_s =~ /^_unmemoized_(.*)/ + end unless frozen? + freeze_without_memoizable end end -- cgit v1.2.3 From 2cf161a384cc361d856aa76639bcb30570d67286 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Sun, 27 Jul 2008 21:46:12 -0700 Subject: Once is enough, mmk --- activesupport/lib/active_support/testing/performance.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb index 71d6f4d9c6..70a7f84023 100644 --- a/activesupport/lib/active_support/testing/performance.rb +++ b/activesupport/lib/active_support/testing/performance.rb @@ -72,13 +72,13 @@ module ActiveSupport protected def run_warmup - 5.times { GC.start } + GC.start time = Metrics::Time.new run_test(time, :benchmark) puts "%s (%s warmup)" % [full_test_name, time.format(time.total)] - 5.times { GC.start } + GC.start end def run_profile(metric) -- cgit v1.2.3 From 7aaf1689dda863b72623f0e52ad87e2b739cb22b Mon Sep 17 00:00:00 2001 From: Jan De Poorter Date: Tue, 29 Jul 2008 13:11:38 +0200 Subject: Fix that label_tag doesn't take a symbol for a name. [#719 state:resolved] Signed-off-by: Pratik Naik --- actionpack/lib/action_view/helpers/form_tag_helper.rb | 2 +- actionpack/test/template/form_tag_helper_test.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index bdfb2eebd7..e8ca02d760 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -129,7 +129,7 @@ module ActionView # label_tag 'name', nil, :class => 'small_label' # # => def label_tag(name, text = nil, options = {}) - content_tag :label, text || name.humanize, { "for" => name }.update(options.stringify_keys) + content_tag :label, text || name.to_s.humanize, { "for" => name }.update(options.stringify_keys) end # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 4e4102aec7..9b41ff8179 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -190,6 +190,12 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_label_tag_with_symbol + actual = label_tag :title + expected = %() + assert_dom_equal expected, actual + end + def test_label_tag_with_text actual = label_tag "title", "My Title" expected = %() -- cgit v1.2.3 From a24398b64757df8c5939b07238c740bddfdab03e Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Tue, 29 Jul 2008 19:49:38 +0200 Subject: Guard the logger's internal buffer to prevent major breakage on genuinely threaded environments --- activesupport/lib/active_support/buffered_logger.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index 67b0a580ea..aec416a6d6 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -40,6 +40,7 @@ module ActiveSupport @buffer = [] @auto_flushing = 1 @no_block = false + @guard = Mutex.new if log.respond_to?(:write) @log = log @@ -66,7 +67,9 @@ module ActiveSupport # If a newline is necessary then create a new message ending with a newline. # Ensures that the original message is not mutated. message = "#{message}\n" unless message[-1] == ?\n - buffer << message + @guard.synchronize do + buffer << message + end auto_flush message end @@ -98,11 +101,16 @@ module ActiveSupport end def flush - unless buffer.empty? - if @no_block - @log.write_nonblock(buffer.slice!(0..-1).join) - else - @log.write(buffer.slice!(0..-1).join) + @guard.synchronize do + unless buffer.empty? + old_buffer = @buffer + @buffer = [] + text_to_write = old_buffer.join + if @no_block + @log.write_nonblock(text_to_write) + else + @log.write(text_to_write) + end end end end -- cgit v1.2.3 From d9452d3ab3063c5e96dfd80cf6056c49192081b3 Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Tue, 29 Jul 2008 20:01:25 +0200 Subject: Remove incomplete non-blocking logger functionality --- activesupport/lib/active_support/buffered_logger.rb | 14 +------------- railties/lib/initializer.rb | 1 - 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index aec416a6d6..cedc1afe7f 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -39,7 +39,6 @@ module ActiveSupport @level = level @buffer = [] @auto_flushing = 1 - @no_block = false @guard = Mutex.new if log.respond_to?(:write) @@ -55,12 +54,6 @@ module ActiveSupport end end - def set_non_blocking_io - if !RUBY_PLATFORM.match(/java|mswin/) && !(@log == STDOUT) && @log.respond_to?(:write_nonblock) - @no_block = true - end - end - def add(severity, message = nil, progname = nil, &block) return if @level > severity message = (message || (block && block.call) || progname).to_s @@ -105,12 +98,7 @@ module ActiveSupport unless buffer.empty? old_buffer = @buffer @buffer = [] - text_to_write = old_buffer.join - if @no_block - @log.write_nonblock(text_to_write) - else - @log.write(text_to_write) - end + @log.write(old_buffer.join) end end end diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 32411e8928..782fbebec2 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -408,7 +408,6 @@ Run `rake gems:install` to install the missing gems. logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase) if configuration.environment == "production" logger.auto_flushing = false - logger.set_non_blocking_io end rescue StandardError => e logger = ActiveSupport::BufferedLogger.new(STDERR) -- cgit v1.2.3 From fea7771d2294bd810d04c03c125ceac5d6bb9806 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Tue, 29 Jul 2008 21:47:02 -0500 Subject: Updated NumberHelper: Full i18n support (except number_to_phone), consolidated API (almost all methods now support :precision, :delimiter and :separator). Added deprecation notices for old API. Added tests for new options [#716 state:resolved] Signed-off-by: Joshua Peek --- .../lib/action_view/helpers/number_helper.rb | 186 ++++++++++++++------- actionpack/lib/action_view/locale/en-US.rb | 35 +++- .../test/template/number_helper_i18n_test.rb | 46 ++++- actionpack/test/template/number_helper_test.rb | 48 +++--- 4 files changed, 219 insertions(+), 96 deletions(-) diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index c4ba7ccc8e..8c1dea2186 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -25,11 +25,11 @@ module ActionView # => +1.123.555.1234 x 1343 def number_to_phone(number, options = {}) number = number.to_s.strip unless number.nil? - options = options.stringify_keys - area_code = options["area_code"] || nil - delimiter = options["delimiter"] || "-" - extension = options["extension"].to_s.strip || nil - country_code = options["country_code"] || nil + options = options.symbolize_keys + area_code = options[:area_code] || nil + delimiter = options[:delimiter] || "-" + extension = options[:extension].to_s.strip || nil + country_code = options[:country_code] || nil begin str = "" @@ -51,10 +51,10 @@ module ActionView # # ==== Options # * :precision - Sets the level of precision (defaults to 2). - # * :unit - Sets the denomination of the currency (defaults to "$"). + # * :unit - Sets the denomination of the currency (defaults to "$"). # * :separator - Sets the separator between the units (defaults to "."). # * :delimiter - Sets the thousands delimiter (defaults to ","). - # * :format - Sets the format of the output string (defaults to "%u%n"). The field types are: + # * :format - Sets the format of the output string (defaults to "%u%n"). The field types are: # # %u The currency unit # %n The number @@ -69,8 +69,11 @@ module ActionView # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) - options = options.symbolize_keys - defaults = I18n.translate(:'currency.format', :locale => options[:locale]) || {} + options.symbolize_keys! + + defaults, currency = I18n.translate([:'number.format', :'number.currency.format'], + :locale => options[:locale]) || [{},{}] + defaults = defaults.merge(currency) precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] @@ -80,8 +83,11 @@ module ActionView separator = '' if precision == 0 begin - parts = number_with_precision(number, precision).split('.') - format.gsub(/%n/, number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s).gsub(/%u/, unit) + format.gsub(/%n/, number_with_precision(number, + :precision => precision, + :delimiter => delimiter, + :separator => separator) + ).gsub(/%u/, unit) rescue number end @@ -93,26 +99,29 @@ module ActionView # ==== Options # * :precision - Sets the level of precision (defaults to 3). # * :separator - Sets the separator between the units (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ""). # # ==== Examples - # number_to_percentage(100) # => 100.000% - # number_to_percentage(100, :precision => 0) # => 100% - # - # number_to_percentage(302.24398923423, :precision => 5) - # # => 302.24399% + # number_to_percentage(100) # => 100.000% + # number_to_percentage(100, :precision => 0) # => 100% + # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000% + # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% def number_to_percentage(number, options = {}) - options = options.stringify_keys - precision = options["precision"] || 3 - separator = options["separator"] || "." + options.symbolize_keys! + + defaults, percentage = I18n.translate([:'number.format', :'number.percentage.format'], + :locale => options[:locale]) || [{},{}] + defaults = defaults.merge(percentage) + + precision = options[:precision] || defaults[:precision] + separator = options[:separator] || defaults[:separator] + delimiter = options[:delimiter] || defaults[:delimiter] begin - number = number_with_precision(number, precision) - parts = number.split('.') - if parts.at(1).nil? - parts[0] + "%" - else - parts[0] + separator + parts[1].to_s + "%" - end + number_with_precision(number, + :precision => precision, + :separator => separator, + :delimiter => delimiter) + "%" rescue number end @@ -136,87 +145,140 @@ module ActionView # You can still use number_with_delimiter with the old API that accepts the # +delimiter+ as its optional second and the +separator+ as its # optional third parameter: - # number_with_delimiter(12345678, " ") # => 12 345.678 - # number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05 + # number_with_delimiter(12345678, " ") # => 12 345.678 + # number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05 def number_with_delimiter(number, *args) options = args.extract_options! + options.symbolize_keys! + + defaults = I18n.translate(:'number.format', :locale => options[:locale]) || {} + unless args.empty? - options[:delimiter] = args[0] || "," - options[:separator] = args[1] || "." + ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' + + 'instead of separate delimiter and precision arguments.', caller) + delimiter = args[0] || defaults[:delimiter] + separator = args[1] || defaults[:separator] end - options.reverse_merge!(:delimiter => ",", :separator => ".") + + delimiter ||= (options[:delimiter] || defaults[:delimiter]) + separator ||= (options[:separator] || defaults[:separator]) begin parts = number.to_s.split('.') - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") - parts.join options[:separator] + parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") + parts.join(separator) rescue number end end # Formats a +number+ with the specified level of :precision (e.g., 112.32 has a precision of 2). - # The default level of precision is 3. + # You can customize the format in the +options+ hash. + # + # ==== Options + # * :precision - Sets the level of precision (defaults to 3). + # * :separator - Sets the separator between the units (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ""). # # ==== Examples # number_with_precision(111.2345) # => 111.235 # number_with_precision(111.2345, :precision => 2) # => 111.23 # number_with_precision(13, :precision => 5) # => 13.00000 # number_with_precision(389.32314, :precision => 0) # => 389 + # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') + # # => 1.111,23 # # You can still use number_with_precision with the old API that accepts the # +precision+ as its optional second parameter: # number_with_precision(number_with_precision(111.2345, 2) # => 111.23 def number_with_precision(number, *args) options = args.extract_options! + options.symbolize_keys! + + defaults, precision_defaults = I18n.translate([:'number.format', :'number.precision.format'], + :locale => options[:locale]) || [{},{}] + defaults = defaults.merge(precision_defaults) + unless args.empty? - options[:precision] = args[0] || 3 + ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' + + 'instead of a separate precision argument.', caller) + precision = args[0] || defaults[:precision] end - options.reverse_merge!(:precision => 3) - "%01.#{options[:precision]}f" % - ((Float(number) * (10 ** options[:precision])).round.to_f / 10 ** options[:precision]) + + precision ||= (options[:precision] || defaults[:precision]) + separator ||= (options[:separator] || defaults[:separator]) + delimiter ||= (options[:delimiter] || defaults[:delimiter]) + + rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision + number_with_delimiter("%01.#{precision}f" % rounded_number, + :separator => separator, + :delimiter => delimiter) rescue number end + STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze + # Formats the bytes in +size+ into a more understandable representation # (e.g., giving it 1500 yields 1.5 KB). This method is useful for # reporting file sizes to users. This method returns nil if - # +size+ cannot be converted into a number. You can change the default - # precision of 1 using the precision parameter :precision. + # +size+ cannot be converted into a number. You can customize the + # format in the +options+ hash. + # + # ==== Options + # * :precision - Sets the level of precision (defaults to 1). + # * :separator - Sets the separator between the units (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ""). # # ==== Examples - # number_to_human_size(123) # => 123 Bytes - # number_to_human_size(1234) # => 1.2 KB - # number_to_human_size(12345) # => 12.1 KB - # number_to_human_size(1234567) # => 1.2 MB - # number_to_human_size(1234567890) # => 1.1 GB - # number_to_human_size(1234567890123) # => 1.1 TB - # number_to_human_size(1234567, :precision => 2) # => 1.18 MB - # number_to_human_size(483989, :precision => 0) # => 473 KB + # number_to_human_size(123) # => 123 Bytes + # number_to_human_size(1234) # => 1.2 KB + # number_to_human_size(12345) # => 12.1 KB + # number_to_human_size(1234567) # => 1.2 MB + # number_to_human_size(1234567890) # => 1.1 GB + # number_to_human_size(1234567890123) # => 1.1 TB + # number_to_human_size(1234567, :precision => 2) # => 1.18 MB + # number_to_human_size(483989, :precision => 0) # => 473 KB + # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,18 MB # # You can still use number_to_human_size with the old API that accepts the # +precision+ as its optional second parameter: # number_to_human_size(1234567, 2) # => 1.18 MB # number_to_human_size(483989, 0) # => 473 KB - def number_to_human_size(size, *args) + def number_to_human_size(number, *args) + return number.nil? ? nil : pluralize(number.to_i, "Byte") if number.to_i < 1024 + options = args.extract_options! + options.symbolize_keys! + + defaults, human = I18n.translate([:'number.format', :'number.human.format'], + :locale => options[:locale]) || [{},{}] + defaults = defaults.merge(human) + unless args.empty? - options[:precision] = args[0] || 1 + ActiveSupport::Deprecation.warn('number_to_human_size takes an option hash ' + + 'instead of a separate precision argument.', caller) + precision = args[0] || defaults[:precision] end - options.reverse_merge!(:precision => 1) - - size = Float(size) - case - when size.to_i == 1; "1 Byte" - when size < 1.kilobyte; "%d Bytes" % size - when size < 1.megabyte; "%.#{options[:precision]}f KB" % (size / 1.0.kilobyte) - when size < 1.gigabyte; "%.#{options[:precision]}f MB" % (size / 1.0.megabyte) - when size < 1.terabyte; "%.#{options[:precision]}f GB" % (size / 1.0.gigabyte) - else "%.#{options[:precision]}f TB" % (size / 1.0.terabyte) - end.sub(/([0-9]\.\d*?)0+ /, '\1 ' ).sub(/\. /,' ') + + precision ||= (options[:precision] || defaults[:precision]) + separator ||= (options[:separator] || defaults[:separator]) + delimiter ||= (options[:delimiter] || defaults[:delimiter]) + + max_exp = STORAGE_UNITS.size - 1 + number = Float(number) + exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024 + exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit + number /= 1024 ** exponent + unit = STORAGE_UNITS[exponent] + + number_with_precision(number, + :precision => precision, + :separator => separator, + :delimiter => delimiter + ).sub(/(\d)(#{Regexp.escape(separator)}[1-9]*)?0+\z/, '\1') + " #{unit}" rescue - nil + number end end end diff --git a/actionpack/lib/action_view/locale/en-US.rb b/actionpack/lib/action_view/locale/en-US.rb index 3adb199681..2c3676dca8 100644 --- a/actionpack/lib/action_view/locale/en-US.rb +++ b/actionpack/lib/action_view/locale/en-US.rb @@ -14,19 +14,40 @@ I18n.backend.store_translations :'en-US', { :over_x_years => ['over 1 year', 'over {{count}} years'] } }, - :currency => { + :number => { :format => { - :unit => '$', - :precision => 2, + :precision => 3, :separator => '.', - :delimiter => ',', - :format => '%u%n', + :delimiter => ',' + }, + :currency => { + :format => { + :unit => '$', + :precision => 2, + :format => '%u%n' + } + }, + :human => { + :format => { + :precision => 1, + :delimiter => '' + } + }, + :percentage => { + :format => { + :delimiter => '' + } + }, + :precision => { + :format => { + :delimiter => '' + } } }, :active_record => { :error => { :header_message => ["1 error prohibited this {{object_name}} from being saved", "{{count}} errors prohibited this {{object_name}} from being saved"], :message => "There were problems with the following fields:" - } - } + } + } } diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index 50c20c3627..ce0da398cc 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -2,17 +2,53 @@ require 'abstract_unit' class NumberHelperI18nTests < Test::Unit::TestCase include ActionView::Helpers::NumberHelper - + attr_reader :request + uses_mocha 'number_helper_i18n_tests' do def setup - @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2} - I18n.backend.store_translations 'en-US', :currency => {:format => @defaults} + @number_defaults = { :precision => 3, :delimiter => ',', :separator => '.' } + @currency_defaults = { :unit => '$', :format => '%u%n', :precision => 2 } + @human_defaults = { :precision => 1 } + @percentage_defaults = { :delimiter => '' } + @precision_defaults = { :delimiter => '' } + + I18n.backend.store_translations 'en-US', :number => { :format => @number_defaults, + :currency => { :format => @currency_defaults }, :human => @human_defaults } end def test_number_to_currency_translates_currency_formats - I18n.expects(:translate).with(:'currency.format', :locale => 'en-US').returns @defaults + I18n.expects(:translate).with( + [:'number.format', :'number.currency.format'], :locale => 'en-US' + ).returns([@number_defaults, @currency_defaults]) number_to_currency(1, :locale => 'en-US') end + + def test_number_with_precision_translates_number_formats + I18n.expects(:translate).with( + [:'number.format', :'number.precision.format'], :locale => 'en-US' + ).returns([@number_defaults, @precision_defaults]) + number_with_precision(1, :locale => 'en-US') + end + + def test_number_with_delimiter_translates_number_formats + I18n.expects(:translate).with(:'number.format', :locale => 'en-US').returns(@number_defaults) + number_with_delimiter(1, :locale => 'en-US') + end + + def test_number_to_percentage_translates_number_formats + I18n.expects(:translate).with( + [:'number.format', :'number.percentage.format'], :locale => 'en-US' + ).returns([@number_defaults, @percentage_defaults]) + number_to_percentage(1, :locale => 'en-US') + end + + def test_number_to_human_size_translates_human_formats + I18n.expects(:translate).with( + [:'number.format', :'number.human.format'], :locale => 'en-US' + ).returns([@number_defaults, @human_defaults]) + # can't be called with 1 because this directly returns without calling I18n.translate + number_to_human_size(1025, :locale => 'en-US') + end end -end \ No newline at end of file +end diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index bff349a754..9c9f54936c 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -26,7 +26,8 @@ class NumberHelperTest < ActionView::TestCase assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) - assert_equal("$x.", number_to_currency("x")) + #assert_equal("$x.", number_to_currency("x")) # fails due to API consolidation + assert_equal("$x", number_to_currency("x")) assert_nil number_to_currency(nil) end @@ -35,7 +36,9 @@ class NumberHelperTest < ActionView::TestCase assert_equal("100%", number_to_percentage(100, {:precision => 0})) assert_equal("302.06%", number_to_percentage(302.0574, {:precision => 2})) assert_equal("100.000%", number_to_percentage("100")) + assert_equal("1000.000%", number_to_percentage("1000")) assert_equal("x%", number_to_percentage("x")) + assert_equal("1.000,000%", number_to_percentage(1000, :delimiter => '.', :separator => ',')) assert_nil number_to_percentage(nil) end @@ -63,28 +66,22 @@ class NumberHelperTest < ActionView::TestCase def test_number_with_precision assert_equal("111.235", number_with_precision(111.2346)) - assert_equal("31.83", number_with_precision(31.825, 2)) - assert_equal("111.23", number_with_precision(111.2346, 2)) - assert_equal("111.00", number_with_precision(111, 2)) + assert_equal("31.83", number_with_precision(31.825, :precision => 2)) + assert_equal("111.23", number_with_precision(111.2346, :precision => 2)) + assert_equal("111.00", number_with_precision(111, :precision => 2)) assert_equal("111.235", number_with_precision("111.2346")) - assert_equal("31.83", number_with_precision("31.825", 2)) - assert_equal("112", number_with_precision(111.50, 0)) - assert_equal("1234567892", number_with_precision(1234567891.50, 0)) + assert_equal("31.83", number_with_precision("31.825", :precision => 2)) + assert_equal("112", number_with_precision(111.50, :precision => 0)) + assert_equal("1234567892", number_with_precision(1234567891.50, :precision => 0)) # Return non-numeric params unchanged. assert_equal("x", number_with_precision("x")) assert_nil number_with_precision(nil) end - def test_number_with_precision_with_options_hash - assert_equal '111.235', number_with_precision(111.2346) - assert_equal '31.83', number_with_precision(31.825, :precision => 2) - assert_equal '111.23', number_with_precision(111.2346, :precision => 2) - assert_equal '111.00', number_with_precision(111, :precision => 2) - assert_equal '111.235', number_with_precision("111.2346") - assert_equal '31.83', number_with_precision("31.825", :precision => 2) - assert_equal '112', number_with_precision(111.50, :precision => 0) - assert_equal '1234567892', number_with_precision(1234567891.50, :precision => 0) + def test_number_with_precision_with_custom_delimiter_and_separator + assert_equal '31,83', number_with_precision(31.825, :precision => 2, :separator => ',') + assert_equal '1.231,83', number_with_precision(1231.825, :precision => 2, :separator => ',', :delimiter => '.') end def test_number_to_human_size @@ -98,18 +95,19 @@ class NumberHelperTest < ActionView::TestCase assert_equal '1.2 MB', number_to_human_size(1234567) assert_equal '1.1 GB', number_to_human_size(1234567890) assert_equal '1.1 TB', number_to_human_size(1234567890123) + assert_equal '1025 TB', number_to_human_size(1025.terabytes) assert_equal '444 KB', number_to_human_size(444.kilobytes) assert_equal '1023 MB', number_to_human_size(1023.megabytes) assert_equal '3 TB', number_to_human_size(3.terabytes) - assert_equal '1.18 MB', number_to_human_size(1234567, 2) - assert_equal '3 Bytes', number_to_human_size(3.14159265, 4) + assert_equal '1.18 MB', number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4) assert_equal("123 Bytes", number_to_human_size("123")) - assert_equal '1.01 KB', number_to_human_size(1.0123.kilobytes, 2) - assert_equal '1.01 KB', number_to_human_size(1.0100.kilobytes, 4) - assert_equal '10 KB', number_to_human_size(10.000.kilobytes, 4) + assert_equal '1.01 KB', number_to_human_size(1.0123.kilobytes, :precision => 2) + assert_equal '1.01 KB', number_to_human_size(1.0100.kilobytes, :precision => 4) + assert_equal '10 KB', number_to_human_size(10.000.kilobytes, :precision => 4) assert_equal '1 Byte', number_to_human_size(1.1) assert_equal '10 Bytes', number_to_human_size(10) - assert_nil number_to_human_size('x') + #assert_nil number_to_human_size('x') # fails due to API consolidation assert_nil number_to_human_size(nil) end @@ -120,4 +118,10 @@ class NumberHelperTest < ActionView::TestCase assert_equal '1.01 KB', number_to_human_size(1.0100.kilobytes, :precision => 4) assert_equal '10 KB', number_to_human_size(10.000.kilobytes, :precision => 4) end + + def test_number_to_human_size_with_custom_delimiter_and_separator + assert_equal '1,01 KB', number_to_human_size(1.0123.kilobytes, :precision => 2, :separator => ',') + assert_equal '1,01 KB', number_to_human_size(1.0100.kilobytes, :precision => 4, :separator => ',') + assert_equal '1.000,1 TB', number_to_human_size(1000.1.terabytes, :delimiter => '.', :separator => ',') + end end -- cgit v1.2.3 From a065b764e5beace51c2fca305d1ed5b8aa46b762 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 29 Jul 2008 22:04:07 -0500 Subject: Reapply 'cab168ac' because it was accidentally patched over in '10d9fe4b' --- actionpack/lib/action_view/helpers/text_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 3c9f7230c3..022edf23c8 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -558,7 +558,7 @@ module ActionView [-\w]+ # subdomain or domain (?:\.[-\w]+)* # remaining subdomains or domain (?::\d+)? # port - (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))+)?)* # path + (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$])))*)* # path (?:\?[\w\+@%&=.;-]+)? # query string (?:\#[\w\-]*)? # trailing anchor ) -- cgit v1.2.3 From c8e80f6389b45134c0514dde6736488cf5507765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 30 Jul 2008 01:41:51 -0700 Subject: Initializer skips prepare_dispatcher if Action Controller isn't in use. [#721 state:resolved] --- railties/lib/initializer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 782fbebec2..8a7461abf5 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -523,6 +523,7 @@ Run `rake gems:install` to install the missing gems. end def prepare_dispatcher + return unless configuration.frameworks.include?(:action_controller) require 'dispatcher' unless defined?(::Dispatcher) Dispatcher.define_dispatcher_callbacks(configuration.cache_classes) Dispatcher.new(RAILS_DEFAULT_LOGGER).send :run_callbacks, :prepare_dispatch -- cgit v1.2.3 From eaab895f83276674891227c656df9b4cebc50200 Mon Sep 17 00:00:00 2001 From: miloops Date: Tue, 22 Jul 2008 11:13:38 -0300 Subject: Prototype helpers should generate Element.insert instead of Insertion.new, which has been deprecated in Prototype 1.6. --- .../assertions/selector_assertions.rb | 26 +++++++++++++--------- .../lib/action_view/helpers/prototype_helper.rb | 12 +++++----- actionpack/test/template/prototype_helper_test.rb | 12 +++++----- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/actionpack/lib/action_controller/assertions/selector_assertions.rb b/actionpack/lib/action_controller/assertions/selector_assertions.rb index 70b0ed53e7..9114894b1d 100644 --- a/actionpack/lib/action_controller/assertions/selector_assertions.rb +++ b/actionpack/lib/action_controller/assertions/selector_assertions.rb @@ -407,6 +407,7 @@ module ActionController if rjs_type == :insert arg = args.shift + position = arg insertion = "insert_#{arg}".to_sym raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion] statement = "(#{RJS_STATEMENTS[insertion]})" @@ -418,6 +419,7 @@ module ActionController else statement = "#{RJS_STATEMENTS[:any]}" end + position ||= Regexp.new(RJS_INSERTIONS.join('|')) # Next argument we're looking for is the element identifier. If missing, we pick # any element. @@ -434,9 +436,14 @@ module ActionController Regexp.new("\\$\\(\"#{id}\"\\)#{statement}\\(#{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) when :remove, :show, :hide, :toggle Regexp.new("#{statement}\\(\"#{id}\"\\)") - else - Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) - end + when :replace, :replace_html + Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)") + when :insert, :insert_html + Regexp.new("Element.insert\\(\\\"#{id}\\\", \\{ #{position}: #{RJS_PATTERN_HTML} \\}\\);") + else + Regexp.union(Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)"), + Regexp.new("Element.insert\\(\\\"#{id}\\\", \\{ #{position}: #{RJS_PATTERN_HTML} \\}\\);")) + end # Duplicate the body since the next step involves destroying it. matches = nil @@ -445,7 +452,7 @@ module ActionController matches = @response.body.match(pattern) else @response.body.gsub(pattern) do |match| - html = unescape_rjs($2) + html = unescape_rjs(match) matches ||= [] matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } "" @@ -585,17 +592,16 @@ module ActionController :hide => /Element\.hide/, :toggle => /Element\.toggle/ } + RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") + RJS_PATTERN_HTML = /"((\\"|[^"])*)"/ RJS_INSERTIONS = [:top, :bottom, :before, :after] RJS_INSERTIONS.each do |insertion| - RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}")) + RJS_STATEMENTS["insert_#{insertion}".to_sym] = /Element.insert\(\"([^\"]*)\", \{ #{insertion.to_s.downcase}: #{RJS_PATTERN_HTML} \}\);/ end - RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do |insertion| - Regexp.quote("new Insertion.#{insertion.to_s.camelize}") + /Element.insert\(\"([^\"]*)\", \{ #{insertion.to_s.downcase}: #{RJS_PATTERN_HTML} \}\);/ end.join('|')) - RJS_PATTERN_HTML = /"((\\"|[^"])*)"/ - RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)", - Regexp::MULTILINE) + RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/ end diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index cb4b53a9f7..5194f00382 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -608,7 +608,7 @@ module ActionView # Example: # # # Generates: - # # new Insertion.Bottom("list", "
  • Some item
  • "); + # # new Element.insert("list", { bottom:
  • Some item
  • " }); # # new Effect.Highlight("list"); # # ["status-indicator", "cancel-link"].each(Element.hide); # update_page do |page| @@ -741,16 +741,16 @@ module ActionView # # # Insert the rendered 'navigation' partial just before the DOM # # element with ID 'content'. - # # Generates: new Insertion.Before("content", "-- Contents of 'navigation' partial --"); + # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" }); # page.insert_html :before, 'content', :partial => 'navigation' # # # Add a list item to the bottom of the
      with ID 'list'. - # # Generates: new Insertion.Bottom("list", "
    • Last item
    • "); + # # Generates: Element.insert("list", { bottom: "
    • Last item
    • " }); # page.insert_html :bottom, 'list', '
    • Last item
    • ' # def insert_html(position, id, *options_for_render) - insertion = position.to_s.camelize - call "new Insertion.#{insertion}", id, render(*options_for_render) + content = javascript_object_for(render(*options_for_render)) + record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });" end # Replaces the inner HTML of the DOM element with the given +id+. @@ -1054,7 +1054,7 @@ module ActionView js_options['asynchronous'] = options[:type] != :synchronous js_options['method'] = method_option_to_s(options[:method]) if options[:method] - js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position] + js_options['insertion'] = options[:position].to_s.downcase if options[:position] js_options['evalScripts'] = options[:script].nil? || options[:script] if options[:form] diff --git a/actionpack/test/template/prototype_helper_test.rb b/actionpack/test/template/prototype_helper_test.rb index 92cc85703b..eb3517ef91 100644 --- a/actionpack/test/template/prototype_helper_test.rb +++ b/actionpack/test/template/prototype_helper_test.rb @@ -287,13 +287,13 @@ class JavaScriptGeneratorTest < PrototypeHelperBaseTest end def test_insert_html_with_string - assert_equal 'new Insertion.Top("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");', + assert_equal 'Element.insert("element", { top: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });', @generator.insert_html(:top, 'element', '

      This is a test

      ') - assert_equal 'new Insertion.Bottom("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");', + assert_equal 'Element.insert("element", { bottom: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });', @generator.insert_html(:bottom, 'element', '

      This is a test

      ') - assert_equal 'new Insertion.Before("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");', + assert_equal 'Element.insert("element", { before: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });', @generator.insert_html(:before, 'element', '

      This is a test

      ') - assert_equal 'new Insertion.After("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");', + assert_equal 'Element.insert("element", { after: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });', @generator.insert_html(:after, 'element', '

      This is a test

      ') end @@ -366,8 +366,8 @@ class JavaScriptGeneratorTest < PrototypeHelperBaseTest @generator.replace_html('baz', '

      This is a test

      ') assert_equal <<-EOS.chomp, @generator.to_s -new Insertion.Top("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); -new Insertion.Bottom("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); +Element.insert("element", { top: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" }); +Element.insert("element", { bottom: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" }); ["foo", "bar"].each(Element.remove); Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); EOS -- cgit v1.2.3 From c4038764d2b4c05178cceb22066e0ece59fe49d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 30 Jul 2008 01:49:49 -0700 Subject: Initializer requires ERB explicitly instead of assuming Action Pack loaded it. [#722 state:resolved] --- railties/lib/initializer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 8a7461abf5..44c24e85f9 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -767,6 +767,7 @@ Run `rake gems:install` to install the missing gems. # contents of the file are processed via ERB before being sent through # YAML::load. def database_configuration + require 'erb' YAML::load(ERB.new(IO.read(database_configuration_file)).result) end -- cgit v1.2.3 From 2617d0dc5ced4b354bff9633bddafdf80ad5a711 Mon Sep 17 00:00:00 2001 From: miloops Date: Tue, 29 Jul 2008 23:14:56 -0300 Subject: Performance: grouping helpers should use yield instead of block as argument. [#723 state:resolved] --- activesupport/lib/active_support/core_ext/array/grouping.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index df37afb053..dd1484f8fa 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -19,7 +19,7 @@ module ActiveSupport #:nodoc: # %w(1 2 3).in_groups_of(2, false) {|g| p g} # ["1", "2"] # ["3"] - def in_groups_of(number, fill_with = nil, &block) + def in_groups_of(number, fill_with = nil) if fill_with == false collection = self else @@ -31,7 +31,7 @@ module ActiveSupport #:nodoc: end if block_given? - collection.each_slice(number, &block) + collection.each_slice(number) { |slice| yield(slice) } else returning [] do |groups| collection.each_slice(number) { |group| groups << group } @@ -87,11 +87,11 @@ module ActiveSupport #:nodoc: # # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] - def split(value = nil, &block) - block ||= Proc.new { |e| e == value } + def split(value = nil) + using_block = block_given? inject([[]]) do |results, element| - if block.call(element) + if (using_block && yield(element)) || (value == element) results << [] else results.last << element -- cgit v1.2.3 From f64bd2ca85595f94cbbe809f51a52cdb9b68af19 Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Thu, 31 Jul 2008 09:45:17 +0200 Subject: Ensure dbconsole includes the -p parameter to mysql as intended --- railties/lib/commands/dbconsole.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/railties/lib/commands/dbconsole.rb b/railties/lib/commands/dbconsole.rb index 17acb7b68f..442526ae32 100644 --- a/railties/lib/commands/dbconsole.rb +++ b/railties/lib/commands/dbconsole.rb @@ -41,6 +41,8 @@ when "mysql" if config['password'] && include_password args << "--password=#{config['password']}" + elsif config['password'] && !config['password'].empty? + args << "-p" end args << config['database'] -- cgit v1.2.3 From 108db00aa90fe266564483ab301cf0669cae600f Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Thu, 31 Jul 2008 15:56:46 +0100 Subject: Raise UnknownAttributeError when unknown attributes are supplied via mass assignment --- activerecord/lib/active_record/base.rb | 10 +++++++++- activerecord/test/cases/base_test.rb | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 9cb64223e2..29c2995334 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -122,6 +122,10 @@ module ActiveRecord #:nodoc: class MissingAttributeError < NoMethodError end + # Raised when unknown attributes are supplied via mass assignment. + class UnknownAttributeError < NoMethodError + end + # Raised when an error occurred while doing a mass assignment to an attribute through the # attributes= method. The exception has an +attribute+ property that is the name of the # offending attribute. @@ -2400,7 +2404,11 @@ module ActiveRecord #:nodoc: attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes attributes.each do |k, v| - k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v) + if k.include?("(") + multi_parameter_attributes << [ k, v ] + else + respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") + end end assign_multiparameter_attributes(multi_parameter_attributes) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 9e4f268db7..e6d1b5ddfd 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -904,6 +904,14 @@ class BasicsTest < ActiveRecord::TestCase assert_nil keyboard.id end + def test_mass_assigning_invalid_attribute + firm = Firm.new + + assert_raises(ActiveRecord::UnknownAttributeError) do + firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 } + end + end + def test_mass_assignment_protection_on_defaults firm = Firm.new firm.attributes = { "id" => 5, "type" => "Client" } -- cgit v1.2.3 From fb5cc19707582fa61ca3e426697cc2b00e9e5ffa Mon Sep 17 00:00:00 2001 From: miloops Date: Thu, 31 Jul 2008 10:57:50 -0300 Subject: Fix HasManyThroughAssociationsTest tests. [#733 state:resolved] Signed-off-by: Pratik Naik --- .../cases/associations/has_many_through_associations_test.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index be5170f830..d51a3c7e1c 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -2,15 +2,18 @@ require "cases/helper" require 'models/post' require 'models/person' require 'models/reader' +require 'models/comment' class HasManyThroughAssociationsTest < ActiveRecord::TestCase - fixtures :posts, :readers, :people + fixtures :posts, :readers, :people, :comments def test_associate_existing assert_queries(2) { posts(:thinking);people(:david) } - + + posts(:thinking).people + assert_queries(1) do - posts(:thinking).people << people(:david) + posts(:thinking).people << people(:david) end assert_queries(1) do -- cgit v1.2.3 From 896a3b9ab8dc02639ffa0b1dbf85011e1f3eda9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Thu, 31 Jul 2008 19:52:35 +0300 Subject: Fixed test_joins_with_namespaced_model_should_use_correct_type for postgresql Signed-off-by: Michael Koziarski --- activerecord/test/cases/associations/has_many_associations_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index f8b8b1f96d..47e4b3527d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1007,7 +1007,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.create({ :name => 'Some Client' }) stats = Namespaced::Firm.find(firm.id, { - :select => "#{Namespaced::Firm.table_name}.*, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", + :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", :joins => :clients, :group => "#{Namespaced::Firm.table_name}.id" }) -- cgit v1.2.3 From 68b207b087588fc9a2cc8edb842408e3be5ba9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Thu, 31 Jul 2008 19:26:18 +0300 Subject: Cast value to string in validates_uniqueness_of if the column is of text type This fixes an error for postgresql where "text_column = 100" fails in version 8.3 Signed-off-by: Michael Koziarski --- activerecord/lib/active_record/validations.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index b957ee3b9e..52f674847e 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -625,7 +625,13 @@ module ActiveRecord # class (which has a database table to query from). finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?) + is_text_column = finder_class.columns_hash[attr_name.to_s].text? + + if !value.nil? && is_text_column + value = value.to_s + end + + if value.nil? || (configuration[:case_sensitive] || !is_text_column) condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" condition_params = [value] else -- cgit v1.2.3 From 030d5854adcf35e6620d667a93a922f5d91725d8 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Thu, 31 Jul 2008 13:42:28 -0500 Subject: Turn cache_classes on by default [#645 state:resolved] --- railties/CHANGELOG | 2 ++ railties/lib/initializer.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 5ff1867568..6df7c568dc 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Turn cache_classes on by default [Josh Peek] + * Added configurable eager load paths. Defaults to app/models, app/controllers, and app/helpers [Josh Peek] * Introduce simple internationalization support. [Ruby i18n team] diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 44c24e85f9..88341b9d73 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -903,7 +903,7 @@ Run `rake gems:install` to install the missing gems. end def default_cache_classes - false + true end def default_whiny_nils -- cgit v1.2.3 From cb68b21a52a12b0773f0b4dac1c9c673c93ba355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Thu, 31 Jul 2008 21:51:50 +0300 Subject: Fixed negative default integer parsing for Postgresql 8.3.3 Signed-off-by: Michael Koziarski --- .../lib/active_record/connection_adapters/postgresql_adapter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6a20f41a4b..856435517a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -182,8 +182,8 @@ module ActiveRecord def self.extract_value_from_default(default) case default # Numeric types - when /\A-?\d+(\.\d*)?\z/ - default + when /\A\(?(-?\d+(\.\d*)?\)?)\z/ + $1 # Character types when /\A'(.*)'::(?:character varying|bpchar|text)\z/m $1 -- cgit v1.2.3 From f3da46effae9f23f6ea473f7296e9c0c9bb49fce Mon Sep 17 00:00:00 2001 From: miloops Date: Thu, 31 Jul 2008 16:09:18 -0300 Subject: In javascript helpers option[:type] = :synchronous should work as described in docs. Signed-off-by: Michael Koziarski --- actionpack/lib/action_view/helpers/prototype_helper.rb | 2 +- actionpack/test/template/prototype_helper_test.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 5194f00382..4c3a8311a5 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -111,7 +111,7 @@ module ActionView (100..599).to_a) AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, :asynchronous, :method, :insertion, :position, - :form, :with, :update, :script ]).merge(CALLBACKS) + :form, :with, :update, :script, :type ]).merge(CALLBACKS) end # Returns a link to a remote action defined by options[:url] diff --git a/actionpack/test/template/prototype_helper_test.rb b/actionpack/test/template/prototype_helper_test.rb index eb3517ef91..abc9f930dd 100644 --- a/actionpack/test/template/prototype_helper_test.rb +++ b/actionpack/test/template/prototype_helper_test.rb @@ -77,6 +77,8 @@ class PrototypeHelperTest < PrototypeHelperBaseTest link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot" }) assert_dom_equal %(Remote outauthor), link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot", :a => '10', :b => '20' }) + assert_dom_equal %(Remote outauthor), + link_to_remote("Remote outauthor", :url => { :action => "whatnot" }, :type => :synchronous) end def test_link_to_remote_html_options @@ -429,6 +431,8 @@ Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); def test_sortable assert_equal %(Sortable.create("blah", {onUpdate:function(){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("blah")})}});), @generator.sortable('blah', :url => { :action => "order" }) + assert_equal %(Sortable.create("blah", {onUpdate:function(){new Ajax.Request('http://www.example.com/order', {asynchronous:false, evalScripts:true, parameters:Sortable.serialize("blah")})}});), + @generator.sortable('blah', :url => { :action => "order" }, :type => :synchronous) end def test_draggable @@ -439,6 +443,8 @@ Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); def test_drop_receiving assert_equal %(Droppables.add("blah", {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});), @generator.drop_receiving('blah', :url => { :action => "order" }) + assert_equal %(Droppables.add("blah", {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:false, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});), + @generator.drop_receiving('blah', :url => { :action => "order" }, :type => :synchronous) end def test_collection_first_and_last -- cgit v1.2.3 From f7eaab96d332528253d14581f747dd4b1b378a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Thu, 31 Jul 2008 22:18:14 +0300 Subject: validates_uniqueness_of uses database case sensitivity support instead of using ruby Signed-off-by: Michael Koziarski --- .../abstract/database_statements.rb | 4 +++ .../connection_adapters/mysql_adapter.rb | 4 +++ activerecord/lib/active_record/validations.rb | 42 ++++++++-------------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 5358491cde..aaf9e2e73f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -149,6 +149,10 @@ module ActiveRecord "INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)" end + def case_sensitive_equality_operator + "=" + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 35b9ed4746..204ebaa2e2 100755 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -511,6 +511,10 @@ module ActiveRecord keys.length == 1 ? [keys.first, nil] : nil end + def case_sensitive_equality_operator + "= BINARY" + end + private def connect @connection.reconnect = true if @connection.respond_to?(:reconnect=) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 52f674847e..e7a9676394 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -627,17 +627,23 @@ module ActiveRecord is_text_column = finder_class.columns_hash[attr_name.to_s].text? - if !value.nil? && is_text_column - value = value.to_s + if value.nil? + comparison_operator = "IS ?" + else + comparison_operator = "#{connection.case_sensitive_equality_operator} ?" + + if is_text_column + value = value.to_s + end end + sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}" + if value.nil? || (configuration[:case_sensitive] || !is_text_column) - condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" + condition_sql = "#{sql_attribute} #{comparison_operator}" condition_params = [value] else - # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't. - # Hence, this is needed only for sqlite. - condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}" + condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" condition_params = [value.downcase] end @@ -654,28 +660,10 @@ module ActiveRecord condition_params << record.send(:id) end - results = finder_class.with_exclusive_scope do - connection.select_all( - construct_finder_sql( - :select => "#{connection.quote_column_name(attr_name)}", - :from => "#{finder_class.quoted_table_name}", - :conditions => [condition_sql, *condition_params] - ) - ) - end - - unless results.length.zero? - found = true - - # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate - # column in ruby when case sensitive option - if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? - found = results.any? { |a| a[attr_name.to_s] == value.to_s } - end - - if found + finder_class.with_exclusive_scope do + if finder_class.exists?([condition_sql, *condition_params]) message = record.errors.generate_message(attr_name, :taken, :default => configuration[:message]) - record.errors.add(attr_name, message) + record.errors.add(attr_name, message) end end end -- cgit v1.2.3 From 656f0e7c6c9a305abaf9f9b7fb80479b6f94efce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Thu, 31 Jul 2008 16:36:23 -0500 Subject: Fix file permissions Signed-off-by: Joshua Peek --- actionmailer/README | 0 actionmailer/Rakefile | 0 actionmailer/lib/action_mailer.rb | 0 actionmailer/test/mail_service_test.rb | 0 actionpack/lib/action_controller.rb | 0 actionpack/lib/action_controller/base.rb | 0 actionpack/lib/action_controller/request.rb | 0 actionpack/lib/action_controller/request_profiler.rb | 0 actionpack/lib/action_controller/response.rb | 0 actionpack/lib/action_controller/session/drb_server.rb | 0 actionpack/lib/action_view/helpers/date_helper.rb | 0 actionpack/test/controller/cgi_test.rb | 0 actionpack/test/controller/redirect_test.rb | 0 actionpack/test/controller/session/cookie_store_test.rb | 0 actionpack/test/template/date_helper_test.rb | 0 activemodel/Rakefile | 0 activerecord/README | 0 activerecord/Rakefile | 0 activerecord/lib/active_record.rb | 0 activerecord/lib/active_record/associations.rb | 0 activerecord/lib/active_record/associations/belongs_to_association.rb | 0 .../lib/active_record/associations/belongs_to_polymorphic_association.rb | 0 activerecord/lib/active_record/associations/has_one_association.rb | 0 activerecord/lib/active_record/base.rb | 0 activerecord/lib/active_record/callbacks.rb | 0 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb | 0 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb | 0 activerecord/lib/active_record/fixtures.rb | 0 activerecord/lib/active_record/validations.rb | 0 activerecord/test/cases/associations/belongs_to_associations_test.rb | 0 activerecord/test/cases/associations/has_one_associations_test.rb | 0 activerecord/test/cases/associations_test.rb | 0 activerecord/test/cases/attribute_methods_test.rb | 0 activerecord/test/cases/base_test.rb | 0 activerecord/test/cases/deprecated_finder_test.rb | 0 activerecord/test/cases/fixtures_test.rb | 0 activerecord/test/cases/inheritance_test.rb | 0 activerecord/test/cases/lifecycle_test.rb | 0 activerecord/test/cases/readonly_test.rb | 0 activerecord/test/cases/unconnected_test.rb | 0 activerecord/test/cases/validations_test.rb | 0 activerecord/test/models/company.rb | 0 activerecord/test/models/reply.rb | 0 activerecord/test/models/topic.rb | 0 activesupport/lib/active_support/multibyte/generators/generate_tables.rb | 0 railties/bin/about | 0 railties/bin/console | 0 railties/bin/destroy | 0 railties/bin/generate | 0 railties/bin/performance/benchmarker | 0 railties/bin/performance/profiler | 0 railties/bin/performance/request | 0 railties/bin/plugin | 0 railties/bin/process/inspector | 0 railties/bin/process/reaper | 0 railties/bin/process/spawner | 0 railties/bin/runner | 0 railties/bin/server | 0 railties/configs/apache.conf | 0 railties/dispatches/gateway.cgi | 0 railties/lib/commands/ncgi/listener | 0 railties/lib/commands/ncgi/tracker | 0 .../lib/rails_generator/generators/components/plugin/templates/Rakefile | 0 63 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 actionmailer/README mode change 100755 => 100644 actionmailer/Rakefile mode change 100755 => 100644 actionmailer/lib/action_mailer.rb mode change 100755 => 100644 actionmailer/test/mail_service_test.rb mode change 100755 => 100644 actionpack/lib/action_controller.rb mode change 100755 => 100644 actionpack/lib/action_controller/base.rb mode change 100755 => 100644 actionpack/lib/action_controller/request.rb mode change 100755 => 100644 actionpack/lib/action_controller/request_profiler.rb mode change 100755 => 100644 actionpack/lib/action_controller/response.rb mode change 100644 => 100755 actionpack/lib/action_controller/session/drb_server.rb mode change 100755 => 100644 actionpack/lib/action_view/helpers/date_helper.rb mode change 100755 => 100644 actionpack/test/controller/cgi_test.rb mode change 100755 => 100644 actionpack/test/controller/redirect_test.rb mode change 100755 => 100644 actionpack/test/controller/session/cookie_store_test.rb mode change 100755 => 100644 actionpack/test/template/date_helper_test.rb mode change 100644 => 100755 activemodel/Rakefile mode change 100755 => 100644 activerecord/README mode change 100755 => 100644 activerecord/Rakefile mode change 100755 => 100644 activerecord/lib/active_record.rb mode change 100755 => 100644 activerecord/lib/active_record/associations.rb mode change 100755 => 100644 activerecord/lib/active_record/associations/belongs_to_association.rb mode change 100755 => 100644 activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb mode change 100755 => 100644 activerecord/lib/active_record/associations/has_one_association.rb mode change 100755 => 100644 activerecord/lib/active_record/base.rb mode change 100755 => 100644 activerecord/lib/active_record/callbacks.rb mode change 100755 => 100644 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb mode change 100755 => 100644 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb mode change 100755 => 100644 activerecord/lib/active_record/fixtures.rb mode change 100755 => 100644 activerecord/lib/active_record/validations.rb mode change 100755 => 100644 activerecord/test/cases/associations/belongs_to_associations_test.rb mode change 100755 => 100644 activerecord/test/cases/associations/has_one_associations_test.rb mode change 100755 => 100644 activerecord/test/cases/associations_test.rb mode change 100755 => 100644 activerecord/test/cases/attribute_methods_test.rb mode change 100755 => 100644 activerecord/test/cases/base_test.rb mode change 100755 => 100644 activerecord/test/cases/deprecated_finder_test.rb mode change 100755 => 100644 activerecord/test/cases/fixtures_test.rb mode change 100755 => 100644 activerecord/test/cases/inheritance_test.rb mode change 100755 => 100644 activerecord/test/cases/lifecycle_test.rb mode change 100755 => 100644 activerecord/test/cases/readonly_test.rb mode change 100755 => 100644 activerecord/test/cases/unconnected_test.rb mode change 100755 => 100644 activerecord/test/cases/validations_test.rb mode change 100755 => 100644 activerecord/test/models/company.rb mode change 100755 => 100644 activerecord/test/models/reply.rb mode change 100755 => 100644 activerecord/test/models/topic.rb mode change 100644 => 100755 activesupport/lib/active_support/multibyte/generators/generate_tables.rb mode change 100644 => 100755 railties/bin/about mode change 100644 => 100755 railties/bin/console mode change 100644 => 100755 railties/bin/destroy mode change 100644 => 100755 railties/bin/generate mode change 100644 => 100755 railties/bin/performance/benchmarker mode change 100644 => 100755 railties/bin/performance/profiler mode change 100644 => 100755 railties/bin/performance/request mode change 100644 => 100755 railties/bin/plugin mode change 100644 => 100755 railties/bin/process/inspector mode change 100644 => 100755 railties/bin/process/reaper mode change 100644 => 100755 railties/bin/process/spawner mode change 100644 => 100755 railties/bin/runner mode change 100644 => 100755 railties/bin/server mode change 100755 => 100644 railties/configs/apache.conf mode change 100644 => 100755 railties/dispatches/gateway.cgi mode change 100644 => 100755 railties/lib/commands/ncgi/listener mode change 100644 => 100755 railties/lib/commands/ncgi/tracker mode change 100755 => 100644 railties/lib/rails_generator/generators/components/plugin/templates/Rakefile diff --git a/actionmailer/README b/actionmailer/README old mode 100755 new mode 100644 diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile old mode 100755 new mode 100644 diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb old mode 100755 new mode 100644 diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb old mode 100755 new mode 100644 diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb old mode 100755 new mode 100644 diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb old mode 100755 new mode 100644 diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb old mode 100755 new mode 100644 diff --git a/actionpack/lib/action_controller/request_profiler.rb b/actionpack/lib/action_controller/request_profiler.rb old mode 100755 new mode 100644 diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb old mode 100755 new mode 100644 diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb old mode 100644 new mode 100755 diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb old mode 100755 new mode 100644 diff --git a/actionpack/test/controller/cgi_test.rb b/actionpack/test/controller/cgi_test.rb old mode 100755 new mode 100644 diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb old mode 100755 new mode 100644 diff --git a/actionpack/test/controller/session/cookie_store_test.rb b/actionpack/test/controller/session/cookie_store_test.rb old mode 100755 new mode 100644 diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb old mode 100755 new mode 100644 diff --git a/activemodel/Rakefile b/activemodel/Rakefile old mode 100644 new mode 100755 diff --git a/activerecord/README b/activerecord/README old mode 100755 new mode 100644 diff --git a/activerecord/Rakefile b/activerecord/Rakefile old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb old mode 100755 new mode 100644 diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/deprecated_finder_test.rb b/activerecord/test/cases/deprecated_finder_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb old mode 100755 new mode 100644 diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb old mode 100755 new mode 100644 diff --git a/activesupport/lib/active_support/multibyte/generators/generate_tables.rb b/activesupport/lib/active_support/multibyte/generators/generate_tables.rb old mode 100644 new mode 100755 diff --git a/railties/bin/about b/railties/bin/about old mode 100644 new mode 100755 diff --git a/railties/bin/console b/railties/bin/console old mode 100644 new mode 100755 diff --git a/railties/bin/destroy b/railties/bin/destroy old mode 100644 new mode 100755 diff --git a/railties/bin/generate b/railties/bin/generate old mode 100644 new mode 100755 diff --git a/railties/bin/performance/benchmarker b/railties/bin/performance/benchmarker old mode 100644 new mode 100755 diff --git a/railties/bin/performance/profiler b/railties/bin/performance/profiler old mode 100644 new mode 100755 diff --git a/railties/bin/performance/request b/railties/bin/performance/request old mode 100644 new mode 100755 diff --git a/railties/bin/plugin b/railties/bin/plugin old mode 100644 new mode 100755 diff --git a/railties/bin/process/inspector b/railties/bin/process/inspector old mode 100644 new mode 100755 diff --git a/railties/bin/process/reaper b/railties/bin/process/reaper old mode 100644 new mode 100755 diff --git a/railties/bin/process/spawner b/railties/bin/process/spawner old mode 100644 new mode 100755 diff --git a/railties/bin/runner b/railties/bin/runner old mode 100644 new mode 100755 diff --git a/railties/bin/server b/railties/bin/server old mode 100644 new mode 100755 diff --git a/railties/configs/apache.conf b/railties/configs/apache.conf old mode 100755 new mode 100644 diff --git a/railties/dispatches/gateway.cgi b/railties/dispatches/gateway.cgi old mode 100644 new mode 100755 diff --git a/railties/lib/commands/ncgi/listener b/railties/lib/commands/ncgi/listener old mode 100644 new mode 100755 diff --git a/railties/lib/commands/ncgi/tracker b/railties/lib/commands/ncgi/tracker old mode 100644 new mode 100755 diff --git a/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile b/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile old mode 100755 new mode 100644 -- cgit v1.2.3 From 0b9bfbdebf402f4a149359a069dbeb05ea989b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Thu, 31 Jul 2008 16:39:48 -0500 Subject: Use "/usr/bin/env ruby" instead of "/usr/local/bin/ruby" Signed-off-by: Joshua Peek --- actionpack/lib/action_controller/session/drb_server.rb | 14 +++++++------- railties/dispatches/dispatch.fcgi | 2 +- railties/dispatches/dispatch.rb | 4 ++-- railties/dispatches/gateway.cgi | 4 ++-- railties/lib/commands/ncgi/listener | 4 ++-- railties/lib/commands/ncgi/tracker | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb index 6f90db6747..2caa27f62a 100755 --- a/actionpack/lib/action_controller/session/drb_server.rb +++ b/actionpack/lib/action_controller/session/drb_server.rb @@ -1,8 +1,8 @@ -#!/usr/local/bin/ruby -w - -# This is a really simple session storage daemon, basically just a hash, +#!/usr/bin/env ruby + +# This is a really simple session storage daemon, basically just a hash, # which is enabled for DRb access. - + require 'drb' session_hash = Hash.new @@ -14,13 +14,13 @@ class < Date: Thu, 31 Jul 2008 16:35:17 -0700 Subject: load_application_classes requires files relative to the load path and without .rb extension, including .rb files in subdirectories --- railties/lib/initializer.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 44c24e85f9..611e348acb 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -349,8 +349,9 @@ Run `rake gems:install` to install the missing gems. def load_application_classes if configuration.cache_classes configuration.eager_load_paths.each do |load_path| - Dir.glob("#{load_path}/*.rb").each do |file| - require_dependency file + matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/ + Dir.glob("#{load_path}/**/*.rb").each do |file| + require_dependency file.sub(matcher, '\1') end end end -- cgit v1.2.3 From ad4553587efb8e758349b3f163e365781e4c6ad1 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Thu, 31 Jul 2008 17:50:53 -0700 Subject: List available actions in UnknownAction exception message --- actionpack/lib/action_controller/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index ac10a956f3..5689a9825e 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1208,7 +1208,7 @@ module ActionController #:nodoc: elsif template_exists? && template_public? default_render else - raise UnknownAction, "No action responded to #{action_name}", caller + raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.to_a.sort.to_sentence}", caller end end -- cgit v1.2.3 From 909a7f430ba37cd7a54ae11a7af529bd949fa31c Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Thu, 31 Jul 2008 17:51:43 -0700 Subject: Ensure mailer view path is loaded when it's assigned. Path#[] raises if it isn't loaded. --- actionmailer/lib/action_mailer/base.rb | 2 +- actionpack/lib/action_view/paths.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index a43296461b..88d34a58d5 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -427,7 +427,7 @@ module ActionMailer #:nodoc: def template_root=(root) root = ActionView::PathSet::Path.new(root) if root.is_a?(String) - write_inheritable_attribute(:template_root, root) + write_inheritable_attribute(:template_root, root.load) end end diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index a37706faee..d97f963540 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -42,6 +42,7 @@ module ActionView #:nodoc: end def [](path) + raise "Unloaded view path! #{@path}" unless @loaded @paths[path] end @@ -51,6 +52,7 @@ module ActionView #:nodoc: def load reload! unless loaded? + self end # Rebuild load path directory cache -- cgit v1.2.3 From cb21db1a334e6ca2695d4e7183b1bdce204b9eb3 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Thu, 31 Jul 2008 20:09:10 -0500 Subject: Treat ActionMailer template_root as a view path set internally to avoid inheritance and dupping issues --- actionmailer/lib/action_mailer/base.rb | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 88d34a58d5..fa29ae2446 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -250,7 +250,7 @@ module ActionMailer #:nodoc: private_class_method :new #:nodoc: - class_inheritable_accessor :template_root + class_inheritable_accessor :view_paths cattr_accessor :logger cattr_accessor :template_extensions @@ -425,9 +425,12 @@ module ActionMailer #:nodoc: template_extensions << extension end + def template_root + self.view_paths && self.view_paths.first + end + def template_root=(root) - root = ActionView::PathSet::Path.new(root) if root.is_a?(String) - write_inheritable_attribute(:template_root, root.load) + self.view_paths = ActionView::Base.process_view_paths(root) end end @@ -541,12 +544,20 @@ module ActionMailer #:nodoc: initialize_template_class(body).render(opts) end + def template_root + self.class.template_root + end + + def template_root=(root) + self.class.template_root = root + end + def template_path "#{template_root}/#{mailer_name}" end def initialize_template_class(assigns) - ActionView::Base.new(template_root, assigns, self) + ActionView::Base.new(view_paths, assigns, self) end def sort_parts(parts, order = []) -- cgit v1.2.3 From 82343859d568799a4151facbde1f8c711ecb7a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Thu, 31 Jul 2008 23:59:53 +0300 Subject: Added missing fixtures for tests which fail to run independently if run after schema reset Signed-off-by: Michael Koziarski --- activerecord/test/cases/associations/cascaded_eager_loading_test.rb | 2 +- activerecord/test/cases/associations/eager_test.rb | 2 +- .../cases/associations/has_and_belongs_to_many_associations_test.rb | 2 +- activerecord/test/cases/associations/has_many_associations_test.rb | 2 +- activerecord/test/cases/base_test.rb | 2 +- activerecord/test/cases/lifecycle_test.rb | 2 +- activerecord/test/cases/method_scoping_test.rb | 2 +- activerecord/test/cases/query_cache_test.rb | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 3631be76a0..1f8a1090eb 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -9,7 +9,7 @@ require 'models/topic' require 'models/reply' class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :authors, :mixins, :companies, :posts, :topics + fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations def test_eager_association_loading_with_cascaded_two_levels authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index f65ada550b..58506574f8 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -21,7 +21,7 @@ class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, - :developers, :projects + :developers, :projects, :developers_projects def test_loading_with_one_association posts = Post.find(:all, :include => :comments) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index b29df68d22..1ed0522e53 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -70,7 +70,7 @@ end class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, - :parrots, :pirates, :treasures, :price_estimates + :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings def test_has_and_belongs_to_many david = Developer.find(1) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 47e4b3527d..b806e885e1 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -14,7 +14,7 @@ require 'models/reader' class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, :author_addresses, - :people, :posts + :people, :posts, :readers def setup Client.destroyed_client_ids.clear diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index e6d1b5ddfd..0f9eda4d09 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -76,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base end class BasicsTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories def test_table_exists assert !NonExistentTable.table_exists? diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index ab005c6b00..54fb3d8c39 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -74,7 +74,7 @@ class MultiObserver < ActiveRecord::Observer end class LifecycleTest < ActiveRecord::TestCase - fixtures :topics, :developers + fixtures :topics, :developers, :minimalistics def test_before_destroy original_count = Topic.count diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index d6b3e341df..ee66ac948d 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -6,7 +6,7 @@ require 'models/post' require 'models/category' class MethodScopingTest < ActiveRecord::TestCase - fixtures :developers, :projects, :comments, :posts + fixtures :developers, :projects, :comments, :posts, :developers_projects def test_set_conditions Developer.with_scope(:find => { :conditions => 'just a test...' }) do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index dc9eeec281..eae2104531 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -58,7 +58,7 @@ end uses_mocha 'QueryCacheExpiryTest' do class QueryCacheExpiryTest < ActiveRecord::TestCase - fixtures :tasks + fixtures :tasks, :posts, :categories, :categories_posts def test_find Task.connection.expects(:clear_query_cache).times(1) -- cgit v1.2.3 From 61842d97c5269af8a36a33115f50543a155f1608 Mon Sep 17 00:00:00 2001 From: Ben Sandofsky Date: Fri, 1 Aug 2008 17:01:10 -0700 Subject: Make requiring gems optional. Signed-off-by: Michael Koziarski [#743 state:resolved] --- railties/lib/initializer.rb | 6 +++++- railties/lib/rails/gem_dependency.rb | 2 +- railties/test/gem_dependency_test.rb | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index a2d08e2938..e876481cf1 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -688,13 +688,17 @@ Run `rake gems:install` to install the missing gems. # You can add gems with the #gem method. attr_accessor :gems - # Adds a single Gem dependency to the rails application. + # Adds a single Gem dependency to the rails application. By default, it will require + # the library with the same name as the gem. Use :lib to specify a different name. # # # gem 'aws-s3', '>= 0.4.0' # # require 'aws/s3' # config.gem 'aws-s3', :lib => 'aws/s3', :version => '>= 0.4.0', \ # :source => "http://code.whytheluckystiff.net" # + # To require a library be installed, but not attempt to load it, pass :lib => false + # + # config.gem 'qrp', :version => '0.4.1', :lib => false def gem(name, options = {}) @gems << Rails::GemDependency.new(name, options) end diff --git a/railties/lib/rails/gem_dependency.rb b/railties/lib/rails/gem_dependency.rb index f8d97840c1..471e03fa5f 100644 --- a/railties/lib/rails/gem_dependency.rb +++ b/railties/lib/rails/gem_dependency.rb @@ -58,7 +58,7 @@ module Rails def load return if @loaded || @load_paths_added == false - require(@lib || @name) + require(@lib || @name) unless @lib == false @loaded = true rescue LoadError puts $!.to_s diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb index b5946aa7b8..964ca50992 100644 --- a/railties/test/gem_dependency_test.rb +++ b/railties/test/gem_dependency_test.rb @@ -11,6 +11,7 @@ uses_mocha "Plugin Tests" do @gem_with_source = Rails::GemDependency.new "hpricot", :source => "http://code.whytheluckystiff.net" @gem_with_version = Rails::GemDependency.new "hpricot", :version => "= 0.6" @gem_with_lib = Rails::GemDependency.new "aws-s3", :lib => "aws/s3" + @gem_without_load = Rails::GemDependency.new "hpricot", :lib => false end def test_configuration_adds_gem_dependency @@ -62,5 +63,13 @@ uses_mocha "Plugin Tests" do @gem_with_lib.add_load_paths @gem_with_lib.load end + + def test_gem_without_lib_loading + @gem_without_load.expects(:gem).with(@gem_without_load.name) + @gem_without_load.expects(:require).with(@gem_without_load.lib).never + @gem_without_load.add_load_paths + @gem_without_load.load + end + end end -- cgit v1.2.3 From ddd552504bd682d64aa63bd06aa3f74818d48493 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 4 Aug 2008 18:37:53 -0700 Subject: Expose Routing::Segment::SAFE_PCHAR list of path characters that don't need escaping --- actionpack/lib/action_controller/routing/segments.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index 75784c3b78..9d4b740a44 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -2,7 +2,8 @@ module ActionController module Routing class Segment #:nodoc: RESERVED_PCHAR = ':@&=+$,;' - UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze + SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" + UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze # TODO: Convert :is_optional accessor to read only attr_accessor :is_optional -- cgit v1.2.3 From 177a35e711e3b21eac0eb19f03aeae7626e490f5 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 2 Aug 2008 00:42:32 -0500 Subject: Added config.threadsafe! to toggle allow concurrency settings and disable the dependency loader --- railties/CHANGELOG | 2 ++ railties/environments/production.rb | 3 +++ railties/lib/initializer.rb | 12 ++++++++++++ 3 files changed, 17 insertions(+) diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 6df7c568dc..3a276d5aad 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Added config.threadsafe! to toggle allow concurrency settings and disable the dependency loader [Josh Peek] + * Turn cache_classes on by default [Josh Peek] * Added configurable eager load paths. Defaults to app/models, app/controllers, and app/helpers [Josh Peek] diff --git a/railties/environments/production.rb b/railties/environments/production.rb index e915e8be73..ec5b7bc865 100644 --- a/railties/environments/production.rb +++ b/railties/environments/production.rb @@ -4,6 +4,9 @@ # Code is not reloaded between requests config.cache_classes = true +# Enable threaded mode +# config.threadsafe! + # Use a different logger for distributed setups # config.logger = SyslogLogger.new diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index e876481cf1..f8b3a78dff 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -768,6 +768,18 @@ Run `rake gems:install` to install the missing gems. ::RAILS_ROOT.replace @root_path end + # Enable threaded mode. Allows concurrent requests to controller actions and + # multiple database connections. Also disables automatic dependency loading + # after boot + def threadsafe! + self.cache_classes = true + self.dependency_loading = false + self.active_record.allow_concurrency = true + self.action_controller.allow_concurrency = true + self.to_prepare { Rails.cache.threadsafe! } + self + end + # Loads and returns the contents of the #database_configuration_file. The # contents of the file are processed via ERB before being sent through # YAML::load. -- cgit v1.2.3 From dc66469e6464bbb6d7bd6f242731395b6574aca2 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Mon, 4 Aug 2008 22:26:14 -0500 Subject: Fixed i18n bulk translate issues in NumberHelper Signed-off-by: Joshua Peek --- .../lib/action_view/helpers/number_helper.rb | 56 ++++++++++++---------- .../test/template/number_helper_i18n_test.rb | 26 +++++----- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 8c1dea2186..77f19b36a6 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -71,9 +71,9 @@ module ActionView def number_to_currency(number, options = {}) options.symbolize_keys! - defaults, currency = I18n.translate([:'number.format', :'number.currency.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(currency) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {} + defaults = defaults.merge(currency) precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] @@ -109,9 +109,9 @@ module ActionView def number_to_percentage(number, options = {}) options.symbolize_keys! - defaults, percentage = I18n.translate([:'number.format', :'number.percentage.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(percentage) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :raise => true) rescue {} + defaults = defaults.merge(percentage) precision = options[:precision] || defaults[:precision] separator = options[:separator] || defaults[:separator] @@ -151,7 +151,7 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale]) || {} + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} unless args.empty? ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' + @@ -195,9 +195,10 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults, precision_defaults = I18n.translate([:'number.format', :'number.precision.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(precision_defaults) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], + :raise => true) rescue {} + defaults = defaults.merge(precision_defaults) unless args.empty? ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' + @@ -209,12 +210,14 @@ module ActionView separator ||= (options[:separator] || defaults[:separator]) delimiter ||= (options[:delimiter] || defaults[:delimiter]) - rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision - number_with_delimiter("%01.#{precision}f" % rounded_number, - :separator => separator, - :delimiter => delimiter) - rescue - number + begin + rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision + number_with_delimiter("%01.#{precision}f" % rounded_number, + :separator => separator, + :delimiter => delimiter) + rescue + number + end end STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze @@ -251,8 +254,8 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults, human = I18n.translate([:'number.format', :'number.human.format'], - :locale => options[:locale]) || [{},{}] + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + human = I18n.translate(:'number.human.format', :locale => options[:locale], :raise => true) rescue {} defaults = defaults.merge(human) unless args.empty? @@ -272,13 +275,16 @@ module ActionView number /= 1024 ** exponent unit = STORAGE_UNITS[exponent] - number_with_precision(number, - :precision => precision, - :separator => separator, - :delimiter => delimiter - ).sub(/(\d)(#{Regexp.escape(separator)}[1-9]*)?0+\z/, '\1') + " #{unit}" - rescue - number + begin + escaped_separator = Regexp.escape(separator) + number_with_precision(number, + :precision => precision, + :separator => separator, + :delimiter => delimiter + ).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}" + rescue + number + end end end end diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index ce0da398cc..2ee7f43a65 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -18,35 +18,35 @@ class NumberHelperI18nTests < Test::Unit::TestCase end def test_number_to_currency_translates_currency_formats - I18n.expects(:translate).with( - [:'number.format', :'number.currency.format'], :locale => 'en-US' - ).returns([@number_defaults, @currency_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.currency.format', :locale => 'en-US', + :raise => true).returns(@currency_defaults) number_to_currency(1, :locale => 'en-US') end def test_number_with_precision_translates_number_formats - I18n.expects(:translate).with( - [:'number.format', :'number.precision.format'], :locale => 'en-US' - ).returns([@number_defaults, @precision_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.precision.format', :locale => 'en-US', + :raise => true).returns(@precision_defaults) number_with_precision(1, :locale => 'en-US') end def test_number_with_delimiter_translates_number_formats - I18n.expects(:translate).with(:'number.format', :locale => 'en-US').returns(@number_defaults) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) number_with_delimiter(1, :locale => 'en-US') end def test_number_to_percentage_translates_number_formats - I18n.expects(:translate).with( - [:'number.format', :'number.percentage.format'], :locale => 'en-US' - ).returns([@number_defaults, @percentage_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.percentage.format', :locale => 'en-US', + :raise => true).returns(@percentage_defaults) number_to_percentage(1, :locale => 'en-US') end def test_number_to_human_size_translates_human_formats - I18n.expects(:translate).with( - [:'number.format', :'number.human.format'], :locale => 'en-US' - ).returns([@number_defaults, @human_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.human.format', :locale => 'en-US', + :raise => true).returns(@human_defaults) # can't be called with 1 because this directly returns without calling I18n.translate number_to_human_size(1025, :locale => 'en-US') end -- cgit v1.2.3 From 8d72b82b8da99eda9ba654b3d3a4da847a212406 Mon Sep 17 00:00:00 2001 From: Jeffrey Hardy Date: Tue, 5 Aug 2008 16:29:56 -0500 Subject: Make assert_template failure message more friendly Signed-off-by: Joshua Peek --- actionpack/lib/action_controller/assertions/response_assertions.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actionpack/lib/action_controller/assertions/response_assertions.rb b/actionpack/lib/action_controller/assertions/response_assertions.rb index 765225ae24..e2e8bbdc71 100644 --- a/actionpack/lib/action_controller/assertions/response_assertions.rb +++ b/actionpack/lib/action_controller/assertions/response_assertions.rb @@ -87,11 +87,11 @@ module ActionController # def assert_template(expected = nil, message=nil) clean_backtrace do - rendered = @response.rendered_template + rendered = @response.rendered_template.to_s msg = build_message(message, "expecting but rendering with ", expected, rendered) assert_block(msg) do if expected.nil? - @response.rendered_template.nil? + @response.rendered_template.blank? else rendered.to_s.match(expected) end -- cgit v1.2.3 From 69e9cbb99ab111ad6b9cb2c9d8ba823ad8600bd6 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Tue, 5 Aug 2008 17:33:51 -0700 Subject: Ensure public superclass methods don't shadow public controller methods. Case in point, ruby-debug's Kernel#start shadowing a controller's start action. --- actionpack/lib/action_controller/base.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5689a9825e..c50e255c98 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1199,7 +1199,7 @@ module ActionController #:nodoc: end def perform_action - if self.class.action_methods.include?(action_name) + if action_methods.include?(action_name) send(action_name) default_render unless performed? elsif respond_to? :method_missing @@ -1208,7 +1208,7 @@ module ActionController #:nodoc: elsif template_exists? && template_public? default_render else - raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.to_a.sort.to_sentence}", caller + raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller end end @@ -1234,7 +1234,7 @@ module ActionController #:nodoc: end def self.action_methods - @action_methods ||= Set.new(public_instance_methods.map { |m| m.to_s }) - hidden_actions + @action_methods ||= Set.new(public_instance_methods.map { |m| m.to_s }) - hidden_actions + public_instance_methods(false).map { |m| m.to_s } end def add_variables_to_assigns -- cgit v1.2.3 From 29a06f10e8600bbda333c30bf294e7896f35b8d5 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Tue, 5 Aug 2008 19:28:52 -0700 Subject: Strip newlines from cookie session data --- actionpack/lib/action_controller/session/cookie_store.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb index b477c1f7da..5bf7503f04 100644 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ b/actionpack/lib/action_controller/session/cookie_store.rb @@ -129,7 +129,7 @@ class CGI::Session::CookieStore private # Marshal a session hash into safe cookie data. Include an integrity hash. def marshal(session) - data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop + data = ActiveSupport::Base64.encode64s(Marshal.dump(session)) "#{data}--#{generate_digest(data)}" end -- cgit v1.2.3 From 080974784582e1e289c2948227b446bc56d404a1 Mon Sep 17 00:00:00 2001 From: Nik Wakelin Date: Sat, 2 Aug 2008 17:44:02 +1200 Subject: Added MigrationProxy to defer loading of Migration classes until they are actually required by the migrator Signed-off-by: Michael Koziarski [#747 state:resolved] --- activerecord/lib/active_record/migration.rb | 32 +++++++++++++++++++++++------ activerecord/test/cases/migration_test.rb | 20 ++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 731a350854..fd77f27b77 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -349,6 +349,27 @@ module ActiveRecord end end + # MigrationProxy is used to defer loading of the actual migration classes + # until they are needed + class MigrationProxy + + attr_accessor :name, :version, :filename + + delegate :migrate, :announce, :write, :to=>:migration + + private + + def migration + @migration ||= load_migration + end + + def load_migration + load(filename) + name.constantize + end + + end + class Migrator#:nodoc: class << self def migrate(migrations_path, target_version = nil) @@ -437,7 +458,7 @@ module ActiveRecord runnable.pop if down? && !target.nil? runnable.each do |migration| - Base.logger.info "Migrating to #{migration} (#{migration.version})" + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" # On our way up, we skip migrating the ones we've already migrated # On our way down, we skip reverting the ones we've never migrated @@ -470,11 +491,10 @@ module ActiveRecord raise DuplicateMigrationNameError.new(name.camelize) end - load(file) - - klasses << returning(name.camelize.constantize) do |klass| - class << klass; attr_accessor :version end - klass.version = version + klasses << returning(MigrationProxy.new) do |migration| + migration.name = name.camelize + migration.version = version + migration.filename = file end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 7ecf755ef8..920f719995 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -922,6 +922,26 @@ if ActiveRecord::Base.connection.supports_migrations? migrations[0].name == 'innocent_jointable' end + def test_only_loads_pending_migrations + # migrate up to 1 + ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) + + # now unload the migrations that have been defined + PeopleHaveLastNames.unloadable + ActiveSupport::Dependencies.remove_unloadable_constants! + + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil) + + assert !defined? PeopleHaveLastNames + + %w(WeNeedReminders, InnocentJointable).each do |migration| + assert defined? migration + end + + ensure + load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb") + end + def test_migrator_interleaved_migrations ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1") -- cgit v1.2.3 From 73056500f88d569fa497d846dfe6b501a9e03739 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 14:39:13 -0500 Subject: Make File.atomic_write copy the original permissions or use the directories default. --- activesupport/lib/active_support/core_ext/file.rb | 24 ++--------- .../lib/active_support/core_ext/file/atomic.rb | 46 ++++++++++++++++++++ activesupport/test/core_ext/file_test.rb | 50 +++++++++++++++++++--- 3 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 activesupport/lib/active_support/core_ext/file/atomic.rb diff --git a/activesupport/lib/active_support/core_ext/file.rb b/activesupport/lib/active_support/core_ext/file.rb index 45d93b220f..e03f8ac44e 100644 --- a/activesupport/lib/active_support/core_ext/file.rb +++ b/activesupport/lib/active_support/core_ext/file.rb @@ -1,21 +1,5 @@ -require 'tempfile' +require 'active_support/core_ext/file/atomic' -# Write to a file atomically. Useful for situations where you don't -# want other processes or threads to see half-written files. -# -# File.atomic_write("important.file") do |file| -# file.write("hello") -# end -# -# If your temp directory is not on the same filesystem as the file you're -# trying to write, you can provide a different temporary directory. -# -# File.atomic_write("/data/something.important", "/data/tmp") do |f| -# file.write("hello") -# end -def File.atomic_write(file_name, temp_dir = Dir.tmpdir) - temp_file = Tempfile.new(File.basename(file_name), temp_dir) - yield temp_file - temp_file.close - File.rename(temp_file.path, file_name) -end \ No newline at end of file +class File #:nodoc: + extend ActiveSupport::CoreExtensions::File::Atomic +end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb new file mode 100644 index 0000000000..4d3cf5423f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -0,0 +1,46 @@ +require 'tempfile' + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module File #:nodoc: + module Atomic + # Write to a file atomically. Useful for situations where you don't + # want other processes or threads to see half-written files. + # + # File.atomic_write("important.file") do |file| + # file.write("hello") + # end + # + # If your temp directory is not on the same filesystem as the file you're + # trying to write, you can provide a different temporary directory. + # + # File.atomic_write("/data/something.important", "/data/tmp") do |f| + # file.write("hello") + # end + def atomic_write(file_name, temp_dir = Dir.tmpdir) + temp_file = Tempfile.new(basename(file_name), temp_dir) + yield temp_file + temp_file.close + + begin + # Get original file permissions + old_stat = stat(file_name) + rescue Errno::ENOENT + # No old permissions, write a temp file to determine the defaults + check_name = ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}" + new(check_name, "w") + old_stat = stat(check_name) + unlink(check_name) + end + + # Overwrite original file with temp file + rename(temp_file.path, file_name) + + # Set correct permissions on new file + chown(old_stat.uid, old_stat.gid, file_name) + chmod(old_stat.mode, file_name) + end + end + end + end +end diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb index 5efe357e9f..63b470684f 100644 --- a/activesupport/test/core_ext/file_test.rb +++ b/activesupport/test/core_ext/file_test.rb @@ -1,9 +1,8 @@ require 'abstract_unit' class AtomicWriteTest < Test::Unit::TestCase - def test_atomic_write_without_errors - contents = "Atomic Text" + contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) assert !File.exist?(file_name) @@ -13,7 +12,7 @@ class AtomicWriteTest < Test::Unit::TestCase ensure File.unlink(file_name) rescue nil end - + def test_atomic_write_doesnt_write_when_block_raises File.atomic_write(file_name) do |file| file.write("testing") @@ -22,8 +21,47 @@ class AtomicWriteTest < Test::Unit::TestCase rescue assert !File.exist?(file_name) end - - def file_name - "atomic.file" + + def test_atomic_write_preserves_file_permissions + contents = "Atomic Text" + File.open(file_name, "w", 0755) do |file| + file.write(contents) + assert File.exist?(file_name) + end + assert File.exist?(file_name) + assert_equal "100755", file_mode + assert_equal contents, File.read(file_name) + + File.atomic_write(file_name, Dir.pwd) do |file| + file.write(contents) + assert File.exist?(file_name) + end + assert File.exist?(file_name) + assert_equal "100755", file_mode + assert_equal contents, File.read(file_name) + ensure + File.unlink(file_name) rescue nil + end + + def test_atomic_write_preserves_default_file_permissions + contents = "Atomic Text" + File.atomic_write(file_name, Dir.pwd) do |file| + file.write(contents) + assert !File.exist?(file_name) + end + assert File.exist?(file_name) + assert_equal "100644", file_mode + assert_equal contents, File.read(file_name) + ensure + File.unlink(file_name) rescue nil end + + private + def file_name + "atomic.file" + end + + def file_mode + sprintf("%o", File.stat(file_name).mode) + end end -- cgit v1.2.3 From e5b1ab7cc39ff57f9789ffda75fb33f72187775d Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 14:50:02 -0500 Subject: MemoryStore is the only "unsafe" store. Make it threadsafe by default. --- .../lib/action_view/helpers/asset_tag_helper.rb | 2 +- activesupport/lib/active_support/cache.rb | 18 ------ .../lib/active_support/cache/memory_store.rb | 55 ++++++++++++++---- activesupport/test/caching_test.rb | 67 ---------------------- railties/lib/initializer.rb | 1 - 5 files changed, 44 insertions(+), 99 deletions(-) diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 769eada120..1e44b166d9 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -463,7 +463,7 @@ module ActionView end private - COMPUTED_PUBLIC_PATHS = ActiveSupport::Cache::MemoryStore.new.silence!.threadsafe! + COMPUTED_PUBLIC_PATHS = ActiveSupport::Cache::MemoryStore.new.silence! # Add the the extension +ext+ if not present. Return full URLs otherwise untouched. # Prefix with /dir/ if lacking a leading +/+. Account for relative URL diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 5a064f8bea..95eae3a77e 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -39,10 +39,6 @@ module ActiveSupport class Store cattr_accessor :logger - def threadsafe! - extend ThreadSafety - end - def silence! @silence = true self @@ -115,20 +111,6 @@ module ActiveSupport logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !@silence && !@logger_off end end - - module ThreadSafety #:nodoc: - def self.extended(object) #:nodoc: - object.instance_variable_set(:@mutex, Mutex.new) - end - - %w(read write delete delete_matched exist? increment decrement).each do |method| - module_eval <<-EOS, __FILE__, __LINE__ - def #{method}(*args) - @mutex.synchronize { super } - end - EOS - end - end end end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 6f114273e4..9332d50f24 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -3,36 +3,67 @@ module ActiveSupport class MemoryStore < Store def initialize @data = {} + @mutex = Mutex.new + end + + def fetch(key, options = {}) + @mutex.synchronize do + super + end end def read(name, options = nil) - super - @data[name] + @mutex.synchronize do + super + @data[name] + end end def write(name, value, options = nil) - super - @data[name] = value + @mutex.synchronize do + super + @data[name] = value + end end def delete(name, options = nil) - super - @data.delete(name) + @mutex.synchronize do + super + @data.delete(name) + end end def delete_matched(matcher, options = nil) - super - @data.delete_if { |k,v| k =~ matcher } + @mutex.synchronize do + super + @data.delete_if { |k,v| k =~ matcher } + end end def exist?(name,options = nil) - super - @data.has_key?(name) + @mutex.synchronize do + super + @data.has_key?(name) + end + end + + def increment(key, amount = 1) + @mutex.synchronize do + super + end + end + + def decrement(key, amount = 1) + @mutex.synchronize do + super + end end def clear - @data.clear + @mutex.synchronize do + @data.clear + end end end end -end \ No newline at end of file +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 0af4251962..f3220d27aa 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -70,70 +70,3 @@ uses_mocha 'high-level cache store tests' do end end end - -class ThreadSafetyCacheStoreTest < Test::Unit::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store).threadsafe! - @cache.write('foo', 'bar') - - # No way to have mocha proxy to the original method - @mutex = @cache.instance_variable_get(:@mutex) - @mutex.instance_eval %( - def calls; @calls; end - def synchronize - @calls ||= 0 - @calls += 1 - yield - end - ) - end - - def test_read_is_synchronized - assert_equal 'bar', @cache.read('foo') - assert_equal 1, @mutex.calls - end - - def test_write_is_synchronized - @cache.write('foo', 'baz') - assert_equal 'baz', @cache.read('foo') - assert_equal 2, @mutex.calls - end - - def test_delete_is_synchronized - assert_equal 'bar', @cache.read('foo') - @cache.delete('foo') - assert_equal nil, @cache.read('foo') - assert_equal 3, @mutex.calls - end - - def test_delete_matched_is_synchronized - assert_equal 'bar', @cache.read('foo') - @cache.delete_matched(/foo/) - assert_equal nil, @cache.read('foo') - assert_equal 3, @mutex.calls - end - - def test_fetch_is_synchronized - assert_equal 'bar', @cache.fetch('foo') { 'baz' } - assert_equal 'fu', @cache.fetch('bar') { 'fu' } - assert_equal 3, @mutex.calls - end - - def test_exist_is_synchronized - assert @cache.exist?('foo') - assert !@cache.exist?('bar') - assert_equal 2, @mutex.calls - end - - def test_increment_is_synchronized - @cache.write('foo_count', 1) - assert_equal 2, @cache.increment('foo_count') - assert_equal 4, @mutex.calls - end - - def test_decrement_is_synchronized - @cache.write('foo_count', 1) - assert_equal 0, @cache.decrement('foo_count') - assert_equal 4, @mutex.calls - end -end diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index f8b3a78dff..4036b14f19 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -776,7 +776,6 @@ Run `rake gems:install` to install the missing gems. self.dependency_loading = false self.active_record.allow_concurrency = true self.action_controller.allow_concurrency = true - self.to_prepare { Rails.cache.threadsafe! } self end -- cgit v1.2.3 From dfc83566b3f52b4b84db52312c01fcc3b8847059 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 14:52:39 -0500 Subject: Make FileStore use atomic writes --- activesupport/lib/active_support/cache/file_store.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 5b771b1da0..0bdca49388 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -15,7 +15,7 @@ module ActiveSupport def write(name, value, options = nil) super ensure_cache_path(File.dirname(real_file_path(name))) - File.open(real_file_path(name), "wb+") { |f| f.write(value) } + File.atomic_write(real_file_path(name)) { |f| f.write(value) } rescue => e RAILS_DEFAULT_LOGGER.error "Couldn't create cache directory: #{name} (#{e.message})" if RAILS_DEFAULT_LOGGER end -- cgit v1.2.3 From fbc6129acd9ecb6b7435931b472d3226985ba4c4 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 17:03:42 -0500 Subject: Treat single C operations in MemoryStore as atomic --- .../lib/active_support/cache/memory_store.rb | 31 ++++++---------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 9332d50f24..a44f877414 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -13,38 +13,25 @@ module ActiveSupport end def read(name, options = nil) - @mutex.synchronize do - super - @data[name] - end + super + @data[name] end def write(name, value, options = nil) - @mutex.synchronize do - super - @data[name] = value - end + super + @data[name] = value end def delete(name, options = nil) - @mutex.synchronize do - super - @data.delete(name) - end + @data.delete(name) end def delete_matched(matcher, options = nil) - @mutex.synchronize do - super - @data.delete_if { |k,v| k =~ matcher } - end + @data.delete_if { |k,v| k =~ matcher } end def exist?(name,options = nil) - @mutex.synchronize do - super - @data.has_key?(name) - end + @data.has_key?(name) end def increment(key, amount = 1) @@ -60,9 +47,7 @@ module ActiveSupport end def clear - @mutex.synchronize do - @data.clear - end + @data.clear end end end -- cgit v1.2.3 From 165120a606577dd81fe1a267899c4415fe25f1fb Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Wed, 6 Aug 2008 15:23:39 -0700 Subject: Be more careful about deducing action_methods --- actionpack/lib/action_controller/base.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index c50e255c98..0fdbcbd26f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -428,11 +428,7 @@ module ActionController #:nodoc: # By default, all methods defined in ActionController::Base and included modules are hidden. # More methods can be hidden using hide_actions. def hidden_actions - unless read_inheritable_attribute(:hidden_actions) - write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods.map { |m| m.to_s }) - end - - read_inheritable_attribute(:hidden_actions) + read_inheritable_attribute(:hidden_actions) || write_inheritable_attribute(:hidden_actions, []) end # Hide each of the given methods from being callable as actions. @@ -1234,7 +1230,15 @@ module ActionController #:nodoc: end def self.action_methods - @action_methods ||= Set.new(public_instance_methods.map { |m| m.to_s }) - hidden_actions + public_instance_methods(false).map { |m| m.to_s } + @action_methods ||= + # All public instance methods of this class, including ancestors + public_instance_methods(true).map { |m| m.to_s }.to_set - + # Except for public instance methods of Base and its ancestors + Base.public_instance_methods(true).map { |m| m.to_s } + + # Be sure to include shadowed public instance methods of this class + public_instance_methods(false).map { |m| m.to_s } - + # And always exclude explicitly hidden actions + hidden_actions end def add_variables_to_assigns -- cgit v1.2.3 From c6b7d0f34472ee7ef03d602c8923fd0ba8dab833 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 17:22:58 -0500 Subject: Ensure file atomic write uses the cache directory as its tmp folder --- activesupport/lib/active_support/cache/file_store.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 0bdca49388..7b6ca39091 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -15,7 +15,7 @@ module ActiveSupport def write(name, value, options = nil) super ensure_cache_path(File.dirname(real_file_path(name))) - File.atomic_write(real_file_path(name)) { |f| f.write(value) } + File.atomic_write(real_file_path(name), cache_path) { |f| f.write(value) } rescue => e RAILS_DEFAULT_LOGGER.error "Couldn't create cache directory: #{name} (#{e.message})" if RAILS_DEFAULT_LOGGER end -- cgit v1.2.3 From f5bcbde1e387020c7f4968515921a3ccee3dcda2 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 17:40:03 -0500 Subject: Make sure ActionView is loaded inorder to build view paths --- railties/lib/initializer.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 4036b14f19..6576cd368b 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -340,9 +340,11 @@ Run `rake gems:install` to install the missing gems. end def load_view_paths - ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes - ActionMailer::Base.template_root.load if configuration.frameworks.include?(:action_mailer) - ActionController::Base.view_paths.load if configuration.frameworks.include?(:action_controller) + if configuration.frameworks.include?(:action_view) + ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes + ActionController::Base.view_paths.load if configuration.frameworks.include?(:action_controller) + ActionMailer::Base.template_root.load if configuration.frameworks.include?(:action_mailer) + end end # Eager load application classes @@ -440,9 +442,11 @@ Run `rake gems:install` to install the missing gems. # paths have already been set, it is not changed, otherwise it is # set to use Configuration#view_path. def initialize_framework_views - view_path = ActionView::PathSet::Path.new(configuration.view_path, false) - ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer) - ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty? + if configuration.frameworks.include?(:action_view) + view_path = ActionView::PathSet::Path.new(configuration.view_path, false) + ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer) + ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty? + end end # If Action Controller is not one of the loaded frameworks (Configuration#frameworks) -- cgit v1.2.3 From ed8a882e47e07b470b71cacd8cd50e251dca4d27 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Wed, 6 Aug 2008 17:31:57 -0700 Subject: JRuby: improve constantize performance. [#410 state:resolved] --- activesupport/lib/active_support/inflector.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb index 6651569d33..c2738b39fc 100644 --- a/activesupport/lib/active_support/inflector.rb +++ b/activesupport/lib/active_support/inflector.rb @@ -291,11 +291,14 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word - raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!" - end + names = camel_cased_word.split('::') + names.shift if names.empty? || names.first.empty? - Object.module_eval("::#{$1}", __FILE__, __LINE__) + constant = Object + names.each do |name| + constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) + end + constant end # Turns a number into an ordinal string used to denote the position in an @@ -326,4 +329,4 @@ require 'active_support/inflections' require 'active_support/core_ext/string/inflections' unless String.included_modules.include?(ActiveSupport::CoreExtensions::String::Inflections) String.send :include, ActiveSupport::CoreExtensions::String::Inflections -end \ No newline at end of file +end -- cgit v1.2.3 From b2504f8ba0f9baadb9298647fd58ef2c136f9aae Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 20:08:27 -0500 Subject: Tidy up ActionMailer rendering logic to take advantage of view path cache instead of using file system lookups --- actionmailer/lib/action_mailer/base.rb | 49 ++++++++++-------------- actionmailer/test/mail_service_test.rb | 31 ++++++--------- actionpack/lib/action_view/base.rb | 5 ++- actionpack/lib/action_view/template.rb | 10 ++++- actionpack/test/controller/assert_select_test.rb | 33 ++++------------ 5 files changed, 53 insertions(+), 75 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index fa29ae2446..1583fb4066 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -216,7 +216,7 @@ module ActionMailer #:nodoc: # * :domain - If you need to specify a HELO domain, you can do it here. # * :user_name - If your mail server requires authentication, set the username in this setting. # * :password - If your mail server requires authentication, set the password in this setting. - # * :authentication - If your mail server requires authentication, you need to specify the authentication type here. + # * :authentication - If your mail server requires authentication, you need to specify the authentication type here. # This is a symbol and one of :plain, :login, :cram_md5. # # * sendmail_settings - Allows you to override options for the :sendmail delivery method. @@ -233,10 +233,10 @@ module ActionMailer #:nodoc: # * deliveries - Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful # for unit and functional testing. # - # * default_charset - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also + # * default_charset - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also # pick a different charset from inside a method with +charset+. # * default_content_type - The default content type used for the main part of the message. Defaults to "text/plain". You - # can also pick a different content type from inside a method with +content_type+. + # can also pick a different content type from inside a method with +content_type+. # * default_mime_version - The default mime version used for the message. Defaults to 1.0. You # can also pick a different value from inside a method with +mime_version+. # * default_implicit_parts_order - When a message is built implicitly (i.e. multiple parts are assembled from templates @@ -253,9 +253,6 @@ module ActionMailer #:nodoc: class_inheritable_accessor :view_paths cattr_accessor :logger - cattr_accessor :template_extensions - @@template_extensions = ['erb', 'builder', 'rhtml', 'rxml'] - @@smtp_settings = { :address => "localhost", :port => 25, @@ -414,15 +411,10 @@ module ActionMailer #:nodoc: new.deliver!(mail) end - # Register a template extension so mailer templates written in a - # templating language other than rhtml or rxml are supported. - # To use this, include in your template-language plugin's init - # code or on a per-application basis, this can be invoked from - # config/environment.rb: - # - # ActionMailer::Base.register_template_extension('haml') def register_template_extension(extension) - template_extensions << extension + ActiveSupport::Deprecation.warn( + "ActionMailer::Base.register_template_extension has been deprecated." + + "Use ActionView::Base.register_template_extension instead", caller) end def template_root @@ -455,16 +447,18 @@ module ActionMailer #:nodoc: # "the_template_file.text.html.erb", etc.). Only do this if parts # have not already been specified manually. if @parts.empty? - templates = Dir.glob("#{template_path}/#{@template}.*") - templates.each do |path| - basename = File.basename(path) - template_regex = Regexp.new("^([^\\\.]+)\\\.([^\\\.]+\\\.[^\\\.]+)\\\.(" + template_extensions.join('|') + ")$") - next unless md = template_regex.match(basename) - template_name = basename - content_type = md.captures[1].gsub('.', '/') - @parts << Part.new(:content_type => content_type, - :disposition => "inline", :charset => charset, - :body => render_message(template_name, @body)) + Dir.glob("#{template_path}/#{@template}.*").each do |path| + template = template_root["#{mailer_name}/#{File.basename(path)}"] + + # Skip unless template has a multipart format + next unless template.multipart? + + @parts << Part.new( + :content_type => template.content_type, + :disposition => "inline", + :charset => charset, + :body => render_message(template, @body) + ) end unless @parts.empty? @content_type = "multipart/alternative" @@ -476,9 +470,8 @@ module ActionMailer #:nodoc: # also render a "normal" template (without the content type). If a # normal template exists (or if there were no implicit parts) we render # it. - template_exists = @parts.empty? - template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 } - @body = render_message(@template, @body) if template_exists + template = template_root["#{mailer_name}/#{@template}"] + @body = render_message(@template, @body) if template # Finally, if there are other message parts and a textual body exists, # we shift it onto the front of the parts and set the body to nil (so @@ -538,7 +531,7 @@ module ActionMailer #:nodoc: def render(opts) body = opts.delete(:body) - if opts[:file] && opts[:file] !~ /\// + if opts[:file] && (opts[:file] !~ /\// && !opts[:file].respond_to?(:render)) opts[:file] = "#{mailer_name}/#{opts[:file]}" end initialize_template_class(body).render(opts) diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb index e5ecb0e254..882b07d675 100644 --- a/actionmailer/test/mail_service_test.rb +++ b/actionmailer/test/mail_service_test.rb @@ -219,7 +219,7 @@ class TestMailer < ActionMailer::Base end attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz" end - + def nested_multipart_with_body(recipient) recipients recipient subject "nested multipart with body" @@ -321,7 +321,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)} assert_equal 2,created.parts.size assert_equal 2,created.parts.first.parts.size - + assert_equal "multipart/mixed", created.content_type assert_equal "multipart/alternative", created.parts.first.content_type assert_equal "bar", created.parts.first.header['foo'].to_s @@ -366,7 +366,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_not_nil ActionMailer::Base.deliveries.first assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end - + def test_custom_template expected = new_mail expected.to = @recipient @@ -382,7 +382,6 @@ class ActionMailerTest < Test::Unit::TestCase end def test_custom_templating_extension - # # N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory expected = new_mail expected.to = @recipient @@ -390,18 +389,10 @@ class ActionMailerTest < Test::Unit::TestCase expected.body = "Hello there, \n\nMr. #{@recipient}" expected.from = "system@loudthinking.com" expected.date = Time.local(2004, 12, 12) - + # Stub the render method so no alternative renderers need be present. ActionView::Base.any_instance.stubs(:render).returns("Hello there, \n\nMr. #{@recipient}") - - # If the template is not registered, there should be no parts. - created = nil - assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) } - assert_not_nil created - assert_equal 0, created.parts.length - - ActionMailer::Base.register_template_extension('haml') - + # Now that the template is registered, there should be one part. The text/plain part. created = nil assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) } @@ -428,7 +419,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_not_nil ActionMailer::Base.deliveries.first assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end - + def test_cc_bcc expected = new_mail expected.to = @recipient @@ -550,7 +541,7 @@ class ActionMailerTest < Test::Unit::TestCase TestMailer.deliver_signed_up(@recipient) assert_equal 1, ActionMailer::Base.deliveries.size end - + def test_doesnt_raise_errors_when_raise_delivery_errors_is_false ActionMailer::Base.raise_delivery_errors = false TestMailer.any_instance.expects(:perform_delivery_test).raises(Exception) @@ -670,7 +661,7 @@ EOF assert_not_nil ActionMailer::Base.deliveries.first assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end - + def test_utf8_body_is_not_quoted @recipient = "Foo áëô îü " expected = new_mail "utf-8" @@ -760,7 +751,7 @@ EOF mail = TestMailer.create_multipart_with_mime_version(@recipient) assert_equal "1.1", mail.mime_version end - + def test_multipart_with_utf8_subject mail = TestMailer.create_multipart_with_utf8_subject(@recipient) assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded) @@ -825,7 +816,7 @@ EOF mail = TestMailer.create_implicitly_multipart_example(@recipient, 'iso-8859-1') assert_equal "multipart/alternative", mail.header['content-type'].body - + assert_equal 'iso-8859-1', mail.parts[0].sub_header("content-type", "charset") assert_equal 'iso-8859-1', mail.parts[1].sub_header("content-type", "charset") assert_equal 'iso-8859-1', mail.parts[2].sub_header("content-type", "charset") @@ -852,7 +843,7 @@ EOF assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body assert_equal "

      line #1

      \n

      line #2

      \n

      line #3

      \n

      line #4

      \n\n", mail.parts[1].body end - + def test_headers_removed_on_smtp_delivery ActionMailer::Base.delivery_method = :smtp TestMailer.deliver_cc_bcc(@recipient) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index bdcb1dc246..ad59d92086 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -300,6 +300,8 @@ module ActionView #:nodoc: # # => 'users/legacy.rhtml' # def pick_template(template_path) + return template_path if template_path.respond_to?(:render) + path = template_path.sub(/^\//, '') if m = path.match(/(.*)\.(\w+)$/) template_file_name, template_file_extension = m[1], m[2] @@ -343,7 +345,8 @@ module ActionView #:nodoc: ActiveSupport::Deprecation.warn("use_full_path option has been deprecated and has no affect.", caller) end - if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/") + if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && + template_path.is_a?(String) && !template_path.include?("/") raise ActionViewError, <<-END_ERROR Due to changes in ActionMailer, you need to provide the mailer_name along with the template name. diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index b281ff61f2..5dc6708431 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -22,6 +22,14 @@ module ActionView #:nodoc: end memoize :format_and_extension + def multipart? + format && format.include?('.') + end + + def content_type + format.gsub('.', '/') + end + def mime_type Mime::Type.lookup_by_extension(format) if format end @@ -84,7 +92,7 @@ module ActionView #:nodoc: # [base_path, name, format, extension] def split(file) if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/) - if m[5] # Mulipart formats + if m[5] # Multipart formats [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]] elsif m[4] # Single format [m[1], m[2], m[3], m[4]] diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 5af579f3e3..1531e7c21a 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -17,6 +17,8 @@ unless defined?(ActionMailer) end end +ActionMailer::Base.template_root = FIXTURE_LOAD_PATH + class AssertSelectTest < Test::Unit::TestCase class AssertSelectController < ActionController::Base def response_with=(content) @@ -69,11 +71,10 @@ class AssertSelectTest < Test::Unit::TestCase ActionMailer::Base.deliveries = [] end - def teardown ActionMailer::Base.deliveries.clear end - + def assert_failure(message, &block) e = assert_raises(AssertionFailedError, &block) assert_match(message, e.message) if Regexp === message @@ -91,7 +92,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_failure(/Expected at least 1 element matching \"p\", found 0/) { assert_select "p" } end - def test_equality_true_false render_html %Q{
      } assert_nothing_raised { assert_select "div" } @@ -102,7 +102,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_nothing_raised { assert_select "p", false } end - def test_equality_string_and_regexp render_html %Q{
      foo
      foo
      } assert_nothing_raised { assert_select "div", "foo" } @@ -116,7 +115,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_raises(AssertionFailedError) { assert_select "p", :text=>/foobar/ } end - def test_equality_of_html render_html %Q{

      \n"This is not a big problem," he said.\n

      } text = "\"This is not a big problem,\" he said." @@ -135,7 +133,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_raises(AssertionFailedError) { assert_select "pre", :html=>text } end - def test_counts render_html %Q{
      foo
      foo
      } assert_nothing_raised { assert_select "div", 2 } @@ -166,7 +163,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_substitution_values render_html %Q{
      foo
      foo
      } assert_select "div#?", /\d+/ do |elements| @@ -181,7 +177,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_nested_assert_select render_html %Q{
      foo
      foo
      } assert_select "div" do |elements| @@ -200,7 +195,7 @@ class AssertSelectTest < Test::Unit::TestCase assert_select "#3", false end end - + assert_failure(/Expected at least 1 element matching \"#4\", found 0\./) do assert_select "div" do assert_select "#4" @@ -208,7 +203,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_assert_select_text_match render_html %Q{
      foo
      bar
      } assert_select "div" do @@ -225,7 +219,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - # With single result. def test_assert_select_from_rjs_with_single_result render_rjs do |page| @@ -255,19 +248,16 @@ class AssertSelectTest < Test::Unit::TestCase end end - # # Test css_select. # - def test_css_select render_html %Q{
      } assert 2, css_select("div").size assert 0, css_select("p").size end - def test_nested_css_select render_html %Q{
      foo
      foo
      } assert_select "div#?", /\d+/ do |elements| @@ -286,7 +276,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - # With one result. def test_css_select_from_rjs_with_single_result render_rjs do |page| @@ -309,12 +298,10 @@ class AssertSelectTest < Test::Unit::TestCase assert_equal 1, css_select("#2").size end - # # Test assert_select_rjs. # - # Test that we can pick up all statements in the result. def test_assert_select_rjs_picks_up_all_statements render_rjs do |page| @@ -381,7 +368,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_raises(AssertionFailedError) { assert_select_rjs "test4" } end - def test_assert_select_rjs_for_replace render_rjs do |page| page.replace "test1", "
      foo
      " @@ -479,7 +465,7 @@ class AssertSelectTest < Test::Unit::TestCase end end end - + # Simple hide def test_assert_select_rjs_for_hide render_rjs do |page| @@ -500,7 +486,7 @@ class AssertSelectTest < Test::Unit::TestCase end end end - + # Simple toggle def test_assert_select_rjs_for_toggle render_rjs do |page| @@ -521,7 +507,7 @@ class AssertSelectTest < Test::Unit::TestCase end end end - + # Non-positioned insert. def test_assert_select_rjs_for_nonpositioned_insert render_rjs do |page| @@ -568,7 +554,7 @@ class AssertSelectTest < Test::Unit::TestCase assert_select "div", 4 end end - + # Simple selection from a single result. def test_nested_assert_select_rjs_with_single_result render_rjs do |page| @@ -600,7 +586,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_feed_item_encoded render_xml <<-EOF @@ -654,7 +639,6 @@ EOF end end - # # Test assert_select_email # @@ -670,7 +654,6 @@ EOF end end - protected def render_html(html) @controller.response_with = html -- cgit v1.2.3 From be0d235a3b82e72de49592913753a1f291a98f86 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 6 Aug 2008 20:21:15 -0500 Subject: Optimize memoized method if there are no arguments --- activesupport/lib/active_support/memoizable.rb | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index 23dd96e4df..7e2e28712c 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -29,14 +29,24 @@ module ActiveSupport raise "Already memoized #{symbol}" if method_defined?(:#{original_method}) alias #{original_method} #{symbol} - def #{symbol}(*args) - #{memoized_ivar} ||= {} - reload = args.pop if args.last == true || args.last == :reload + if instance_method(:#{symbol}).arity == 0 + def #{symbol}(reload = false) + if !reload && defined? #{memoized_ivar} + #{memoized_ivar} + else + #{memoized_ivar} = #{original_method}.freeze + end + end + else + def #{symbol}(*args) + #{memoized_ivar} ||= {} + reload = args.pop if args.last == true || args.last == :reload - if !reload && #{memoized_ivar} && #{memoized_ivar}.has_key?(args) - #{memoized_ivar}[args] - else - #{memoized_ivar}[args] = #{original_method}(*args).freeze + if !reload && #{memoized_ivar} && #{memoized_ivar}.has_key?(args) + #{memoized_ivar}[args] + else + #{memoized_ivar}[args] = #{original_method}(*args).freeze + end end end EOS -- cgit v1.2.3 From 105093f90728f81268367bd52581fccfa165f170 Mon Sep 17 00:00:00 2001 From: Clemens Kofler Date: Thu, 7 Aug 2008 13:13:47 -0500 Subject: Refactor DateHelper to use DateTimeSelector presenter pattern Signed-off-by: Joshua Peek --- actionpack/lib/action_view/helpers/date_helper.rb | 576 ++++++++++++++-------- actionpack/test/template/date_helper_i18n_test.rb | 22 +- actionpack/test/template/date_helper_test.rb | 299 +++++++++-- 3 files changed, 638 insertions(+), 259 deletions(-) diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index c7a1d40ff2..953a2a9f86 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -13,9 +13,6 @@ module ActionView # the select_month method would use simply "date" (which can be overwritten using :prefix) instead of # "date[month]". module DateHelper - include ActionView::Helpers::TagHelper - DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') - # Reports the approximate distance in time between two Time or Date objects or integers as seconds. # Set include_seconds to true if you want more detailed approximations when distance < 1 min, 29 secs # Distances are reported based on the following table: @@ -52,7 +49,7 @@ module ActionView # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year - # distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years + # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => over 4 years # # to_time = Time.now + 6.years + 19.days # distance_of_time_in_words(from_time, to_time, true) # => over 6 years @@ -109,19 +106,36 @@ module ActionView alias_method :distance_of_time_in_words_to_now, :time_ago_in_words # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based - # attribute (identified by +method+) on an object assigned to the template (identified by +object+). It's - # possible to tailor the selects through the +options+ hash, which accepts all the keys that each of the - # individual select builders do (like :use_month_numbers for select_month) as well as a range of discard - # options. The discard options are :discard_year, :discard_month and :discard_day. Set - # to true, they'll drop the respective select. Discarding the month select will also automatically discard the - # day select. It's also possible to explicitly set the order of the tags using the :order option with an - # array of symbols :year, :month and :day in the desired order. Symbols may be omitted - # and the respective select is not included. - # - # Pass the :default option to set the default date. Use a Time object or a Hash of :year, - # :month, :day, :hour, :minute, and :second. - # - # Passing :disabled => true as part of the +options+ will make elements inaccessible for change. + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). You can + # the output in the +options+ hash. + # + # ==== Options + # * :use_month_numbers - Set to true if you want to use month numbers rather than month names (e.g. + # "2" instead of "February"). + # * :use_short_month - Set to true if you want to use the abbreviated month name instead of the full + # name (e.g. "Feb" instead of "February"). + # * :add_month_number - Set to true if you want to show both, the month's number and name (e.g. + # "2 - February" instead of "February"). + # * :use_month_names - Set to an array with 12 month names if you want to customize month names. + # Note: You can also use Rails' new i18n functionality for this. + # * :date_separator - Specifies a string to separate the date fields. Default is "" (i.e. nothing). + # * :start_year - Set the start year for the year select. Default is Time.now.year - 5. + # * :end_year - Set the end year for the year select. Default is Time.now.year + 5. + # * :discard_day - Set to true if you don't want to show a day select. This includes the day + # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the + # first of the given month in order to not create invalid dates like 31 February. + # * :discard_month - Set to true if you don't want to show a month select. This includes the month + # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true. + # * :discard_year - Set to true if you don't want to show a year select. This includes the year + # as a hidden field instead of showing a select field. + # * :order - Set to an array containing :day, :month and :year do + # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective + # select will not be shown (like when you set :discard_xxx => true. Defaults to the order defined in + # the respective locale (e.g. [:year, :month, :day] in the en-US locale that ships with Rails). + # * :include_blank - Include a blank option in every select field so it's possible to set empty + # dates. + # * :default - Set a default date if the affected date isn't set or is nil. + # * :disabled - Set to true if you want show the select fields as disabled. # # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. # @@ -165,9 +179,9 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options) end - # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified - # time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+). - # You can include the seconds with :include_seconds. + # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a + # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by + # +object+). You can include the seconds with :include_seconds. # # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option # :ignore_date is set to +true+. @@ -178,7 +192,8 @@ module ActionView # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute # time_select("post", "sunrise") # - # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute + # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted + # # attribute # time_select("order", "submitted") # # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute @@ -210,7 +225,8 @@ module ActionView # If anything is passed in the html_options hash it will be applied to every select tag in the set. # # ==== Examples - # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute + # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on + # # attribute # datetime_select("post", "written_on") # # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the @@ -230,12 +246,12 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options) end - # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. - # It's also possible to explicitly set the order of the tags using the :order option with an array of - # symbols :year, :month and :day in the desired order. If you do not supply a Symbol, - # it will be appended onto the :order passed in. You can also add :date_separator, - # :datetime_separator and :time_separator keys to the +options+ to control visual display of - # the elements. + # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the + # +datetime+. It's also possible to explicitly set the order of the tags using the :order option with + # an array of symbols :year, :month and :day in the desired order. If you do not + # supply a Symbol, it will be appended onto the :order passed in. You can also add + # :date_separator, :datetime_separator and :time_separator keys to the +options+ to + # control visual display of the elements. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -270,14 +286,13 @@ module ActionView # select_datetime(my_date_time, :prefix => 'payday') # def select_datetime(datetime = Time.current, options = {}, html_options = {}) - separator = options[:datetime_separator] || '' - select_date(datetime, options, html_options) + separator + select_time(datetime, options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_datetime end # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. # It's possible to explicitly set the order of the tags using the :order option with an array of - # symbols :year, :month and :day in the desired order. If you do not supply a Symbol, it - # will be appended onto the :order passed in. + # symbols :year, :month and :day in the desired order. If you do not supply a Symbol, + # it will be appended onto the :order passed in. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -307,12 +322,7 @@ module ActionView # select_date(my_date, :prefix => 'payday') # def select_date(date = Date.current, options = {}, html_options = {}) - options.reverse_merge!(:order => [], :date_separator => '') - [:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) } - - options[:order].inject([]) { |s, o| - s << self.send("select_#{o}", date, options, html_options) - }.join(options[:date_separator]) + DateTimeSelector.new(date, options, html_options).select_date end # Returns a set of html select-tags (one for hour and minute) @@ -343,9 +353,7 @@ module ActionView # select_time(my_time, :time_separator => ':', :include_seconds => true) # def select_time(datetime = Time.current, options = {}, html_options = {}) - separator = options[:time_separator] || '' - select_hour(datetime, options, html_options) + separator + select_minute(datetime, options, html_options) + - (options[:include_seconds] ? separator + select_second(datetime, options, html_options) : '') + DateTimeSelector.new(datetime, options, html_options).select_time end # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. @@ -366,15 +374,12 @@ module ActionView # select_second(my_time, :field_name => 'interval') # def select_second(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : '' - options[:use_hidden] ? - (options[:include_seconds] ? _date_hidden_html(options[:field_name] || 'second', val, options) : '') : - _date_select_html(options[:field_name] || 'second', _date_build_options(val), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_second end # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. - # Also can return a select tag with options by minute_step from 0 through 59 with the 00 minute selected - # The minute can also be substituted for a minute number. + # Also can return a select tag with options by minute_step from 0 through 59 with the 00 minute + # selected. The minute can also be substituted for a minute number. # Override the field name using the :field_name option, 'minute' by default. # # ==== Examples @@ -391,11 +396,7 @@ module ActionView # select_minute(my_time, :field_name => 'stride') # def select_minute(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : '' - options[:use_hidden] ? - _date_hidden_html(options[:field_name] || 'minute', val, options) : - _date_select_html(options[:field_name] || 'minute', - _date_build_options(val, :step => options[:minute_step]), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_minute end # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. @@ -416,9 +417,7 @@ module ActionView # select_minute(my_time, :field_name => 'stride') # def select_hour(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : '' - options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'hour', val, options) : - _date_select_html(options[:field_name] || 'hour', _date_build_options(val, :end => 23), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_hour end # Returns a select tag with options for each of the days 1 through 31 with the current day selected. @@ -439,11 +438,7 @@ module ActionView # select_day(my_time, :field_name => 'due') # def select_day(date, options = {}, html_options = {}) - val = date ? (date.kind_of?(Fixnum) ? date : date.day) : '' - options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'day', val, options) : - _date_select_html(options[:field_name] || 'day', - _date_build_options(val, :start => 1, :end => 31, :leading_zeros => false), - options, html_options) + DateTimeSelector.new(date, options, html_options).select_day end # Returns a select tag with options for each of the months January through December with the current month @@ -481,36 +476,7 @@ module ActionView # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # def select_month(date, options = {}, html_options = {}) - locale = options[:locale] - - val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' - if options[:use_hidden] - _date_hidden_html(options[:field_name] || 'month', val, options) - else - month_options = [] - month_names = options[:use_month_names] || begin - key = options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' - I18n.translate key, :locale => locale - end - month_names.unshift(nil) if month_names.size < 13 - - 1.upto(12) do |month_number| - month_name = if options[:use_month_numbers] - month_number - elsif options[:add_month_numbers] - month_number.to_s + ' - ' + month_names[month_number] - else - month_names[month_number] - end - - month_options << ((val == month_number) ? - content_tag(:option, month_name, :value => month_number, :selected => "selected") : - content_tag(:option, month_name, :value => month_number) - ) - month_options << "\n" - end - _date_select_html(options[:field_name] || 'month', month_options.join, options, html_options) - end + DateTimeSelector.new(date, options, html_options).select_month end # Returns a select tag with options for each of the five years on each side of the current, which is selected. @@ -537,158 +503,369 @@ module ActionView # select_year(2006, :start_year => 2000, :end_year => 2010) # def select_year(date, options = {}, html_options = {}) - if !date || date == 0 + DateTimeSelector.new(date, options, html_options).select_year + end + end + + class DateTimeSelector #:nodoc: + extend ActiveSupport::Memoizable + include ActionView::Helpers::TagHelper + + DEFAULT_PREFIX = 'date'.freeze unless const_defined?('DEFAULT_PREFIX') + POSITION = { + :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 + }.freeze unless const_defined?('POSITION') + + def initialize(datetime, options = {}, html_options = {}) + @options = options.dup + @html_options = html_options.dup + @datetime = datetime + end + + def select_datetime + # TODO: Remove tag conditional + # Ideally we could just join select_date and select_date for the tag case + if @options[:tag] && @options[:ignore_date] + select_time + elsif @options[:tag] + order = date_order.dup + order -= [:hour, :minute, :second] + + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + @options[:discard_minute] ||= true if @options[:discard_hour] + @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute] + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) + end + + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + order += [:hour, :minute, :second] unless @options[:discard_hour] + + build_selects_from_types(order) + else + "#{select_date}#{@options[:datetime_separator]}#{select_time}" + end + end + + def select_date + order = date_order.dup + + # TODO: Remove tag conditional + if @options[:tag] + @options[:discard_hour] = true + @options[:discard_minute] = true + @options[:discard_second] = true + + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) + end + end + + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + + build_selects_from_types(order) + end + + def select_time + order = [] + + # TODO: Remove tag conditional + if @options[:tag] + @options[:discard_month] = true + @options[:discard_year] = true + @options[:discard_day] = true + @options[:discard_second] ||= true unless @options[:include_seconds] + + order += [:year, :month, :day] unless @options[:ignore_date] + end + + order += [:hour, :minute] + order << :second if @options[:include_seconds] + + build_selects_from_types(order) + end + + def select_second + if @options[:use_hidden] || @options[:discard_second] + build_hidden(:second, sec) if @options[:include_seconds] + else + build_options_and_select(:second, sec) + end + end + + def select_minute + if @options[:use_hidden] || @options[:discard_minute] + build_hidden(:minute, min) + else + build_options_and_select(:minute, min, :step => @options[:minute_step]) + end + end + + def select_hour + if @options[:use_hidden] || @options[:discard_hour] + build_hidden(:hour, hour) + else + build_options_and_select(:hour, hour, :end => 23) + end + end + + def select_day + if @options[:use_hidden] || @options[:discard_day] + build_hidden(:day, day) + else + build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false) + end + end + + def select_month + if @options[:use_hidden] || @options[:discard_month] + build_hidden(:month, month) + else + month_options = [] + 1.upto(12) do |month_number| + options = { :value => month_number } + options[:selected] = "selected" if month == month_number + month_options << content_tag(:option, month_name(month_number), options) + "\n" + end + build_select(:month, month_options.join) + end + end + + def select_year + if !@datetime || @datetime == 0 val = '' middle_year = Date.today.year - elsif date.kind_of?(Fixnum) - val = middle_year = date else - val = middle_year = date.year + val = middle_year = year end - if options[:use_hidden] - _date_hidden_html(options[:field_name] || 'year', val, options) + if @options[:use_hidden] || @options[:discard_year] + build_hidden(:year, val) else - options[:start_year] ||= middle_year - 5 - options[:end_year] ||= middle_year + 5 - step = options[:start_year] < options[:end_year] ? 1 : -1 - - _date_select_html(options[:field_name] || 'year', - _date_build_options(val, - :start => options[:start_year], - :end => options[:end_year], - :step => step, - :leading_zeros => false - ), options, html_options) + options = {} + options[:start] = @options[:start_year] || middle_year - 5 + options[:end] = @options[:end_year] || middle_year + 5 + options[:step] = options[:start] < options[:end] ? 1 : -1 + options[:leading_zeros] = false + + build_options_and_select(:year, val, options) end end private - def _date_build_options(selected, options={}) - options.reverse_merge!(:start => 0, :end => 59, :step => 1, :leading_zeros => true) + %w( sec min hour day month year ).each do |method| + define_method(method) do + @datetime.kind_of?(Fixnum) ? @datetime : @datetime.send(method) if @datetime + end + end + + # Returns translated month names, but also ensures that a custom month + # name array has a leading nil element + def month_names + month_names = @options[:use_month_names] || translated_month_names + month_names.unshift(nil) if month_names.size < 13 + month_names + end + memoize :month_names + + # Returns translated month names + # => [nil, "January", "February", "March", + # "April", "May", "June", "July", + # "August", "September", "October", + # "November", "December"] + # + # If :use_short_month option is set + # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", + # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + def translated_month_names + begin + key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' + I18n.translate(key, :locale => @options[:locale]) + end + end + + # Lookup month name for number + # month_name(1) => "January" + # + # If :use_month_numbers option is passed + # month_name(1) => 1 + # + # If :add_month_numbers option is passed + # month_name(1) => "1 - January" + def month_name(number) + if @options[:use_month_numbers] + number + elsif @options[:add_month_numbers] + "#{number} - #{month_names[number]}" + else + month_names[number] + end + end + + def date_order + @options[:order] || translated_date_order + end + memoize :date_order + + def translated_date_order + begin + I18n.translate(:'date.order', :locale => @options[:locale]) || [] + end + end + + # Build full select tag from date type and options + def build_options_and_select(type, selected, options = {}) + build_select(type, build_options(selected, options)) + end + + # Build select option html from date value and options + # build_options(15, :start => 1, :end => 31) + # => " + # + # ..." + def build_options(selected, options = {}) + start = options.delete(:start) || 0 + stop = options.delete(:end) || 59 + step = options.delete(:step) || 1 + leading_zeros = options.delete(:leading_zeros).nil? ? true : false select_options = [] - (options[:start] || 0).step((options[:end] || 59), options[:step] || 1) do |i| - value = options[:leading_zeros] ? sprintf("%02d", i) : i + start.step(stop, step) do |i| + value = leading_zeros ? sprintf("%02d", i) : i tag_options = { :value => value } tag_options[:selected] = "selected" if selected == i - select_options << content_tag(:option, value, tag_options) end select_options.join("\n") + "\n" end - def _date_select_html(type, html_options, options, select_tag_options = {}) - _date_name_and_id_from_options(options, type) - select_options = {:id => options[:id], :name => options[:name]} - select_options.merge!(:disabled => 'disabled') if options[:disabled] - select_options.merge!(select_tag_options) unless select_tag_options.empty? + # Builds select tag from date type and html select options + # build_select(:month, "...") + # => "" + def build_select(type, select_options_as_html) + select_options = { + :id => input_id_from_type(type), + :name => input_name_from_type(type) + }.merge(@html_options) + select_options.merge!(:disabled => 'disabled') if @options[:disabled] + select_html = "\n" - select_html << content_tag(:option, '', :value => '') + "\n" if options[:include_blank] - select_html << html_options.to_s + select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank] + select_html << select_options_as_html.to_s + content_tag(:select, select_html, select_options) + "\n" end - def _date_hidden_html(type, value, options) - _date_name_and_id_from_options(options, type) - hidden_html = tag(:input, :type => "hidden", :id => options[:id], :name => options[:name], :value => value) + "\n" + # Builds hidden input tag for date part and value + # build_hidden(:year, 2008) + # => "" + def build_hidden(type, value) + tag(:input, { + :type => "hidden", + :id => input_id_from_type(type), + :name => input_name_from_type(type), + :value => value + }) + "\n" end - def _date_name_and_id_from_options(options, type) - options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]") - options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + # Returns the name attribute for the input tag + # => post[written_on(1i)] + def input_name_from_type(type) + prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX + prefix += "[#{@options[:index]}]" if @options[:index] + + field_name = @options[:field_name] || type + if @options[:include_position] + field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)" + end + + @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]" + end + + # Returns the id attribute for the input tag + # => "post_written_on_1i" + def input_id_from_type(type) + input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + end + + # Given an ordering of datetime components, create the selection html + # and join them with their appropriate seperators + def build_selects_from_types(order) + select = '' + order.reverse.each do |type| + separator = separator(type) unless type == order.first # don't add on last field + select.insert(0, separator.to_s + send("select_#{type}").to_s) + end + select + end + + # Returns the separator for a given datetime component + def separator(type) + case type + when :month, :day + @options[:date_separator] + when :hour + (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] + when :minute + @options[:time_separator] + when :second + @options[:include_seconds] ? @options[:time_separator] : "" + end end end class InstanceTag #:nodoc: - include DateHelper - def to_date_select_tag(options = {}, html_options = {}) - date_or_time_select(options.merge(:discard_hour => true), html_options) + datetime_selector(options, html_options).select_date end def to_time_select_tag(options = {}, html_options = {}) - date_or_time_select(options.merge(:discard_year => true, :discard_month => true), html_options) + datetime_selector(options, html_options).select_time end def to_datetime_select_tag(options = {}, html_options = {}) - date_or_time_select(options, html_options) + datetime_selector(options, html_options).select_datetime end private - def date_or_time_select(options, html_options = {}) - locale = options[:locale] - - defaults = { :discard_type => true } - options = defaults.merge(options) - datetime = value(object) - datetime ||= default_time_from_options(options[:default]) unless options[:include_blank] - - position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } - - order = options[:order] ||= I18n.translate(:'date.order', :locale => locale) - - # Discard explicit and implicit by not being included in the :order - discard = {} - discard[:year] = true if options[:discard_year] or !order.include?(:year) - discard[:month] = true if options[:discard_month] or !order.include?(:month) - discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day) - discard[:hour] = true if options[:discard_hour] - discard[:minute] = true if options[:discard_minute] or discard[:hour] - discard[:second] = true unless options[:include_seconds] && !discard[:minute] - - # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid - # (otherwise it could be 31 and february wouldn't be a valid date) - if datetime && discard[:day] && !discard[:month] - datetime = datetime.change(:day => 1) - end - - # Maintain valid dates by including hidden fields for discarded elements - [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } - - # Ensure proper ordering of :hour, :minute and :second - [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) } - - date_or_time_select = '' - order.reverse.each do |param| - # Send hidden fields for discarded elements once output has started - # This ensures AR can reconstruct valid dates using ParseDate - next if discard[param] && (date_or_time_select.empty? || options[:ignore_date]) - - date_or_time_select.insert(0, - self.send("select_#{param}", - datetime, - options_with_prefix(position[param], options.merge(:use_hidden => discard[param])), - html_options)) - date_or_time_select.insert(0, - case param - when :hour then (discard[:year] && discard[:day] ? "" : " — ") - when :minute then " : " - when :second then options[:include_seconds] ? " : " : "" - else "" - end) - end - - date_or_time_select + def datetime_selector(options, html_options) + datetime = value(object) || default_datetime(options) + + options = options.dup + options[:field_name] = @method_name + options[:include_position] = true + options[:prefix] ||= @object_name + options[:index] ||= @auto_index + options[:datetime_separator] ||= ' — ' + options[:time_separator] ||= ' : ' + + DateTimeSelector.new(datetime, options.merge(:tag => true), html_options) end - def options_with_prefix(position, options) - prefix = "#{@object_name}" - if options[:index] - prefix << "[#{options[:index]}]" - elsif @auto_index - prefix << "[#{@auto_index}]" - end - options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]") - end + def default_datetime(options) + return if options[:include_blank] - def default_time_from_options(default) - case default + case options[:default] when nil Time.current when Date, Time - default + options[:default] else + default = options[:default].dup + # Rename :minute and :second to :min and :sec default[:min] ||= default[:minute] default[:sec] ||= default[:second] @@ -699,8 +876,11 @@ module ActionView default[key] ||= time.send(key) end - Time.utc_time(default[:year], default[:month], default[:day], default[:hour], default[:min], default[:sec]) - end + Time.utc_time( + default[:year], default[:month], default[:day], + default[:hour], default[:min], default[:sec] + ) + end end end diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index aca3593921..2b40074498 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -3,22 +3,22 @@ require 'abstract_unit' class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase include ActionView::Helpers::DateHelper attr_reader :request - + def setup @from = Time.mktime(2004, 6, 6, 21, 45, 0) end - + uses_mocha 'date_helper_distance_of_time_in_words_i18n_test' do # distance_of_time_in_words def test_distance_of_time_in_words_calls_i18n { # with include_seconds - [2.seconds, true] => [:'less_than_x_seconds', 5], - [9.seconds, true] => [:'less_than_x_seconds', 10], - [19.seconds, true] => [:'less_than_x_seconds', 20], - [30.seconds, true] => [:'half_a_minute', nil], - [59.seconds, true] => [:'less_than_x_minutes', 1], - [60.seconds, true] => [:'x_minutes', 1], + [2.seconds, true] => [:'less_than_x_seconds', 5], + [9.seconds, true] => [:'less_than_x_seconds', 10], + [19.seconds, true] => [:'less_than_x_seconds', 20], + [30.seconds, true] => [:'half_a_minute', nil], + [59.seconds, true] => [:'less_than_x_minutes', 1], + [60.seconds, true] => [:'x_minutes', 1], # without include_seconds [29.seconds, false] => [:'less_than_x_minutes', 1], @@ -38,7 +38,7 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase def assert_distance_of_time_in_words_translates_key(passed, expected) diff, include_seconds = *passed - key, count = *expected + key, count = *expected to = @from + diff options = {:locale => 'en-US', :scope => :'datetime.distance_in_words'} @@ -49,11 +49,11 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase end end end - + class DateHelperSelectTagsI18nTests < Test::Unit::TestCase include ActionView::Helpers::DateHelper attr_reader :request - + uses_mocha 'date_helper_select_tags_i18n_tests' do def setup I18n.stubs(:translate).with(:'date.month_names', :locale => 'en-US').returns Date::MONTHNAMES diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index d8c07e731b..1a645bccc6 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -557,11 +557,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_incomplete_order - expected = %(\n" - - expected << %(\n) expected << %(\n\n\n) expected << "\n" @@ -569,6 +566,10 @@ class DateHelperTest < ActionView::TestCase expected << %(\n\n\n\n\n\n\n\n\n\n\n\n) expected << "\n" + expected << %(\n" + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day]) end @@ -909,6 +910,10 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "—", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector') end + def test_select_datetime_should_work_with_date + assert_nothing_raised { select_datetime(Date.today) } + end + def test_select_time expected = %(\n} + expected << %{\n\n\n\n\n\n\n\n\n\n\n} + expected << "\n" + + expected << " / " + + expected << %{\n" + + expected << " / " + + expected << %{\n" + + assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " }) + end + def test_time_select @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) @@ -1330,6 +1336,33 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_time_select_with_separator + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{\n} + expected << %{\n} + expected << %{\n} + + expected << %(\n" + + expected << " - " + + expected << %(\n" + + expected << " - " + + expected << %(\n" + + assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true }) + end + def test_datetime_select @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 16, 35) @@ -1412,6 +1445,47 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_datetime_select_with_separators + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{\n" + + expected << " / " + + expected << %{\n" + + expected << " / " + + expected << %{\n" + + expected << " , " + + expected << %(\n" + + expected << " - " + + expected << %(\n" + + expected << " - " + + expected << %(\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true }) + end + def test_date_select_with_zero_value_and_no_start_year expected = %(