pb-will_paginate 2.3.12

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 (51) hide show
  1. data/.gitignore +4 -0
  2. data/.manifest +43 -0
  3. data/CHANGELOG.rdoc +139 -0
  4. data/LICENSE +18 -0
  5. data/README.rdoc +107 -0
  6. data/Rakefile +71 -0
  7. data/VERSION +1 -0
  8. data/examples/apple-circle.gif +0 -0
  9. data/examples/index.haml +69 -0
  10. data/examples/index.html +92 -0
  11. data/examples/pagination.css +90 -0
  12. data/examples/pagination.sass +91 -0
  13. data/init.rb +2 -0
  14. data/lib/will_paginate.rb +90 -0
  15. data/lib/will_paginate/array.rb +16 -0
  16. data/lib/will_paginate/collection.rb +144 -0
  17. data/lib/will_paginate/core_ext.rb +43 -0
  18. data/lib/will_paginate/finder.rb +264 -0
  19. data/lib/will_paginate/i18n.rb +178 -0
  20. data/lib/will_paginate/named_scope.rb +170 -0
  21. data/lib/will_paginate/named_scope_patch.rb +37 -0
  22. data/lib/will_paginate/version.rb +9 -0
  23. data/lib/will_paginate/view_helpers.rb +397 -0
  24. data/locales/en.yml +11 -0
  25. data/pb-will_paginate.gemspec +106 -0
  26. data/test/boot.rb +21 -0
  27. data/test/collection_test.rb +143 -0
  28. data/test/console +8 -0
  29. data/test/database.yml +22 -0
  30. data/test/finder_test.rb +473 -0
  31. data/test/fixtures/admin.rb +3 -0
  32. data/test/fixtures/developer.rb +14 -0
  33. data/test/fixtures/developers_projects.yml +13 -0
  34. data/test/fixtures/project.rb +15 -0
  35. data/test/fixtures/projects.yml +6 -0
  36. data/test/fixtures/replies.yml +29 -0
  37. data/test/fixtures/reply.rb +7 -0
  38. data/test/fixtures/schema.rb +38 -0
  39. data/test/fixtures/topic.rb +10 -0
  40. data/test/fixtures/topics.yml +30 -0
  41. data/test/fixtures/user.rb +2 -0
  42. data/test/fixtures/users.yml +35 -0
  43. data/test/helper.rb +37 -0
  44. data/test/i18n_test.rb +194 -0
  45. data/test/lib/activerecord_test_case.rb +43 -0
  46. data/test/lib/activerecord_test_connector.rb +75 -0
  47. data/test/lib/load_fixtures.rb +11 -0
  48. data/test/lib/view_test_process.rb +179 -0
  49. data/test/tasks.rake +59 -0
  50. data/test/view_test.rb +289 -0
  51. metadata +122 -0
