aboutsummaryrefslogtreecommitdiffstats
path: root/actionmailer
diff options
context:
space:
mode:
authorStan Lo <a22301613@yahoo.com.tw>2015-12-23 16:01:32 +0800
committerRafael Mendonça França <rafaelmfranca@gmail.com>2016-02-23 21:04:16 -0300
commit049b6e670f0a9d7dc03ee4ddf9334ddbeb6c7900 (patch)
treeae64580a2ead0ab9d076b400d2c382969844a321 /actionmailer
parent2c02bc0a47777ad8cf98e1465c08b1a68151803e (diff)
downloadrails-049b6e670f0a9d7dc03ee4ddf9334ddbeb6c7900.tar.gz
rails-049b6e670f0a9d7dc03ee4ddf9334ddbeb6c7900.tar.bz2
rails-049b6e670f0a9d7dc03ee4ddf9334ddbeb6c7900.zip
Porting ActionController::Caching to ActionMailer::Caching
Diffstat (limited to 'actionmailer')
-rw-r--r--actionmailer/lib/action_mailer.rb1
-rw-r--r--actionmailer/lib/action_mailer/base.rb1
-rw-r--r--actionmailer/lib/action_mailer/caching.rb73
-rw-r--r--actionmailer/lib/action_mailer/caching/fragments.rb148
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb1
-rw-r--r--actionmailer/test/caching_test.rb232
-rw-r--r--actionmailer/test/fixtures/caching_mailer/_partial.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb1
-rw-r--r--actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb3
-rw-r--r--actionmailer/test/mailers/caching_mailer.rb15
11 files changed, 481 insertions, 0 deletions
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index 55c017e338..26fbb48600 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -44,6 +44,7 @@ module ActionMailer
autoload :MailHelper
autoload :Preview
autoload :Previews, 'action_mailer/preview'
+ autoload :Caching
autoload :TestCase
autoload :TestHelper
autoload :MessageDelivery
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 4259eb0bee..9b14a96d48 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -420,6 +420,7 @@ module ActionMailer
class Base < AbstractController::Base
include DeliveryMethods
include Previews
+ include Caching
abstract!
diff --git a/actionmailer/lib/action_mailer/caching.rb b/actionmailer/lib/action_mailer/caching.rb
new file mode 100644
index 0000000000..27da8148d1
--- /dev/null
+++ b/actionmailer/lib/action_mailer/caching.rb
@@ -0,0 +1,73 @@
+require 'active_support/descendants_tracker'
+
+module ActionMailer
+ module Caching
+ extend ActiveSupport::Concern
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Fragments
+ end
+
+ module ConfigMethods
+ def cache_store
+ config.cache_store
+ end
+
+ def cache_store=(store)
+ config.cache_store = ActiveSupport::Cache.lookup_store(store)
+ end
+
+ private
+ def cache_configured?
+ perform_caching && cache_store
+ end
+ end
+
+ include AbstractController::Helpers
+ include ConfigMethods
+ include Fragments
+
+ included do
+ extend ConfigMethods
+
+ config_accessor :default_static_extension
+ self.default_static_extension ||= '.html'
+
+ config_accessor :perform_caching
+ self.perform_caching = true if perform_caching.nil?
+
+ class_attribute :_view_cache_dependencies
+ self._view_cache_dependencies = []
+ helper_method :view_cache_dependencies if respond_to?(:helper_method)
+ end
+
+ module ClassMethods
+ def view_cache_dependency(&dependency)
+ self._view_cache_dependencies += [dependency]
+ end
+ end
+
+ def view_cache_dependencies
+ self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
+ end
+
+ protected
+ # Convenience accessor.
+ def cache(key, options = {}, &block)
+ if cache_configured?
+ cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
+ else
+ yield
+ end
+ end
+
+ def perform_caching
+ Base.perform_caching
+ end
+
+ def controller_name
+ "ActionMailer"
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/caching/fragments.rb b/actionmailer/lib/action_mailer/caching/fragments.rb
new file mode 100644
index 0000000000..6c70137626
--- /dev/null
+++ b/actionmailer/lib/action_mailer/caching/fragments.rb
@@ -0,0 +1,148 @@
+module ActionMailer
+ module Caching
+ # Fragment caching is used for caching various blocks within
+ # views without caching the entire action as a whole. This is
+ # useful when certain elements of an action change frequently or
+ # depend on complicated state while other parts rarely change or
+ # can be shared amongst multiple parties. The caching is done using
+ # the +cache+ helper available in the Action View. See
+ # ActionView::Helpers::CacheHelper for more information.
+ #
+ # While it's strongly recommended that you use key-based cache
+ # expiration (see links in CacheHelper for more information),
+ # it is also possible to manually expire caches. For example:
+ #
+ # expire_fragment('name_of_cache')
+ module Fragments
+ extend ActiveSupport::Concern
+
+ included do
+ if respond_to?(:class_attribute)
+ class_attribute :fragment_cache_keys
+ else
+ mattr_writer :fragment_cache_keys
+ end
+
+ self.fragment_cache_keys = []
+
+ helper_method :fragment_cache_key if respond_to?(:helper_method)
+ end
+
+ module ClassMethods
+ # Allows you to specify controller-wide key prefixes for
+ # cache fragments. Pass either a constant +value+, or a block
+ # which computes a value each time a cache key is generated.
+ #
+ # For example, you may want to prefix all fragment cache keys
+ # with a global version identifier, so you can easily
+ # invalidate all caches.
+ #
+ # class ApplicationController
+ # fragment_cache_key "v1"
+ # end
+ #
+ # When it's time to invalidate all fragments, simply change
+ # the string constant. Or, progressively roll out the cache
+ # invalidation using a computed value:
+ #
+ # class ApplicationController
+ # fragment_cache_key do
+ # @account.id.odd? ? "v1" : "v2"
+ # end
+ # end
+ def fragment_cache_key(value = nil, &key)
+ self.fragment_cache_keys += [key || ->{ value }]
+ end
+ end
+
+ # Given a key (as described in +expire_fragment+), returns
+ # a key suitable for use in reading, writing, or expiring a
+ # cached fragment. All keys begin with <tt>views/</tt>,
+ # followed by any controller-wide key prefix values, ending
+ # with the specified +key+ value. The key is expanded using
+ # ActiveSupport::Cache.expand_cache_key.
+ def fragment_cache_key(key)
+ head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
+ tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
+ ActiveSupport::Cache.expand_cache_key([*head, *tail], :views)
+ end
+
+ # Writes +content+ to the location signified by
+ # +key+ (see +expire_fragment+ for acceptable formats).
+ def write_fragment(key, content, options = nil)
+ return content unless cache_configured?
+
+ key = fragment_cache_key(key)
+ instrument_fragment_cache :write_fragment, key do
+ content = content.to_str
+ cache_store.write(key, content, options)
+ end
+ content
+ end
+
+ # Reads a cached fragment from the location signified by +key+
+ # (see +expire_fragment+ for acceptable formats).
+ def read_fragment(key, options = nil)
+ return unless cache_configured?
+
+ key = fragment_cache_key(key)
+ instrument_fragment_cache :read_fragment, key do
+ result = cache_store.read(key, options)
+ result.respond_to?(:html_safe) ? result.html_safe : result
+ end
+ end
+
+ # Check if a cached fragment from the location signified by
+ # +key+ exists (see +expire_fragment+ for acceptable formats).
+ def fragment_exist?(key, options = nil)
+ return unless cache_configured?
+ key = fragment_cache_key(key)
+
+ instrument_fragment_cache :exist_fragment?, key do
+ cache_store.exist?(key, options)
+ end
+ end
+
+ # Removes fragments from the cache.
+ #
+ # +key+ can take one of three forms:
+ #
+ # * String - This would normally take the form of a path, like
+ # <tt>pages/45/notes</tt>.
+ # * Hash - Treated as an implicit call to +url_for+, like
+ # <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
+ # * Regexp - Will remove any fragment that matches, so
+ # <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
+ # don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
+ # the actual filename matched looks like
+ # <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
+ # only supported on caches that can iterate over all keys (unlike
+ # memcached).
+ #
+ # +options+ is passed through to the cache store's +delete+
+ # method (or <tt>delete_matched</tt>, for Regexp keys).
+ def expire_fragment(key, options = nil)
+ return unless cache_configured?
+ key = fragment_cache_key(key) unless key.is_a?(Regexp)
+
+ instrument_fragment_cache :expire_fragment, key do
+ if key.is_a?(Regexp)
+ cache_store.delete_matched(key, options)
+ else
+ cache_store.delete(key, options)
+ end
+ end
+ end
+
+ def instrument_fragment_cache(name, key) # :nodoc:
+ payload = {
+ controller: controller_name,
+ action: action_name,
+ key: key
+ }
+
+ ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield }
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index ae89492b0f..fce0d0f9f1 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -25,6 +25,7 @@ module ActionMailer
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
options.show_previews = Rails.env.development? if options.show_previews.nil?
+ options.perform_caching ||= true
if options.show_previews
options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb
new file mode 100644
index 0000000000..a92f43a6c5
--- /dev/null
+++ b/actionmailer/test/caching_test.rb
@@ -0,0 +1,232 @@
+require 'fileutils'
+require 'abstract_unit'
+require 'mailers/base_mailer'
+require 'mailers/caching_mailer'
+require 'byebug'
+
+CACHE_DIR = 'test_cache'
+# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
+FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR)
+
+class FragmentCachingMailer < ActionMailer::Base
+ abstract!
+
+ include ActionMailer::Caching
+
+ def some_action; end
+end
+
+class BaseCachingTest < ActiveSupport::TestCase
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @mailer = FragmentCachingMailer.new
+ @mailer.perform_caching = true
+ @mailer.cache_store = @store
+ end
+
+ def test_fragment_cache_key
+ assert_equal 'views/what a key', @mailer.fragment_cache_key('what a key')
+ end
+end
+
+class FragmentCachingTest < BaseCachingTest
+ def test_read_fragment_with_caching_enabled
+ @store.write('views/name', 'value')
+ assert_equal 'value', @mailer.read_fragment('name')
+ end
+
+ def test_read_fragment_with_caching_disabled
+ @mailer.perform_caching = false
+ @store.write('views/name', 'value')
+ assert_nil @mailer.read_fragment('name')
+ end
+
+ def test_fragment_exist_with_caching_enabled
+ @store.write('views/name', 'value')
+ assert @mailer.fragment_exist?('name')
+ assert !@mailer.fragment_exist?('other_name')
+ end
+
+ def test_fragment_exist_with_caching_disabled
+ @mailer.perform_caching = false
+ @store.write('views/name', 'value')
+ assert !@mailer.fragment_exist?('name')
+ assert !@mailer.fragment_exist?('other_name')
+ end
+
+ def test_write_fragment_with_caching_enabled
+ assert_nil @store.read('views/name')
+ assert_equal 'value', @mailer.write_fragment('name', 'value')
+ assert_equal 'value', @store.read('views/name')
+ end
+
+ def test_write_fragment_with_caching_disabled
+ assert_nil @store.read('views/name')
+ @mailer.perform_caching = false
+ assert_equal 'value', @mailer.write_fragment('name', 'value')
+ assert_nil @store.read('views/name')
+ end
+
+ def test_expire_fragment_with_simple_key
+ @store.write('views/name', 'value')
+ @mailer.expire_fragment 'name'
+ assert_nil @store.read('views/name')
+ end
+
+ def test_expire_fragment_with_regexp
+ @store.write('views/name', 'value')
+ @store.write('views/another_name', 'another_value')
+ @store.write('views/primalgrasp', 'will not expire ;-)')
+
+ @mailer.expire_fragment(/name/)
+
+ assert_nil @store.read('views/name')
+ assert_nil @store.read('views/another_name')
+ assert_equal 'will not expire ;-)', @store.read('views/primalgrasp')
+ end
+
+ def test_fragment_for
+ @store.write('views/expensive', 'fragment content')
+ fragment_computed = false
+
+ view_context = @mailer.view_context
+
+ buffer = 'generated till now -> '.html_safe
+ buffer << view_context.send(:fragment_for, 'expensive') { fragment_computed = true }
+
+ assert !fragment_computed
+ assert_equal 'generated till now -> fragment content', buffer
+ end
+
+ def test_html_safety
+ assert_nil @store.read('views/name')
+ content = 'value'.html_safe
+ assert_equal content, @mailer.write_fragment('name', content)
+
+ cached = @store.read('views/name')
+ assert_equal content, cached
+ assert_equal String, cached.class
+
+ html_safe = @mailer.read_fragment('name')
+ assert_equal content, html_safe
+ assert html_safe.html_safe?
+ end
+end
+
+class FunctionalFragmentCachingTest < BaseCachingTest
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @mailer = CachingMailer.new
+ @mailer.perform_caching = true
+ @mailer.cache_store = @store
+ end
+
+ def test_fragment_caching
+ email = @mailer.fragment_cache
+ expected_body = "\"Welcome\""
+
+ assert_match expected_body, email.body.encoded
+ assert_match "\"Welcome\"",
+ @store.read("views/caching/#{template_digest("caching_mailer/fragment_cache")}")
+ end
+
+ def test_fragment_caching_in_partials
+ email = @mailer.fragment_cache_in_partials
+ assert_match(/Old fragment caching in a partial/, email.body.encoded)
+
+ assert_match("Old fragment caching in a partial",
+ @store.read("views/caching/#{template_digest("caching_mailer/_partial")}"))
+ end
+
+ def test_skip_fragment_cache_digesting
+ email = @mailer.skip_fragment_cache_digesting
+ expected_body = "No Digest"
+
+ assert_match expected_body, email.body.encoded
+ assert_match expected_body, @store.read("views/no_digest")
+ end
+
+ private
+
+ def template_digest(name)
+ ActionView::Digestor.digest(name: name, finder: @mailer.lookup_context)
+ end
+end
+
+class CacheHelperOutputBufferTest < BaseCachingTest
+
+ class MockController
+ def read_fragment(name, options)
+ return false
+ end
+
+ def write_fragment(name, fragment, options)
+ fragment
+ end
+ end
+
+ def setup
+ super
+ end
+
+ def test_output_buffer
+ output_buffer = ActionView::OutputBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end;
+ def self.output_buffer; end;
+ def self.output_buffer=; end;
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ end
+ end
+ end
+ end
+ end
+
+ def test_safe_buffer
+ output_buffer = ActiveSupport::SafeBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end;
+ def self.output_buffer; end;
+ def self.output_buffer=; end;
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ end
+ end
+ end
+ end
+ end
+end
+
+class ViewCacheDependencyTest < BaseCachingTest
+ class NoDependenciesMailer < ActionMailer::Base
+ end
+ class HasDependenciesMailer < ActionMailer::Base
+ view_cache_dependency { "trombone" }
+ view_cache_dependency { "flute" }
+ end
+
+ def test_view_cache_dependencies_are_empty_by_default
+ assert NoDependenciesMailer.new.view_cache_dependencies.empty?
+ end
+
+ def test_view_cache_dependencies_are_listed_in_declaration_order
+ assert_equal %w(trombone flute), HasDependenciesMailer.new.view_cache_dependencies
+ end
+end
diff --git a/actionmailer/test/fixtures/caching_mailer/_partial.html.erb b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb
new file mode 100644
index 0000000000..8e965f52b4
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb
@@ -0,0 +1,3 @@
+<% cache :caching do %>
+ Old fragment caching in a partial
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb
new file mode 100644
index 0000000000..90189627da
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb
@@ -0,0 +1,3 @@
+<% cache :caching do %>
+"Welcome"
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb
new file mode 100644
index 0000000000..2957d083e8
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb
@@ -0,0 +1 @@
+<%= render "partial" %>
diff --git a/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb
new file mode 100644
index 0000000000..0d52429a81
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb
@@ -0,0 +1,3 @@
+<%= cache :no_digest, skip_digest: true do %>
+ No Digest
+<% end %>
diff --git a/actionmailer/test/mailers/caching_mailer.rb b/actionmailer/test/mailers/caching_mailer.rb
new file mode 100644
index 0000000000..345d267a36
--- /dev/null
+++ b/actionmailer/test/mailers/caching_mailer.rb
@@ -0,0 +1,15 @@
+class CachingMailer < ActionMailer::Base
+ self.mailer_name = "caching_mailer"
+
+ def fragment_cache
+ mail(subject: "welcome", template_name: "fragment_cache")
+ end
+
+ def fragment_cache_in_partials
+ mail(subject: "welcome", template_name: "fragment_cache_in_partials")
+ end
+
+ def skip_fragment_cache_digesting
+ mail(subject: "welcome", template_name: "skip_fragment_cache_digesting")
+ end
+end