composite_primary_keys 12.0.9 → 13.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +15 -1
  3. data/README.rdoc +1 -0
  4. data/Rakefile +37 -37
  5. data/lib/composite_primary_keys/active_model/attribute_assignment.rb +19 -19
  6. data/lib/composite_primary_keys/arel/sqlserver.rb +37 -37
  7. data/lib/composite_primary_keys/arel/to_sql.rb +18 -18
  8. data/lib/composite_primary_keys/associations/association.rb +23 -23
  9. data/lib/composite_primary_keys/associations/association_scope.rb +66 -68
  10. data/lib/composite_primary_keys/associations/collection_association.rb +31 -31
  11. data/lib/composite_primary_keys/associations/foreign_association.rb +15 -15
  12. data/lib/composite_primary_keys/associations/has_many_association.rb +35 -35
  13. data/lib/composite_primary_keys/associations/{join_dependency.rb → join_association.rb} +137 -103
  14. data/lib/composite_primary_keys/associations/through_association.rb +25 -25
  15. data/lib/composite_primary_keys/attribute_methods/primary_key.rb +0 -2
  16. data/lib/composite_primary_keys/attribute_methods/read.rb +30 -30
  17. data/lib/composite_primary_keys/attribute_methods/write.rb +35 -35
  18. data/lib/composite_primary_keys/attribute_methods.rb +21 -9
  19. data/lib/composite_primary_keys/autosave_association.rb +60 -60
  20. data/lib/composite_primary_keys/base.rb +141 -141
  21. data/lib/composite_primary_keys/composite_arrays.rb +86 -86
  22. data/lib/composite_primary_keys/composite_predicates.rb +2 -1
  23. data/lib/composite_primary_keys/composite_relation.rb +29 -29
  24. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +37 -37
  25. data/lib/composite_primary_keys/connection_adapters/abstract_adapter.rb +10 -10
  26. data/lib/composite_primary_keys/connection_adapters/postgresql/database_statements.rb +26 -26
  27. data/lib/composite_primary_keys/connection_adapters/sqlserver/database_statements.rb +44 -44
  28. data/lib/composite_primary_keys/core.rb +48 -48
  29. data/lib/composite_primary_keys/counter_cache.rb +15 -15
  30. data/lib/composite_primary_keys/fixtures.rb +21 -21
  31. data/lib/composite_primary_keys/nested_attributes.rb +1 -1
  32. data/lib/composite_primary_keys/persistence.rb +3 -2
  33. data/lib/composite_primary_keys/reflection.rb +91 -29
  34. data/lib/composite_primary_keys/relation/batches.rb +15 -7
  35. data/lib/composite_primary_keys/relation/calculations.rb +46 -23
  36. data/lib/composite_primary_keys/relation/finder_methods.rb +235 -235
  37. data/lib/composite_primary_keys/relation/predicate_builder/association_query_value.rb +39 -20
  38. data/lib/composite_primary_keys/relation/query_methods.rb +42 -42
  39. data/lib/composite_primary_keys/relation/where_clause.rb +18 -23
  40. data/lib/composite_primary_keys/relation.rb +197 -193
  41. data/lib/composite_primary_keys/sanitization.rb +42 -42
  42. data/lib/composite_primary_keys/table_metadata.rb +11 -0
  43. data/lib/composite_primary_keys/transactions.rb +34 -34
  44. data/lib/composite_primary_keys/validations/uniqueness.rb +31 -31
  45. data/lib/composite_primary_keys/version.rb +2 -2
  46. data/lib/composite_primary_keys.rb +4 -2
  47. data/scripts/console.rb +48 -48
  48. data/scripts/txt2html +76 -76
  49. data/scripts/txt2js +65 -65
  50. data/tasks/databases/mysql.rake +40 -40
  51. data/tasks/databases/oracle.rake +41 -41
  52. data/tasks/databases/postgresql.rake +38 -38
  53. data/tasks/databases/sqlite.rake +25 -25
  54. data/tasks/databases/sqlserver.rake +43 -43
  55. data/tasks/website.rake +18 -18
  56. data/test/README_tests.rdoc +56 -56
  57. data/test/connections/connection_spec.rb +27 -27
  58. data/test/connections/databases.ci.yml +22 -22
  59. data/test/connections/databases.example.yml +40 -40
  60. data/test/connections/databases.yml +40 -39
  61. data/test/fixtures/article.rb +10 -10
  62. data/test/fixtures/articles.yml +7 -7
  63. data/test/fixtures/capitol.rb +3 -3
  64. data/test/fixtures/capitols.yml +16 -16
  65. data/test/fixtures/comment.rb +5 -5
  66. data/test/fixtures/comments.yml +17 -17
  67. data/test/fixtures/db_definitions/db2-create-tables.sql +112 -112
  68. data/test/fixtures/db_definitions/db2-drop-tables.sql +16 -16
  69. data/test/fixtures/db_definitions/mysql.sql +180 -180
  70. data/test/fixtures/db_definitions/oracle.drop.sql +41 -41
  71. data/test/fixtures/db_definitions/oracle.sql +199 -199
  72. data/test/fixtures/db_definitions/postgresql.sql +182 -182
  73. data/test/fixtures/db_definitions/sqlite.sql +169 -169
  74. data/test/fixtures/db_definitions/sqlserver.sql +176 -176
  75. data/test/fixtures/departments.yml +19 -15
  76. data/test/fixtures/dorm.rb +2 -2
  77. data/test/fixtures/dorms.yml +4 -4
  78. data/test/fixtures/employee.rb +5 -5
  79. data/test/fixtures/employees.yml +33 -28
  80. data/test/fixtures/group.rb +2 -2
  81. data/test/fixtures/groups.yml +6 -6
  82. data/test/fixtures/membership.rb +2 -0
  83. data/test/fixtures/membership_status.rb +2 -2
  84. data/test/fixtures/membership_statuses.yml +16 -16
  85. data/test/fixtures/memberships.yml +10 -10
  86. data/test/fixtures/product.rb +9 -9
  87. data/test/fixtures/product_tariff.rb +5 -5
  88. data/test/fixtures/product_tariffs.yml +14 -14
  89. data/test/fixtures/products.yml +11 -11
  90. data/test/fixtures/reading.rb +4 -4
  91. data/test/fixtures/readings.yml +10 -10
  92. data/test/fixtures/reference_code.rb +7 -7
  93. data/test/fixtures/reference_codes.yml +28 -28
  94. data/test/fixtures/reference_type.rb +12 -12
  95. data/test/fixtures/reference_types.yml +9 -9
  96. data/test/fixtures/restaurant.rb +9 -9
  97. data/test/fixtures/restaurants.yml +14 -14
  98. data/test/fixtures/restaurants_suburb.rb +2 -2
  99. data/test/fixtures/restaurants_suburbs.yml +10 -10
  100. data/test/fixtures/room.rb +11 -11
  101. data/test/fixtures/room_assignment.rb +13 -13
  102. data/test/fixtures/room_assignments.yml +24 -24
  103. data/test/fixtures/room_attribute.rb +2 -2
  104. data/test/fixtures/room_attribute_assignment.rb +4 -4
  105. data/test/fixtures/room_attribute_assignments.yml +4 -4
  106. data/test/fixtures/room_attributes.yml +2 -2
  107. data/test/fixtures/rooms.yml +12 -12
  108. data/test/fixtures/street.rb +2 -2
  109. data/test/fixtures/streets.yml +16 -16
  110. data/test/fixtures/student.rb +3 -3
  111. data/test/fixtures/students.yml +15 -15
  112. data/test/fixtures/suburb.rb +5 -5
  113. data/test/fixtures/suburbs.yml +14 -14
  114. data/test/fixtures/tariff.rb +5 -5
  115. data/test/fixtures/tariffs.yml +14 -14
  116. data/test/fixtures/topic_sources.yml +3 -3
  117. data/test/fixtures/topics.yml +8 -8
  118. data/test/fixtures/user.rb +11 -11
  119. data/test/fixtures/users.yml +10 -10
  120. data/test/plugins/pagination.rb +405 -405
  121. data/test/plugins/pagination_helper.rb +135 -135
  122. data/test/test_associations.rb +14 -0
  123. data/test/test_attribute_methods.rb +63 -63
  124. data/test/test_attributes.rb +75 -60
  125. data/test/test_calculations.rb +49 -42
  126. data/test/test_callbacks.rb +99 -99
  127. data/test/test_composite_arrays.rb +38 -38
  128. data/test/test_counter_cache.rb +30 -30
  129. data/test/test_create.rb +218 -206
  130. data/test/test_delete.rb +188 -179
  131. data/test/test_dumpable.rb +15 -15
  132. data/test/test_dup.rb +37 -37
  133. data/test/test_equal.rb +26 -26
  134. data/test/test_exists.rb +39 -39
  135. data/test/test_find.rb +170 -164
  136. data/test/test_habtm.rb +141 -141
  137. data/test/test_ids.rb +112 -112
  138. data/test/test_miscellaneous.rb +32 -32
  139. data/test/test_nested_attributes.rb +67 -67
  140. data/test/test_optimistic.rb +18 -18
  141. data/test/test_pagination.rb +35 -35
  142. data/test/test_polymorphic.rb +43 -43
  143. data/test/test_predicates.rb +59 -59
  144. data/test/test_preload.rb +102 -102
  145. data/test/test_santiago.rb +23 -23
  146. data/test/test_touch.rb +23 -23
  147. data/test/test_tutorial_example.rb +25 -25
  148. data/test/test_update.rb +102 -96
  149. data/test/test_validations.rb +13 -13
  150. metadata +7 -6
