actionpack 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (39) hide show
  1. data/CHANGELOG +94 -0
  2. data/README +24 -0
  3. data/lib/action_controller.rb +2 -0
  4. data/lib/action_controller/assertions/action_pack_assertions.rb +1 -1
  5. data/lib/action_controller/base.rb +15 -2
  6. data/lib/action_controller/caching.rb +6 -16
  7. data/lib/action_controller/components.rb +1 -1
  8. data/lib/action_controller/flash.rb +125 -29
  9. data/lib/action_controller/pagination.rb +378 -0
  10. data/lib/action_controller/request.rb +13 -6
  11. data/lib/action_controller/routing.rb +37 -3
  12. data/lib/action_controller/test_process.rb +7 -3
  13. data/lib/action_controller/url_rewriter.rb +5 -4
  14. data/lib/action_view/helpers/asset_tag_helper.rb +35 -4
  15. data/lib/action_view/helpers/capture_helper.rb +95 -0
  16. data/lib/action_view/helpers/form_helper.rb +1 -1
  17. data/lib/action_view/helpers/form_options_helper.rb +2 -0
  18. data/lib/action_view/helpers/form_tag_helper.rb +28 -10
  19. data/lib/action_view/helpers/javascript_helper.rb +192 -0
  20. data/lib/action_view/helpers/javascripts/prototype.js +336 -0
  21. data/lib/action_view/helpers/pagination_helper.rb +71 -0
  22. data/lib/action_view/helpers/tag_helper.rb +2 -1
  23. data/lib/action_view/helpers/text_helper.rb +15 -2
  24. data/lib/action_view/helpers/url_helper.rb +3 -20
  25. data/lib/action_view/partials.rb +4 -2
  26. data/rakefile +2 -2
  27. data/test/controller/action_pack_assertions_test.rb +1 -2
  28. data/test/controller/flash_test.rb +30 -5
  29. data/test/controller/request_test.rb +33 -10
  30. data/test/controller/routing_tests.rb +26 -0
  31. data/test/template/asset_tag_helper_test.rb +87 -2
  32. data/test/template/form_helper_test.rb +1 -0
  33. data/test/template/form_options_helper_test.rb +11 -0
  34. data/test/template/form_tag_helper_test.rb +84 -16
  35. data/test/template/tag_helper_test.rb +2 -15
  36. data/test/template/text_helper_test.rb +6 -0
  37. data/test/template/url_helper_test.rb +13 -18
  38. metadata +10 -5
  39. data/test/controller/url_obsolete.rb.rej +0 -747
