cells 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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