aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_view/locale/en-US.yml22
-rw-r--r--actionpack/test/template/url_helper_test.rb1
-rw-r--r--activerecord/CHANGELOG2
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb2
-rw-r--r--activerecord/lib/active_record/callbacks.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb8
-rw-r--r--activerecord/lib/active_record/transactions.rb16
-rw-r--r--activerecord/test/cases/column_definition_test.rb8
-rw-r--r--activerecord/test/cases/i18n_test.rb2
-rw-r--r--activerecord/test/cases/transactions_test.rb62
-rw-r--r--activesupport/lib/active_support/inflector.rb69
-rwxr-xr-xactivesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb8
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb338
-rw-r--r--activesupport/test/inflector_test.rb7
-rwxr-xr-xci/ci_build.rb117
-rw-r--r--ci/ci_setup_notes.txt120
-rw-r--r--ci/cruise_config.rb5
-rw-r--r--ci/geminstaller.yml17
-rw-r--r--ci/site.css13
-rw-r--r--ci/site_config.rb72
-rw-r--r--railties/lib/commands/dbconsole.rb2
-rw-r--r--railties/lib/rails_generator/generated_attribute.rb4
-rw-r--r--railties/lib/rails_generator/generators/components/model/templates/model.rb3
-rw-r--r--railties/test/generators/rails_model_generator_test.rb16
24 files changed, 704 insertions, 222 deletions
diff --git a/actionpack/lib/action_view/locale/en-US.yml b/actionpack/lib/action_view/locale/en-US.yml
index eabdbf33d6..57987f4e02 100644
--- a/actionpack/lib/action_view/locale/en-US.yml
+++ b/actionpack/lib/action_view/locale/en-US.yml
@@ -51,41 +51,41 @@
half_a_minute: "half a minute"
less_than_x_seconds:
one: "less than 1 second"
- other: "less than {{count}} seconds"
+ many: "less than {{count}} seconds"
x_seconds:
one: "1 second"
- other: "{{count}} seconds"
+ many: "{{count}} seconds"
less_than_x_minutes:
one: "less than a minute"
- other: "less than {{count}} minutes"
+ many: "less than {{count}} minutes"
x_minutes:
one: "1 minute"
- other: "{{count}} minutes"
+ many: "{{count}} minutes"
about_x_hours:
one: "about 1 hour"
- other: "about {{count}} hours"
+ many: "about {{count}} hours"
x_days:
one: "1 day"
- other: "{{count}} days"
+ many: "{{count}} days"
about_x_months:
one: "about 1 month"
- other: "about {{count}} months"
+ many: "about {{count}} months"
x_months:
one: "1 month"
- other: "{{count}} months"
+ many: "{{count}} months"
about_x_years:
one: "about 1 year"
- other: "about {{count}} years"
+ many: "about {{count}} years"
over_x_years:
one: "over 1 year"
- other: "over {{count}} years"
+ many: "over {{count}} years"
activerecord:
errors:
template:
header:
one: "1 error prohibited this {{model}} from being saved"
- other: "{{count}} errors prohibited this {{model}} from being saved"
+ many: "{{count}} errors prohibited this {{model}} from being saved"
# The variable :count is also available
body: "There were problems with the following fields:"
diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb
index 3065d33c1b..85e967ac1c 100644
--- a/actionpack/test/template/url_helper_test.rb
+++ b/actionpack/test/template/url_helper_test.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
require 'abstract_unit'
RequestMock = Struct.new("Request", :request_uri, :protocol, :host_with_port, :env)
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 74a1239aeb..12295ac799 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,7 @@
*Edge*
+* before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria]
+
* Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav]
* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 981be3b1a9..99b8748a48 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -213,7 +213,7 @@ module ActiveRecord
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
- array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
+ array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
end
def owner_quoted_id
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index be2621fdb6..eec531c514 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -169,6 +169,18 @@ module ActiveRecord
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
# defined as methods on the model, which are called last.
+ #
+ # == Transactions
+ #
+ # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
+ # within a transaction. That includes <tt>after_*</tt> hooks. If everything
+ # goes fine a COMMIT is executed once the chain has been completed.
+ #
+ # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
+ # can also trigger a ROLLBACK raising an exception in any of the callbacks,
+ # including <tt>after_*</tt> hooks. Note, however, that in that case the client
+ # needs to be aware of it because an ordinary +save+ will raise such exception
+ # instead of quietly returning +false+.
module Callbacks
CALLBACKS = %w(
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 0f60a91ef1..bececf82a0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -384,12 +384,8 @@ module ActiveRecord
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
# must explicitly check for :null to allow change_column to work on migrations
- if options.has_key? :null
- if options[:null] == false
- sql << " NOT NULL"
- else
- sql << " NULL"
- end
+ if options[:null] == false
+ sql << " NOT NULL"
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 0531afbb52..81462a2917 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -91,11 +91,11 @@ module ActiveRecord
end
def destroy_with_transactions #:nodoc:
- transaction { destroy_without_transactions }
+ with_transaction_returning_status(:destroy_without_transactions)
end
def save_with_transactions(perform_validation = true) #:nodoc:
- rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
+ rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
end
def save_with_transactions! #:nodoc:
@@ -118,5 +118,17 @@ module ActiveRecord
end
raise
end
+
+ # Executes +method+ within a transaction and captures its return value as a
+ # status flag. If the status is true the transaction is committed, otherwise
+ # a ROLLBACK is issued. In any case the status flag is returned.
+ def with_transaction_returning_status(method, *args)
+ status = nil
+ transaction do
+ status = send(method, *args)
+ raise ActiveRecord::Rollback unless status
+ end
+ status
+ end
end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index 540f42f4b6..98abc8eac8 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -9,13 +9,13 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
end
# Avoid column definitions in create table statements like:
- # `title` varchar(255) DEFAULT NULL NULL
+ # `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
column = ActiveRecord::ConnectionAdapters::Column.new("title", nil, "varchar(20)")
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal "title varchar(20) NULL", column_def.to_sql
+ assert_equal "title varchar(20)", column_def.to_sql
end
def test_should_include_default_clause_when_default_is_present
@@ -23,7 +23,7 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello' NULL}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql
end
def test_should_specify_not_null_if_null_option_is_false
@@ -33,4 +33,4 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column.limit, column.precision, column.scale, column.default, column.null)
assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb
index 06036733f5..9f934ab569 100644
--- a/activerecord/test/cases/i18n_test.rb
+++ b/activerecord/test/cases/i18n_test.rb
@@ -40,7 +40,7 @@ class ActiveRecordI18nTests < Test::Unit::TestCase
private
def reset_translations
- I18n.backend = I18n::Backend::Simple.new
+ I18n.backend = I18n::Backend::Simple
end
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 06a76eacc3..af3ee6ddba 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/book'
class TransactionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -86,8 +87,7 @@ class TransactionTest < ActiveRecord::TestCase
assert Topic.find(2).approved?, "Second should still be approved"
end
-
- def test_callback_rollback_in_save
+ def test_raising_exception_in_callback_rollbacks_in_save
add_exception_raising_after_save_callback_to_topic
begin
@@ -102,6 +102,54 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_cancellation_from_before_destroy_rollbacks_in_destroy
+ add_cancelling_before_destroy_with_db_side_effect_to_topic
+ begin
+ nbooks_before_destroy = Book.count
+ status = @first.destroy
+ assert !status
+ assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload }
+ assert_equal nbooks_before_destroy, Book.count
+ ensure
+ remove_cancelling_before_destroy_with_db_side_effect_to_topic
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ status = @first.save
+ assert !status
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save!
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ @first.save!
+ flunk
+ rescue => e
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
def test_callback_rollback_in_create
new_topic = Topic.new(
:title => "A new topic",
@@ -221,6 +269,16 @@ class TransactionTest < ActiveRecord::TestCase
def remove_exception_raising_after_create_callback_to_topic
Topic.class_eval { remove_method :after_create }
end
+
+ %w(validation save destroy).each do |filter|
+ define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "def before_#{filter}() Book.create; false end"
+ end
+
+ define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "remove_method :before_#{filter}"
+ end
+ end
end
if current_adapter?(:PostgreSQLAdapter)
diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb
index 1e189465bd..7ae9e0c6ab 100644
--- a/activesupport/lib/active_support/inflector.rb
+++ b/activesupport/lib/active_support/inflector.rb
@@ -39,12 +39,16 @@ module ActiveSupport
# Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
# The replacement should always be a string that may include references to the matched data from the rule.
def plural(rule, replacement)
+ @uncountables.delete(rule) if rule.is_a?(String)
+ @uncountables.delete(replacement)
@plurals.insert(0, [rule, replacement])
end
# Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
# The replacement should always be a string that may include references to the matched data from the rule.
def singular(rule, replacement)
+ @uncountables.delete(rule) if rule.is_a?(String)
+ @uncountables.delete(replacement)
@singulars.insert(0, [rule, replacement])
end
@@ -55,6 +59,8 @@ module ActiveSupport
# irregular 'octopus', 'octopi'
# irregular 'person', 'people'
def irregular(singular, plural)
+ @uncountables.delete(singular)
+ @uncountables.delete(plural)
if singular[0,1].upcase == plural[0,1].upcase
plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
@@ -273,32 +279,47 @@ module ActiveSupport
underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
end
- # Tries to find a constant with the name specified in the argument string:
- #
- # "Module".constantize # => Module
- # "Test::Unit".constantize # => Test::Unit
- #
- # The name is assumed to be the one of a top-level constant, no matter whether
- # it starts with "::" or not. No lexical context is taken into account:
- #
- # C = 'outside'
- # module M
- # C = 'inside'
- # C # => 'inside'
- # "C".constantize # => 'outside', same as ::C
- # end
- #
- # NameError is raised when the name is not in CamelCase or the constant is
- # unknown.
- def constantize(camel_cased_word)
- names = camel_cased_word.split('::')
- names.shift if names.empty? || names.first.empty?
+ # Ruby 1.9 introduces an inherit argument for Module#const_get and
+ # #const_defined? and changes their default behavior.
+ if Module.method(:const_get).arity == 1
+ # Tries to find a constant with the name specified in the argument string:
+ #
+ # "Module".constantize # => Module
+ # "Test::Unit".constantize # => Test::Unit
+ #
+ # The name is assumed to be the one of a top-level constant, no matter whether
+ # it starts with "::" or not. No lexical context is taken into account:
+ #
+ # C = 'outside'
+ # module M
+ # C = 'inside'
+ # C # => 'inside'
+ # "C".constantize # => 'outside', same as ::C
+ # end
+ #
+ # NameError is raised when the name is not in CamelCase or the constant is
+ # unknown.
+ def constantize(camel_cased_word)
+ names = camel_cased_word.split('::')
+ names.shift if names.empty? || names.first.empty?
+
+ constant = Object
+ names.each do |name|
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
+ end
+ constant
+ end
+ else
+ def constantize(camel_cased_word) #:nodoc:
+ names = camel_cased_word.split('::')
+ names.shift if names.empty? || names.first.empty?
- constant = Object
- names.each do |name|
- constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
+ constant = Object
+ names.each do |name|
+ constant = constant.const_get(name, false) || constant.const_missing(name)
+ end
+ constant
end
- constant
end
# Turns a number into an ordinal string used to denote the position in an
diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb
index 0e2a2d7051..d4adbde727 100755
--- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb
@@ -9,14 +9,14 @@ require 'i18n/backend/simple'
require 'i18n/exceptions'
module I18n
- @@backend = nil
+ @@backend = Backend::Simple
@@default_locale = 'en-US'
@@exception_handler = :default_exception_handler
class << self
# Returns the current backend. Defaults to +Backend::Simple+.
def backend
- @@backend ||= Backend::Simple.new
+ @@backend
end
# Sets the current backend. Used to set a custom backend.
@@ -183,8 +183,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/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb
index 6c7c54777c..917646fffe 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb
@@ -2,188 +2,194 @@ require 'strscan'
module I18n
module Backend
- class Simple
- # Allow client libraries to pass a block that populates the translation
- # 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)
- yield
- end
-
- # Accepts a list of paths to translation files. Loads translations from
- # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
- # for details.
- def load_translations(*filenames)
- filenames.each {|filename| load_file filename }
- end
-
- # Stores translations for the given locale in memory.
- # This uses a deep merge for the translations hash, so existing
- # translations will be overwritten by new ones only at the deepest
- # level of the hash.
- def store_translations(locale, data)
- merge_translations(locale, data)
- end
-
- 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
-
- reserved = :scope, :default
- count, scope, default = options.values_at(:count, *reserved)
- options.delete(:default)
- values = options.reject{|name, value| reserved.include? name }
-
- entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
- entry = pluralize locale, entry, count
- entry = interpolate locale, entry, values
- entry
- end
-
- # Acts the same as +strftime+, but returns a localized version of the
- # formatted date string. Takes a key from the date/time formats
- # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
- def localize(locale, object, format = :default)
- raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
-
- type = object.respond_to?(:sec) ? 'time' : 'date'
- formats = translate(locale, :"#{type}.formats")
- format = formats[format.to_sym] if formats && formats[format.to_sym]
- # TODO raise exception unless format found?
- format = format.to_s.dup
-
- format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
- format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
- format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
- format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
- format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
- object.strftime(format)
- end
+ module Simple
+ @@translations = {}
- protected
-
- def translations
- @translations ||= {}
- end
-
- # Looks up a translation from the translations hash. Returns nil if
- # eiher key is nil, or locale, scope or key do not exist as a key in the
- # nested translations hash. Splits keys or scopes containing dots
- # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
- # <tt>%w(currency format)</tt>.
- 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 }
+ class << self
+ # Allow client libraries to pass a block that populates the translation
+ # 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)
+ yield
end
- # Evaluates a default translation.
- # If the given default is a String it is used literally. If it is a Symbol
- # it will be translated with the given options. If it is an Array the first
- # translation yielded will be returned.
- #
- # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
- # <tt>translate(locale, :foo)</tt> does not yield a result.
- def default(locale, default, options = {})
- case default
- when String then default
- when Symbol then translate locale, default, options
- when Array then default.each do |obj|
- result = default(locale, obj, options.dup) and return result
- end and nil
- end
- rescue MissingTranslationData
- nil
+ # Accepts a list of paths to translation files. Loads translations from
+ # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
+ # for details.
+ def load_translations(*filenames)
+ filenames.each {|filename| load_file filename }
end
-
- # Picks a translation from an array according to English pluralization
- # rules. It will pick the first translation if count is not equal to 1
- # and the second translation if it is equal to 1. Other backends can
- # implement more flexible or complex pluralization rules.
- def pluralize(locale, entry, count)
- return entry unless entry.is_a?(Hash) and count
- # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
- key = :zero if count == 0 && entry.has_key?(:zero)
- key ||= count == 1 ? :one : :other
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
- entry[key]
+
+ # Stores translations for the given locale in memory.
+ # This uses a deep merge for the translations hash, so existing
+ # translations will be overwritten by new ones only at the deepest
+ # level of the hash.
+ def store_translations(locale, data)
+ merge_translations(locale, data)
end
-
- # Interpolates values into a given string.
- #
- # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
- # # => "file test.txt opened by {{user}}"
- #
- # Note that you have to double escape the <tt>\\</tt> when you want to escape
- # the <tt>{{...}}</tt> key in a string (once for the string and once for the
- # interpolation).
- def interpolate(locale, string, values = {})
- return string if !string.is_a?(String)
-
- map = {'%d' => '{{count}}', '%s' => '{{value}}'} # TODO deprecate this?
- string.gsub!(/#{map.keys.join('|')}/){|key| map[key]}
- s = StringScanner.new string.dup
- while s.skip_until(/\{\{/)
- s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
- start_pos = s.pos - 2
- key = s.scan_until(/\}\}/)[0..-3]
- end_pos = s.pos - 1
+ def translate(locale, key, options = {})
+ raise InvalidLocale.new(locale) if locale.nil?
+ return key.map{|k| translate locale, k, options } if key.is_a? Array
- raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
- raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
+ reserved = :scope, :default
+ count, scope, default = options.values_at(:count, *reserved)
+ options.delete(:default)
+ values = options.reject{|name, value| reserved.include? name }
- s.string[start_pos..end_pos] = values[key.to_sym].to_s
- s.unscan
- end
- s.string
+ entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
+ entry = pluralize locale, entry, count
+ entry = interpolate locale, entry, values
+ entry
end
- # Loads a single translations file by delegating to #load_rb or
- # #load_yml depending on the file extension and directly merges the
- # data to the existing translations. Raises I18n::UnknownFileType
- # for all other file extensions.
- def load_file(filename)
- type = File.extname(filename).tr('.', '').downcase
- raise UnknownFileType.new(type, filename) unless respond_to? :"load_#{type}"
- data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
- data.each do |locale, data|
- merge_translations locale, data
- end
+ # Acts the same as +strftime+, but returns a localized version of the
+ # formatted date string. Takes a key from the date/time formats
+ # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
+ def localize(locale, object, format = :default)
+ raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
+
+ type = object.respond_to?(:sec) ? 'time' : 'date'
+ formats = translate(locale, :"#{type}.formats")
+ format = formats[format.to_sym] if formats && formats[format.to_sym]
+ # TODO raise exception unless format found?
+ format = format.to_s.dup
+
+ format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
+ format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
+ format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
+ format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
+ format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
+ object.strftime(format)
end
- # Loads a plain Ruby translations file. eval'ing the file must yield
- # a Hash containing translation data with locales as toplevel keys.
- def load_rb(filename)
- eval IO.read(filename), binding, filename
- end
+ protected
- # Loads a YAML translations file. The data must have locales as
- # toplevel keys.
- def load_yml(filename)
- YAML::load IO.read(filename)
- end
+ # Looks up a translation from the translations hash. Returns nil if
+ # eiher key is nil, or locale, scope or key do not exist as a key in the
+ # nested translations hash. Splits keys or scopes containing dots
+ # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
+ # <tt>%w(currency format)</tt>.
+ def lookup(locale, key, scope = [])
+ return unless key
+ keys = I18n.send :normalize_translation_keys, locale, key, scope
+ keys.inject(@@translations){|result, k| result[k.to_sym] or return nil }
+ end
- # Deep merges the given translations hash with the existing translations
- # for the given locale
- def merge_translations(locale, data)
- locale = locale.to_sym
- translations[locale] ||= {}
- data = deep_symbolize_keys data
-
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
- merger = proc{|key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
- translations[locale].merge! data, &merger
- end
+ # Evaluates a default translation.
+ # If the given default is a String it is used literally. If it is a Symbol
+ # it will be translated with the given options. If it is an Array the first
+ # translation yielded will be returned.
+ #
+ # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
+ # <tt>translate(locale, :foo)</tt> does not yield a result.
+ def default(locale, default, options = {})
+ case default
+ when String then default
+ when Symbol then translate locale, default, options
+ when Array then default.each do |obj|
+ result = default(locale, obj, options.dup) and return result
+ end and nil
+ end
+ rescue MissingTranslationData
+ nil
+ end
- # Return a new hash with all keys and nested keys converted to symbols.
- def deep_symbolize_keys(hash)
- hash.inject({}){|result, (key, value)|
- value = deep_symbolize_keys(value) if value.is_a? Hash
- result[(key.to_sym rescue key) || key] = value
+ # Picks a translation from an array according to English pluralization
+ # rules. It will pick the first translation if count is not equal to 1
+ # and the second translation if it is equal to 1. Other backends can
+ # implement more flexible or complex pluralization rules.
+ def pluralize(locale, entry, count)
+ return entry unless entry.is_a?(Hash) and count
+ # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
+ key = :zero if count == 0 && entry.has_key?(:zero)
+ key ||= count == 1 ? :one : :many
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
+ entry[key]
+ end
+
+ # Interpolates values into a given string.
+ #
+ # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
+ # # => "file test.txt opened by {{user}}"
+ #
+ # Note that you have to double escape the <tt>\\</tt> when you want to escape
+ # the <tt>{{...}}</tt> key in a string (once for the string and once for the
+ # interpolation).
+ def interpolate(locale, string, values = {})
+ return string if !string.is_a?(String)
+
+ string = string.gsub(/%d/, '{{count}}').gsub(/%s/, '{{value}}')
+ if string.respond_to?(:force_encoding)
+ original_encoding = string.encoding
+ string.force_encoding(Encoding::BINARY)
+ end
+
+ s = StringScanner.new(string)
+ while s.skip_until(/\{\{/)
+ s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
+ start_pos = s.pos - 2
+ key = s.scan_until(/\}\}/)[0..-3]
+ end_pos = s.pos - 1
+
+ raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
+ raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
+
+ s.string[start_pos..end_pos] = values[key.to_sym].to_s
+ s.unscan
+ end
+
+ result = s.string
+ result.force_encoding(original_encoding) if original_encoding
result
- }
- end
+ end
+
+ # Loads a single translations file by delegating to #load_rb or
+ # #load_yml depending on the file extension and directly merges the
+ # data to the existing translations. Raises I18n::UnknownFileType
+ # for all other file extensions.
+ def load_file(filename)
+ type = File.extname(filename).tr('.', '').downcase
+ raise UnknownFileType.new(type, filename) unless respond_to? :"load_#{type}"
+ data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
+ data.each do |locale, d|
+ merge_translations locale, d
+ end
+ end
+
+ # Loads a plain Ruby translations file. eval'ing the file must yield
+ # a Hash containing translation data with locales as toplevel keys.
+ def load_rb(filename)
+ eval IO.read(filename), binding, filename
+ end
+
+ # Loads a YAML translations file. The data must have locales as
+ # toplevel keys.
+ def load_yml(filename)
+ YAML::load IO.read(filename)
+ end
+
+ # Deep merges the given translations hash with the existing translations
+ # for the given locale
+ def merge_translations(locale, data)
+ locale = locale.to_sym
+ @@translations[locale] ||= {}
+ data = deep_symbolize_keys data
+
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
+ merger = proc{|key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
+ @@translations[locale].merge! data, &merger
+ end
+
+ # Return a new hash with all keys and nested keys converted to symbols.
+ def deep_symbolize_keys(hash)
+ hash.inject({}){|result, (key, value)|
+ value = deep_symbolize_keys(value) if value.is_a? Hash
+ result[(key.to_sym rescue key) || key] = value
+ result
+ }
+ end
+ end
end
end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index b2fad4a9bc..8eebe1be25 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -34,6 +34,13 @@ class InflectorTest < Test::Unit::TestCase
end
end
+ def test_overwrite_previous_inflectors
+ assert_equal("series", ActiveSupport::Inflector.singularize("series"))
+ ActiveSupport::Inflector.inflections.singular "series", "serie"
+ assert_equal("serie", ActiveSupport::Inflector.singularize("series"))
+ ActiveSupport::Inflector.inflections.uncountable "series" # Return to normal
+ end
+
MixtureToTitleCase.each do |before, titleized|
define_method "test_titleize_#{before}" do
assert_equal(titleized, ActiveSupport::Inflector.titleize(before))
diff --git a/ci/ci_build.rb b/ci/ci_build.rb
new file mode 100755
index 0000000000..7d9cf679e2
--- /dev/null
+++ b/ci/ci_build.rb
@@ -0,0 +1,117 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+
+include FileUtils
+
+puts "[CruiseControl] Rails build"
+
+build_results = {}
+root_dir = File.expand_path(File.dirname(__FILE__) + "/..")
+
+# Requires gem home and path to be writeable and/or overridden to be ~/.gem,
+# Will enable when RubyGems supports this properly (in a coming release)
+# build_results[:geminstaller] = system 'geminstaller --exceptions'
+
+# for now, use the no-passwd sudoers approach (documented in ci_setup_notes.txt)
+# A security hole, but there is nothing valuable on rails CI box anyway.
+build_results[:geminstaller] = system 'sudo geminstaller --config=#{root_dir}/ci/geminstaller.yml --exceptions'
+
+cd "#{root_dir}/activesupport" do
+ puts
+ puts "[CruiseControl] Building ActiveSupport"
+ puts
+ build_results[:activesupport] = system 'rake'
+end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building ActiveRecord with MySQL"
+ puts
+ build_results[:activerecord_mysql] = system 'rake test_mysql'
+end
+
+# Postgres is disabled until tests are fixed
+# cd "#{root_dir}/activerecord" do
+# puts
+# puts "[CruiseControl] Building ActiveRecord with PostgreSQL"
+# puts
+# build_results[:activerecord_postgresql8] = system 'rake test_postgresql'
+# end
+
+# Sqlite2 is disabled until tests are fixed
+# cd "#{root_dir}/activerecord" do
+# puts
+# puts "[CruiseControl] Building ActiveRecord with SQLite 2"
+# puts
+# build_results[:activerecord_sqlite] = system 'rake test_sqlite'
+# end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building ActiveRecord with SQLite 3"
+ puts
+ build_results[:activerecord_sqlite3] = system 'rake test_sqlite3'
+end
+
+cd "#{root_dir}/activemodel" do
+ puts
+ puts "[CruiseControl] Building ActiveModel"
+ puts
+ build_results[:activemodel] = system 'rake'
+end
+
+cd "#{root_dir}/activeresource" do
+ puts
+ puts "[CruiseControl] Building ActiveResource"
+ puts
+ build_results[:activeresource] = system 'rake'
+end
+
+cd "#{root_dir}/actionpack" do
+ puts
+ puts "[CruiseControl] Building ActionPack"
+ puts
+ build_results[:actionpack] = system 'rake'
+end
+
+cd "#{root_dir}/actionmailer" do
+ puts
+ puts "[CruiseControl] Building ActionMailer"
+ puts
+ build_results[:actionmailer] = system 'rake'
+end
+
+cd "#{root_dir}/railties" do
+ puts
+ puts "[CruiseControl] Building RailTies"
+ puts
+ build_results[:railties] = system 'rake'
+end
+
+
+puts
+puts "[CruiseControl] Build environment:"
+puts "[CruiseControl] #{`cat /etc/issue`}"
+puts "[CruiseControl] #{`uname -a`}"
+puts "[CruiseControl] #{`ruby -v`}"
+puts "[CruiseControl] #{`mysql --version`}"
+puts "[CruiseControl] #{`pg_config --version`}"
+puts "[CruiseControl] SQLite2: #{`sqlite -version`}"
+puts "[CruiseControl] SQLite3: #{`sqlite3 -version`}"
+`gem env`.each {|line| print "[CruiseControl] #{line}"}
+puts "[CruiseControl] Local gems:"
+`gem list`.each {|line| print "[CruiseControl] #{line}"}
+
+failures = build_results.select { |key, value| value == false }
+
+if failures.empty?
+ puts
+ puts "[CruiseControl] Rails build finished sucessfully"
+ exit(0)
+else
+ puts
+ puts "[CruiseControl] Rails build FAILED"
+ puts "[CruiseControl] Failed components: #{failures.map { |component| component.first }.join(', ')}"
+ exit(-1)
+end
+
diff --git a/ci/ci_setup_notes.txt b/ci/ci_setup_notes.txt
new file mode 100644
index 0000000000..86df33c443
--- /dev/null
+++ b/ci/ci_setup_notes.txt
@@ -0,0 +1,120 @@
+# Rails Continuous Integration Server Setup Notes
+# This procedure was used to set up http://ci.rubyonrails.org on Ubuntu 8.04
+# It can be used as a guideline for setting up your own CI server against your local rails branches
+
+* Set up ci user:
+# log in as root
+$ adduser ci
+enter user info and password
+$ visudo
+# give ci user same sudo rights as root
+
+* Disable root login:
+# log in as ci
+$ sudo vi /etc/shadow
+# overwrite and disable encrypted root password to disable root login:
+root:*:14001:0:99999:7:::
+
+* Change Hostname:
+$ sudo vi /etc/hostname
+change to 'ci'
+$ sudo vi /etc/hosts
+replace old hostname with 'ci'
+# reboot to use new hostname (and test reboot)
+$ sudo shutdown -r now
+
+* Update aptitude:
+$ sudo aptitude update
+
+* Use cinabox to perform rest of ruby/ccrb setup:
+* http://github.com/thewoolleyman/cinabox/tree/master/README.txt
+
+# This is not yet properly supported by RubyGems...
+# * Configure RubyGems to not require root access for gem installation
+# $ vi ~/.profile
+# # add this line at bottom:
+# PATH="$HOME/.gem/ruby/1.8/bin:$PATH"
+# $ sudo vi /etc/init.d/cruise
+# # edit the start_cruise line to source CRUISE_USER/.profile:
+# start_cruise "cd #{CRUISE_HOME} && source /home/#{CRUISE_USER}/.profile && ./cruise start -d"
+# $ vi ~/.gemrc
+# # add these lines:
+# ---
+# gemhome: /home/ci/.gem/ruby/1.8
+# gempath:
+# - /home/ci/.gem/ruby/1.8
+
+* If you did not configure no-root-gem installation via ~/.gemrc as shown above, then allow no-password sudo for gem installation:
+$ sudo visudo
+# add this line to bottom:
+ci ALL=NOPASSWD: /usr/local/bin/geminstaller, /usr/local/bin/ruby, /usr/local/bin/gem
+
+* Start ccrb via init script and check for default homepage at port 3333
+
+* Install/setup nginx:
+$ sudo aptitude install nginx
+$ sudo vi /etc/nginx/sites-available/default
+# comment two lines and add one to proxy to ccrb:
+# root /var/www/nginx-default;
+# index index.html index.htm;
+ proxy_pass http://127.0.0.1:3333;
+$ sudo /etc/init.d/nginx start
+
+* Add project to cruise (It will still fail until everything is set up):
+$ cd ~/ccrb
+$ ./cruise add rails -s git -r git://github.com/rails/rails.git # or the URI of your branch
+
+* Copy and configure cruise site config file:
+$ cp ~/.cruise/projects/rails/work/ci/site_config.rb ~/.cruise/site_config.rb
+# Edit ~/.cruise/site_config.rb as desired, for example:
+ActionMailer::Base.smtp_settings = {
+ :address => "localhost",
+ :domain => "ci.yourdomain.com",
+}
+Configuration.dashboard_refresh_interval = 60.seconds
+Configuration.dashboard_url = 'http://ci.yourdomain.com/'
+Configuration.serialize_builds = true
+Configuration.serialized_build_timeout = 1.hours
+BuildReaper.number_of_builds_to_keep = 100
+
+* Copy and configure cruise project config file
+$ cp ~/.cruise/projects/rails/work/ci/cruise_config.rb ~/.cruise/projects/rails
+$ vi ~/.cruise/projects/rails/cruise_config.rb:
+# Edit ~/.cruise/projects/rails/cruise_config.rb as desired, for example:
+Project.configure do |project|
+ project.build_command = 'ruby ci/ci_build.rb'
+ project.email_notifier.emails = ['recipient@yourdomain.com']
+ project.email_notifier.from = 'sender@yourdomain.com'
+end
+
+* Set up mysql
+$ sudo aptitude install mysql-server-5.0 libmysqlclient-dev
+# no password for mysql root user
+
+* setup sqlite
+$ sudo aptitude install sqlite sqlite3 libsqlite-dev libsqlite3-dev
+# Note: there's some installation bugs with sqlite3-ruby 1.2.2 gem file permissions:
+# http://www.icoretech.org/2008/07/06/no-such-file-to-load-sqlite3-database
+# cd /usr/local/lib/ruby/gems/1.8/gems/sqlite3-ruby-1.2.2 && sudo find . -perm 0662 -exec chmod 664 {} \;
+
+* setup postgres
+$ sudo aptitude install postgresql postgresql-server-dev-8.3
+$ sudo su - postgres -c 'createuser -s ci'
+
+* Install and run GemInstaller to get all dependency gems
+$ sudo gem install geminstaller
+$ cd ~/.cruise/projects/rails/work
+$ sudo geminstaller --config=ci/geminstaller.yml # turn up debugging with these options: --geminstaller-output=all --rubygems-output=all
+
+* Create ActiveRecord test databases for mysql
+$ mysql -uroot -e 'grant all on *.* to rails@localhost;'
+$ mysql -urails -e 'create database activerecord_unittest;'
+$ mysql -urails -e 'create database activerecord_unittest2;'
+
+* Create ActiveRecord test databases for postgres
+# cd to rails activerecord dir
+$ rake postgresql:build_databases
+
+* Reboot and make sure everything is working
+$ sudo shutdown -r now
+$ http://ci.yourdomain.com \ No newline at end of file
diff --git a/ci/cruise_config.rb b/ci/cruise_config.rb
new file mode 100644
index 0000000000..ec1c82a8bf
--- /dev/null
+++ b/ci/cruise_config.rb
@@ -0,0 +1,5 @@
+Project.configure do |project|
+ project.build_command = 'ruby ci/ci_build.rb'
+ project.email_notifier.emails = ['thewoolleyman@gmail.com','michael@koziarski.com']
+ project.email_notifier.from = 'thewoolleyman+railsci@gmail.com'
+end
diff --git a/ci/geminstaller.yml b/ci/geminstaller.yml
new file mode 100644
index 0000000000..3a1862c3c3
--- /dev/null
+++ b/ci/geminstaller.yml
@@ -0,0 +1,17 @@
+---
+gems:
+- name: geminstaller
+ version: >= 0.4.3
+- name: mocha
+ version: >= 0.9.0
+- name: mysql
+ #version: >= 2.7
+ version: = 2.7
+- name: postgres
+ version: >= 0.7.9.2008.01.28
+- name: rake
+ version: >= 0.8.1
+- name: sqlite-ruby
+ version: >= 2.2.3
+- name: sqlite3-ruby
+ version: >= 1.2.2
diff --git a/ci/site.css b/ci/site.css
new file mode 100644
index 0000000000..e771c5d1fd
--- /dev/null
+++ b/ci/site.css
@@ -0,0 +1,13 @@
+/* this is a copy of /home/ci/.cruise/site.css, please make any changes to it there */
+
+/* this is a copy of /home/ci/.cruise/site.css, please make any changes to it there */
+
+/* if you'd like to add custom styles to cruise, add them here */
+/* the following will make successful builds green */
+a.success, a.success:visited {
+ color: #0A0;
+}
+
+.build_success {
+ background-image: url(/images/green_gradient.png);
+}
diff --git a/ci/site_config.rb b/ci/site_config.rb
new file mode 100644
index 0000000000..47b7e92120
--- /dev/null
+++ b/ci/site_config.rb
@@ -0,0 +1,72 @@
+# site_config.rb contains examples of various configuration options for the local installation
+# of CruiseControl.rb.
+
+# YOU MUST RESTART YOUR CRUISE CONTROL SERVER FOR ANY CHANGES MADE HERE TO TAKE EFFECT!!!
+
+# EMAIL NOTIFICATION
+# ------------------
+
+# CruiseControl.rb can notify you about build status via email. It uses ActionMailer component of Ruby on Rails
+# framework. Obviously, ActionMailer needs to know how to send out email messages.
+# If you have an SMTP server on your network, and it needs no authentication, write this in your site_config.rb:
+#
+ActionMailer::Base.smtp_settings = {
+ :address => "localhost",
+ :domain => "ci.rubyonrails.org",
+}
+#
+# If you have no SMTP server at hand, you can configure email notification to use GMail SMTP server, as follows
+# (of course, you'll need to create a GMail account):
+#
+# ActionMailer::Base.smtp_settings = {
+# :address => "smtp.gmail.com",
+# :port => 587,
+# :domain => "yourdomain.com",
+# :authentication => :plain,
+# :user_name => "yourgmailaccount",
+# :password => "yourgmailpassword"
+# }
+#
+# The same approach works for other SMTP servers thet require authentication. Note that GMail's SMTP server runs on a
+# non-standard port 587 (standard port for SMTP is 25).
+#
+# For further details about configuration of outgoing email, see Ruby On Rails documentation for ActionMailer::Base.
+
+# Other site-wide options are available through Configuration class:
+
+# Change how often CC.rb pings Subversion for new requests. Default is 10.seconds, which should be OK for a local
+# SVN repository, but probably isn't very polite for a public repository, such as RubyForge. This can also be set for
+# each project individually, through project.scheduler.polling_interval option:
+# Configuration.default_polling_interval = 1.minute
+
+# How often the dashboard page refreshes itself. If you have more than 10-20 dashboards open,
+# it is advisable to set it to something higher than the default 5 seconds:
+Configuration.dashboard_refresh_interval = 60.seconds
+
+# Site-wide setting for the email "from" field. This can also be set on per-project basis,
+# through project.email.notifier.from attribute
+Configuration.email_from = 'thewoolleyman+railsci@gmail.com'
+
+# Root URL of the dashboard application. Setting this attribute allows various notifiers to include a link to the
+# build page in the notification message.
+Configuration.dashboard_url = 'http://ci.rubyonrails.org/'
+
+# If you don't want to allow triggering builds through dashboard Build Now button. Useful when you host CC.rb as a
+# public web site (such as http://cruisecontrolrb.thoughtworks.com/projects - try clicking on Build Now button there
+# and see what happens):
+# Configuration.disable_build_now = true
+
+# If you want to only allow one project to build at a time, uncomment this line
+# by default, cruise allows multiple projects to build at a time
+Configuration.serialize_builds = true
+
+# Amount of time a project will wait to build before failing when build serialization is on
+Configuration.serialized_build_timeout = 3.hours
+
+# To delete build when there are more than a certain number present, uncomment this line - it will make the dashboard
+# perform better
+BuildReaper.number_of_builds_to_keep = 100
+
+# any files that you'd like to override in cruise, keep in ~/.cruise, and copy over when this file is loaded like this
+site_css = CRUISE_DATA_ROOT + "/site.css"
+FileUtils.cp site_css, RAILS_ROOT + "/public/stylesheets/site.css" if File.exists? site_css
diff --git a/railties/lib/commands/dbconsole.rb b/railties/lib/commands/dbconsole.rb
index 442526ae32..5be3b5dd8e 100644
--- a/railties/lib/commands/dbconsole.rb
+++ b/railties/lib/commands/dbconsole.rb
@@ -47,7 +47,7 @@ when "mysql"
args << config['database']
- exec(find_cmd('mysql5', 'mysql'), *args)
+ exec(find_cmd('mysql', 'mysql5'), *args)
when "postgresql"
ENV['PGUSER'] = config["username"] if config["username"]
diff --git a/railties/lib/rails_generator/generated_attribute.rb b/railties/lib/rails_generator/generated_attribute.rb
index 25af3931de..a3d4a01142 100644
--- a/railties/lib/rails_generator/generated_attribute.rb
+++ b/railties/lib/rails_generator/generated_attribute.rb
@@ -37,6 +37,10 @@ module Rails
""
end
end
+
+ def reference?
+ [ :references, :belongs_to ].include?(self.type)
+ end
end
end
end
diff --git a/railties/lib/rails_generator/generators/components/model/templates/model.rb b/railties/lib/rails_generator/generators/components/model/templates/model.rb
index 8d4c89e912..6fcf393bdf 100644
--- a/railties/lib/rails_generator/generators/components/model/templates/model.rb
+++ b/railties/lib/rails_generator/generators/components/model/templates/model.rb
@@ -1,2 +1,5 @@
class <%= class_name %> < ActiveRecord::Base
+<% attributes.select(&:reference?).each do |attribute| -%>
+ belongs_to :<%= attribute.name %>
+<% end -%>
end
diff --git a/railties/test/generators/rails_model_generator_test.rb b/railties/test/generators/rails_model_generator_test.rb
index 0bfc338566..aea2abafba 100644
--- a/railties/test/generators/rails_model_generator_test.rb
+++ b/railties/test/generators/rails_model_generator_test.rb
@@ -29,4 +29,20 @@ class RailsModelGeneratorTest < GeneratorTestCase
assert_generated_column t, :created_at, :timestamp
end
end
+
+ def test_model_with_reference_attributes_generates_belongs_to_associations
+ run_generator('model', %w(Product name:string supplier:references))
+
+ assert_generated_model_for :product do |body|
+ assert body =~ /^\s+belongs_to :supplier/, "#{body.inspect} should contain 'belongs_to :supplier'"
+ end
+ end
+
+ def test_model_with_belongs_to_attributes_generates_belongs_to_associations
+ run_generator('model', %w(Product name:string supplier:belongs_to))
+
+ assert_generated_model_for :product do |body|
+ assert body =~ /^\s+belongs_to :supplier/, "#{body.inspect} should contain 'belongs_to :supplier'"
+ end
+ end
end