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.
- data/CHANGELOG +94 -0
- data/README +24 -0
- data/lib/action_controller.rb +2 -0
- data/lib/action_controller/assertions/action_pack_assertions.rb +1 -1
- data/lib/action_controller/base.rb +15 -2
- data/lib/action_controller/caching.rb +6 -16
- data/lib/action_controller/components.rb +1 -1
- data/lib/action_controller/flash.rb +125 -29
- data/lib/action_controller/pagination.rb +378 -0
- data/lib/action_controller/request.rb +13 -6
- data/lib/action_controller/routing.rb +37 -3
- data/lib/action_controller/test_process.rb +7 -3
- data/lib/action_controller/url_rewriter.rb +5 -4
- data/lib/action_view/helpers/asset_tag_helper.rb +35 -4
- data/lib/action_view/helpers/capture_helper.rb +95 -0
- data/lib/action_view/helpers/form_helper.rb +1 -1
- data/lib/action_view/helpers/form_options_helper.rb +2 -0
- data/lib/action_view/helpers/form_tag_helper.rb +28 -10
- data/lib/action_view/helpers/javascript_helper.rb +192 -0
- data/lib/action_view/helpers/javascripts/prototype.js +336 -0
- data/lib/action_view/helpers/pagination_helper.rb +71 -0
- data/lib/action_view/helpers/tag_helper.rb +2 -1
- data/lib/action_view/helpers/text_helper.rb +15 -2
- data/lib/action_view/helpers/url_helper.rb +3 -20
- data/lib/action_view/partials.rb +4 -2
- data/rakefile +2 -2
- data/test/controller/action_pack_assertions_test.rb +1 -2
- data/test/controller/flash_test.rb +30 -5
- data/test/controller/request_test.rb +33 -10
- data/test/controller/routing_tests.rb +26 -0
- data/test/template/asset_tag_helper_test.rb +87 -2
- data/test/template/form_helper_test.rb +1 -0
- data/test/template/form_options_helper_test.rb +11 -0
- data/test/template/form_tag_helper_test.rb +84 -16
- data/test/template/tag_helper_test.rb +2 -15
- data/test/template/text_helper_test.rb +6 -0
- data/test/template/url_helper_test.rb +13 -18
- metadata +10 -5
- 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
|
data/lib/action_controller.rb
CHANGED
@@ -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[
|
99
|
+
# @session[:person] = Person.authenticate(user_name, password)
|
100
100
|
#
|
101
101
|
# And retrieved again through the same hash:
|
102
102
|
#
|
103
|
-
# Hello #{@session[
|
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
|
-
|
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
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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
|
-
|
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(:
|
31
|
+
base.after_filter(:sweep_flash)
|
29
32
|
end
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
@
|
40
|
-
end
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
#
|
49
|
-
def
|
50
|
-
|
51
|
-
@
|
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
|
-
|
126
|
+
keys.each{|key| use key, v }
|
55
127
|
end
|
56
128
|
end
|
57
129
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|