data/CHANGELOG CHANGED
@@ -1,3 +1,97 @@
1
+ *1.6.0* (22th March, 2005)
2
+
3
+ * Added a JavascriptHelper and accompanying prototype.js library that opens the world of Ajax to Action Pack with a large array of options for dynamically interacting with an application without reloading the page #884 [Sam Stephenson/David]
4
+
5
+ * Added pagination support through both a controller and helper add-on #817 [Sam Stephenson]
6
+
7
+ * Fixed routing and helpers to make Rails work on non-vhost setups #826 [Nicholas Seckar/Tobias Luetke]
8
+
9
+ * Added a much improved Flash module that allows for finer-grained control on expiration and allows you to flash the current action #839 [Caio Chassot]. Example of flash.now:
10
+
11
+ class SomethingController < ApplicationController
12
+ def save
13
+ ...
14
+ if @something.save
15
+ # will redirect, use flash
16
+ flash[:message] = 'Save successful'
17
+ redirect_to :action => 'list'
18
+ else
19
+ # no redirect, message is for current action, use flash.now
20
+ flash.now[:message] = 'Save failed, review'
21
+ render_action 'edit'
22
+ end
23
+ end
24
+ end
25
+
26
+ * Added to_param call for parameters when composing an url using url_for from something else than strings #812 [Sam Stephenson]. Example:
27
+
28
+ class Page
29
+   def initialize(number)
30
+     @number = number
31
+   end
32
+   # ...
33
+   def to_param
34
+     @number.to_s
35
+   end
36
+ end
37
+
38
+ You can now use instances of Page with url_for:
39
+
40
+ class BarController < ApplicationController
41
+   def baz
42
+     page = Page.new(4)
43
+     url = url_for :page => page # => "http://foo/bar/baz?page=4"
44
+   end
45
+ end
46
+
47
+ * Fixed form helpers to query Model#id_before_type_cast instead of Model#id as a temporary workaround for Ruby 1.8.2 warnings #818 [DeLynn B]
48
+
49
+ * Fixed TextHelper#markdown to use blank? instead of empty? so it can deal with nil strings passed #814 [Johan Sörensen]
50
+
51
+ * Added TextHelper#simple_format as a non-dependency text presentation helper #814 [Johan Sörensen]
52
+
53
+ * Added that the html options disabled, readonly, and multiple can all be treated as booleans. So specifying <tt>disabled => :true</tt> will give <tt>disabled="disabled"</tt>. #809 [mindel]
54
+
55
+ * Added path collection syntax for Routes that will gobble up the rest of the url and pass it on to the controller #830 [rayners]. Example:
56
+
57
+ map.connect 'categories/*path_info', :controller => 'categories', :action => 'show'
58
+
59
+ A request for /categories/top-level-cat, would give @params[:path_info] with "top-level-cat".
60
+ A request for /categories/top-level-cat/level-1-cat, would give @params[:path_info] with "top-level-cat/level-1-cat" and so forth.
61
+
62
+ The @params[:path_info] return is really an array, but where to_s has been overwritten to do join("/").
63
+
64
+ * Fixed options_for_select on selected line issue #624 [Florian Weber]
65
+
66
+ * Added CaptureHelper with CaptureHelper#capture and CaptureHelper#content_for. See documentation in helper #837 [Tobias Luetke]
67
+
68
+ * Fixed :anchor use in url_for #821 [Nicholas Seckar]
69
+
70
+ * Removed the reliance on PATH_INFO as it was causing problems for caching and inhibited the new non-vhost support #822 [Nicholas Seckar]
71
+
72
+ * Added assigns shortcut for @response.template.assigns to controller test cases [bitsweat]. Example:
73
+
74
+ Before:
75
+
76
+ def test_list
77
+ assert_equal 5, @response.template.assigns['recipes'].size
78
+ assert_equal 8, @response.template.assigns['categories'].size
79
+ end
80
+
81
+ After:
82
+
83
+ def test_list
84
+ assert_equal 5, assigns(:recipes).size
85
+ assert_equal 8, assigns(:categories).size
86
+ end
87
+
88
+ * Added TagHelper#image_tag and deprecated UrlHelper#link_image_to (recommended approach is to combine image_tag and link_to instead)
89
+
90
+ * Fixed textilize to be resilient to getting nil parsed (by using Object#blank? instead of String#empty?)
91
+
92
+ * Fixed that the :multipart option in FormTagHelper#form_tag would be ignored [Yonatan Feldman]
93
+
94
+
1
95
  *1.5.1* (7th March, 2005)
2
96
 
3
97
  * Fixed that the routes.rb file wouldn't be found on symlinked setups due to File.expand_path #793 [piotr@t-p-l.com]
data/README CHANGED
@@ -168,6 +168,30 @@ A short rundown of the major features:
168
168
  {Learn more}[link:classes/ActionController/Base.html]
169
169
 
170
170
 
171
+ * Javascript and Ajax integration.
172
+
173
+ link_to_function "Greeting", "alert('Hello world!')"
174
+ link_to_remote "Delete this post", :update => "posts",
175
+ :url => { :action => "destroy", :id => post.id }
176
+
177
+ {Learn more}[link:classes/ActionView/Helpers/JavascriptHelper.html]
178
+
179
+
180
+ * Pagination for navigating lists of results.
181
+
182
+ # controller
183
+ def list
184
+ @pages, @people =
185
+ paginate :people, :order_by => 'last_name, first_name'
186
+ end
187
+
188
+ # view
189
+ <%= link_to "Previous page", { :page => @pages.current.previous } if @pages.current.previous %>
190
+ <%= link_to "Next page", { :page => @pages.current.next } of @pages.current.next =%>
191
+
192
+ {Learn more}[link:classes/ActionController/Pagination.html]
193
+
194
+
171
195
  * Easy testing of both controller and template result through TestRequest/Response
172
196
 
173
197
  class LoginControllerTest < Test::Unit::TestCase
@@ -39,6 +39,7 @@ require 'action_controller/layout'
39
39
  require 'action_controller/flash'
40
40
  require 'action_controller/session'
41
41
  require 'action_controller/dependencies'
42
+ require 'action_controller/pagination'
42
43
  require 'action_controller/scaffolding'
43
44
  require 'action_controller/helpers'
44
45
  require 'action_controller/cookies'
@@ -56,6 +57,7 @@ ActionController::Base.class_eval do
56
57
  include ActionController::Benchmarking
57
58
  include ActionController::Rescue
58
59
  include ActionController::Dependencies
60
+ include ActionController::Pagination
59
61
  include ActionController::Scaffolding
60
62
  include ActionController::Helpers
61
63
  include ActionController::Cookies
@@ -137,7 +137,7 @@ module Test #:nodoc:
137
137
  if options.is_a?(Symbol)
138
138
  response.redirected_to == options
139
139
  else
140
- options.keys.all? { |k| options[k] == response.redirected_to[k] }
140
+ options.keys.all? { |k| options[k] == ( response.redirected_to[k].respond_to?(:to_param) ? response.redirected_to[k].to_param : response.redirected_to[k] if response.redirected_to[k] ) }
141
141
  end
142
142
  end
143
143
  end
@@ -96,16 +96,19 @@ module ActionController #:nodoc:
96
96
  #
97
97
  # You can place objects in the session by using the <tt>@session</tt> hash:
98
98
  #
99
- # @session["person"] = Person.authenticate(user_name, password)
99
+ # @session[:person] = Person.authenticate(user_name, password)
100
100
  #
101
101
  # And retrieved again through the same hash:
102
102
  #
103
- # Hello #{@session["person"]}
103
+ # Hello #{@session[:person]}
104
104
  #
105
105
  # Any object can be placed in the session (as long as it can be Marshalled). But remember that 1000 active sessions each storing a
106
106
  # 50kb object could lead to a 50MB memory overhead. In other words, think carefully about size and caching before resorting to the use
107
107
  # of the session.
108
108
  #
109
+ # For removing objects from the session, you can either assign a single key to nil, like <tt>@session[:person] = nil</tt>, or you can
110
+ # remove the entire session with reset_session.
111
+ #
109
112
  # == Responses
110
113
  #
111
114
  # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
@@ -476,6 +479,16 @@ module ActionController #:nodoc:
476
479
  add_variables_to_assigns
477
480
  @template.render_file(template_name)
478
481
  end
482
+
483
+ def render_partial(partial_path, object = nil, local_assigns = {}) #:doc:
484
+ add_variables_to_assigns
485
+ render_text(@template.render_partial(partial_path, object, local_assigns))
486
+ end
487
+
488
+ def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {})#:doc:
489
+ add_variables_to_assigns
490
+ render_text(@template.render_collection_of_partials(partial_name, collection, partial_spacer_template, local_assigns))
491
+ end
479
492
 
