aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--actionpack/actionpack.gemspec2
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb3
-rw-r--r--actionpack/test/template/form_helper_test.rb12
-rw-r--r--activemodel/lib/active_model/observer_array.rb98
-rw-r--r--activemodel/lib/active_model/observing.rb33
-rw-r--r--activemodel/test/cases/observer_array_test.rb122
-rw-r--r--activemodel/test/cases/observing_test.rb5
-rw-r--r--activemodel/test/models/observers.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb1
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb10
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb2
-rw-r--r--activesupport/test/caching_test.rb38
-rw-r--r--activesupport/test/inflector_test.rb42
14 files changed, 375 insertions, 22 deletions
diff --git a/Gemfile b/Gemfile
index 98272c7261..8b6ddbd7c9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -54,7 +54,7 @@ platforms :ruby do
group :db do
gem "pg", ">= 0.11.0"
gem "mysql", ">= 2.8.1"
- gem "mysql2", :git => "git://github.com/brianmario/mysql2.git"
+ gem "mysql2", ">= 0.3.0"
end
end
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index f771737779..0d667a76a7 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency('i18n', '~> 0.6.0beta1')
s.add_dependency('rack', '~> 1.2.1')
s.add_dependency('rack-test', '~> 0.5.7')
- s.add_dependency('rack-mount', '~> 0.7.1')
+ s.add_dependency('rack-mount', '~> 0.7.2')
s.add_dependency('tzinfo', '~> 0.3.23')
s.add_dependency('erubis', '~> 2.7.0')
end
diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb
index 440acafa88..efe30441b1 100644
--- a/actionpack/lib/action_view/helpers/form_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_helper.rb
@@ -947,7 +947,8 @@ module ActionView
label_tag(name_and_id["id"], options, &block)
else
content = if text.blank?
- I18n.t("helpers.label.#{object_name}.#{method_name}", :default => "").presence
+ method_and_value = tag_value.present? ? "#{method_name}.#{tag_value}" : method_name
+ I18n.t("helpers.label.#{object_name}.#{method_and_value}", :default => "").presence
else
text.to_s
end
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb
index 7afab3179c..c25c850eb3 100644
--- a/actionpack/test/template/form_helper_test.rb
+++ b/actionpack/test/template/form_helper_test.rb
@@ -24,7 +24,10 @@ class FormHelperTest < ActionView::TestCase
:helpers => {
:label => {
:post => {
- :body => "Write entire text here"
+ :body => "Write entire text here",
+ :color => {
+ :red => "Rojo"
+ }
}
}
}
@@ -141,6 +144,13 @@ class FormHelperTest < ActionView::TestCase
I18n.locale = old_locale
end
+ def test_label_with_locales_and_value
+ old_locale, I18n.locale = I18n.locale, :label
+ assert_dom_equal('<label for="post_color_red">Rojo</label>', label(:post, :color, :value => "red"))
+ ensure
+ I18n.locale = old_locale
+ end
+
def test_label_with_for_attribute_as_symbol
assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, :for => "my_for"))
end
diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb
new file mode 100644
index 0000000000..b8aa9cc1e2
--- /dev/null
+++ b/activemodel/lib/active_model/observer_array.rb
@@ -0,0 +1,98 @@
+require 'set'
+
+module ActiveModel
+ # Stores the enabled/disabled state of individual observers for
+ # a particular model classes.
+ class ObserverArray < Array
+ INSTANCES = Hash.new do |hash, model_class|
+ hash[model_class] = new(model_class)
+ end
+
+ def self.for(model_class)
+ return nil unless model_class < ActiveModel::Observing
+ INSTANCES[model_class]
+ end
+
+ # returns false if:
+ # - the ObserverArray for the given model's class has the given observer
+ # in its disabled_observers set.
+ # - or that is the case at any level of the model's superclass chain.
+ def self.observer_enabled?(observer, model)
+ klass = model.class
+ observer_class = observer.class
+
+ loop do
+ break unless array = self.for(klass)
+ return false if array.disabled_observers.include?(observer_class)
+ klass = klass.superclass
+ end
+
+ true # observers are enabled by default
+ end
+
+ def disabled_observers
+ @disabled_observers ||= Set.new
+ end
+
+ attr_reader :model_class
+ def initialize(model_class, *args)
+ @model_class = model_class
+ super(*args)
+ end
+
+ def disable(*observers, &block)
+ set_enablement(false, observers, &block)
+ end
+
+ def enable(*observers, &block)
+ set_enablement(true, observers, &block)
+ end
+
+ private
+
+ def observer_class_for(observer)
+ return observer if observer.is_a?(Class)
+
+ if observer.respond_to?(:to_sym) # string/symbol
+ observer.to_s.camelize.constantize
+ else
+ raise ArgumentError, "#{observer} was not a class or a " +
+ "lowercase, underscored class name as expected."
+ end
+ end
+
+ def transaction
+ orig_disabled_observers = disabled_observers.dup
+
+ begin
+ yield
+ ensure
+ @disabled_observers = orig_disabled_observers
+ end
+ end
+
+ def set_enablement(enabled, observers)
+ if block_given?
+ transaction do
+ set_enablement(enabled, observers)
+ yield
+ end
+ else
+ observers = ActiveModel::Observer.all_observers if observers == [:all]
+ observers.each do |obs|
+ klass = observer_class_for(obs)
+
+ unless klass < ActiveModel::Observer
+ raise ArgumentError.new("#{obs} does not refer to a valid observer")
+ end
+
+ if enabled
+ disabled_observers.delete(klass)
+ else
+ disabled_observers << klass
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb
index 3c80d584fe..e1a2ce218d 100644
--- a/activemodel/lib/active_model/observing.rb
+++ b/activemodel/lib/active_model/observing.rb
@@ -1,8 +1,10 @@
require 'singleton'
+require 'active_model/observer_array'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/enumerable'
module ActiveModel
module Observing
@@ -30,12 +32,12 @@ module ActiveModel
# +instantiate_observers+ is called during startup, and before
# each development request.
def observers=(*values)
- @observers = values.flatten
+ observers.replace(values.flatten)
end
# Gets the current observers.
def observers
- @observers ||= []
+ @observers ||= ObserverArray.for(self)
end
# Gets the current observer instances.
@@ -76,7 +78,11 @@ module ActiveModel
elsif observer.respond_to?(:instance)
observer.instance
else
- raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
+ raise ArgumentError,
+ "#{observer} must be a lowercase, underscored class name (or an " +
+ "instance of the class itself) responding to the instance " +
+ "method. Example: Person.observers = :big_brother # calls " +
+ "BigBrother.instance"
end
end
@@ -197,6 +203,23 @@ module ActiveModel
nil
end
end
+
+ def subclasses
+ @subclasses ||= []
+ end
+
+ # List of all observer subclasses, sub-subclasses, etc.
+ # Necessary so we can disable or enable all observers.
+ def all_observers
+ subclasses.each_with_object(subclasses.dup) do |subclass, array|
+ array.concat(subclass.all_observers)
+ end
+ end
+ end
+
+ def self.inherited(subclass)
+ subclasses << subclass
+ super
end
# Start observing the declared classes and their subclasses.
@@ -210,7 +233,9 @@ module ActiveModel
# Send observed_method(object) if the method exists.
def update(observed_method, object) #:nodoc:
- send(observed_method, object) if respond_to?(observed_method)
+ if respond_to?(observed_method) && ObserverArray.observer_enabled?(self, object)
+ send(observed_method, object)
+ end
end
# Special method sent by the observed class when it is inherited.
diff --git a/activemodel/test/cases/observer_array_test.rb b/activemodel/test/cases/observer_array_test.rb
new file mode 100644
index 0000000000..215ca80bb4
--- /dev/null
+++ b/activemodel/test/cases/observer_array_test.rb
@@ -0,0 +1,122 @@
+require 'cases/helper'
+require 'models/observers'
+
+class ObserverArrayTest < ActiveModel::TestCase
+ def teardown
+ ORM.observers.enable :all
+ Budget.observers.enable :all
+ Widget.observers.enable :all
+ end
+
+ def assert_observer_notified(model_class, observer_class)
+ observer_class.instance.before_save_invocations.clear
+ model_instance = model_class.new
+ model_instance.save
+ assert_equal [model_instance], observer_class.instance.before_save_invocations
+ end
+
+ def assert_observer_not_notified(model_class, observer_class)
+ observer_class.instance.before_save_invocations.clear
+ model_instance = model_class.new
+ model_instance.save
+ assert_equal [], observer_class.instance.before_save_invocations
+ end
+
+ test "all observers are enabled by default" do
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable individual observers using a class constant" do
+ ORM.observers.disable WidgetObserver
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable individual observers using a symbol" do
+ ORM.observers.disable :budget_observer
+
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable all observers using :all" do
+ ORM.observers.disable :all
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_not_notified Budget, AuditTrail
+ end
+
+ test "can disable observers on individual models without affecting observers on other models" do
+ Widget.observers.disable :all
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable observers for the duration of a block" do
+ yielded = false
+ ORM.observers.disable :budget_observer do
+ yielded = true
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ assert yielded
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can enable observers for the duration of a block" do
+ yielded = false
+ Widget.observers.disable :all
+
+ Widget.observers.enable :all do
+ yielded = true
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ assert yielded
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "raises an appropriate error when a developer accidentally enables or disables the wrong class (i.e. Widget instead of WidgetObserver)" do
+ assert_raise ArgumentError do
+ ORM.observers.enable :widget
+ end
+
+ assert_raise ArgumentError do
+ ORM.observers.enable Widget
+ end
+
+ assert_raise ArgumentError do
+ ORM.observers.disable :widget
+ end
+
+ assert_raise ArgumentError do
+ ORM.observers.disable Widget
+ end
+ end
+end
+
diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb
index 63686843b6..99b1f407ae 100644
--- a/activemodel/test/cases/observing_test.rb
+++ b/activemodel/test/cases/observing_test.rb
@@ -43,6 +43,11 @@ class ObservingTest < ActiveModel::TestCase
assert ObservedModel.observers.include?(:bar), ":bar not in #{ObservedModel.observers.inspect}"
end
+ test "uses an ObserverArray so observers can be disabled" do
+ ObservedModel.observers = [:foo, :bar]
+ assert ObservedModel.observers.is_a?(ActiveModel::ObserverArray)
+ end
+
test "instantiates observer names passed as strings" do
ObservedModel.observers << 'foo_observer'
FooObserver.expects(:instance)
diff --git a/activemodel/test/models/observers.rb b/activemodel/test/models/observers.rb
new file mode 100644
index 0000000000..3729b3435e
--- /dev/null
+++ b/activemodel/test/models/observers.rb
@@ -0,0 +1,27 @@
+class ORM
+ include ActiveModel::Observing
+
+ def save
+ notify_observers :before_save
+ end
+
+ class Observer < ActiveModel::Observer
+ def before_save_invocations
+ @before_save_invocations ||= []
+ end
+
+ def before_save(record)
+ before_save_invocations << record
+ end
+ end
+end
+
+class Widget < ORM; end
+class Budget < ORM; end
+class WidgetObserver < ORM::Observer; end
+class BudgetObserver < ORM::Observer; end
+class AuditTrail < ORM::Observer
+ observe :widget, :budget
+end
+
+ORM.instantiate_observers
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 7ac72acd58..cf68ddc2da 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,5 +1,6 @@
# encoding: utf-8
+gem 'mysql2', '~> 0.3.0'
require 'mysql2'
module ActiveRecord
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 45263d482f..7ef1497ac2 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -4,7 +4,9 @@ rescue LoadError => e
$stderr.puts "You don't have memcache-client installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
+
require 'digest/md5'
+require 'active_support/core_ext/string/encoding'
module ActiveSupport
module Cache
@@ -157,8 +159,14 @@ module ActiveSupport
end
private
+
+ # Memcache keys are binaries. So we need to force their encoding to binary
+ # before applying the regular expression to ensure we are escaping all
+ # characters properly.
def escape_key(key)
- key = key.to_s.gsub(ESCAPE_KEY_CHARS){|match| "%#{match.getbyte(0).to_s(16).upcase}"}
+ key = key.to_s.dup
+ key = key.force_encoding("BINARY") if key.encoding_aware?
+ key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" }
key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
key
end
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index e136e4c5b3..d5d55b7207 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -96,7 +96,7 @@ module ActiveSupport
def clear(scope = :all)
case scope
when :all
- @plurals, @singulars, @uncountables = [], [], []
+ @plurals, @singulars, @uncountables, @humans = [], [], [], []
else
instance_variable_set "@#{scope}", []
end
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index e5668e29d7..476d55fffd 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -352,6 +352,43 @@ module CacheStoreBehavior
end
end
+# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
+# The error is caused by charcter encodings that can't be compared with ASCII-8BIT regular expressions and by special
+# characters like the umlaut in UTF-8.
+module EncodedKeyCacheBehavior
+ if defined?(Encoding)
+ Encoding.list.each do |encoding|
+ define_method "test_#{encoding.name.underscore}_encoded_values" do
+ key = "foo".force_encoding(encoding)
+ assert_equal true, @cache.write(key, "1", :raw => true)
+ assert_equal "1", @cache.read(key)
+ assert_equal "1", @cache.fetch(key)
+ assert_equal true, @cache.delete(key)
+ assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
+ assert_equal 3, @cache.increment(key)
+ assert_equal 2, @cache.decrement(key)
+ end
+ end
+
+ def test_common_utf8_values
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
+ assert_equal true, @cache.write(key, "1", :raw => true)
+ assert_equal "1", @cache.read(key)
+ assert_equal "1", @cache.fetch(key)
+ assert_equal true, @cache.delete(key)
+ assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
+ assert_equal 3, @cache.increment(key)
+ assert_equal 2, @cache.decrement(key)
+ end
+
+ def test_retains_encoding
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
+ assert_equal true, @cache.write(key, "1", :raw => true)
+ assert_equal Encoding::UTF_8, key.encoding
+ end
+ end
+end
+
module CacheDeleteMatchedBehavior
def test_delete_matched
@cache.write("foo", "bar")
@@ -617,6 +654,7 @@ uses_memcached 'memcached backed store' do
include CacheStoreBehavior
include LocalCacheBehavior
include CacheIncrementDecrementBehavior
+ include EncodedKeyCacheBehavior
def test_raw_values
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index 1670d9ee7d..95f18126d4 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -255,12 +255,21 @@ class InflectorTest < Test::Unit::TestCase
end
def test_clear_all
- cached_values = ActiveSupport::Inflector.inflections.plurals, ActiveSupport::Inflector.inflections.singulars, ActiveSupport::Inflector.inflections.uncountables, ActiveSupport::Inflector.inflections.humans
- ActiveSupport::Inflector.inflections.clear :all
- assert ActiveSupport::Inflector.inflections.plurals.empty?
- assert ActiveSupport::Inflector.inflections.singulars.empty?
- assert ActiveSupport::Inflector.inflections.uncountables.empty?
- assert ActiveSupport::Inflector.inflections.humans.empty?
+ cached_values = ActiveSupport::Inflector.inflections.plurals.dup, ActiveSupport::Inflector.inflections.singulars.dup, ActiveSupport::Inflector.inflections.uncountables.dup, ActiveSupport::Inflector.inflections.humans.dup
+ ActiveSupport::Inflector.inflections do |inflect|
+ # ensure any data is present
+ inflect.plural(/(quiz)$/i, '\1zes')
+ inflect.singular(/(database)s$/i, '\1')
+ inflect.uncountable('series')
+ inflect.human("col_rpted_bugs", "Reported bugs")
+
+ inflect.clear :all
+
+ assert inflect.plurals.empty?
+ assert inflect.singulars.empty?
+ assert inflect.uncountables.empty?
+ assert inflect.humans.empty?
+ end
ActiveSupport::Inflector.inflections.instance_variable_set :@plurals, cached_values[0]
ActiveSupport::Inflector.inflections.instance_variable_set :@singulars, cached_values[1]
ActiveSupport::Inflector.inflections.instance_variable_set :@uncountables, cached_values[2]
@@ -268,12 +277,21 @@ class InflectorTest < Test::Unit::TestCase
end
def test_clear_with_default
- cached_values = ActiveSupport::Inflector.inflections.plurals, ActiveSupport::Inflector.inflections.singulars, ActiveSupport::Inflector.inflections.uncountables, ActiveSupport::Inflector.inflections.humans
- ActiveSupport::Inflector.inflections.clear
- assert ActiveSupport::Inflector.inflections.plurals.empty?
- assert ActiveSupport::Inflector.inflections.singulars.empty?
- assert ActiveSupport::Inflector.inflections.uncountables.empty?
- assert ActiveSupport::Inflector.inflections.humans.empty?
+ cached_values = ActiveSupport::Inflector.inflections.plurals.dup, ActiveSupport::Inflector.inflections.singulars.dup, ActiveSupport::Inflector.inflections.uncountables.dup, ActiveSupport::Inflector.inflections.humans.dup
+ ActiveSupport::Inflector.inflections do |inflect|
+ # ensure any data is present
+ inflect.plural(/(quiz)$/i, '\1zes')
+ inflect.singular(/(database)s$/i, '\1')
+ inflect.uncountable('series')
+ inflect.human("col_rpted_bugs", "Reported bugs")
+
+ inflect.clear
+
+ assert inflect.plurals.empty?
+ assert inflect.singulars.empty?
+ assert inflect.uncountables.empty?
+ assert inflect.humans.empty?
+ end
ActiveSupport::Inflector.inflections.instance_variable_set :@plurals, cached_values[0]
ActiveSupport::Inflector.inflections.instance_variable_set :@singulars, cached_values[1]
ActiveSupport::Inflector.inflections.instance_variable_set :@uncountables, cached_values[2]