@@ -0,0 +1,43 @@
1
+ require 'set'
2
+ require 'will_paginate/array'
3
+
4
+ # helper to check for method existance in ruby 1.8- and 1.9-compatible way
5
+ # because `methods`, `instance_methods` and others return strings in 1.8 and symbols in 1.9
6
+ #
7
+ # ['foo', 'bar'].include_method?(:foo) # => true
8
+ class Array
9
+ def include_method?(name)
10
+ name = name.to_sym
11
+ !!(find { |item| item.to_sym == name })
12
+ end
13
+ end
14
+
15
+ unless Hash.instance_methods.include_method? :except
16
+ Hash.class_eval do
17
+ # Returns a new hash without the given keys.
18
+ def except(*keys)
19
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
20
+ reject { |key,| rejected.include?(key) }
21
+ end
22
+
23
+ # Replaces the hash without only the given keys.
24
+ def except!(*keys)
25
+ replace(except(*keys))
26
+ end
27
+ end
28
+ end
29
+
30
+ unless Hash.instance_methods.include_method? :slice
31
+ Hash.class_eval do
32
+ # Returns a new hash with only the given keys.
33
+ def slice(*keys)
34
+ allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
35
+ reject { |key,| !allowed.include?(key) }
36
+ end
37
+
38
+ # Replaces the hash with only the given keys.
39
+ def slice!(*keys)
40
+ replace(slice(*keys))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,264 @@
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)
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) { |*a| yield(*a) if block_given? })
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 = {})
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
+ with_exclusive_scope(:find => {}) do
108
+ # using exclusive scope so that the block is yielded in scope-free context
109
+ total += collection.each { |item| yield item }.size
110
+ end
111
+ options[:page] += 1
112
+ end until collection.size < collection.per_page
113
+
114
+ total
115
+ end
116
+
117
+ # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
118
+ # based on the params otherwise used by paginating finds: +page+ and
119
+ # +per_page+.
120
+ #
121
+ # Example:
122
+ #
123
+ # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
124
+ # :page => params[:page], :per_page => 3
125
+ #
126
+ # A query for counting rows will automatically be generated if you don't
127
+ # supply <tt>:total_entries</tt>. If you experience problems with this
128
+ # generated SQL, you might want to perform the count manually in your
129
+ # application.
130
+ #
131
+ def paginate_by_sql(sql, options)
132
+ WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
133
+ query = sanitize_sql(sql.dup)
134
+ original_query = query.dup
135
+ # add limit, offset
136
+ add_limit! query, :offset => pager.offset, :limit => pager.per_page
137
+ # perfom the find
138
+ pager.replace find_by_sql(query)
139
+
140
+ unless pager.total_entries
141
+ count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
142
+ count_query = "SELECT COUNT(*) FROM (#{count_query})"
143
+
144
+ unless self.connection.adapter_name =~ /^(oracle|oci$)/i
145
+ count_query << ' AS count_table'
146
+ end
147
+ # perform the count query
148
+ pager.total_entries = count_by_sql(count_query)
149
+ end
150
+ end
151
+ end
152
+
153
+ def respond_to?(method, include_priv = false) #:nodoc:
154
+ case method.to_sym
155
+ when :paginate, :paginate_by_sql
156
+ true
157
+ else
158
+ super(method.to_s.sub(/^paginate/, 'find'), include_priv)
159
+ end
160
+ end
161
+
162
+ protected
163
+
164
+ def method_missing_with_paginate(method, *args) #:nodoc:
165
+ # did somebody tried to paginate? if not, let them be
166
+ unless method.to_s.index('paginate') == 0
167
+ if block_given?
168
+ return method_missing_without_paginate(method, *args) { |*a| yield(*a) }
169
+ else
170
+ return method_missing_without_paginate(method, *args)
171
+ end
172
+ end
173
+
174
+ # paginate finders are really just find_* with limit and offset
175
+ finder = method.to_s.sub('paginate', 'find')
176
+ finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
177
+
178
+ options = args.pop
179
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
180
+ options = options.dup
181
+ options[:finder] = finder
182
+ args << options
183
+
184
+ paginate(*args) { |*a| yield(*a) if block_given? }
185
+ end
186
+
187
+ # Does the not-so-trivial job of finding out the total number of entries
188
+ # in the database. It relies on the ActiveRecord +count+ method.
189
+ def wp_count(options, args, finder)
190
+ excludees = [:count, :order, :limit, :offset, :readonly]
191
+ excludees << :from unless ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
192
+
193
+ # we may be in a model or an association proxy
194
+ klass = (@owner and @reflection) ? @reflection.klass : self
195
+
196
+ # Use :select from scope if it isn't already present.
197
+ options[:select] = scope(:find, :select) unless options[:select]
198
+
199
+ if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
200
+ # Remove quoting and check for table_name.*-like statement.
201
+ if options[:select].gsub('`', '') =~ /\w+\.\*/
202
+ options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
203
+ end
204
+ else
205
+ excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
206
+ end
207
+
208
+ # count expects (almost) the same options as find
209
+ count_options = options.except *excludees
210
+
211
+ # merge the hash found in :count
212
+ # this allows you to specify :select, :order, or anything else just for the count query
213
+ count_options.update options[:count] if options[:count]
214
+
215
+ # forget about includes if they are irrelevant (Rails 2.1)
216
+ if count_options[:include] and
217
+ klass.private_methods.include_method?(:references_eager_loaded_tables?) and
218
+ !klass.send(:references_eager_loaded_tables?, count_options)
219
+ count_options.delete :include
220
+ end
221
+
222
+ # we may have to scope ...
223
+ counter = Proc.new { count(count_options) }
224
+
225
+ count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
226
+ # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
227
+ # then execute the count with the scoping provided by the with_finder
228
+ send(scoper, &counter)
229
+ elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
230
+ # extract conditions from calls like "paginate_by_foo_and_bar"
231
+ attribute_names = $2.split('_and_')
232
+ conditions = construct_attributes_from_arguments(attribute_names, args)
233
+ with_scope(:find => { :conditions => conditions }, &counter)
234
+ else
235
+ counter.call
236
+ end
237
+
238
+ count.respond_to?(:length) ? count.length : count
239
+ end
240
+
241
+ def wp_parse_options(options) #:nodoc:
242
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
243
+ options = options.symbolize_keys
244
+ raise ArgumentError, ':page parameter required' unless options.key? :page
245
+
246
+ if options[:count] and options[:total_entries]
247
+ raise ArgumentError, ':count and :total_entries are mutually exclusive'
248
+ end
249
+
250
+ page = options[:page] || 1
251
+ per_page = options[:per_page] || self.per_page
252
+ total = options[:total_entries]
253
+ [page, per_page, total]
254
+ end
255
+
256
+ private
257
+
258
+ # def find_every_with_paginate(options)
259
+ # @options_from_last_find = options
260
+ # find_every_without_paginate(options)
261
+ # end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,178 @@
1
+ module WillPaginate
2
+
3
+ # Contains all of the internationalization (i18n)
4
+ # information that WillPaginate::LinkRender uses.
5
+ #
6
+ # Each of several strings can be customized. Each string has
7
+ # a default value that can be overridden on a site-wide or
8
+ # page-specific basis. (NB: page-specific translations won't work
9
+ # for inline templates; only for the more typical views.)
10
+ #
11
+ # ==== Previous
12
+ # * Default: "&laquo; Previous" (an HTML left-angle-quote and 'Previous')
13
+ # * Site-wide key: "will_paginate.previous_label"
14
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.previous_label"
15
+ #
16
+ # ==== Next
17
+ # * Default: "Next &raquo;" ('Next' and an HTML right-angle-quote)
18
+ # * Site-wide key: "will_paginate.next_label"
19
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.next_label"
20
+ #
21
+ # ==== Gap Marker
22
+ # * Default: "<span class="gap">&hellip;</span>" (an HTML ellipsis wrapped in a span)
23
+ # * Site-wide key: "will_paginate.gap_marker"
24
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.gap_marker"
25
+ #
26
+ # ==== Entry Name
27
+ # Used when the type of paginated object cannot be otherwise determined.
28
+ # * Default: "entry"
29
+ # * Site-wide key: "will_paginate.entry_name"
30
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.entry_name"
31
+ #
32
+ # ==== Page entries info (zero elements)
33
+ # * Default: "No {{pluralized_entry_name}} found"
34
+ # * Site-wide key: "will_paginate.page_entries_info.zero"
35
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.page_entries_info.zero"
36
+ # * Interpolation options: "pluralized_entry_name"
37
+ #
38
+ # # ==== Page entries info (one element)
39
+ # * Default: "Displaying <em>1</em> {{entry_name}}"
40
+ # * Site-wide key: "will_paginate.page_entries_info.one"
41
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.page_entries_info.one"
42
+ # * Interpolation options: "entry_name"
43
+ #
44
+ # # ==== Page entries info (one page of elements)
45
+ # * Default: "Displaying <em>all #{total_count}</em> {{pluralized_entry_name}}"
46
+ # * Site-wide key: "will_paginate.page_entries_info.all"
47
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.page_entries_info.all"
48
+ # * Interpolation options: "pluralized_entry_name", "total_count"
49
+ #
50
+ # # ==== Page entries info (n-m of x elements)
51
+ # * Default: "Displaying {{pluralized_entry_name}} <em>{{start_count}}&nbsp;-&nbsp;{{end_count}}</em> of <em>{{total_count}}</em> in total"
52
+ # * Site-wide key: "will_paginate.page_entries_info.n_to_m_of_x"
53
+ # * Page-specific key: "#{controller}.#{view}.will_paginate.page_entries_info.n_to_m_of_x"
54
+ # * Interpolation options: "pluralized_entry_name", "start_count", "end_count", "total_count"
55
+ #
56
+ # Example: set some site-wide values and page-specific overrides for blog posts:
57
+ #
58
+ # # in RAILS_ROOT/config/locales/en.yml:
59
+ # en:
60
+ # will_paginate:
61
+ # previous_label: "<-- Previous"
62
+ # next_label: "Next -->"
63
+ # gap_marker: " - "
64
+ # page_entries_info:
65
+ # one: "One {{entry_name}} found:"
66
+ # all: "All {{pluralized_entry_name}}:"
67
+ # n_to_m_of_x: "{{pluralized_entry_name}} {{start_count}} to {{end_count}} of {{total_count}}:"
68
+ # posts:
69
+ # index:
70
+ # will_paginate:
71
+ # previous_label: "<-- Earlier"
72
+ # next_label: "Later -->"
73
+ module I18n
74
+
75
+ def self.append_translations_load_path
76
+ locales_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'locales'))
77
+ Dir.glob("#{locales_dir}/**/*.{rb,yml}").each do |path|
78
+ unless ::I18n.load_path.include?(path)
79
+ ::I18n.load_path << path
80
+ end
81
+ end
82
+ end
83
+
84
+ def previous_label
85
+ if options[:prev_label]
86
+ WillPaginate::Deprecation::warn(":prev_label view parameter is deprecated; please see WillPaginate::I18n.", caller)
87
+ options[:prev_label]
88
+ elsif options[:previous_label]
89
+ WillPaginate::Deprecation::warn(":previous_label view parameter is deprecated; please see WillPaginate::I18n.", caller)
90
+ options[:previous_label]
91
+ else
92
+ translate :previous_label
93
+ end
94
+ end
95
+
96
+ def next_label
97
+ if options[:next_label]
98
+ WillPaginate::Deprecation::warn(":next_label view parameter is deprecated; please see WillPaginate::I18n.", caller)
99
+ options[:next_label]
100
+ else
101
+ translate :next_label
102
+ end
103
+ end
104
+
105
+ def gap_marker
106
+ if @gap_marker
107
+ WillPaginate::Deprecation::warn("WillPaginate::LinkRenderer#gap_marker is deprecated; please see WillPaginate::I18n.", caller)
108
+ @gap_marker
109
+ else
110
+ translate :gap_marker
111
+ end
112
+ end
113
+
114
+ def page_entries_info
115
+ entry_name, pluralized_entry_name = entry_name_and_pluralized_for_collection
116
+ interpolations = {
117
+ :entry_name => entry_name,
118
+ :pluralized_entry_name => pluralized_entry_name,
119
+ :total_count => collection.size
120
+ }
121
+ if self.collection.total_pages < 2
122
+ case collection.size
123
+ when 0; key = 'page_entries_info.zero'
124
+ when 1; key = 'page_entries_info.one'
125
+ else; key = 'page_entries_info.all'
126
+ end
127
+ else
128
+ key = 'page_entries_info.n_to_m_of_x'
129
+ interpolations.merge!({
130
+ :start_count => self.collection.offset + 1,
131
+ :end_count => self.collection.offset + self.collection.length,
132
+ :total_count => self.collection.total_entries
133
+ })
134
+ end
135
+ translate key, interpolations
136
+ end
137
+
138
+ protected
139
+
140
+ attr_reader :template, :collection, :options
141
+
142
+ def entry_name_and_pluralized_for_collection
143
+ if self.options[:entry_name]
144
+ WillPaginate::Deprecation::warn(":entry_name view parameter is deprecated; please see WillPaginate::I18n", caller)
145
+ [ self.options[:entry_name], self.options[:entry_name].pluralize ]
146
+ elsif self.collection.empty?
147
+ e = translate :entry_name
148
+ [e, e.pluralize]
149
+ elsif self.collection.first.class.respond_to?(:human_name)
150
+ [
151
+ self.collection.first.class.human_name(:count => 1),
152
+ self.collection.first.class.human_name(:count => 10)
153
+ ]
154
+ else
155
+ e = self.collection.first.class.name.underscore.sub('_', ' ')
156
+ [e, e.pluralize]
157
+ end
158
+ end
159
+
160
+ def translate(suffix, interpolations = {})
161
+ if template_supports_page_specific_translations?
162
+ primary = ".will_paginate.#{suffix}"
163
+ default = [*interpolations.fetch(:default, [])]
164
+ default.unshift "will_paginate.#{suffix}"
165
+ interpolations[:default] = default
166
+ else
167
+ primary = "will_paginate.#{suffix}"
168
+ end
169
+ template.t primary, self.options.merge(interpolations)
170
+ end
171
+
172
+ def template_supports_page_specific_translations?
173
+ template.respond_to? :path_without_format_and_extension
174
+ end
175
+
176
+ end
177
+
178
+ end