480
493
  # Sends the file by streaming it 4096 bytes at a time. This way the
481
494
  # whole file doesn't need to be read into memory at once. This makes
@@ -292,11 +292,7 @@ module ActionController #:nodoc:
292
292
  end
293
293
 
294
294
  def read(name, options = {}) #:nodoc:
295
- begin
296
- @mutex.synchronize { @data[name] }
297
- rescue
298
- nil
299
- end
295
+ @mutex.synchronize { @data[name] } rescue nil
300
296
  end
301
297
 
302
298
  def write(name, value, options = {}) #:nodoc:
@@ -326,20 +322,14 @@ module ActionController #:nodoc:
326
322
  end
327
323
 
328
324
  def write(name, value, options = {}) #:nodoc:
329
- begin
330
- ensure_cache_path(File.dirname(real_file_path(name)))
331
- File.open(real_file_path(name), "w+") { |f| f.write(value) }
332
- rescue => e
333
- Base.logger.info "Couldn't create cache directory: #{name} (#{e.message})" unless Base.logger.nil?
334
- end
325
+ ensure_cache_path(File.dirname(real_file_path(name)))
326
+ File.open(real_file_path(name), "w+") { |f| f.write(value) }
327
+ rescue => e
328
+ Base.logger.info "Couldn't create cache directory: #{name} (#{e.message})" unless Base.logger.nil?
335
329
  end