@@ -1,405 +1,405 @@
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 => '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 => '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, :order => 'last_name, first_name',
52
- # :limit => @person_pages.items_per_page,
53
- # :offset => @person_pages.current.offset
54
- # end
55
- #
56
- # Explicitly creates the paginator from the previous example and uses
57
- # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
58
- #
59
- module Pagination
60
- unless const_defined?(:OPTIONS)
61
- # A hash holding options for controllers using macro-style pagination
62
- OPTIONS = Hash.new
63
-
64
- # The default options for pagination
65
- DEFAULT_OPTIONS = {
66
- :class_name => nil,
67
- :singular_name => nil,
68
- :per_page => 10,
69
- :conditions => nil,
70
- :order_by => nil,
71
- :order => nil,
72
- :join => nil,
73
- :joins => nil,
74
- :count => nil,
75
- :include => nil,
76
- :select => nil,
77
- :group => nil,
78
- :parameter => 'page'
79
- }
80
- else
81
- DEFAULT_OPTIONS[:group] = nil
82
- end
83
-
84
- def self.included(base) #:nodoc:
85
- super
86
- base.extend(ClassMethods)
87
- end
88
-
89
- def self.validate_options!(collection_id, options, in_action) #:nodoc:
90
- options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
91
-
92
- valid_options = DEFAULT_OPTIONS.keys
93
- valid_options << :actions unless in_action
94
-
95
- unknown_option_keys = options.keys - valid_options
96
- raise ActionController::ActionControllerError,
97
- "Unknown options: #{unknown_option_keys.join(', ')}" unless
98
- unknown_option_keys.empty?
99
-
100
- options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
101
- options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
102
- end
103
-
104
- # Returns a paginator and a collection of Active Record model instances
105
- # for the paginator's current page. This is designed to be used in a
106
- # single action; to automatically paginate multiple actions, consider
107
- # ClassMethods#paginate.
108
- #
109
- # +options+ are:
110
- # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
111
- # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
112
- # camelizing the singular name
113
- # <tt>:per_page</tt>:: the maximum number of items to include in a
114
- # single page. Defaults to 10
115
- # <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
116
- # Model.count
117
- # <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
118
- # <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
119
- # <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
120
- # and Model.count
121
- # <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
122
- # and Model.count
123
- # <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
124
- # and Model.count
125
- # <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
126
- #
127
- # <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
128
- #
129
- # <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
130
- #
131
- def paginate(collection_id, options={})
132
- Pagination.validate_options!(collection_id, options, true)
133
- paginator_and_collection_for(collection_id, options)
134
- end
135
-
136
- # These methods become class methods on any controller
137
- module ClassMethods
138
- # Creates a +before_filter+ which automatically paginates an Active
139
- # Record model for all actions in a controller (or certain actions if
140
- # specified with the <tt>:actions</tt> option).
141
- #
142
- # +options+ are the same as PaginationHelper#paginate, with the addition
143
- # of:
144
- # <tt>:actions</tt>:: an array of actions for which the pagination is
145
- # active. Defaults to +nil+ (i.e., every action)
146
- def paginate(collection_id, options={})
147
- Pagination.validate_options!(collection_id, options, false)
148
- module_eval do
149
- before_filter :create_paginators_and_retrieve_collections
150
- OPTIONS[self] ||= Hash.new
151
- OPTIONS[self][collection_id] = options
152
- end
153
- end
154
- end
155
-
156
- def create_paginators_and_retrieve_collections #:nodoc:
157
- Pagination::OPTIONS[self.class].each do |collection_id, options|
158
- next unless options[:actions].include? action_name if
159
- options[:actions]
160
-
161
- paginator, collection =
162
- paginator_and_collection_for(collection_id, options)
163
-
164
- paginator_name = "@#{options[:singular_name]}_pages"
165
- self.instance_variable_set(paginator_name, paginator)
166
-
167
- collection_name = "@#{collection_id.to_s}"
168
- self.instance_variable_set(collection_name, collection)
169
- end
170
- end
171
-
172
- # Returns the total number of items in the collection to be paginated for
173
- # the +model+ and given +conditions+. Override this method to implement a
174
- # custom counter.
175
- def count_collection_for_pagination(model, options)
176
- model.count(:conditions => options[:conditions],
177
- :joins => options[:join] || options[:joins],
178
- :include => options[:include],
179
- :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
180
- end
181
-
182
- # Returns a collection of items for the given +model+ and +options[conditions]+,
183
- # ordered by +options[order]+, for the current page in the given +paginator+.
184
- # Override this method to implement a custom finder.
185
- def find_collection_for_pagination(model, options, paginator)
186
- model.find(:all, :conditions => options[:conditions],
187
- :order => options[:order_by] || options[:order],
188
- :joins => options[:join] || options[:joins], :include => options[:include],
189
- :select => options[:select], :limit => options[:per_page],
190
- :group => options[:group], :offset => paginator.current.offset)
191
- end
192
-
193
- protected :create_paginators_and_retrieve_collections,
194
- :count_collection_for_pagination,
195
- :find_collection_for_pagination
196
-
197
- def paginator_and_collection_for(collection_id, options) #:nodoc:
198
- klass = options[:class_name].constantize
199
- page = params[options[:parameter]]
200
- count = count_collection_for_pagination(klass, options)
201
- paginator = Paginator.new(self, count, options[:per_page], page)
202
- collection = find_collection_for_pagination(klass, options, paginator)
203
-
204
- return paginator, collection
205
- end
206
-
207
- private :paginator_and_collection_for
208
-
209
- # A class representing a paginator for an Active Record collection.
210
- class Paginator
211
- include Enumerable
212
-
213
- # Creates a new Paginator on the given +controller+ for a set of items
214
- # of size +item_count+ and having +items_per_page+ items per page.
215
- # Raises ArgumentError if items_per_page is out of bounds (i.e., less
216
- # than or equal to zero). The page CGI parameter for links defaults to
217
- # "page" and can be overridden with +page_parameter+.
218
- def initialize(controller, item_count, items_per_page, current_page=1)
219
- raise ArgumentError, 'must have at least one item per page' if
220
- items_per_page <= 0
221
-
222
- @controller = controller
223
- @item_count = item_count || 0
224
- @items_per_page = items_per_page
225
- @pages = {}
226
-
227
- self.current_page = current_page
228
- end
229
- attr_reader :controller, :item_count, :items_per_page
230
-
231
- # Sets the current page number of this paginator. If +page+ is a Page
232
- # object, its +number+ attribute is used as the value; if the page does
233
- # not belong to this Paginator, an ArgumentError is raised.
234
- def current_page=(page)
235
- if page.is_a? Page
236
- raise ArgumentError, 'Page/Paginator mismatch' unless
237
- page.paginator == self
238
- end
239
- page = page.to_i
240
- @current_page_number = has_page_number?(page) ? page : 1
241
- end
242
-
243
- # Returns a Page object representing this paginator's current page.
244
- def current_page
245
- @current_page ||= self[@current_page_number]
246
- end
247
- alias current :current_page
248
-
249
- # Returns a new Page representing the first page in this paginator.
250
- def first_page
251
- @first_page ||= self[1]
252
- end
253
- alias first :first_page
254
-
255
- # Returns a new Page representing the last page in this paginator.
256
- def last_page
257
- @last_page ||= self[page_count]
258
- end
259
- alias last :last_page
260
-
261
- # Returns the number of pages in this paginator.
262
- def page_count
263
- @page_count ||= @item_count.zero? ? 1 :
264
- (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
265
- end
266
-
267
- alias length :page_count
268
-
269
- # Returns true if this paginator contains the page of index +number+.
270
- def has_page_number?(number)
271
- number >= 1 and number <= page_count
272
- end
273
-
274
- # Returns a new Page representing the page with the given index
275
- # +number+.
276
- def [](number)
277
- @pages[number] ||= Page.new(self, number)
278
- end
279
-
280
- # Successively yields all the paginator's pages to the given block.
281
- def each(&block)
282
- page_count.times do |n|
283
- yield self[n+1]
284
- end
285
- end
286
-
287
- # A class representing a single page in a paginator.
288
- class Page
289
- include Comparable
290
-
291
- # Creates a new Page for the given +paginator+ with the index
292
- # +number+. If +number+ is not in the range of valid page numbers or
293
- # is not a number at all, it defaults to 1.
294
- def initialize(paginator, number)
295
- @paginator = paginator
296
- @number = number.to_i
297
- @number = 1 unless @paginator.has_page_number? @number
298
- end
299
- attr_reader :paginator, :number
300
- alias to_i :number
301
-
302
- # Compares two Page objects and returns true when they represent the
303
- # same page (i.e., their paginators are the same and they have the
304
- # same page number).
305
- def ==(page)
306
- return false if page.nil?
307
- @paginator == page.paginator and
308
- @number == page.number
309
- end
310
-
311
- # Compares two Page objects and returns -1 if the left-hand page comes
312
- # before the right-hand page, 0 if the pages are equal, and 1 if the
313
- # left-hand page comes after the right-hand page. Raises ArgumentError
314
- # if the pages do not belong to the same Paginator object.
315
- def <=>(page)
316
- raise ArgumentError unless @paginator == page.paginator
317
- @number <=> page.number
318
- end
319
-
320
- # Returns the item offset for the first item in this page.
321
- def offset
322
- @paginator.items_per_page * (@number - 1)
323
- end
324
-
325
- # Returns the number of the first item displayed.
326
- def first_item
327
- offset + 1
328
- end
329
-
330
- # Returns the number of the last item displayed.
331
- def last_item
332
- [@paginator.items_per_page * @number, @paginator.item_count].min
333
- end
334
-
335
- # Returns true if this page is the first page in the paginator.
336
- def first?
337
- self == @paginator.first
338
- end
339
-
340
- # Returns true if this page is the last page in the paginator.
341
- def last?
342
- self == @paginator.last
343
- end
344
-
345
- # Returns a new Page object representing the page just before this
346
- # page, or nil if this is the first page.
347
- def previous
348
- if first? then nil else @paginator[@number - 1] end
349
- end
350
-
351
- # Returns a new Page object representing the page just after this
352
- # page, or nil if this is the last page.
353
- def next
354
- if last? then nil else @paginator[@number + 1] end
355
- end
356
-
357
- # Returns a new Window object for this page with the specified
358
- # +padding+.
359
- def window(padding=2)
360
- Window.new(self, padding)
361
- end
362
-
363
- # Returns the limit/offset array for this page.
364
- def to_sql
365
- [@paginator.items_per_page, offset]
366
- end
367
-
368
- def to_param #:nodoc:
369
- @number.to_s
370
- end
371
- end
372
-
373
- # A class for representing ranges around a given page.
374
- class Window
375
- # Creates a new Window object for the given +page+ with the specified
376
- # +padding+.
377
- def initialize(page, padding=2)
378
- @paginator = page.paginator
379
- @page = page
380
- self.padding = padding
381
- end
382
- attr_reader :paginator, :page
383
-
384
- # Sets the window's padding (the number of pages on either side of the
385
- # window page).
386
- def padding=(padding)
387
- @padding = padding < 0 ? 0 : padding
388
- # Find the beginning and end pages of the window
389
- @first = @paginator.has_page_number?(@page.number - @padding) ?
390
- @paginator[@page.number - @padding] : @paginator.first
391
- @last = @paginator.has_page_number?(@page.number + @padding) ?
392
- @paginator[@page.number + @padding] : @paginator.last
393
- end
394
- attr_reader :padding, :first, :last
395
-
396
- # Returns an array of Page objects in the current window.
397
- def pages
398
- (@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
399
- end
400
- alias to_a :pages
401
- end
402
- end
403
-
404
- end
405
- end
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 => '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 => '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, :order => 'last_name, first_name',
52
+ # :limit => @person_pages.items_per_page,
53
+ # :offset => @person_pages.current.offset
54
+ # end
55
+ #
56
+ # Explicitly creates the paginator from the previous example and uses
57
+ # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
58
+ #
59
+ module Pagination
60
+ unless const_defined?(:OPTIONS)
61
+ # A hash holding options for controllers using macro-style pagination
62
+ OPTIONS = Hash.new
63
+
64
+ # The default options for pagination
65
+ DEFAULT_OPTIONS = {
66
+ :class_name => nil,
67
+ :singular_name => nil,
68
+ :per_page => 10,
69
+ :conditions => nil,
70
+ :order_by => nil,
71
+ :order => nil,
72
+ :join => nil,
73
+ :joins => nil,
74
+ :count => nil,
75
+ :include => nil,
76
+ :select => nil,
77
+ :group => nil,
78
+ :parameter => 'page'
79
+ }
80
+ else
81
+ DEFAULT_OPTIONS[:group] = nil
82
+ end
83
+
84
+ def self.included(base) #:nodoc:
85
+ super
86
+ base.extend(ClassMethods)
87
+ end
88
+
89
+ def self.validate_options!(collection_id, options, in_action) #:nodoc:
90
+ options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
91
+
92
+ valid_options = DEFAULT_OPTIONS.keys
93
+ valid_options << :actions unless in_action
94
+
95
+ unknown_option_keys = options.keys - valid_options
96
+ raise ActionController::ActionControllerError,
97
+ "Unknown options: #{unknown_option_keys.join(', ')}" unless
98
+ unknown_option_keys.empty?
99
+
100
+ options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
101
+ options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
102
+ end
103
+
104
+ # Returns a paginator and a collection of Active Record model instances
105
+ # for the paginator's current page. This is designed to be used in a
106
+ # single action; to automatically paginate multiple actions, consider
107
+ # ClassMethods#paginate.
108
+ #
109
+ # +options+ are:
110
+ # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
111
+ # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
112
+ # camelizing the singular name
113
+ # <tt>:per_page</tt>:: the maximum number of items to include in a
114
+ # single page. Defaults to 10
115
+ # <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
116
+ # Model.count
117
+ # <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
118
+ # <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
119
+ # <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
120
+ # and Model.count
121
+ # <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
122
+ # and Model.count
123
+ # <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
124
+ # and Model.count
125
+ # <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
126
+ #
127
+ # <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
128
+ #
129
+ # <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
130
+ #
131
+ def paginate(collection_id, options={})
132
+ Pagination.validate_options!(collection_id, options, true)
133
+ paginator_and_collection_for(collection_id, options)
134
+ end
135
+
136
+ # These methods become class methods on any controller
137
+ module ClassMethods
138
+ # Creates a +before_filter+ which automatically paginates an Active
139
+ # Record model for all actions in a controller (or certain actions if
140
+ # specified with the <tt>:actions</tt> option).
141
+ #
142
+ # +options+ are the same as PaginationHelper#paginate, with the addition
143
+ # of:
144
+ # <tt>:actions</tt>:: an array of actions for which the pagination is
145
+ # active. Defaults to +nil+ (i.e., every action)
146
+ def paginate(collection_id, options={})
147
+ Pagination.validate_options!(collection_id, options, false)
148
+ module_eval do
149
+ before_filter :create_paginators_and_retrieve_collections
150
+ OPTIONS[self] ||= Hash.new
151
+ OPTIONS[self][collection_id] = options
152
+ end
153
+ end
154
+ end
155
+
156
+ def create_paginators_and_retrieve_collections #:nodoc:
157
+ Pagination::OPTIONS[self.class].each do |collection_id, options|
158
+ next unless options[:actions].include? action_name if
159
+ options[:actions]
160
+
161
+ paginator, collection =
162
+ paginator_and_collection_for(collection_id, options)
163
+
164
+ paginator_name = "@#{options[:singular_name]}_pages"
165
+ self.instance_variable_set(paginator_name, paginator)
166
+
167
+ collection_name = "@#{collection_id.to_s}"
168
+ self.instance_variable_set(collection_name, collection)
169
+ end
170
+ end
171
+
172
+ # Returns the total number of items in the collection to be paginated for
173
+ # the +model+ and given +conditions+. Override this method to implement a
174
+ # custom counter.
175
+ def count_collection_for_pagination(model, options)
176
+ model.count(:conditions => options[:conditions],
177
+ :joins => options[:join] || options[:joins],
178
+ :include => options[:include],
179
+ :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
180
+ end
181
+
182
+ # Returns a collection of items for the given +model+ and +options[conditions]+,
183
+ # ordered by +options[order]+, for the current page in the given +paginator+.
184
+ # Override this method to implement a custom finder.
185
+ def find_collection_for_pagination(model, options, paginator)
186
+ model.find(:all, :conditions => options[:conditions],
187
+ :order => options[:order_by] || options[:order],
188
+ :joins => options[:join] || options[:joins], :include => options[:include],
189
+ :select => options[:select], :limit => options[:per_page],
190
+ :group => options[:group], :offset => paginator.current.offset)
191
+ end
192
+
193
+ protected :create_paginators_and_retrieve_collections,
194
+ :count_collection_for_pagination,
195
+ :find_collection_for_pagination
196
+
197
+ def paginator_and_collection_for(collection_id, options) #:nodoc:
198
+ klass = options[:class_name].constantize
199
+ page = params[options[:parameter]]
200
+ count = count_collection_for_pagination(klass, options)
201
+ paginator = Paginator.new(self, count, options[:per_page], page)
202
+ collection = find_collection_for_pagination(klass, options, paginator)
203
+
204
+ return paginator, collection
205
+ end
206
+
207
+ private :paginator_and_collection_for
208
+
209
+ # A class representing a paginator for an Active Record collection.
210
+ class Paginator
211
+ include Enumerable
212
+
213
+ # Creates a new Paginator on the given +controller+ for a set of items
214
+ # of size +item_count+ and having +items_per_page+ items per page.
215
+ # Raises ArgumentError if items_per_page is out of bounds (i.e., less
216
+ # than or equal to zero). The page CGI parameter for links defaults to
217
+ # "page" and can be overridden with +page_parameter+.
218
+ def initialize(controller, item_count, items_per_page, current_page=1)
219
+ raise ArgumentError, 'must have at least one item per page' if
220
+ items_per_page <= 0
221
+
222
+ @controller = controller
223
+ @item_count = item_count || 0
224
+ @items_per_page = items_per_page
225
+ @pages = {}
226
+
227
+ self.current_page = current_page
228
+ end
229
+ attr_reader :controller, :item_count, :items_per_page
230
+
231
+ # Sets the current page number of this paginator. If +page+ is a Page
232
+ # object, its +number+ attribute is used as the value; if the page does
233
+ # not belong to this Paginator, an ArgumentError is raised.
234
+ def current_page=(page)
235
+ if page.is_a? Page
236
+ raise ArgumentError, 'Page/Paginator mismatch' unless
237
+ page.paginator == self
238
+ end
239
+ page = page.to_i
240
+ @current_page_number = has_page_number?(page) ? page : 1
241
+ end
242
+
243
+ # Returns a Page object representing this paginator's current page.
244
+ def current_page
245
+ @current_page ||= self[@current_page_number]
246
+ end
247
+ alias current :current_page
248
+
249
+ # Returns a new Page representing the first page in this paginator.
250
+ def first_page
251
+ @first_page ||= self[1]
252
+ end
253
+ alias first :first_page
254
+
255
+ # Returns a new Page representing the last page in this paginator.
256
+ def last_page
257
+ @last_page ||= self[page_count]
258
+ end
259
+ alias last :last_page
260
+
261
+ # Returns the number of pages in this paginator.
262
+ def page_count
263
+ @page_count ||= @item_count.zero? ? 1 :
264
+ (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
265
+ end
266
+
267
+ alias length :page_count
268
+
269
+ # Returns true if this paginator contains the page of index +number+.
270
+ def has_page_number?(number)
271
+ number >= 1 and number <= page_count
272
+ end
273
+
274
+ # Returns a new Page representing the page with the given index
275
+ # +number+.
276
+ def [](number)
277
+ @pages[number] ||= Page.new(self, number)
278
+ end
279
+
280
+ # Successively yields all the paginator's pages to the given block.
281
+ def each(&block)
282
+ page_count.times do |n|
283
+ yield self[n+1]
284
+ end
285
+ end
286
+
287
+ # A class representing a single page in a paginator.
288
+ class Page
289
+ include Comparable
290
+
291
+ # Creates a new Page for the given +paginator+ with the index
292
+ # +number+. If +number+ is not in the range of valid page numbers or
293
+ # is not a number at all, it defaults to 1.
294
+ def initialize(paginator, number)
295
+ @paginator = paginator
296
+ @number = number.to_i
297
+ @number = 1 unless @paginator.has_page_number? @number
298
+ end
299
+ attr_reader :paginator, :number
300
+ alias to_i :number
301
+
302
+ # Compares two Page objects and returns true when they represent the
303
+ # same page (i.e., their paginators are the same and they have the
304
+ # same page number).
305
+ def ==(page)
306
+ return false if page.nil?
307
+ @paginator == page.paginator and
308
+ @number == page.number
309
+ end
310
+
311
+ # Compares two Page objects and returns -1 if the left-hand page comes
312
+ # before the right-hand page, 0 if the pages are equal, and 1 if the
313
+ # left-hand page comes after the right-hand page. Raises ArgumentError
314
+ # if the pages do not belong to the same Paginator object.
315
+ def <=>(page)
316
+ raise ArgumentError unless @paginator == page.paginator
317
+ @number <=> page.number
318
+ end
319
+
320
+ # Returns the item offset for the first item in this page.
321
+ def offset
322
+ @paginator.items_per_page * (@number - 1)
323
+ end
324
+
325
+ # Returns the number of the first item displayed.
326
+ def first_item
327
+ offset + 1
328
+ end
329
+
330
+ # Returns the number of the last item displayed.
331
+ def last_item
332
+ [@paginator.items_per_page * @number, @paginator.item_count].min
333
+ end
334
+
335
+ # Returns true if this page is the first page in the paginator.
336
+ def first?
337
+ self == @paginator.first
338
+ end
339
+
340
+ # Returns true if this page is the last page in the paginator.
341
+ def last?
342
+ self == @paginator.last
343
+ end
344
+
345
+ # Returns a new Page object representing the page just before this
346
+ # page, or nil if this is the first page.
347
+ def previous
348
+ if first? then nil else @paginator[@number - 1] end
349
+ end
350
+
351
+ # Returns a new Page object representing the page just after this
352
+ # page, or nil if this is the last page.
353
+ def next
354
+ if last? then nil else @paginator[@number + 1] end
355
+ end
356
+
357
+ # Returns a new Window object for this page with the specified
358
+ # +padding+.
359
+ def window(padding=2)
360
+ Window.new(self, padding)
361
+ end
362
+
363
+ # Returns the limit/offset array for this page.
364
+ def to_sql
365
+ [@paginator.items_per_page, offset]
366
+ end
367
+
368
+ def to_param #:nodoc:
369
+ @number.to_s
370
+ end
371
+ end
372
+
373
+ # A class for representing ranges around a given page.
374
+ class Window
375
+ # Creates a new Window object for the given +page+ with the specified
376
+ # +padding+.
377
+ def initialize(page, padding=2)
378
+ @paginator = page.paginator
379
+ @page = page
380
+ self.padding = padding
381
+ end
382
+ attr_reader :paginator, :page
383
+
384
+ # Sets the window's padding (the number of pages on either side of the
385
+ # window page).
386
+ def padding=(padding)
387
+ @padding = padding < 0 ? 0 : padding
388
+ # Find the beginning and end pages of the window
389
+ @first = @paginator.has_page_number?(@page.number - @padding) ?
390
+ @paginator[@page.number - @padding] : @paginator.first
391
+ @last = @paginator.has_page_number?(@page.number + @padding) ?
392
+ @paginator[@page.number + @padding] : @paginator.last
393
+ end
394
+ attr_reader :padding, :first, :last
395
+
396
+ # Returns an array of Page objects in the current window.
397
+ def pages
398
+ (@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
399
+ end
400
+ alias to_a :pages
401
+ end
402
+ end
403
+
404
+ end
405
+ end