dweinand-will_paginate 2.3.4

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.
Files changed (45) hide show
  1. data/CHANGELOG.rdoc +100 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +196 -0
  4. data/Rakefile +62 -0
  5. data/examples/apple-circle.gif +0 -0
  6. data/examples/index.haml +69 -0
  7. data/examples/index.html +92 -0
  8. data/examples/pagination.css +90 -0
  9. data/examples/pagination.sass +91 -0
  10. data/init.rb +1 -0
  11. data/lib/will_paginate/array.rb +16 -0
  12. data/lib/will_paginate/collection.rb +175 -0
  13. data/lib/will_paginate/core_ext.rb +32 -0
  14. data/lib/will_paginate/deserializer.rb +97 -0
  15. data/lib/will_paginate/finder.rb +256 -0
  16. data/lib/will_paginate/named_scope.rb +132 -0
  17. data/lib/will_paginate/named_scope_patch.rb +39 -0
  18. data/lib/will_paginate/version.rb +9 -0
  19. data/lib/will_paginate/view_helpers.rb +383 -0
  20. data/lib/will_paginate.rb +94 -0
  21. data/test/boot.rb +22 -0
  22. data/test/collection_test.rb +161 -0
  23. data/test/console +8 -0
  24. data/test/database.yml +22 -0
  25. data/test/finder_test.rb +448 -0
  26. data/test/fixtures/admin.rb +3 -0
  27. data/test/fixtures/developer.rb +14 -0
  28. data/test/fixtures/developers_projects.yml +13 -0
  29. data/test/fixtures/project.rb +15 -0
  30. data/test/fixtures/projects.yml +6 -0
  31. data/test/fixtures/replies.yml +29 -0
  32. data/test/fixtures/reply.rb +7 -0
  33. data/test/fixtures/schema.rb +38 -0
  34. data/test/fixtures/topic.rb +6 -0
  35. data/test/fixtures/topics.yml +30 -0
  36. data/test/fixtures/user.rb +2 -0
  37. data/test/fixtures/users.yml +35 -0
  38. data/test/helper.rb +42 -0
  39. data/test/lib/activerecord_test_case.rb +36 -0
  40. data/test/lib/activerecord_test_connector.rb +73 -0
  41. data/test/lib/load_fixtures.rb +11 -0
  42. data/test/lib/view_test_process.rb +165 -0
  43. data/test/tasks.rake +59 -0
  44. data/test/view_test.rb +363 -0
  45. metadata +133 -0