336
330
 
337
331
  def read(name, options = {}) #:nodoc:
338
- begin
339
- IO.read(real_file_path(name))
340
- rescue
341
- nil
342
- end
332
+ IO.read(real_file_path(name)) rescue nil
343
333
  end
344
334
 
345
335
  def delete(name, options) #:nodoc:
@@ -54,7 +54,7 @@ module ActionController #:nodoc:
54
54
  request_for_component = @request.dup
55
55
  request_for_component.send(
56
56
  :instance_variable_set, :@parameters,
57
- (options[:params] || {}).merge({ "controller" => options[:controller], "action" => options[:action], "id" => options[:id] })
57
+ (options[:params] || {}).merge({ "controller" => options[:controller], "action" => options[:action], "id" => options[:id] }).with_indifferent_access
58
58
  )
59
59
  return request_for_component
60
60
  end
@@ -21,45 +21,141 @@ module ActionController #:nodoc:
21
21
  #
22
22
  # This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
23
23
  # as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
24
+ #
25
+ # See docs on the FlashHash class for more details about the flash.
24
26
  module Flash
27
+
25
28
  def self.append_features(base) #:nodoc:
26
29
  super
27
30
  base.before_filter(:fire_flash)
28
- base.after_filter(:clear_flash)
31
+ base.after_filter(:sweep_flash)
29
32
  end
30
33
 
31
- protected
32
- # Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
33
- # <tt>flash["notice"] = "hello"</tt> to put a new one.
34
- def flash #:doc:
35
- if @session["flash"].nil?
36
- @session["flash"] = {}
37
- @session["flashes"] ||= 0
34
+ class FlashNow #:nodoc:
35
+ def initialize flash
36
+ @flash = flash
37
+ end
38
+
39
+ def []=(k, v)
40
+ @flash[k] = v
41
+ @flash.discard(k)
42
+ v
43
+ end
44
+ end
45
+
46
+ class FlashHash < Hash
47
+
48
+ def initialize #:nodoc:
49
+ super
50
+ @used = {}
51
+ end
52
+
53
+ def []=(k, v) #:nodoc:
54
+ keep(k)
55
+ super
56
+ end
57
+
58
+ def update h #:nodoc:
59
+ h.keys.each{|k| discard k }
60
+ super
61
+ end
62
+
63
+ alias merge! update
64
+
65
+ def replace h #:nodoc:
66
+ @used = {}
67
+ super
68
+ end
69
+
70
+ # Sets a flash that will not be available to the next action, only to the current.
71
+ #
72
+ # flash.now["message"] = "Hello current action"
73
+ #
74
+ # This method enables you to use the flash as a central messaging system in your app.
75
+ # When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
76
+ # When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
77
+ # vanish when the current action is done.
78
+ #
79
+ # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
80
+ def now
81
+ FlashNow.new self
82
+ end
83
+
84
+ # Keeps either the entire current flash or a specific flash entry available for the next action:
85
+ #
86
+ # flash.keep # keeps the entire flash
87
+ # flash.keep("notice") # keeps only the "notice" entry, the rest of the flash is discarded
88
+ def keep(k=nil)
89
+ use(k, false)
90
+ end
91
+
92
+ # Marks the entire flash or a single flash entry to be discarded by the end of the current action
93
+ #
94
+ # flash.keep # keep entire flash available for the next action
95
+ # flash.discard('warning') # discard the "warning" entry (it'll still be available for the current action)
96
+ def discard(k=nil)
97
+ use(k)
98
+ end
99
+
100
+ # Mark for removal entries that were kept, and delete unkept ones.
101
+ #
102
+ # This method is called automatically by filters, so you generally don't need to care about it.
103
+ def sweep #:nodoc:
104
+ keys.each do |k|
105
+ unless @used[k]
106
+ use(k)
107
+ else
108
+ delete(k)
109
+ @used.delete(k)
110
+ end
38
111
  end
