agnostic-will_paginate 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. data/.autotest +54 -0
  2. data/.gitignore +4 -0
  3. data/.gitmodules +3 -0
  4. data/.manifest +61 -0
  5. data/CHANGELOG.rdoc +105 -0
  6. data/LICENSE +18 -0
  7. data/README.rdoc +125 -0
  8. data/Rakefile +58 -0
  9. data/init.rb +1 -0
  10. data/lib/will_paginate.rb +45 -0
  11. data/lib/will_paginate/array.rb +33 -0
  12. data/lib/will_paginate/collection.rb +145 -0
  13. data/lib/will_paginate/core_ext.rb +69 -0
  14. data/lib/will_paginate/deprecation.rb +50 -0
  15. data/lib/will_paginate/finders.rb +9 -0
  16. data/lib/will_paginate/finders/active_record.rb +192 -0
  17. data/lib/will_paginate/finders/active_record/named_scope.rb +170 -0
  18. data/lib/will_paginate/finders/active_record/named_scope_patch.rb +39 -0
  19. data/lib/will_paginate/finders/active_resource.rb +51 -0
  20. data/lib/will_paginate/finders/base.rb +112 -0
  21. data/lib/will_paginate/finders/data_mapper.rb +30 -0
  22. data/lib/will_paginate/finders/sequel.rb +22 -0
  23. data/lib/will_paginate/version.rb +9 -0
  24. data/lib/will_paginate/view_helpers.rb +42 -0
  25. data/lib/will_paginate/view_helpers/action_view.rb +158 -0
  26. data/lib/will_paginate/view_helpers/base.rb +126 -0
  27. data/lib/will_paginate/view_helpers/link_renderer.rb +130 -0
  28. data/lib/will_paginate/view_helpers/link_renderer_base.rb +83 -0
  29. data/lib/will_paginate/view_helpers/merb.rb +13 -0
  30. data/spec/collection_spec.rb +147 -0
  31. data/spec/console +8 -0
  32. data/spec/console_fixtures.rb +8 -0
  33. data/spec/database.yml +22 -0
  34. data/spec/finders/active_record_spec.rb +461 -0
  35. data/spec/finders/active_resource_spec.rb +52 -0
  36. data/spec/finders/activerecord_test_connector.rb +108 -0
  37. data/spec/finders/data_mapper_spec.rb +62 -0
  38. data/spec/finders/data_mapper_test_connector.rb +20 -0
  39. data/spec/finders/sequel_spec.rb +53 -0
  40. data/spec/finders/sequel_test_connector.rb +9 -0
  41. data/spec/finders_spec.rb +76 -0
  42. data/spec/fixtures/admin.rb +3 -0
  43. data/spec/fixtures/developer.rb +13 -0
  44. data/spec/fixtures/developers_projects.yml +13 -0
  45. data/spec/fixtures/project.rb +15 -0
  46. data/spec/fixtures/projects.yml +6 -0
  47. data/spec/fixtures/replies.yml +29 -0
  48. data/spec/fixtures/reply.rb +7 -0
  49. data/spec/fixtures/schema.rb +38 -0
  50. data/spec/fixtures/topic.rb +6 -0
  51. data/spec/fixtures/topics.yml +30 -0
  52. data/spec/fixtures/user.rb +2 -0
  53. data/spec/fixtures/users.yml +35 -0
  54. data/spec/rcov.opts +2 -0
  55. data/spec/spec.opts +2 -0
  56. data/spec/spec_helper.rb +75 -0
  57. data/spec/tasks.rake +60 -0
  58. data/spec/view_helpers/action_view_spec.rb +344 -0
  59. data/spec/view_helpers/base_spec.rb +64 -0
  60. data/spec/view_helpers/link_renderer_base_spec.rb +84 -0
  61. data/spec/view_helpers/view_example_group.rb +111 -0
  62. metadata +152 -0
