cells 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,151 @@
1
+ # To improve performance rendered state views can be cached using Rails' caching
2
+ # mechanism.
3
+ # If this it configured (e.g. using our fast friend memcached) all you have to do is to
4
+ # tell Cells which state you want to cache. You can further attach a proc to expire the
5
+ # cached view.
6
+ #
7
+ # As always I stole a lot of code, this time from Lance Ivy <cainlevy@gmail.com> and
8
+ # his fine components plugin at http://github.com/cainlevy/components.
9
+
10
+ module Cell::Caching
11
+
12
+ def self.included(base) #:nodoc:
13
+ base.class_eval do
14
+ # mixin Cell::Base#cache, setup vars and extend #render_state if caching's on.
15
+ extend ClassMethods
16
+
17
+ return unless cache_configured?
18
+
19
+ alias_method_chain :render_state, :caching
20
+ end
21
+
22
+
23
+ end
24
+
25
+
26
+
27
+ module ClassMethods
28
+ # Activate caching for the state <tt>state</tt>. If no other options are passed
29
+ # the view will be cached forever.
30
+ #
31
+ # You may pass a Proc or a Symbol as cache expiration <tt>version_proc</tt>.
32
+ # This method is called every time the state is rendered, and is expected to return a
33
+ # Hash containing the cache key ingredients.
34
+ #
35
+ # Additional options will be passed directly to the cache store when caching the state.
36
+ # Useful for simply setting a TTL for a cached state.
37
+ # Note that you may omit the <tt>version_proc</tt>.
38
+ #
39
+ #
40
+ # Example:
41
+ # class CachingCell < Cell::Base
42
+ # cache :versioned_cached_state, Proc.new{ {:version => 0} }
43
+ # would result in the complete cache key
44
+ # cells/CachingCell/versioned_cached_state/version=0
45
+ #
46
+ # If you provide a symbol, you can access the cell instance directly in the versioning
47
+ # method:
48
+ #
49
+ # class CachingCell < Cell::Base
50
+ # cache :cached_state, :my_cache_version
51
+ #
52
+ # def my_cache_version
53
+ # { :user => current_user.id,
54
+ # :item_id => params[:item] }
55
+ # }
56
+ # end
57
+ # results in a very specific cache key, for customized caching:
58
+ # cells/CachingCell/cached_state/user=18/item_id=1
59
+ #
60
+ # You may also set a TTL only, e.g. when using the memcached store:
61
+ #
62
+ # cache :cached_state, :expires_in => 3.minutes
63
+ #
64
+ # Or use both, having a versioning proc <em>and</em> a TTL expiring the state as a fallback
65
+ # after a certain amount of time.
66
+ #
67
+ # cache :cached_state, Proc.new { {:version => 0} }, :expires_in => 10.minutes
68
+ #--
69
+ ### TODO: implement for string, nil.
70
+ ### DISCUSS: introduce return method #sweep ? so the Proc can explicitly
71
+ ### delegate re-rendering to the outside.
72
+ #--
73
+ def cache(state, version_proc=nil, cache_opts={})
74
+ if version_proc.is_a?(Hash)
75
+ cache_opts = version_proc
76
+ version_proc = nil
77
+ end
78
+
79
+ version_procs[state] = version_proc
80
+ cache_options[state] = cache_opts
81
+ end
82
+
83
+ def version_procs; @version_procs ||= {}; end
84
+ def cache_options; @cache_options ||= {}; end
85
+
86
+ def cache_store #:nodoc:
87
+ @cache_store ||= ActionController::Base.cache_store
88
+ end
89
+
90
+ def cache_key_for(cell_class, state, args = {}) #:nodoc:
91
+ key_pieces = [cell_class, state]
92
+
93
+ args.collect{|a,b| [a.to_s, b]}.sort.each{ |k,v| key_pieces << "#{k}=#{v}" }
94
+ key = key_pieces.join('/')
95
+
96
+ ActiveSupport::Cache.expand_cache_key(key, :cells)
97
+ end
98
+
99
+ def expire_cache_key(key, opts=nil)
100
+ cache_store.delete(key, opts)
101
+ end
102
+ end
103
+
104
+
105
+
106
+ def render_state_with_caching(state)
107
+ return render_state_without_caching(state) unless state_cached?(state)
108
+
109
+ key = cache_key(state, call_version_proc_for_state(state))
110
+ ### DISCUSS: see sweep discussion at #cache.
111
+
112
+ # cache hit:
113
+ if content = read_fragment(key)
114
+ return content
115
+ end
116
+ # re-render:
117
+ return write_fragment(key, render_state_without_caching(state), cache_options[state])
118
+ end
119
+
120
+
121
+ def read_fragment(key, cache_options = nil) #:nodoc:
122
+ returning self.class.cache_store.read(key, cache_options) do |content|
123
+ @controller.logger.debug "Cell Cache hit: #{key}" unless content.blank?
124
+ end
125
+ end
126
+
127
+ def write_fragment(key, content, cache_opts = nil) #:nodoc:
128
+ @controller.logger.debug "Cell Cache miss: #{key}"
129
+ self.class.cache_store.write(key, content, cache_opts)
130
+ content
131
+ end
132
+
133
+ # Call the versioning Proc for the respective state.
134
+ def call_version_proc_for_state(state)
135
+ version_proc = version_procs[state]
136
+
137
+ return {} unless version_proc # call to #cache was without any args.
138
+
139
+ return version_proc.call(self) if version_proc.kind_of? Proc
140
+ send(version_proc)
141
+ end
142
+
143
+ def cache_key(state, args = {}) #:nodoc:
144
+ self.class.cache_key_for(self.cell_name, state, args)
145
+ end
146
+
147
+ def state_cached?(state); self.class.version_procs.has_key?(state); end
148
+ def version_procs; self.class.version_procs; end
149
+ def cache_options; self.class.cache_options; end
150
+
151
+ end
data/lib/cell/view.rb ADDED
@@ -0,0 +1,55 @@
1
+ module Cell
2
+ class View < ::ActionView::Base
3
+
4
+ attr_accessor :cell
5
+
6
+ alias_method :render_for, :render
7
+
8
+ # Tries to find the passed template in view_paths. Returns the view on success-
9
+ # otherwise it will throw an ActionView::MissingTemplate exception.
10
+ def try_picking_template_for_path(template_path)
11
+ self.view_paths.find_template(template_path, template_format)
12
+ end
13
+
14
+ ### TODO: this should just be a thin helper.
15
+ ### dear rails folks, could you guys please provide a helper #render and an internal #render_for
16
+ ### so that we can overwrite the helper and cleanly reuse the internal method? using the same
17
+ ### method both internally and externally sucks ass.
18
+ def render(options = {}, local_assigns = {}, &block)
19
+ ### TODO: delegate dynamically:
20
+ ### TODO: we have to find out if this is a call to the cells #render method, or to the rails
21
+ ### method (e.g. when rendering a layout). what a shit.
22
+ if view = options[:view]
23
+ return cell.render_view_for(options, view)
24
+ end
25
+
26
+
27
+ # rails compatibility we should get rid of:
28
+ if partial_path = options[:partial]
29
+ # adds the cell name to the partial name.
30
+ options[:partial] = expand_view_path(partial_path)
31
+ end
32
+ #throw Exception.new
33
+
34
+ super(options, local_assigns, &block)
35
+ end
36
+
37
+
38
+ def expand_view_path(path)
39
+ path = "#{cell.cell_name}/#{path}" unless path.include?('/')
40
+ path
41
+ end
42
+
43
+ # this prevents cell ivars from being overwritten by same-named
44
+ # controller ivars.
45
+ # we'll hopefully get a cleaner way, or an API, to handle this in rails 3.
46
+ def _copy_ivars_from_controller #:nodoc:
47
+ if @controller
48
+ variables = @controller.instance_variable_names
49
+ variables -= @controller.protected_instance_variables if @controller.respond_to?(:protected_instance_variables)
50
+ variables -= assigns.keys.collect {|key| "@#{key}"} # cell ivars override controller ivars.
51
+ variables.each { |name| instance_variable_set(name, @controller.instance_variable_get(name)) }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,49 @@
1
+ # Sorry for the interface violations, but it looks as if there are
2
+ # no interfaces in rails at all.
3
+ module CellsHelper
4
+
5
+ # Executes #capture on the global ActionView and sets <tt>name</tt> as the
6
+ # instance variable name.
7
+ #
8
+ # Example:
9
+ #
10
+ # <p>
11
+ # <% global_capture :greeting do
12
+ # <h1>Hi, Nick!</h1>
13
+ # <% end %>
14
+ #
15
+ # The captured markup can be accessed in your global action view or in your layout.
16
+ #
17
+ # <%= @greeting %>
18
+ def global_capture(name, &block)
19
+ global_view = controller.instance_variable_get( "@template" )
20
+ content = capture &block
21
+ global_view.send("instance_variable_set", "@#{name}", content)
22
+ end
23
+
24
+
25
+ # Executes #content_for on the global ActionView.
26
+ #
27
+ # Example:
28
+ #
29
+ # <p>
30
+ # <% global_content_for :greetings do
31
+ # <h1>Hi, Michal!</h1>
32
+ # <% end %>
33
+ #
34
+ # As in global_capture, the markup can be accessed in your global action view or in your layout.
35
+ #
36
+ # <%= yield :greetings %>
37
+ def global_content_for(name, content = nil, &block)
38
+ # OMG.
39
+ global_view = controller.instance_variable_get( "@template" )
40
+ ivar = "@content_for_#{name}"
41
+ content = capture(&block) if block_given?
42
+ old_content = global_view.send("instance_variable_get", ivar)
43
+
44
+
45
+ global_view.send("instance_variable_set", ivar, "#{old_content}#{content}")
46
+
47
+ nil
48
+ end
49
+ end
@@ -0,0 +1,75 @@
1
+ # The Cells plugin defines a number of new methods for ActionView::Base. These allow
2
+ # you to render cells from within normal controller views as well as from Cell state views.
3
+ module Cell
4
+
5
+ module ActionView
6
+ # Call a cell state and return its rendered view.
7
+ #
8
+ # ERB example:
9
+ # <div id="login">
10
+ # <%= render_cell :user, :login_prompt, :message => "Please login" %>
11
+ # </div>
12
+ #
13
+ # If you have a <tt>UserCell</tt> cell in <tt>app/cells/user_cell.rb</tt>, which has a
14
+ # <tt>UserCell#login_prompt</tt> method, this will call that method and then will
15
+ # find the view <tt>app/cells/user/login_prompt.html.erb</tt> and render it. This is
16
+ # called the <tt>:login_prompt</tt> <em>state</em> in Cells terminology.
17
+ #
18
+ # If this view file looks like this:
19
+ # <h1><%= @opts[:message] %></h1>
20
+ # <label>name: <input name="user[name]" /></label>
21
+ # <label>password: <input name="user[password]" /></label>
22
+ #
23
+ # The resulting view in the controller will be roughly equivalent to:
24
+ # <div id="login">
25
+ # <h1><%= "Please login" %></h1>
26
+ # <label>name: <input name="user[name]" /></label>
27
+ # <label>password: <input name="user[password]" /></label>
28
+ # </div>
29
+ def render_cell(name, state, opts = {})
30
+ cell = Cell::Base.create_cell_for(@controller, name, opts)
31
+ cell.render_state(state)
32
+ end
33
+ end
34
+
35
+
36
+ # These ControllerMethods are automatically added to all Controllers when
37
+ # the cells plugin is loaded.
38
+ module ActionController
39
+
40
+ # Equivalent to ActionController#render_to_string, except it renders a cell
41
+ # rather than a regular templates.
42
+ def render_cell(name, state, opts={})
43
+ cell = Cell::Base.create_cell_for(self, name, opts)
44
+
45
+ return cell.render_state(state)
46
+ end
47
+
48
+ alias_method :render_cell_to_string, :render_cell # just for backward compatibility.
49
+
50
+
51
+ # Expires the cached cell state view, similar to ActionController::expire_fragment.
52
+ # Usually, this method is used in Sweepers.
53
+ # Beside the obvious first two args <tt>cell_name</tt> and <tt>state</tt> you can pass
54
+ # in additional cache key <tt>args</tt> and cache store specific <tt>opts</tt>.
55
+ #
56
+ # Example:
57
+ #
58
+ # class ListSweeper < ActionController::Caching::Sweeper
59
+ # observe List, Item
60
+ #
61
+ # def after_save(record)
62
+ # expire_cell_state :my_listing, :display_list
63
+ # end
64
+ #
65
+ # will expire the view for state <tt>:display_list</tt> in the cell <tt>MyListingCell</tt>.
66
+ def expire_cell_state(cell_name, state, args={}, opts=nil)
67
+ key = Cell::Base.cache_key_for(cell_name, state, args)
68
+ Cell::Base.expire_cache_key(key, opts)
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+
75
+
data/test/bugs_test.rb ADDED
@@ -0,0 +1,26 @@
1
+ require File.dirname(__FILE__) + '/../../../../test/test_helper'
2
+ require File.dirname(__FILE__) + '/testing_helper'
3
+
4
+
5
+ class MyTestCell < Cell::Base
6
+ def state_with_instance_var
7
+ @my_ivar = "value from cell"
8
+ render
9
+ end
10
+ end
11
+
12
+
13
+ class CellsTest < ActionController::TestCase
14
+ include CellsTestMethods
15
+
16
+ def test_controller_overriding_cell_ivars
17
+ @controller.class_eval do
18
+ attr_accessor :my_ivar
19
+ end
20
+ @controller.my_ivar = "value from controller"
21
+
22
+ cell = MyTestCell.new(@controller)
23
+ c = cell.render_state(:state_with_instance_var)
24
+ assert_equal "value from cell", c
25
+ end
26
+ end
@@ -0,0 +1,266 @@
1
+ require File.dirname(__FILE__) + '/../../../../test/test_helper'
2
+ require File.dirname(__FILE__) + '/testing_helper'
3
+
4
+ # usually done by rails' autoloading:
5
+ require File.dirname(__FILE__) + '/cells/test_cell'
6
+
7
+ ### NOTE: add
8
+ ### config.action_controller.cache_store = :memory_store
9
+ ### config.action_controller.perform_caching = true
10
+ ### to config/environments/test.rb to make this tests work.
11
+
12
+ class CellsCachingTest < Test::Unit::TestCase
13
+ include CellsTestMethods
14
+
15
+ def setup
16
+ super
17
+ @controller.session= {}
18
+ @cc = CachingCell.new(@controller)
19
+ @c2 = AnotherCachingCell.new(@controller)
20
+ end
21
+
22
+ #def self.path_to_test_views
23
+ # RAILS_ROOT + "/vendor/plugins/cells/test/views/"
24
+ #end
25
+
26
+
27
+ def test_state_cached?
28
+ assert @cc.state_cached?(:cached_state)
29
+ assert ! @cc.state_cached?(:not_cached_state)
30
+ end
31
+
32
+ def test_cache_without_options
33
+ # :cached_state is cached without any options:
34
+ assert_nil @cc.version_procs[:cached_state]
35
+ assert_nil @cc.version_procs[:not_cached_state]
36
+
37
+ # cache_options must at least return an empty hash for a cached state:
38
+ assert_equal( {}, @cc.cache_options[:cached_state])
39
+ assert_nil @cc.cache_options[:not_cached_state]
40
+ end
41
+
42
+
43
+ def test_cache_with_proc_only
44
+ CachingCell.class_eval do
45
+ cache :my_state, Proc.new {}
46
+ end
47
+
48
+ assert_kind_of Proc, @cc.version_procs[:my_state]
49
+ assert_equal( {}, @cc.cache_options[:my_state])
50
+ end
51
+
52
+
53
+ def test_cache_with_proc_and_cache_options
54
+ CachingCell.class_eval do
55
+ cache :my_state, Proc.new{}, {:expires_in => 10.seconds}
56
+ end
57
+
58
+ assert_kind_of Proc, @cc.version_procs[:my_state]
59
+ assert_equal( {:expires_in => 10.seconds}, @cc.cache_options[:my_state])
60
+ end
61
+
62
+
63
+ def test_cache_with_cache_options_only
64
+ CachingCell.class_eval do
65
+ cache :my_state, :expires_in => 10.seconds
66
+ end
67
+
68
+ assert @cc.version_procs.has_key?(:my_state)
69
+ assert_nil @cc.version_procs[:my_state]
70
+ assert_equal( {:expires_in => 10.seconds}, @cc.cache_options[:my_state])
71
+ end
72
+
73
+
74
+
75
+ def test_if_caching_works
76
+ c = @cc.render_state(:cached_state)
77
+ assert_equal c, "1 should remain the same forever!"
78
+
79
+ c = @cc.render_state(:cached_state)
80
+ assert_equal c, "1 should remain the same forever!", ":cached_state was invoked again"
81
+ end
82
+
83
+ def test_cache_key
84
+ assert_equal "cells/caching/some_state", @cc.cache_key(:some_state)
85
+ assert_equal @cc.cache_key(:some_state), Cell::Base.cache_key_for(:caching, :some_state)
86
+ assert_equal "cells/caching/some_state/param=9", @cc.cache_key(:some_state, :param => 9)
87
+ assert_equal "cells/caching/some_state/a=1/b=2", @cc.cache_key(:some_state, :b => 2, :a => 1)
88
+ end
89
+
90
+ def test_render_state_without_caching
91
+ c = @cc.render_state(:not_cached_state)
92
+ assert_equal c, "i'm really static"
93
+ c = @cc.render_state(:not_cached_state)
94
+ assert_equal c, "i'm really static"
95
+ end
96
+
97
+ def test_caching_with_version_proc
98
+ @controller.session[:version] = 0
99
+ # render state, as it's not cached:
100
+ c = @cc.render_state(:versioned_cached_state)
101
+ assert_equal c, "0 should change every third call!"
102
+
103
+ @controller.session[:version] = -1
104
+ c = @cc.render_state(:versioned_cached_state)
105
+ assert_equal c, "0 should change every third call!"
106
+
107
+
108
+ @controller.session[:version] = 1
109
+ c = @cc.render_state(:versioned_cached_state)
110
+ assert_equal c, "1 should change every third call!"
111
+
112
+
113
+ @controller.session[:version] = 2
114
+ c = @cc.render_state(:versioned_cached_state)
115
+ assert_equal c, "2 should change every third call!"
116
+
117
+ @controller.session[:version] = 3
118
+ c = @cc.render_state(:versioned_cached_state)
119
+ assert_equal c, "3 should change every third call!"
120
+ end
121
+
122
+ def test_caching_with_instance_version_proc
123
+ CachingCell.class_eval do
124
+ cache :versioned_cached_state, :my_version_proc
125
+ end
126
+ @controller.session[:version] = 0
127
+ c = @cc.render_state(:versioned_cached_state)
128
+ assert_equal c, "0 should change every third call!"
129
+
130
+ @controller.session[:version] = 1
131
+ c = @cc.render_state(:versioned_cached_state)
132
+ assert_equal c, "1 should change every third call!"
133
+ end
134
+
135
+ def test_caching_with_two_same_named_states
136
+ c = @cc.render_state(:cheers)
137
+ assert_equal c, "cheers!"
138
+ c = @c2.render_state(:cheers)
139
+ assert_equal c, "prost!"
140
+ c = @cc.render_state(:cheers)
141
+ assert_equal c, "cheers!"
142
+ c = @c2.render_state(:cheers)
143
+ assert_equal c, "prost!"
144
+ end
145
+
146
+ def test_caching_one_of_two_same_named_states
147
+ ### DISCUSS with drogus: the problem was that CachingCell and AnotherCachingCell keep
148
+ ### overwriting their version_procs, wasn't it? why don't we test that with different
149
+ ### version_procs in each cell?
150
+ @cc = CachingCell.new(@controller, :str => "foo1")
151
+ c = @cc.render_state(:another_state)
152
+ assert_equal c, "foo1"
153
+
154
+ @c2 = AnotherCachingCell.new(@controller, :str => "foo2")
155
+ c = @c2.render_state(:another_state)
156
+ assert_equal c, "foo2"
157
+
158
+ @cc = CachingCell.new(@controller, :str => "bar1")
159
+ c = @cc.render_state(:another_state)
160
+ assert_equal c, "foo1"
161
+
162
+ @c2 = AnotherCachingCell.new(@controller, :str => "bar2")
163
+ c = @c2.render_state(:another_state)
164
+ assert_equal c, "bar2"
165
+ end
166
+
167
+ def test_expire_cache_key
168
+ k = @cc.cache_key(:cached_state)
169
+ @cc.render_state(:cached_state)
170
+ assert Cell::Base.cache_store.read(k)
171
+ Cell::Base.expire_cache_key(k)
172
+ assert ! Cell::Base.cache_store.read(k)
173
+
174
+ # test via ActionController::expire_cell_state, which is called from Sweepers.
175
+ @cc.render_state(:cached_state)
176
+ assert Cell::Base.cache_store.read(k)
177
+ @controller.expire_cell_state(:caching, :cached_state)
178
+ assert ! Cell::Base.cache_store.read(k)
179
+
180
+ # ..and additionally test if passing cache key args works:
181
+ k = @cc.cache_key(:cached_state, :more => :yes)
182
+ assert Cell::Base.cache_store.write(k, "test content")
183
+ @controller.expire_cell_state(:caching, :cached_state, :more => :yes)
184
+ assert ! Cell::Base.cache_store.read(k)
185
+ end
186
+
187
+
188
+ def test_find_family_view_for_state_with_caching
189
+ # test environment: --------------------------------------
190
+ assert_equal({}, ACell.state2view_cache)
191
+
192
+ a = ACell.new(@controller)
193
+ a.class.instance_eval do
194
+ def cache_configured?; false; end
195
+ end
196
+ a.render_state :existing_view
197
+ # in development/test environment, no view name caching should happen,
198
+ # if perform_caching is false.
199
+ assert_equal({}, ACell.state2view_cache)
200
+
201
+ # production environment: --------------------------------
202
+ a = ACell.new(@controller)
203
+ a.class.instance_eval do
204
+ def cache_configured?; true; end
205
+ end
206
+ a.render_state :existing_view
207
+ assert ACell.state2view_cache.has_key?("existing_view/html")
208
+ end
209
+ end
210
+
211
+ class CachingCell < Cell::Base
212
+
213
+ cache :cached_state
214
+
215
+ def cached_state
216
+ cnt = controller.session[:cache_count]
217
+ cnt ||= 0
218
+ cnt += 1
219
+ "#{cnt} should remain the same forever!"
220
+ end
221
+
222
+ def not_cached_state
223
+ "i'm really static"
224
+ end
225
+
226
+ cache :versioned_cached_state, Proc.new { |cell|
227
+ if (v = cell.session[:version]) > 0
228
+ {:version=>v}
229
+ else
230
+ {:version=>0}; end
231
+ }
232
+ def versioned_cached_state
233
+ "#{session[:version].inspect} should change every third call!"
234
+ end
235
+
236
+
237
+ def my_version_proc
238
+ if (v = session[:version]) > 0
239
+ {:version=>v}
240
+ else
241
+ {:version=>0}; end
242
+ end
243
+ #def cached_state_with_symbol_proc
244
+ #
245
+ #end
246
+ cache :cheers
247
+ def cheers
248
+ "cheers!"
249
+ end
250
+
251
+ cache :another_state
252
+ def another_state
253
+ @opts[:str]
254
+ end
255
+ end
256
+
257
+ class AnotherCachingCell < Cell::Base
258
+ cache :cheers
259
+ def cheers
260
+ "prost!"
261
+ end
262
+
263
+ def another_state
264
+ @opts[:str]
265
+ end
266
+ end