39
- @session["flash"]
40
- end
41
-
42
- # Can be called by any action that would like to keep the current content of the flash around for one more action.
43
- def keep_flash #:doc:
44
- @session["flashes"] = 0
45
- end
46
-
47
- private
48
- # Records that the contents of @session["flash"] was flashed to the action
49
- def fire_flash
50
- if @session["flash"]
51
- @session["flashes"] += 1 unless @session["flash"].empty?
52
- @assigns["flash"] = @session["flash"]
112
+ (@used.keys - keys).each{|k| @used.delete k } # clean up after keys that could have been left over by calling reject! or shift on the flash
113
+ end
114
+
115
+ private
116
+
117
+ # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
118
+ # use() # marks the entire flash as used
119
+ # use('msg') # marks the "msg" entry as used
120
+ # use(nil, false) # marks the entire flash as unused (keeps it around for one more action)
121
+ # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
122
+ def use(k=nil, v=true)
123
+ unless k.nil?
124
+ @used[k] = v
53
125
  else
54
- @assigns["flash"] = {}
126
+ keys.each{|key| use key, v }
55
127
  end
56
128
  end
57
129
 
58
- def clear_flash
59
- if @session["flash"] && (@session["flashes"].nil? || @session["flashes"] >= 1)
60
- @session["flash"] = {}
61
- @session["flashes"] = 0
62
- end
63
- end
130
+ end
131
+
132
+
133
+ protected
134
+
135
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
136
+ # <tt>flash["notice"] = "hello"</tt> to put a new one.
137
+ def flash #:doc:
138
+ @session['flash'] ||= FlashHash.new
139
+ end
140
+
141
+ # deprecated. use <tt>flash.keep</tt> instead
142
+ def keep_flash #:doc:
143
+ flash.keep
144
+ end
145
+
146
+
147
+ private
148
+
149
+ # marks flash entries as used and expose the flash to the view
150
+ def fire_flash
151
+ flash.discard
152
+ @assigns["flash"] = flash
153
+ end
154
+
155
+ # deletes the flash entries that were not marked for keeping
156
+ def sweep_flash
157
+ flash.sweep
158
+ end
159
+
64
160
  end
65
161
  end
