merb_paginate 0.9.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 (56) hide show
  1. data/CHANGELOG.rdoc +105 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +125 -0
  4. data/Rakefile +59 -0
  5. data/lib/merbtasks.rb +17 -0
  6. data/lib/will_paginate.rb +40 -0
  7. data/lib/will_paginate/array.rb +35 -0
  8. data/lib/will_paginate/collection.rb +145 -0
  9. data/lib/will_paginate/core_ext.rb +58 -0
  10. data/lib/will_paginate/deprecation.rb +50 -0
  11. data/lib/will_paginate/finders.rb +9 -0
  12. data/lib/will_paginate/finders/active_record.rb +192 -0
  13. data/lib/will_paginate/finders/active_record/named_scope.rb +170 -0
  14. data/lib/will_paginate/finders/active_record/named_scope_patch.rb +39 -0
  15. data/lib/will_paginate/finders/active_resource.rb +51 -0
  16. data/lib/will_paginate/finders/base.rb +112 -0
  17. data/lib/will_paginate/finders/data_mapper.rb +30 -0
  18. data/lib/will_paginate/finders/sequel.rb +21 -0
  19. data/lib/will_paginate/version.rb +9 -0
  20. data/lib/will_paginate/view_helpers.rb +42 -0
  21. data/lib/will_paginate/view_helpers/base.rb +126 -0
  22. data/lib/will_paginate/view_helpers/link_renderer.rb +130 -0
  23. data/lib/will_paginate/view_helpers/link_renderer_base.rb +83 -0
  24. data/lib/will_paginate/view_helpers/merb.rb +13 -0
  25. data/spec/collection_spec.rb +147 -0
  26. data/spec/console +8 -0
  27. data/spec/console_fixtures.rb +8 -0
  28. data/spec/database.yml +22 -0
  29. data/spec/finders/active_record_spec.rb +461 -0
  30. data/spec/finders/active_resource_spec.rb +52 -0
  31. data/spec/finders/activerecord_test_connector.rb +108 -0
  32. data/spec/finders/data_mapper_spec.rb +62 -0
  33. data/spec/finders/data_mapper_test_connector.rb +20 -0
  34. data/spec/finders/sequel_spec.rb +53 -0
  35. data/spec/finders/sequel_test_connector.rb +9 -0
  36. data/spec/finders_spec.rb +76 -0
  37. data/spec/fixtures/admin.rb +3 -0
  38. data/spec/fixtures/developer.rb +13 -0
  39. data/spec/fixtures/developers_projects.yml +13 -0
  40. data/spec/fixtures/project.rb +15 -0
  41. data/spec/fixtures/projects.yml +6 -0
  42. data/spec/fixtures/replies.yml +29 -0
  43. data/spec/fixtures/reply.rb +7 -0
  44. data/spec/fixtures/schema.rb +38 -0
  45. data/spec/fixtures/topic.rb +6 -0
  46. data/spec/fixtures/topics.yml +30 -0
  47. data/spec/fixtures/user.rb +2 -0
  48. data/spec/fixtures/users.yml +35 -0
  49. data/spec/rcov.opts +2 -0
  50. data/spec/spec.opts +2 -0
  51. data/spec/spec_helper.rb +75 -0
  52. data/spec/tasks.rake +60 -0
  53. data/spec/view_helpers/base_spec.rb +64 -0
  54. data/spec/view_helpers/link_renderer_base_spec.rb +84 -0
  55. data/spec/view_helpers/view_example_group.rb +111 -0
  56. metadata +126 -0