@@ -0,0 +1,91 @@
1
+ .digg_pagination
2
+ :background white
3
+ a, span
4
+ :padding .2em .5em
5
+ :display block
6
+ :float left
7
+ :margin-right 1px
8
+ span.disabled
9
+ :color #999
10
+ :border 1px solid #DDD
11
+ span.current
12
+ :font-weight bold
13
+ :background #2E6AB1
14
+ :color white
15
+ :border 1px solid #2E6AB1
16
+ a
17
+ :text-decoration none
18
+ :color #105CB6
19
+ :border 1px solid #9AAFE5
20
+ &:hover, &:focus
21
+ :color #003
22
+ :border-color #003
23
+ .page_info
24
+ :background #2E6AB1
25
+ :color white
26
+ :padding .4em .6em
27
+ :width 22em
28
+ :margin-bottom .3em
29
+ :text-align center
30
+ b
31
+ :color #003
32
+ :background = #2E6AB1 + 60
33
+ :padding .1em .25em
34
+
35
+ /* self-clearing method:
36
+ &:after
37
+ :content "."
38
+ :display block
39
+ :height 0
40
+ :clear both
41
+ :visibility hidden
42
+ * html &
43
+ :height 1%
44
+ *:first-child+html &
45
+ :overflow hidden
46
+
47
+ .apple_pagination
48
+ :background #F1F1F1
49
+ :border 1px solid #E5E5E5
50
+ :text-align center
51
+ :padding 1em
52
+ a, span
53
+ :padding .2em .3em
54
+ span.disabled
55
+ :color #AAA
56
+ span.current
57
+ :font-weight bold
58
+ :background transparent url(apple-circle.gif) no-repeat 50% 50%
59
+ a
60
+ :text-decoration none
61
+ :color black
62
+ &:hover, &:focus
63
+ :text-decoration underline
64
+
65
+ .flickr_pagination
66
+ :text-align center
67
+ :padding .3em
68
+ a, span
69
+ :padding .2em .5em
70
+ span.disabled
71
+ :color #AAA
72
+ span.current
73
+ :font-weight bold
74
+ :color #FF0084
75
+ a
76
+ :border 1px solid #DDDDDD
77
+ :color #0063DC
78
+ :text-decoration none
79
+ &:hover, &:focus
80
+ :border-color #003366
81
+ :background #0063DC
82
+ :color white
83
+ .page_info
84
+ :color #aaa
85
+ :padding-top .8em
86
+ .prev_page, .next_page
87
+ :border-width 2px
88
+ .prev_page
89
+ :margin-right 1em
90
+ .next_page
91
+ :margin-left 1em
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'will_paginate'
@@ -0,0 +1,16 @@
1
+ require 'will_paginate/collection'
2
+
3
+ # http://www.desimcadam.com/archives/8
4
+ Array.class_eval do
5
+ def paginate(options = {})
6
+ raise ArgumentError, "parameter hash expected (got #{options.inspect})" unless Hash === options
7
+
8
+ WillPaginate::Collection.create(
9
+ options[:page] || 1,
10
+ options[:per_page] || 30,
11
+ options[:total_entries] || self.length
12
+ ) { |pager|
13
+ pager.replace self[pager.offset, pager.per_page].to_a
14
+ }
15
+ end
16
+ end
@@ -0,0 +1,175 @@
1
+ module WillPaginate
2
+ # = Invalid page number error
3
+ # This is an ArgumentError raised in case a page was requested that is either
4
+ # zero or negative number. You should decide how do deal with such errors in
5
+ # the controller.
6
+ #
7
+ # If you're using Rails 2, then this error will automatically get handled like
8
+ # 404 Not Found. The hook is in "will_paginate.rb":
9
+ #
10
+ # ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
11
+ #
12
+ # If you don't like this, use your preffered method of rescuing exceptions in
13
+ # public from your controllers to handle this differently. The +rescue_from+
14
+ # method is a nice addition to Rails 2.
15
+ #
16
+ # This error is *not* raised when a page further than the last page is
17
+ # requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
18
+ # check for those cases and manually deal with them as you see fit.
19
+ class InvalidPage < ArgumentError
20
+ def initialize(page, page_num)
21
+ super "#{page.inspect} given as value, which translates to '#{page_num}' as page number"
22
+ end
23
+ end
24
+
25
+ # = The key to pagination
26
+ # Arrays returned from paginating finds are, in fact, instances of this little
27
+ # class. You may think of WillPaginate::Collection as an ordinary array with
28
+ # some extra properties. Those properties are used by view helpers to generate
29
+ # correct page links.
30
+ #
31
+ # WillPaginate::Collection also assists in rolling out your own pagination
32
+ # solutions: see +create+.
33
+ #
34
+ # If you are writing a library that provides a collection which you would like
35
+ # to conform to this API, you don't have to copy these methods over; simply
36
+ # make your plugin/gem dependant on the "mislav-will_paginate" gem:
37
+ #
38
+ # gem 'mislav-will_paginate'
39
+ # require 'will_paginate/collection'
40
+ #
41
+ # # WillPaginate::Collection is now available for use
42
+ class Collection < Array
43
+ require 'active_support'
44
+ attr_reader :current_page, :per_page, :total_entries, :total_pages
45
+
46
+ # Arguments to the constructor are the current page number, per-page limit
47
+ # and the total number of entries. The last argument is optional because it
48
+ # is best to do lazy counting; in other words, count *conditionally* after
49
+ # populating the collection using the +replace+ method.
50
+ def initialize(page, per_page, total = nil)
51
+ @current_page = page.to_i
52
+ raise InvalidPage.new(page, @current_page) if @current_page < 1
53
+ @per_page = per_page.to_i
54
+ raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1
55
+
56
+ self.total_entries = total if total
57
+ end
58
+
59
+ # Just like +new+, but yields the object after instantiation and returns it
60
+ # afterwards. This is very useful for manual pagination:
61
+ #
62
+ # @entries = WillPaginate::Collection.create(1, 10) do |pager|
63
+ # result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset)
64
+ # # inject the result array into the paginated collection:
65
+ # pager.replace(result)
66
+ #
67
+ # unless pager.total_entries
68
+ # # the pager didn't manage to guess the total count, do it manually
69
+ # pager.total_entries = Post.count
70
+ # end
71
+ # end
72
+ #
73
+ # The possibilities with this are endless. For another example, here is how
74
+ # WillPaginate used to define pagination for Array instances:
75
+ #
76
+ # Array.class_eval do
77
+ # def paginate(page = 1, per_page = 15)
78
+ # WillPaginate::Collection.create(page, per_page, size) do |pager|
79
+ # pager.replace self[pager.offset, pager.per_page].to_a
80
+ # end
81
+ # end
82
+ # end
83
+ #
84
+ # The Array#paginate API has since then changed, but this still serves as a
85
+ # fine example of WillPaginate::Collection usage.
86
+ def self.create(page, per_page, total = nil, &block)
87
+ pager = new(page, per_page, total)
88
+ yield pager
89
+ pager
90
+ end
91
+
92
+ # Helper method that is true when someone tries to fetch a page with a
93
+ # larger number than the last page. Can be used in combination with flashes
94
+ # and redirecting.
95
+ def out_of_bounds?
96
+ current_page > total_pages
97
+ end
98
+
99
+ # Current offset of the paginated collection. If we're on the first page,
100
+ # it is always 0. If we're on the 2nd page and there are 30 entries per page,
101
+ # the offset is 30. This property is useful if you want to render ordinals
102
+ # side by side with records in the view: simply start with offset + 1.
103
+ def offset
104
+ (current_page - 1) * per_page
105
+ end
106
+
107
+ # current_page - 1 or nil if there is no previous page
108
+ def previous_page
109
+ current_page > 1 ? (current_page - 1) : nil
110
+ end
111
+
112
+ # current_page + 1 or nil if there is no next page
113
+ def next_page
114
+ current_page < total_pages ? (current_page + 1) : nil
115
+ end
116
+
117
+ # sets the <tt>total_entries</tt> property and calculates <tt>total_pages</tt>
118
+ def total_entries=(number)
119
+ @total_entries = number.to_i
120
+ @total_pages = (@total_entries / per_page.to_f).ceil
121
+ end
122
+
123
+ # This is a magic wrapper for the original Array#replace method. It serves
124
+ # for populating the paginated collection after initialization.
125
+ #
126
+ # Why magic? Because it tries to guess the total number of entries judging
127
+ # by the size of given array. If it is shorter than +per_page+ limit, then we
128
+ # know we're on the last page. This trick is very useful for avoiding
129
+ # unnecessary hits to the database to do the counting after we fetched the
130
+ # data for the current page.
131
+ #
132
+ # However, after using +replace+ you should always test the value of
133
+ # +total_entries+ and set it to a proper value if it's +nil+. See the example
134
+ # in +create+.
135
+ def replace(array)
136
+ result = super
137
+
138
+ # The collection is shorter then page limit? Rejoice, because
139
+ # then we know that we are on the last page!
140
+ if total_entries.nil? and length < per_page and (current_page == 1 or length > 0)
141
+ self.total_entries = offset + length
142
+ end
143
+
144
+ result
145
+ end
146
+
147
+ # ActiveSupport's Array#to_xml outputs in the form of:
148
+ #
149
+ # <records type="array">...</records>
150
+ #
151
+ # A WillPaginate::Collection needs page, per_page, and total_entries,
152
+ # so the class is distinguished by a special type and those values. This special
153
+ # type is understood by ActiveResource, and looks like:
154
+ #
155
+ # <records type="collection">
156
+ # <current-page>1</current-page>
157
+ # <per-page>30</per-page>
158
+ # <total-entries>1337</total-entries>
159
+ # ...
160
+ # </records>
161
+ def to_xml_with_collection_type(options = {})
162
+ serializeable_collection.to_xml_without_collection_type(options) do |xml|
163
+ xml.tag!(:current_page, {:type => ActiveSupport::CoreExtensions::Hash::Conversions::XML_TYPE_NAMES[current_page.class.name]}, current_page)
164
+ xml.tag!(:per_page, {:type => ActiveSupport::CoreExtensions::Hash::Conversions::XML_TYPE_NAMES[per_page.class.name]}, per_page)
165
+ xml.tag!(:total_entries, {:type => ActiveSupport::CoreExtensions::Hash::Conversions::XML_TYPE_NAMES[total_entries.class.name]}, total_entries)
166
+ end.sub(%{type="array"}, %{type="collection"})
167
+ end
168
+ alias_method_chain :to_xml, :collection_type
169
+
170
+ def serializeable_collection #:nodoc:
171
+ # Ugly hack because to_xml will not yield the XML Builder object when empty?
172
+ empty? ? returning(self.clone) { |c| c.instance_eval {|i| def empty?; false; end } } : self
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,32 @@
1
+ require 'set'
2
+ require 'will_paginate/array'
3
+
4
+ unless Hash.instance_methods.include? 'except'
5
+ Hash.class_eval do
6
+ # Returns a new hash without the given keys.
7
+ def except(*keys)
8
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
9
+ reject { |key,| rejected.include?(key) }
10
+ end
11
+
12
+ # Replaces the hash without only the given keys.
13
+ def except!(*keys)
14
+ replace(except(*keys))
15
+ end
16
+ end
17
+ end
18
+
19
+ unless Hash.instance_methods.include? 'slice'
20
+ Hash.class_eval do
21
+ # Returns a new hash with only the given keys.
22
+ def slice(*keys)
23
+ allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
24
+ reject { |key,| !allowed.include?(key) }
25
+ end
26
+
27
+ # Replaces the hash with only the given keys.
28
+ def slice!(*keys)
29
+ replace(slice(*keys))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,97 @@
1
+ module WillPaginate
2
+ # A mixin for ActiveResource::Base. Provides +per_page+ class method
3
+ # and hooks things up to provide paginating finders.
4
+ #
5
+ # Find out more in WillPaginate::Deserializer::ClassMethods
6
+ #
7
+ module Deserializer
8
+
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ class << base
12
+ alias_method_chain :instantiate_collection, :collection
13
+ define_method(:per_page) { 30 } unless respond_to?(:per_page)
14
+ end
15
+ end
16
+
17
+ # = Paginating finders for ActiveResource models
18
+ #
19
+ # WillPaginate adds +paginate+, +per_page+ and other methods to
20
+ # ActiveResource::Base class methods and associations.
21
+ #
22
+ # In short, paginating finders are equivalent to ActiveResource finders; the
23
+ # only difference is that we start with paginate instead of <tt>find</tt>, exclude the first <tt>:all</tt> parameter optionally, and
24
+ # the <tt>:params</tt> and its containing <tt>:page</tt> parameter are both required:
25
+ #
26
+ # @posts = Post.paginate :all, :params => {:page => params[:page], :order => 'created_at DESC'}
27
+ #
28
+ # In paginating finders, "all" is implicit. There is no sense in paginating
29
+ # a single record, right? So, you can drop the <tt>:all</tt> argument:
30
+ #
31
+ # Post.paginate(:params => {:page => params[:page]}) => Post.find(:all, :params => {:page => params[:page]})
32
+ #
33
+ # == The importance of the <tt>:params</tt> parameter
34
+ #
35
+ # In ActiveResource, all parameters in <tt>:params</tt> just get
36
+ # appeneded to the request url as the query string. It is up to the server
37
+ # to correctly snatch the options out of the params and do something
38
+ # meaningful with them.
39
+ #
40
+ # This is especially important for the <tt>:page</tt> and <tt>:per_page</tt>
41
+ # parameters, as they are indicators for the server to paginate. If the server
42
+ # does not use WillPaginate's ActiveRecord.paginate and returns a standard
43
+ # Array from <tt>to_xml</tt>, then that index action is likely to return all
44
+ # records. ActiveResource's paginate method is smart enough to paginate the resultset
45
+ # for Arrays returned by the server, but realize that grabbing all records constantly
46
+ # will be performance intensive. However, if the server uses ActiveResource.paginate
47
+ # then <tt>to_xml</tt> will return in a special format that will properly be
48
+ # parsed into a plain vanilla WillPaginate::Collection object that works with all
49
+ # the standard view helpers.
50
+ module ClassMethods
51
+ # This is the main paginating finder.
52
+ #
53
+ # == Special parameters for paginating finders
54
+ # * <tt>:params => :page</tt> -- REQUIRED, but defaults to 1 if false or nil
55
+ # * <tt>:params => :per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
56
+ #
57
+ # All other options (ie: +from+) work as they normally would in <tt>ActiveResource.find(:all)</tt>.
58
+ def paginate(*args)
59
+ options = wp_parse_options(args.pop)
60
+ results = find(:all, options)
61
+ results.is_a?(WillPaginate::Collection) ? results : results.paginate(:page => options[:params][:page], :per_page => options[:params][:per_page])
62
+ end
63
+
64
+ # Takes the format that Hash.from_xml produces out of an unknown type
65
+ # (produced by WillPaginate::Collection#to_xml_with_collection_type),
66
+ # parses it into a WillPaginate::Collection,
67
+ # and forwards the result to the former +instantiate_collection+ method.
68
+ # It only does this for hashes that have a :type => "collection".
69
+ def instantiate_collection_with_collection(collection, prefix_options = {})
70
+ if collection.is_a?(Hash) && collection["type"] == "collection"
71
+ collectables = collection.values.find{|c| c.is_a?(Hash) || c.is_a?(Array) }
72
+ collectables = [collectables].compact unless collectables.kind_of?(Array)
73
+ instantiated_collection = WillPaginate::Collection.create(collection["current_page"], collection["per_page"], collection["total_entries"]) do |pager|
74
+ pager.replace instantiate_collection_without_collection(collectables, prefix_options)
75
+ end
76
+ else
77
+ instantiate_collection_without_collection(collection, prefix_options)
78
+ end
79
+ end
80
+
81
+ protected
82
+
83
+ def wp_parse_options(options) #:nodoc:
84
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
85
+ options = options.symbolize_keys
86
+ raise ArgumentError, ':params hash parameter required' unless options.key?(:params) && options[:params].respond_to?(:symbolize_keys)
87
+ options[:params] = options[:params].symbolize_keys
88
+ raise ArgumentError, ':params => :page parameter required' unless options[:params].key? :page
89
+
90
+ options[:params][:per_page] ||= per_page
91
+ options[:params][:page] ||= 1
92
+ options
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,256 @@
1
+ require 'will_paginate/core_ext'
2
+
3
+ module WillPaginate
4
+ # A mixin for ActiveRecord::Base. Provides +per_page+ class method
5
+ # and hooks things up to provide paginating finders.
6
+ #
7
+ # Find out more in WillPaginate::Finder::ClassMethods
8
+ #
9
+ module Finder
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ class << base
13
+ alias_method_chain :method_missing, :paginate
14
+ # alias_method_chain :find_every, :paginate
15
+ define_method(:per_page) { 30 } unless respond_to?(:per_page)
16
+ end
17
+ end
18
+
19
+ # = Paginating finders for ActiveRecord models
20
+ #
21
+ # WillPaginate adds +paginate+, +per_page+ and other methods to
22
+ # ActiveRecord::Base class methods and associations. It also hooks into
23
+ # +method_missing+ to intercept pagination calls to dynamic finders such as
24
+ # +paginate_by_user_id+ and translate them to ordinary finders
25
+ # (+find_all_by_user_id+ in this case).
26
+ #
27
+ # In short, paginating finders are equivalent to ActiveRecord finders; the
28
+ # only difference is that we start with "paginate" instead of "find" and
29
+ # that <tt>:page</tt> is required parameter:
30
+ #
31
+ # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
32
+ #
33
+ # In paginating finders, "all" is implicit. There is no sense in paginating
34
+ # a single record, right? So, you can drop the <tt>:all</tt> argument:
35
+ #
36
+ # Post.paginate(...) => Post.find :all
37
+ # Post.paginate_all_by_something => Post.find_all_by_something
38
+ # Post.paginate_by_something => Post.find_all_by_something
39
+ #
40
+ # == The importance of the <tt>:order</tt> parameter
41
+ #
42
+ # In ActiveRecord finders, <tt>:order</tt> parameter specifies columns for
43
+ # the <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
44
+ # pagination only makes sense with ordered sets. Without the <tt>ORDER
45
+ # BY</tt> clause, databases aren't required to do consistent ordering when
46
+ # performing <tt>SELECT</tt> queries; this is especially true for
47
+ # PostgreSQL.
48
+ #
49
+ # Therefore, make sure you are doing ordering on a column that makes the
50
+ # most sense in the current context. Make that obvious to the user, also.
51
+ # For perfomance reasons you will also want to add an index to that column.
52
+ module ClassMethods
53
+ # This is the main paginating finder.
54
+ #
55
+ # == Special parameters for paginating finders
56
+ # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
57
+ # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
58
+ # * <tt>:total_entries</tt> -- use only if you manually count total entries
59
+ # * <tt>:count</tt> -- additional options that are passed on to +count+
60
+ # * <tt>:finder</tt> -- name of the ActiveRecord finder used (default: "find")
61
+ #
62
+ # All other options (+conditions+, +order+, ...) are forwarded to +find+
63
+ # and +count+ calls.
64
+ def paginate(*args, &block)
65
+ options = args.pop
66
+ page, per_page, total_entries = wp_parse_options(options)
67
+ finder = (options[:finder] || 'find').to_s
68
+
69
+ if finder == 'find'
70
+ # an array of IDs may have been given:
71
+ total_entries ||= (Array === args.first and args.first.size)
72
+ # :all is implicit
73
+ args.unshift(:all) if args.empty?
74
+ end
75
+
76
+ WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
77
+ count_options = options.except :page, :per_page, :total_entries, :finder
78
+ find_options = count_options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
79
+
80
+ args << find_options
81
+ # @options_from_last_find = nil
82
+ pager.replace send(finder, *args, &block)
83
+
84
+ # magic counting for user convenience:
85
+ pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
86
+ end
87
+ end
88
+
89
+ # Iterates through all records by loading one page at a time. This is useful
90
+ # for migrations or any other use case where you don't want to load all the
91
+ # records in memory at once.
92
+ #
93
+ # It uses +paginate+ internally; therefore it accepts all of its options.
94
+ # You can specify a starting page with <tt>:page</tt> (default is 1). Default
95
+ # <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
96
+ #
97
+ # See {Faking Cursors in ActiveRecord}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
98
+ # where Jamis Buck describes this and a more efficient way for MySQL.
99
+ def paginated_each(options = {}, &block)
100
+ options = { :order => 'id', :page => 1 }.merge options
101
+ options[:page] = options[:page].to_i
102
+ options[:total_entries] = 0 # skip the individual count queries
103
+ total = 0
104
+
105
+ begin
106
+ collection = paginate(options)
107
+ total += collection.each(&block).size
108
+ options[:page] += 1
109
+ end until collection.size < collection.per_page
110
+
111
+ total
112
+ end
113
+
114
+ # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
115
+ # based on the params otherwise used by paginating finds: +page+ and
116
+ # +per_page+.
117
+ #
118
+ # Example:
119
+ #
120
+ # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
121
+ # :page => params[:page], :per_page => 3
122
+ #
123
+ # A query for counting rows will automatically be generated if you don't
124
+ # supply <tt>:total_entries</tt>. If you experience problems with this
125
+ # generated SQL, you might want to perform the count manually in your
126
+ # application.
127
+ #
128
+ def paginate_by_sql(sql, options)
129
+ WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
130
+ query = sanitize_sql(sql.dup)
131
+ original_query = query.dup
132
+ # add limit, offset
133
+ add_limit! query, :offset => pager.offset, :limit => pager.per_page
134
+ # perfom the find
135
+ pager.replace find_by_sql(query)
136
+
137
+ unless pager.total_entries
138
+ count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
139
+ count_query = "SELECT COUNT(*) FROM (#{count_query})"
140
+
141
+ unless ['oracle', 'oci'].include?(self.connection.adapter_name.downcase)
142
+ count_query << ' AS count_table'
143
+ end
144
+ # perform the count query
145
+ pager.total_entries = count_by_sql(count_query)
146
+ end
147
+ end
148
+ end
149
+
150
+ def respond_to?(method, include_priv = false) #:nodoc:
151
+ case method.to_sym
152
+ when :paginate, :paginate_by_sql
153
+ true
154
+ else
155
+ super(method.to_s.sub(/^paginate/, 'find'), include_priv)
156
+ end
157
+ end
158
+
159
+ protected
160
+
161
+ def method_missing_with_paginate(method, *args, &block) #:nodoc:
162
+ # did somebody tried to paginate? if not, let them be
163
+ unless method.to_s.index('paginate') == 0
164
+ return method_missing_without_paginate(method, *args, &block)
165
+ end
166
+
167
+ # paginate finders are really just find_* with limit and offset
168
+ finder = method.to_s.sub('paginate', 'find')
169
+ finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
170
+
171
+ options = args.pop
172
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
173
+ options = options.dup
174
+ options[:finder] = finder
175
+ args << options
176
+
177
+ paginate(*args, &block)
178
+ end
179
+
180
+ # Does the not-so-trivial job of finding out the total number of entries
181
+ # in the database. It relies on the ActiveRecord +count+ method.
182
+ def wp_count(options, args, finder)
183
+ excludees = [:count, :order, :limit, :offset, :readonly]
184
+
185
+ # we may be in a model or an association proxy
186
+ klass = (@owner and @reflection) ? @reflection.klass : self
187
+
188
+ # Use :select from scope if it isn't already present.
189
+ options[:select] = scope(:find, :select) unless options[:select]
190
+
191
+ if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
192
+ # Remove quoting and check for table_name.*-like statement.
193
+ if options[:select].gsub('`', '') =~ /\w+\.\*/
194
+ options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
195
+ end
196
+ else
197
+ excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
198
+ end
199
+
200
+ # count expects (almost) the same options as find
201
+ count_options = options.except *excludees
202
+
203
+ # merge the hash found in :count
204
+ # this allows you to specify :select, :order, or anything else just for the count query
205
+ count_options.update options[:count] if options[:count]
206
+
207
+ # forget about includes if they are irrelevant (Rails 2.1)
208
+ if count_options[:include] and
209
+ klass.private_methods.include?('references_eager_loaded_tables?') and
210
+ !klass.send(:references_eager_loaded_tables?, count_options)
211
+ count_options.delete :include
212
+ end
213
+
214
+ # we may have to scope ...
215
+ counter = Proc.new { count(count_options) }
216
+
217
+ count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
218
+ # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
219
+ # then execute the count with the scoping provided by the with_finder
220
+ send(scoper, &counter)
221
+ elsif match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(finder)
222
+ # extract conditions from calls like "paginate_by_foo_and_bar"
223
+ attribute_names = extract_attribute_names_from_match(match)
224
+ conditions = construct_attributes_from_arguments(attribute_names, args)
225
+ with_scope(:find => { :conditions => conditions }, &counter)
226
+ else
227
+ counter.call
228
+ end
229
+
230
+ count.respond_to?(:length) ? count.length : count
231
+ end
232
+
233
+ def wp_parse_options(options) #:nodoc:
234
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
235
+ options = options.symbolize_keys
236
+ raise ArgumentError, ':page parameter required' unless options.key? :page
237
+
238
+ if options[:count] and options[:total_entries]
239
+ raise ArgumentError, ':count and :total_entries are mutually exclusive'
240
+ end
241
+
242
+ page = options[:page] || 1
243
+ per_page = options[:per_page] || self.per_page
244
+ total = options[:total_entries]
245
+ [page, per_page, total]
246
+ end
247
+
248
+ private
249
+
250
+ # def find_every_with_paginate(options)
251
+ # @options_from_last_find = options
252
+ # find_every_without_paginate(options)
253
+ # end
254
+ end
255
+ end
256
+ end