@@ -0,0 +1,378 @@
1
+ module ActionController
2
+ # === Action Pack pagination for Active Record collections
3
+ #
4
+ # The Pagination module aids in the process of paging large collections of
5
+ # Active Record objects. It offers macro-style automatic fetching of your
6
+ # model for multiple views, or explicit fetching for single actions. And if
7
+ # the magic isn't flexible enough for your needs, you can create your own
8
+ # paginators with a minimal amount of code.
9
+ #
10
+ # The Pagination module can handle as much or as little as you wish. In the
11
+ # controller, have it automatically query your model for pagination; or,
12
+ # if you prefer, create Paginator objects yourself.
13
+ #
14
+ # Pagination is included automatically for all controllers.
15
+ #
16
+ # For help rendering pagination links, see
17
+ # ActionView::Helpers::PaginationHelper.
18
+ #
19
+ # ==== Automatic pagination for every action in a controller
20
+ #
21
+ # class PersonController < ApplicationController
22
+ # model :person
23
+ #
24
+ # paginate :people, :order_by => 'last_name, first_name',
25
+ # :per_page => 20
26
+ #
27
+ # # ...
28
+ # end
29
+ #
30
+ # Each action in this controller now has access to a <tt>@people</tt>
31
+ # instance variable, which is an ordered collection of model objects for the
32
+ # current page (at most 20, sorted by last name and first name), and a
33
+ # <tt>@person_pages</tt> Paginator instance. The current page is determined
34
+ # by the <tt>@params['page']</tt> variable.
35
+ #
36
+ # ==== Pagination for a single action
37
+ #
38
+ # def list
39
+ # @person_pages, @people =
40
+ # paginate :people, :order_by => 'last_name, first_name'
41
+ # end
42
+ #
43
+ # Like the previous example, but explicitly creates <tt>@person_pages</tt>
44
+ # and <tt>@people</tt> for a single action, and uses the default of 10 items
45
+ # per page.
46
+ #
47
+ # ==== Custom/"classic" pagination
48
+ #
49
+ # def list
50
+ # @person_pages = Paginator.new self, Person.count, 10, @params['page']
51
+ # @people = Person.find_all nil, 'last_name, first_name',
52
+ # @person_pages.current.to_sql
53
+ # end
54
+ #
55
+ # Explicitly creates the paginator from the previous example and uses
56
+ # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
57
+ #
58
+ module Pagination
59
+ unless const_defined?(:OPTIONS)
60
+ # A hash holding options for controllers using macro-style pagination
61
+ OPTIONS = Hash.new
62
+
63
+ # The default options for pagination
64
+ DEFAULT_OPTIONS = {
65
+ :class_name => nil,
66
+ :per_page => 10,
67
+ :conditions => nil,
68
+ :order_by => nil,
69
+ :join => nil,
70
+ :parameter => 'page'
71
+ }
72
+ end
73
+
74
+ def self.included(base) #:nodoc:
75
+ super
76
+ base.extend(ClassMethods)
77
+ end
78
+
79
+ def self.validate_options!(collection_id, options, in_action) #:nodoc:
80
+ options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
81
+
82
+ valid_options = DEFAULT_OPTIONS.keys
83
+ valid_options << :actions unless in_action
84
+
85
+ unknown_option_keys = options.keys - valid_options
86
+ raise ActionController::ActionControllerError,
87
+ "Unknown options: #{unknown_option_keys.join(', ')}" unless
88
+ unknown_option_keys.empty?
89
+
90
+ options[:singular_name] = Inflector.singularize(collection_id.to_s)
91
+ options[:class_name] ||= Inflector.camelize(options[:singular_name])
92
+ end
93
+
94
+ # Returns a paginator and a collection of Active Record model instances
95
+ # for the paginator's current page. This is designed to be used in a
96
+ # single action; to automatically paginate multiple actions, consider
97
+ # ClassMethods#paginate.
98
+ #
99
+ # +options+ are:
100
+ # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
101
+ # singularizing the collection name
102
+ # <tt>:per_page</tt>:: the maximum number of items to include in a
103
+ # single page. Defaults to 10
104
+ # <tt>:conditions</tt>:: optional conditions passed to Model.find_all and
105
+ # Model.count
106
+ # <tt>:order_by</tt>:: optional order parameter passed to Model.find_all
107
+ # <tt>:join</tt>:: optional join parameter passed to Model.find_all
108
+ # and Model.count
109
+ def paginate(collection_id, options={})
110
+ Pagination.validate_options!(collection_id, options, true)
111
+ paginator_and_collection_for(collection_id, options)
112
+ end
113
+
114
+ # These methods become class methods on any controller
115
+ module ClassMethods
116
+ # Creates a +before_filter+ which automatically paginates an Active
117
+ # Record model for all actions in a controller (or certain actions if
118
+ # specified with the <tt>:actions</tt> option).
119
+ #
120
+ # +options+ are the same as PaginationHelper#paginate, with the addition
121
+ # of:
122
+ # <tt>:actions</tt>:: an array of actions for which the pagination is
123
+ # active. Defaults to +nil+ (i.e., every action)
124
+ def paginate(collection_id, options={})
125
+ Pagination.validate_options!(collection_id, options, false)
126
+ module_eval do
127
+ before_filter :create_paginators_and_retrieve_collections
128
+ OPTIONS[self] ||= Hash.new
129
+ OPTIONS[self][collection_id] = options
130
+ end
131
+ end
132
+ end
133
+
134
+ def create_paginators_and_retrieve_collections #:nodoc:
135
+ Pagination::OPTIONS[self.class].each do |collection_id, options|
136
+ next unless options[:actions].include? action_name if
137
+ options[:actions]
138
+
139
+ paginator, collection =
140
+ paginator_and_collection_for(collection_id, options)
141
+
142
+ paginator_name = "@#{options[:singular_name]}_pages"
143
+ self.instance_variable_set(paginator_name, paginator)
144
+
145
+ collection_name = "@#{collection_id.to_s}"
146
+ self.instance_variable_set(collection_name, collection)
147
+ end
148
+ end
149
+
150
+ # Returns the total number of items in the collection to be paginated for
151
+ # the +model+ and given +conditions+. Override this method to implement a
152
+ # custom counter.
153
+ def count_collection_for_pagination(model, conditions)
154
+ model.count(conditions)
155
+ end
156
+
157
+ # Returns a collection of items for the given +model+ and +conditions+,
158
+ # ordered by +order_by+, for the current page in the given +paginator+.
159
+ # Override this method to implement a custom finder.
160
+ def find_collection_for_pagination(model, conditions, order_by, join, paginator)
161
+ model.find_all(conditions, order_by, paginator.current.to_sql, join)
162
+ end
163
+
164
+ protected :create_paginators_and_retrieve_collections,
165
+ :count_collection_for_pagination,
166
+ :find_collection_for_pagination
167
+
168
+ def paginator_and_collection_for(collection_id, options) #:nodoc:
169
+ klass = eval options[:class_name]
170
+ page = @params[options[:parameter]]
171
+ count = count_collection_for_pagination(klass, options[:conditions])
172
+
173
+ paginator = Paginator.new(self, count, options[:per_page], page)
174
+
175
+ collection = find_collection_for_pagination(klass,
176
+ options[:conditions], options[:order_by], options[:join], paginator)
177
+
178
+ return paginator, collection
179
+ end
180
+
181
+ private :paginator_and_collection_for
182
+
183
+ # A class representing a paginator for an Active Record collection.
184
+ class Paginator
185
+ include Enumerable
186
+
187
+ # Creates a new Paginator on the given +controller+ for a set of items
188
+ # of size +item_count+ and having +items_per_page+ items per page.
189
+ # Raises ArgumentError if items_per_page is out of bounds (i.e., less
190
+ # than or equal to zero). The page CGI parameter for links defaults to
191
+ # "page" and can be overridden with +page_parameter+.
192
+ def initialize(controller, item_count, items_per_page, current_page=1)
193
+ raise ArgumentError, 'must have at least one item per page' if
194
+ items_per_page <= 0
195
+
196
+ @controller = controller
197
+ @item_count = item_count || 0
198
+ @items_per_page = items_per_page
199
+
200
+ self.current_page = current_page
201
+ end
202
+ attr_reader :controller, :item_count, :items_per_page
203
+
204
+ # Sets the current page number of this paginator. If +page+ is a Page
205
+ # object, its +number+ attribute is used as the value; if the page does
206
+ # not belong to this Paginator, an ArgumentError is raised.
207
+ def current_page=(page)
208
+ if page.is_a? Page
209
+ raise ArgumentError, 'Page/Paginator mismatch' unless
210
+ page.paginator == self
211
+ end
212
+ page = page.to_i
213
+ @current_page = has_page_number?(page) ? page : 1
214
+ end
215
+
216
+ # Returns a Page object representing this paginator's current page.
217
+ def current_page
218
+ self[@current_page]
219
+ end
220
+ alias current :current_page
221
+
222
+ # Returns a new Page representing the first page in this paginator.
223
+ def first_page
224
+ self[1]
225
+ end
226
+ alias first :first_page
227
+
228
+ # Returns a new Page representing the last page in this paginator.
229
+ def last_page
230
+ self[page_count]
231
+ end
232
+ alias last :last_page
233
+
234
+ # Returns the number of pages in this paginator.
235
+ def page_count
236
+ return 1 if @item_count.zero?
237
+ (@item_count / @items_per_page.to_f).ceil
238
+ end
239
+ alias length :page_count
240
+
241
+ # Returns true if this paginator contains the page of index +number+.
242
+ def has_page_number?(number)
243
+ return false unless number.is_a? Fixnum
244
+ number >= 1 and number <= page_count
245
+ end
246
+
247
+ # Returns a new Page representing the page with the given index
248
+ # +number+.
249
+ def [](number)
250
+ Page.new(self, number)
251
+ end
252
+
253
+ # Successively yields all the paginator's pages to the given block.
254
+ def each(&block)
255
+ page_count.times do |n|
256
+ yield self[n+1]
257
+ end
258
+ end
259
+
260
+ # A class representing a single page in a paginator.
261
+ class Page
262
+ include Comparable
263
+
264
+ # Creates a new Page for the given +paginator+ with the index
265
+ # +number+. If +number+ is not in the range of valid page numbers or
266
+ # is not a number at all, it defaults to 1.
267
+ def initialize(paginator, number)
268
+ @paginator = paginator
269
+ @number = number.to_i
270
+ @number = 1 unless @paginator.has_page_number? @number
271
+ end
272
+ attr_reader :paginator, :number
273
+ alias to_i :number
274
+
275
+ # Compares two Page objects and returns true when they represent the
276
+ # same page (i.e., their paginators are the same and they have the
277
+ # same page number).
278
+ def ==(page)
279
+ return false if page.nil?
280
+ @paginator == page.paginator and
281
+ @number == page.number
282
+ end
283
+
284
+ # Compares two Page objects and returns -1 if the left-hand page comes
285
+ # before the right-hand page, 0 if the pages are equal, and 1 if the
286
+ # left-hand page comes after the right-hand page. Raises ArgumentError
287
+ # if the pages do not belong to the same Paginator object.
288
+ def <=>(page)
289
+ raise ArgumentError unless @paginator == page.paginator
290
+ @number <=> page.number
291
+ end
292
+
293
+ # Returns the item offset for the first item in this page.
294
+ def offset
295
+ @paginator.items_per_page * (@number - 1)
296
+ end
297
+
298
+ # Returns the number of the first item displayed.
299
+ def first_item
300
+ offset + 1
301
+ end
302
+
303
+ # Returns the number of the last item displayed.
304
+ def last_item
305
+ [@paginator.items_per_page * @number, @paginator.item_count].min
306
+ end
307
+
308
+ # Returns true if this page is the first page in the paginator.
309
+ def first?
310
+ self == @paginator.first
311
+ end
312
+
313
+ # Returns true if this page is the last page in the paginator.
314
+ def last?
315
+ self == @paginator.last
316
+ end
317
+
318
+ # Returns a new Page object representing the page just before this
319
+ # page, or nil if this is the first page.
320
+ def previous
321
+ if first? then nil else Page.new(@paginator, @number - 1) end
322
+ end
323
+
324
+ # Returns a new Page object representing the page just after this
325
+ # page, or nil if this is the last page.
326
+ def next
327
+ if last? then nil else Page.new(@paginator, @number + 1) end
328
+ end
329
+
330
+ # Returns a new Window object for this page with the specified
331
+ # +padding+.
332
+ def window(padding=2)
333
+ Window.new(self, padding)
334
+ end
335
+
336
+ # Returns the limit/offset array for this page.
337
+ def to_sql
338
+ [@paginator.items_per_page, offset]
339
+ end
340
+
341
+ def to_param #:nodoc:
342
+ @number.to_s
343
+ end
344
+ end
345
+
346
+ # A class for representing ranges around a given page.
347
+ class Window
348
+ # Creates a new Window object for the given +page+ with the specified
349
+ # +padding+.
350
+ def initialize(page, padding=2)
351
+ @paginator = page.paginator
352
+ @page = page
353
+ self.padding = padding
354
+ end
355
+ attr_reader :paginator, :page
356
+
357
+ # Sets the window's padding (the number of pages on either side of the
358
+ # window page).
359
+ def padding=(padding)
360
+ @padding = padding < 0 ? 0 : padding
361
+ # Find the beginning and end pages of the window
362
+ @first = @paginator.has_page_number?(@page.number - @padding) ?
363
+ @paginator[@page.number - @padding] : @paginator.first
364
+ @last = @paginator.has_page_number?(@page.number + @padding) ?
365
+ @paginator[@page.number + @padding] : @paginator.last
366
+ end
367
+ attr_reader :padding, :first, :last
368
+
369
+ # Returns an array of Page objects in the current window.
370
+ def pages
371
+ (@first.number..@last.number).to_a.map {|n| @paginator[n]}
372
+ end
373
+ alias to_a :pages
374
+ end
375
+ end
376
+
377
+ end
378
+ end