@@ -0,0 +1,58 @@
1
+ require 'set'
2
+ require 'will_paginate/array'
3
+
4
+ ## Everything below blatantly stolen from ActiveSupport :o
5
+
6
+ unless Hash.instance_methods.include? 'except'
7
+ Hash.class_eval do
8
+ # Returns a new hash without the given keys.
9
+ def except(*keys)
10
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
11
+ reject { |key,| rejected.include?(key) }
12
+ end
13
+
14
+ # Replaces the hash without only the given keys.
15
+ def except!(*keys)
16
+ replace(except(*keys))
17
+ end
18
+ end
19
+ end
20
+
21
+ unless Hash.instance_methods.include? 'slice'
22
+ Hash.class_eval do
23
+ # Returns a new hash with only the given keys.
24
+ def slice(*keys)
25
+ allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
26
+ reject { |key,| !allowed.include?(key) }
27
+ end
28
+
29
+ # Replaces the hash with only the given keys.
30
+ def slice!(*keys)
31
+ replace(slice(*keys))
32
+ end
33
+ end
34
+ end
35
+
36
+ unless String.instance_methods.include? 'constantize'
37
+ String.class_eval do
38
+ def constantize
39
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
40
+ raise NameError, "#{self.inspect} is not a valid constant name!"
41
+ end
42
+
43
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
44
+ end
45
+ end
46
+ end
47
+
48
+ unless String.instance_methods.include? 'underscore'
49
+ String.class_eval do
50
+ def underscore
51
+ self.to_s.gsub(/::/, '/').
52
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
53
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
54
+ tr("-", "_").
55
+ downcase
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ # borrowed from ActiveSupport::Deprecation
2
+ module WillPaginate
3
+ module Deprecation
4
+ def self.debug() @debug; end
5
+ def self.debug=(value) @debug = value; end
6
+ self.debug = false
7
+
8
+ # Choose the default warn behavior according to RAILS_ENV.
9
+ # Ignore deprecation warnings in production.
10
+ BEHAVIORS = {
11
+ 'test' => Proc.new { |message, callstack|
12
+ $stderr.puts(message)
13
+ $stderr.puts callstack.join("\n ") if debug
14
+ },
15
+ 'development' => Proc.new { |message, callstack|
16
+ logger = defined?(::RAILS_DEFAULT_LOGGER) ? ::RAILS_DEFAULT_LOGGER : Logger.new($stderr)
17
+ logger.warn message
18
+ logger.debug callstack.join("\n ") if debug
19
+ }
20
+ }
21
+
22
+ def self.warn(message, callstack = caller)
23
+ if behavior
24
+ message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
25
+ behavior.call(message, callstack)
26
+ end
27
+ end
28
+
29
+ def self.default_behavior
30
+ if defined?(RAILS_ENV)
31
+ BEHAVIORS[RAILS_ENV.to_s]
32
+ else
33
+ BEHAVIORS['test']
34
+ end
35
+ end
36
+
37
+ # Behavior is a block that takes a message argument.
38
+ def self.behavior() @behavior; end
39
+ def self.behavior=(value) @behavior = value; end
40
+ self.behavior = default_behavior
41
+
42
+ def self.silence
43
+ old_behavior = self.behavior
44
+ self.behavior = nil
45
+ yield
46
+ ensure
47
+ self.behavior = old_behavior
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ require 'will_paginate/core_ext'
2
+
3
+ module WillPaginate
4
+ # Database logic for different ORMs
5
+ #
6
+ # See WillPaginate::Finders::Base
7
+ module Finders
8
+ end
9
+ end
@@ -0,0 +1,192 @@
1
+ require 'will_paginate/finders/base'
2
+ require 'active_record'
3
+
4
+ module WillPaginate::Finders
5
+ # = Paginating finders for ActiveRecord models
6
+ #
7
+ # WillPaginate adds +paginate+, +per_page+ and other methods to
8
+ # ActiveRecord::Base class methods and associations. It also hooks into
9
+ # +method_missing+ to intercept pagination calls to dynamic finders such as
10
+ # +paginate_by_user_id+ and translate them to ordinary finders
11
+ # (+find_all_by_user_id+ in this case).
12
+ #
13
+ # In short, paginating finders are equivalent to ActiveRecord finders; the
14
+ # only difference is that we start with "paginate" instead of "find" and
15
+ # that <tt>:page</tt> is required parameter:
16
+ #
17
+ # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
18
+ #
19
+ # In paginating finders, "all" is implicit. There is no sense in paginating
20
+ # a single record, right? So, you can drop the <tt>:all</tt> argument:
21
+ #
22
+ # Post.paginate(...) => Post.find :all
23
+ # Post.paginate_all_by_something => Post.find_all_by_something
24
+ # Post.paginate_by_something => Post.find_all_by_something
25
+ #
26
+ module ActiveRecord
27
+ include WillPaginate::Finders::Base
28
+
29
+ # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
30
+ # based on the params otherwise used by paginating finds: +page+ and
31
+ # +per_page+.
32
+ #
33
+ # Example:
34
+ #
35
+ # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
36
+ # :page => params[:page], :per_page => 3
37
+ #
38
+ # A query for counting rows will automatically be generated if you don't
39
+ # supply <tt>:total_entries</tt>. If you experience problems with this
40
+ # generated SQL, you might want to perform the count manually in your
41
+ # application.
42
+ #
43
+ def paginate_by_sql(sql, options)
44
+ WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
45
+ query = sanitize_sql(sql.dup)
46
+ original_query = query.dup
47
+ # add limit, offset
48
+ add_limit! query, :offset => pager.offset, :limit => pager.per_page
49
+ # perfom the find
50
+ pager.replace find_by_sql(query)
51
+
52
+ unless pager.total_entries
53
+ count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
54
+ count_query = "SELECT COUNT(*) FROM (#{count_query})"
55
+
56
+ unless ['oracle', 'oci'].include?(self.connection.adapter_name.downcase)
57
+ count_query << ' AS count_table'
58
+ end
59
+ # perform the count query
60
+ pager.total_entries = count_by_sql(count_query)
61
+ end
62
+ end
63
+ end
64
+
65
+ def respond_to?(method, include_priv = false) #:nodoc:
66
+ super(method.to_s.sub(/^paginate/, 'find'), include_priv)
67
+ end
68
+
69
+ protected
70
+
71
+ def method_missing_with_paginate(method, *args, &block) #:nodoc:
72
+ # did somebody tried to paginate? if not, let them be
73
+ unless method.to_s.index('paginate') == 0
74
+ return method_missing_without_paginate(method, *args, &block)
75
+ end
76
+
77
+ # paginate finders are really just find_* with limit and offset
78
+ finder = method.to_s.sub('paginate', 'find')
79
+ finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
80
+
81
+ options = args.pop
82
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
83
+ options = options.dup
84
+ options[:finder] = finder
85
+ args << options
86
+
87
+ paginate(*args, &block)
88
+ end
89
+
90
+ def wp_query(options, pager, args, &block) #:nodoc:
91
+ finder = (options.delete(:finder) || 'find').to_s
92
+ find_options = options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
93
+
94
+ if finder == 'find'
95
+ if Array === args.first and !pager.total_entries
96
+ pager.total_entries = args.first.size
97
+ end
98
+ args << :all if args.empty?
99
+ end
100
+
101
+ args << find_options
102
+ pager.replace send(finder, *args, &block)
103
+
104
+ unless pager.total_entries
105
+ # magic counting
106
+ pager.total_entries = wp_count(options, args, finder)
107
+ end
108
+ end
109
+
110
+ # Does the not-so-trivial job of finding out the total number of entries
111
+ # in the database. It relies on the ActiveRecord +count+ method.
112
+ def wp_count(options, args, finder) #:nodoc:
113
+ # find out if we are in a model or an association proxy
114
+ klass = (@owner and @reflection) ? @reflection.klass : self
115
+ count_options = wp_parse_count_options(options, klass)
116
+
117
+ # we may have to scope ...
118
+ counter = Proc.new { count(count_options) }
119
+
120
+ count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
121
+ # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
122
+ # then execute the count with the scoping provided by the with_finder
123
+ send(scoper, &counter)
124
+ elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
125
+ # extract conditions from calls like "paginate_by_foo_and_bar"
126
+ attribute_names = $2.split('_and_')
127
+ conditions = construct_attributes_from_arguments(attribute_names, args)
128
+ with_scope(:find => { :conditions => conditions }, &counter)
129
+ else
130
+ counter.call
131
+ end
132
+
133
+ count.respond_to?(:length) ? count.length : count
134
+ end
135
+
136
+ def wp_parse_count_options(options, klass) #:nodoc:
137
+ excludees = [:count, :order, :limit, :offset, :readonly]
138
+
139
+ unless ::ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
140
+ # :from parameter wasn't supported in count() before this change
141
+ excludees << :from
142
+ end
143
+
144
+ # Use :select from scope if it isn't already present.
145
+ options[:select] = scope(:find, :select) unless options[:select]
146
+
147
+ if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
148
+ # Remove quoting and check for table_name.*-like statement.
149
+ if options[:select].gsub('`', '') =~ /\w+\.\*/
150
+ options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
151
+ end
152
+ else
153
+ excludees << :select
154
+ end
155
+
156
+ # count expects (almost) the same options as find
157
+ count_options = options.except *excludees
158
+
159
+ # merge the hash found in :count
160
+ # this allows you to specify :select, :order, or anything else just for the count query
161
+ count_options.update options[:count] if options[:count]
162
+
163
+ # forget about includes if they are irrelevant (Rails 2.1)
164
+ if count_options[:include] and
165
+ klass.private_methods.include?('references_eager_loaded_tables?') and
166
+ !klass.send(:references_eager_loaded_tables?, count_options)
167
+ count_options.delete :include
168
+ end
169
+
170
+ count_options
171
+ end
172
+ end
173
+ end
174
+
175
+ ActiveRecord::Base.class_eval do
176
+ extend WillPaginate::Finders::ActiveRecord
177
+ class << self
178
+ alias_method_chain :method_missing, :paginate
179
+ end
180
+ end
181
+
182
+ # support pagination on associations
183
+ a = ActiveRecord::Associations
184
+ returning([ a::AssociationCollection ]) { |classes|
185
+ # detect http://dev.rubyonrails.org/changeset/9230
186
+ unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation
187
+ classes << a::HasManyThroughAssociation
188
+ end
189
+ }.each do |klass|
190
+ klass.send :include, WillPaginate::Finders::ActiveRecord
191
+ klass.class_eval { alias_method_chain :method_missing, :paginate }
192
+ end
@@ -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