composite_primary_keys 9.0.9 → 9.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +4 -0
  3. data/lib/composite_primary_keys.rb +3 -3
  4. data/lib/composite_primary_keys/autosave_association.rb +32 -0
  5. data/lib/composite_primary_keys/connection_adapters/abstract_adapter.rb +10 -10
  6. data/lib/composite_primary_keys/locking/optimistic.rb +7 -7
  7. data/lib/composite_primary_keys/version.rb +1 -1
  8. data/scripts/console.rb +48 -48
  9. data/scripts/txt2html +76 -76
  10. data/scripts/txt2js +65 -65
  11. data/tasks/website.rake +18 -18
  12. data/test/README_tests.rdoc +56 -56
  13. data/test/connections/databases.yml +40 -30
  14. data/test/db_test.rb +52 -52
  15. data/test/fixtures/articles.yml +6 -6
  16. data/test/fixtures/capitol.rb +3 -3
  17. data/test/fixtures/capitols.yml +16 -16
  18. data/test/fixtures/comments.yml +15 -15
  19. data/test/fixtures/department.rb +5 -5
  20. data/test/fixtures/departments.yml +15 -15
  21. data/test/fixtures/dorms.yml +4 -4
  22. data/test/fixtures/group.rb +2 -2
  23. data/test/fixtures/groups.yml +6 -6
  24. data/test/fixtures/hack.rb +4 -4
  25. data/test/fixtures/hacks.yml +2 -2
  26. data/test/fixtures/membership_status.rb +2 -2
  27. data/test/fixtures/product.rb +9 -9
  28. data/test/fixtures/product_tariff.rb +5 -5
  29. data/test/fixtures/products.yml +11 -11
  30. data/test/fixtures/reading.rb +4 -4
  31. data/test/fixtures/readings.yml +10 -10
  32. data/test/fixtures/reference_code_using_composite_key_alias.rb +8 -8
  33. data/test/fixtures/reference_code_using_simple_key_alias.rb +8 -8
  34. data/test/fixtures/reference_codes.yml +28 -28
  35. data/test/fixtures/reference_types.yml +9 -9
  36. data/test/fixtures/restaurant.rb +9 -9
  37. data/test/fixtures/restaurants.yml +14 -14
  38. data/test/fixtures/restaurants_suburbs.yml +10 -10
  39. data/test/fixtures/room.rb +11 -11
  40. data/test/fixtures/room_assignment.rb +13 -13
  41. data/test/fixtures/room_assignments.yml +24 -24
  42. data/test/fixtures/room_attribute.rb +2 -2
  43. data/test/fixtures/room_attribute_assignment.rb +4 -4
  44. data/test/fixtures/room_attribute_assignments.yml +4 -4
  45. data/test/fixtures/room_attributes.yml +2 -2
  46. data/test/fixtures/rooms.yml +12 -12
  47. data/test/fixtures/seat.rb +5 -5
  48. data/test/fixtures/seats.yml +8 -8
  49. data/test/fixtures/street.rb +2 -2
  50. data/test/fixtures/streets.yml +16 -16
  51. data/test/fixtures/student.rb +3 -3
  52. data/test/fixtures/students.yml +15 -15
  53. data/test/fixtures/suburbs.yml +14 -14
  54. data/test/plugins/pagination.rb +405 -405
  55. data/test/plugins/pagination_helper.rb +135 -135
  56. data/test/setup.rb +50 -50
  57. data/test/test_aliases.rb +18 -18
  58. data/test/test_associations.rb +10 -0
  59. data/test/test_composite_arrays.rb +24 -24
  60. data/test/test_dup.rb +37 -37
  61. data/test/test_exists.rb +39 -39
  62. data/test/test_miscellaneous.rb +32 -32
  63. data/test/test_pagination.rb +35 -35
  64. data/test/test_validations.rb +13 -13
  65. metadata +10 -4
  66. data/lib/composite_primary_keys/attribute_set/builder.rb +0 -22
@@ -1,5 +1,5 @@
1
- class RoomAttributeAssignment < ActiveRecord::Base
2
- self.primary_keys = :dorm_id, :room_id, :room_attribute_id
3
- belongs_to :room, :foreign_key => [:dorm_id, :room_id]
4
- belongs_to :room_attribute
1
+ class RoomAttributeAssignment < ActiveRecord::Base
2
+ self.primary_keys = :dorm_id, :room_id, :room_attribute_id
3
+ belongs_to :room, :foreign_key => [:dorm_id, :room_id]
4
+ belongs_to :room_attribute
5
5
  end