@@ -0,0 +1,170 @@
1
+ module WillPaginate
2
+ # This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate,
3
+ # but in other aspects when managing complex conditions that you want to be reusable.
4
+ module NamedScope
5
+ # All subclasses of ActiveRecord::Base have two named_scopes:
6
+ # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
7
+ # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
8
+ #
9
+ # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
10
+ # intermediate values (scopes) around as first-class objects is convenient.
11
+ def self.included(base)
12
+ base.class_eval do
13
+ extend ClassMethods
14
+ named_scope :scoped, lambda { |scope| scope }
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def scopes
20
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
21
+ end
22
+
23
+ # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
24
+ # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
25
+ #
26
+ # class Shirt < ActiveRecord::Base
27
+ # named_scope :red, :conditions => {:color => 'red'}
28
+ # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
29
+ # end
30
+ #
31
+ # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
32
+ # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
33
+ #
34
+ # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
35
+ # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
36
+ # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
37
+ # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
38
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
39
+ #
40
+ # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
41
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
42
+ # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
43
+ #
44
+ # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
45
+ # <tt>has_many</tt> associations. If,
46
+ #
47
+ # class Person < ActiveRecord::Base
48
+ # has_many :shirts
49
+ # end
50
+ #
51
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
52
+ # only shirts.
53
+ #
54
+ # Named scopes can also be procedural.
55
+ #
56
+ # class Shirt < ActiveRecord::Base
57
+ # named_scope :colored, lambda { |color|
58
+ # { :conditions => { :color => color } }
59
+ # }
60
+ # end
61
+ #
62
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
63
+ #
64
+ # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
65
+ #
66
+ # class Shirt < ActiveRecord::Base
67
+ # named_scope :red, :conditions => {:color => 'red'} do
68
+ # def dom_id
69
+ # 'red_shirts'
70
+ # end
71
+ # end
72
+ # end
73
+ #
74
+ #
75
+ # For testing complex named scopes, you can examine the scoping options using the
76
+ # <tt>proxy_options</tt> method on the proxy itself.
77
+ #
78
+ # class Shirt < ActiveRecord::Base
79
+ # named_scope :colored, lambda { |color|
80
+ # { :conditions => { :color => color } }
81
+ # }
82
+ # end
83
+ #
84
+ # expected_options = { :conditions => { :colored => 'red' } }
85
+ # assert_equal expected_options, Shirt.colored('red').proxy_options
86
+ def named_scope(name, options = {}, &block)
87
+ name = name.to_sym
88
+ scopes[name] = lambda do |parent_scope, *args|
89
+ Scope.new(parent_scope, case options
90
+ when Hash
91
+ options
92
+ when Proc
93
+ options.call(*args)
94
+ end, &block)
95
+ end
96
+ (class << self; self end).instance_eval do
97
+ define_method name do |*args|
98
+ scopes[name].call(self, *args)
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ class Scope
105
+ attr_reader :proxy_scope, :proxy_options
106
+
107
+ [].methods.each do |m|
108
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
109
+ delegate m, :to => :proxy_found
110
+ end
111
+ end
112
+
113
+ delegate :scopes, :with_scope, :to => :proxy_scope
114
+
115
+ def initialize(proxy_scope, options, &block)
116
+ [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
117
+ extend Module.new(&block) if block_given?
118
+ @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
119
+ end
120
+
121
+ def reload
122
+ load_found; self
123
+ end
124
+
125
+ def first(*args)
126
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
127
+ proxy_found.first(*args)
128
+ else
129
+ find(:first, *args)
130
+ end
131
+ end
132
+
133
+ def last(*args)
134
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
135
+ proxy_found.last(*args)
136
+ else
137
+ find(:last, *args)
138
+ end
139
+ end
140
+
141
+ def empty?
142
+ @found ? @found.empty? : count.zero?
143
+ end
144
+
145
+ def respond_to?(method, include_private = false)
146
+ super || @proxy_scope.respond_to?(method, include_private)
147
+ end
148
+
149
+ protected
150
+ def proxy_found
151
+ @found || load_found
152
+ end
153
+
154
+ private
155
+ def method_missing(method, *args, &block)
156
+ if scopes.include?(method)
157
+ scopes[method].call(self, *args)
158
+ else
159
+ with_scope :find => proxy_options do
160
+ proxy_scope.send(method, *args, &block)
161
+ end
162
+ end
163
+ end
164
+
165
+ def load_found
166
+ @found = find(:all)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,39 @@
1
+ ## based on http://dev.rubyonrails.org/changeset/9084
2
+
3
+ ActiveRecord::Associations::AssociationProxy.class_eval do
4
+ protected
5
+ def with_scope(*args, &block)
6
+ @reflection.klass.send :with_scope, *args, &block
7
+ end
8
+ end
9
+
10
+ [ ActiveRecord::Associations::AssociationCollection,
11
+ ActiveRecord::Associations::HasManyThroughAssociation ].each do |klass|
12
+ klass.class_eval do
13
+ protected
14
+ alias :method_missing_without_scopes :method_missing_without_paginate
15
+ def method_missing_without_paginate(method, *args, &block)
16
+ if @reflection.klass.scopes.include?(method)
17
+ @reflection.klass.scopes[method].call(self, *args, &block)
18
+ else
19
+ method_missing_without_scopes(method, *args, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ # Rails 1.2.6
26
+ ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
27
+ protected
28
+ def method_missing(method, *args, &block)
29
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
30
+ super
31
+ elsif @reflection.klass.scopes.include?(method)
32
+ @reflection.klass.scopes[method].call(self, *args)
33
+ else
34
+ @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
35
+ @reflection.klass.send(method, *args, &block)
36
+ end
37
+ end
38
+ end
39
+ end if ActiveRecord::Base.respond_to? :find_first
@@ -0,0 +1,51 @@
1
+ require 'will_paginate/finders/base'
2
+ require 'active_resource'
3
+
4
+ module WillPaginate::Finders
5
+ # Paginate your ActiveResource models.
6
+ #
7
+ # @posts = Post.paginate :all, :params => {
8
+ # :page => params[:page], :order => 'created_at DESC'
9
+ # }
10
+ #
11
+ module ActiveResource
12
+ include WillPaginate::Finders::Base
13
+
14
+ protected
15
+
16
+ def wp_query(options, pager, args, &block) #:nodoc:
17
+ unless args.empty? or args.first == :all
18
+ raise ArgumentError, "finder arguments other than :all are not supported for pagination (#{args.inspect} given)"
19
+ end
20
+ params = (options[:params] ||= {})
21
+ params[:page] = pager.current_page
22
+ params[:per_page] = pager.per_page
23
+
24
+ pager.replace find_every(options, &block)
25
+ end
26
+
27
+ # Takes the format that Hash.from_xml produces out of an unknown type
28
+ # (produced by WillPaginate::Collection#to_xml_with_collection_type),
29
+ # parses it into a WillPaginate::Collection,
30
+ # and forwards the result to the former +instantiate_collection+ method.
31
+ # It only does this for hashes that have a :type => "collection".
32
+ def instantiate_collection_with_collection(collection, prefix_options = {}) #:nodoc:
33
+ if collection.is_a?(Hash) && collection["type"] == "collection"
34
+ collectables = collection.values.find{ |c| c.is_a?(Hash) || c.is_a?(Array) }
35
+ collectables = [collectables].compact unless collectables.kind_of?(Array)
36
+ instantiated_collection = WillPaginate::Collection.create(collection["current_page"], collection["per_page"], collection["total_entries"]) do |pager|
37
+ pager.replace instantiate_collection_without_collection(collectables, prefix_options)
38
+ end
39
+ else
40
+ instantiate_collection_without_collection(collection, prefix_options)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ ActiveResource::Base.class_eval do
47
+ extend WillPaginate::Finders::ActiveResource
48
+ class << self
49
+ # alias_method_chain :instantiate_collection, :collection
50
+ end
51
+ end
@@ -0,0 +1,112 @@
1
+ require 'will_paginate/core_ext'
2
+
3
+ module WillPaginate
4
+ module Finders
5
+ # = Database-agnostic finder module
6
+ #
7
+ # Out of the box, will_paginate supports hooking in several ORMs to
8
+ # provide paginating finders based on their API. As of this writing, the
9
+ # supported libraries are:
10
+ #
11
+ # * ActiveRecord
12
+ # * DataMapper
13
+ # * Sequel
14
+ #
15
+ # It's easy to write your own adapter for anything that can load data with
16
+ # explicit limit and offset settings. DataMapper adapter is a nice and
17
+ # compact example of writing an adapter to bring the +paginate+ method to
18
+ # DataMapper models.
19
+ #
20
+ # == The importance of SQL's <tt>ORDER BY</tt>
21
+ #
22
+ # In most ORMs, <tt>:order</tt> parameter specifies columns for the
23
+ # <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
24
+ # pagination only makes sense with ordered sets. Without the order clause,
25
+ # databases aren't required to do consistent ordering when performing
26
+ # <tt>SELECT</tt> queries.
27
+ #
28
+ # Ordering by a field for which many records share the same value (e.g.
29
+ # "status") can still result in incorrect ordering with some databases (MS
30
+ # SQL and Postgres for instance). With these databases it's recommend that
31
+ # you order by primary key as well. That is, instead of ordering by
32
+ # "status DESC", use the alternative "status DESC, id DESC" and this will
33
+ # yield consistent results.
34
+ #
35
+ # Therefore, make sure you are doing ordering on a column that makes the
36
+ # most sense in the current context. Make that obvious to the user, also.
37
+ # For perfomance reasons you will also want to add an index to that column.
38
+ module Base
39
+ def per_page
40
+ @per_page ||= 30
41
+ end
42
+
43
+ def per_page=(limit)
44
+ @per_page = limit.to_i
45
+ end
46
+
47
+ # This is the main paginating finder.
48
+ #
49
+ # == Special parameters for paginating finders
50
+ # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
51
+ # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
52
+ # * <tt>:total_entries</tt> -- use only if you manually count total entries
53
+ # * <tt>:count</tt> -- additional options that are passed on to +count+
54
+ # * <tt>:finder</tt> -- name of the finder method to use (default: "find")
55
+ #
56
+ # All other options (+conditions+, +order+, ...) are forwarded to +find+
57
+ # and +count+ calls.
58
+ def paginate(*args, &block)
59
+ options = args.pop
60
+ page, per_page, total_entries = wp_parse_options(options)
61
+
62
+ WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
63
+ query_options = options.except :page, :per_page, :total_entries
64
+ wp_query(query_options, pager, args, &block)
65
+ end
66
+ end
67
+
68
+ # Iterates through all records by loading one page at a time. This is useful
69
+ # for migrations or any other use case where you don't want to load all the
70
+ # records in memory at once.
71
+ #
72
+ # It uses +paginate+ internally; therefore it accepts all of its options.
73
+ # You can specify a starting page with <tt>:page</tt> (default is 1). Default
74
+ # <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
75
+ #
76
+ # {Jamis Buck describes this}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
77
+ # and also uses a more efficient way for MySQL.
78
+ def paginated_each(options = {}, &block)
79
+ options = { :order => 'id', :page => 1 }.merge options
80
+ options[:page] = options[:page].to_i
81
+ options[:total_entries] = 0 # skip the individual count queries
82
+ total = 0
83
+
84
+ begin
85
+ collection = paginate(options)
86
+ total += collection.each(&block).size
87
+ options[:page] += 1
88
+ end until collection.size < collection.per_page
89
+
90
+ total
91
+ end
92
+
93
+ protected
94
+
95
+ def wp_parse_options(options) #:nodoc:
96
+ raise ArgumentError, 'parameter hash expected' unless Hash === options
97
+ raise ArgumentError, ':page parameter required' unless options.key? :page
98
+
99
+ if options[:count] and options[:total_entries]
100
+ raise ArgumentError, ':count and :total_entries are mutually exclusive'
101
+ end
102
+
103
+ page = options[:page] || 1
104
+ per_page = options[:per_page] || self.per_page
105
+ total = options[:total_entries]
106
+
107
+ return [page, per_page, total]
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,30 @@
1
+ require 'will_paginate/finders/base'
2
+ require 'dm-core'
3
+
4
+ module WillPaginate::Finders
5
+ module DataMapper
6
+ include WillPaginate::Finders::Base
7
+
8
+ protected
9
+
10
+ def wp_query(options, pager, args, &block) #:nodoc
11
+ find_options = options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
12
+
13
+ pager.replace all(find_options, &block)
14
+
15
+ unless pager.total_entries
16
+ pager.total_entries = wp_count(options)
17
+ end
18
+ end
19
+
20
+ def wp_count(options) #:nodoc
21
+ count_options = options.except(:count, :order)
22
+ # merge the hash found in :count
23
+ count_options.update options[:count] if options[:count]
24
+
25
+ count_options.empty?? count() : count(count_options)
26
+ end
27
+ end
28
+ end
29
+
30
+ DataMapper::Model.send(:include, WillPaginate::Finders::DataMapper)
@@ -0,0 +1,22 @@
1
+ require 'sequel'
2
+ require 'sequel/extensions/pagination'
3
+
4
+ existing_methods = Sequel::Dataset::Pagination.instance_methods
5
+
6
+ Sequel::Dataset::Pagination.module_eval do
7
+ # it should quack like a WillPaginate::Collection
8
+
9
+ alias :total_pages :page_count unless existing_methods.include_method? :total_pages
10
+ alias :per_page :page_size unless existing_methods.include_method? :per_page
11
+ alias :previous_page :prev_page unless existing_methods.include_method? :previous_page
12
+ alias :total_entries :pagination_record_count unless existing_methods.include_method? :total_entries
13
+
14
+ def out_of_bounds?
15
+ current_page > total_pages
16
+ end
17
+
18
+ # Current offset of the paginated collection
19
+ def offset
20
+ (current_page - 1) * per_page
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ module WillPaginate #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 2
4
+ MINOR = 5
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ require 'will_paginate/deprecation'
2
+
3
+ module WillPaginate
4
+ # = Will Paginate view helpers
5
+ #
6
+ # The main view helper is +will_paginate+. It renders the pagination links
7
+ # for the given collection. The helper itself is lightweight and serves only
8
+ # as a wrapper around LinkRenderer instantiation; the renderer then does
9
+ # all the hard work of generating the HTML.
10
+ #
11
+ # Read more in WillPaginate::ViewHelpers::Base
12
+ module ViewHelpers
13
+ # ==== Global options for helpers
14
+ #
15
+ # Options for pagination helpers are optional and get their default values
16
+ # from the WillPaginate::ViewHelpers.pagination_options hash. You can write
17
+ # to this hash to override default options on the global level:
18
+ #
19
+ # WillPaginate::ViewHelpers.pagination_options[:previous_label] = 'Previous page'
20
+ #
21
+ # By putting this into your environment.rb you can easily translate link
22
+ # texts to previous and next pages, as well as override some other defaults
23
+ # to your liking.
24
+ def self.pagination_options() @pagination_options; end
25
+ # Overrides the default +pagination_options+
26
+ def self.pagination_options=(value) @pagination_options = value; end
27
+
28
+ self.pagination_options = {
29
+ :class => 'pagination',
30
+ :previous_label => '&#8592; Previous',
31
+ :next_label => 'Next &#8594;',
32
+ :inner_window => 4, # links around the current page
33
+ :outer_window => 1, # links around beginning and end
34
+ :separator => ' ', # single space is friendly to spiders and non-graphic browsers
35
+ :param_name => :page,
36
+ :params => nil,
37
+ :renderer => 'WillPaginate::ViewHelpers::LinkRenderer',
38
+ :page_links => true,
39
+ :container => true
40
+ }
41
+ end
42
+ end