@@ -1,4 +1,4 @@
1
- assignment:
2
- dorm_id: 1
3
- room_id: 1
4
- room_attribute_id: 1
1
+ assignment:
2
+ dorm_id: 1
3
+ room_id: 1
4
+ room_attribute_id: 1
@@ -1,3 +1,3 @@
1
- attribute_1:
2
- id: 1
1
+ attribute_1:
2
+ id: 1
3
3
  name: 'type'
@@ -1,12 +1,12 @@
1
- branner_room_1:
2
- dorm_id: 1
3
- room_id: 1
4
-
5
- branner_room_2:
6
- dorm_id: 1
7
- room_id: 2
8
-
9
- branner_room_3:
10
- dorm_id: 1
11
- room_id: 3
12
-
1
+ branner_room_1:
2
+ dorm_id: 1
3
+ room_id: 1
4
+
5
+ branner_room_2:
6
+ dorm_id: 1
7
+ room_id: 2
8
+
9
+ branner_room_3:
10
+ dorm_id: 1
11
+ room_id: 3
12
+
@@ -1,5 +1,5 @@
1
- class Seat < ActiveRecord::Base
2
- self.primary_keys = [:flight_number, :seat]
3
-
4
- validates_uniqueness_of :customer
5
- end
1
+ class Seat < ActiveRecord::Base
2
+ self.primary_keys = [:flight_number, :seat]
3
+
4
+ validates_uniqueness_of :customer
5
+ end
@@ -1,9 +1,9 @@
1
- seat1:
2
- flight_number: 1
3
- seat: 1
4
- customer: 1
5
-
6
- seat2:
7
- flight_number: 1
8
- seat: 2
1
+ seat1:
2
+ flight_number: 1
3
+ seat: 1
4
+ customer: 1
5
+
6
+ seat2:
7
+ flight_number: 1
8
+ seat: 2
9
9
  customer: 2
@@ -1,3 +1,3 @@
1
- class Street < ActiveRecord::Base
2
- belongs_to :suburb, :foreign_key => [:city_id, :suburb_id]
1
+ class Street < ActiveRecord::Base
2
+ belongs_to :suburb, :foreign_key => [:city_id, :suburb_id]
3
3
  end
@@ -1,17 +1,17 @@
1
- first:
2
- id: 1
3
- city_id: 1
4
- suburb_id: 1
5
- name: First Street
6
-
7
- second1:
8
- id: 2
9
- city_id: 2
10
- suburb_id: 1
11
- name: First Street
12
-
13
- second2:
14
- id: 3
15
- city_id: 2
16
- suburb_id: 1
1
+ first:
2
+ id: 1
3
+ city_id: 1
4
+ suburb_id: 1
5
+ name: First Street
6
+
7
+ second1:
8
+ id: 2
9
+ city_id: 2
10
+ suburb_id: 1
11
+ name: First Street
12
+
13
+ second2:
14
+ id: 3
15
+ city_id: 2
16
+ suburb_id: 1
17
17
  name: Second Street
@@ -1,4 +1,4 @@
1
- class Student < ActiveRecord::Base
2
- has_many :room_assignments, :dependent => :destroy
3
- has_many :rooms, :through => :room_assignments, :foreign_key => [:building_code, :room_number]
1
+ class Student < ActiveRecord::Base
2
+ has_many :room_assignments, :dependent => :destroy
3
+ has_many :rooms, :through => :room_assignments, :foreign_key => [:building_code, :room_number]
4
4
  end
@@ -1,15 +1,15 @@
1
- jackson:
2
- id: 1
3
-
4
- bob:
5
- id: 2
6
-
7
- kelly:
8
- id: 3
9
-
10
- jordan:
11
- id: 4
12
-
13
- brad:
14
- id: 5
15
-
1
+ jackson:
2
+ id: 1
3
+
4
+ bob:
5
+ id: 2
6
+
7
+ kelly:
8
+ id: 3
9
+
10
+ jordan:
11
+ id: 4
12
+
13
+ brad:
14
+ id: 5
15
+
@@ -1,14 +1,14 @@
1
- first:
2
- city_id: 1
3
- suburb_id: 1
4
- name: First Suburb
5
-
6
- second:
7
- city_id: 2
8
- suburb_id: 1
9
- name: Second Suburb
10
-
11
- no_mcdonalds:
12
- city_id: 1
13
- suburb_id: 2
14
- name: Third Suburb
1
+ first:
2
+ city_id: 1
3
+ suburb_id: 1
4
+ name: First Suburb
5
+
6
+ second:
7
+ city_id: 2
8
+ suburb_id: 1
9
+ name: Second Suburb
10
+
11
+ no_mcdonalds:
12
+ city_id: 1
13
+ suburb_id: 2
14
+ name: Third Suburb
@@ -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