mholling-paged_scopes 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.textile ADDED
@@ -0,0 +1,471 @@
1
+ h1. Paged Scopes: A Will_paginate Alternative
2
+
3
+ The first time I needed to paginate data in a Rails site, I went straight for the de-facto standard, which, since Rails 2.0, has undoubtedly been "will_paginate":http://wiki.github.com/mislav/will_paginate. However, it didn't take me long to discover it couldn't do all that I wanted it to.
4
+
5
+ Most importantly, I wanted to be able to redirect from a resource member action (the update action, say) back to the index action, with the page set so that the edited resource would be part of the paged list. I couldn't see a way to do that with will_paginate. I found the will_paginate helper a bit messy - ever heard of block helpers? And finally, I wanted my pages to be objects, not just numbers. This would let me load them in controllers and pass them to named routes and have them just work. Will_paginate didn't seem to fit the bill.
6
+
7
+ Now don't get me wrong; will_paginate must be pretty great - it's the "third most watched repo on GitHub":http://github.com/popular/watched as I write this. But choice is always good, and to me, will_paginate seems a bit bloated and ill-fitting to the way I like to structure my code.
8
+
9
+ So, naturally, I rolled my own pagination solution. I've finally packaged it up and released it as a new ActiveRecord pagination gem, _PagedScopes_. It's everything I need in Rails pagination and nothing I don't. It's also lightweight and pretty solid. Check it out!
10
+
11
+ h2. Features
12
+
13
+ The bullet-point summary of the PagedScopes gem goes something like this:
14
+
15
+ * Pages are instances of a class which belongs to the collection it's paginating;
16
+ * Pages can be found by number or by contained object;
17
+ * Each page has its own paged collection, which is a scope on the underlying collection; and
18
+ * Flexible, Digg-style pagination links are achieved using a block helper.
19
+
20
+ h2. A Console Session Is Worth a Thousand Words
21
+
22
+ Let's take a look at how pagination works with PagedScopes. Consider a collection of articles obtained using a <code>published</code> named scope.
23
+
24
+ <pre>
25
+ @articles = Article.published
26
+ => [#<Article id: 1, title: "Article #1">, ..., #<Article id: 5, title: "Article #5">]
27
+ @articles.count
28
+ => 5
29
+ </pre>
30
+
31
+ The PagedScopes gem adds a <code>per_page</code> attribute directly to <code>named_scope</code> collections (and to association collections, too). This value determines how many objects each page contains, and needs to be set before we can paginate the collection:
32
+
33
+ <pre>
34
+ @articles.per_page = 2
35
+ => 2
36
+ </pre>
37
+
38
+ Paginating this collection will now give us three pages.
39
+
40
+ How do we access these pages? By calling <code>pages</code>, the other main method added to <code>ActiveRecord</code> collections. It returns an enumerated class, the instances of which represent the pages of the collection. We can interact with the pages class in some familiar ways:
41
+
42
+ <pre>
43
+ @articles.pages
44
+ => #<Class:0x24ea99c>
45
+ @articles.pages.count
46
+ => 3
47
+ @articles.pages.first
48
+ => #<Page, for: Article, number: 1>
49
+ @articles.pages.find(1)
50
+ => #<Page, for: Article, number: 1>
51
+ @articles.pages.last
52
+ => #<Page, for: Article, number: 3>
53
+ @articles.pages.find(4)
54
+ => # PagedScopes::PageNotFound: couldn't find page number 4
55
+ @articles.pages.all
56
+ => [#<Page, for: Article, number: 1>, #<Page, for: Article, number: 2>, #<Page, for: Article, number: 3>]
57
+ @articles.first.to_param
58
+ => "1"
59
+ </pre>
60
+
61
+ Looks just like any other model - each page is its own self-contained object, as it should be. We can access the collection objects in the page using the same name as the underlying model. In our example, our collection contains <code>Article</code> instances, so the articles in the page are accessed using an <code>articles</code> method:
62
+
63
+ <pre>
64
+ @articles.pages.first.articles
65
+ => [#<Article id: 1, title: "Article #1">, #<Article id: 2, title: "Article #2">]
66
+ @articles.pages.last.articles
67
+ => [#<Article id: 5, title: "Article #5">]
68
+ @articles.pages.map(&:articles).map(&:size)
69
+ => [2, 2, 1]
70
+ @articles.pages.map { |page| page.articles.map(&:title) }
71
+ => [["Article #1", "Article #2"], ["Article #3", "Article #4"], ["Article #5"]]
72
+ </pre>
73
+
74
+ So far, so good. Bu what, exactly, is return by the <code>articles</code> method? Let's see:
75
+
76
+ <pre>
77
+ @articles.pages.first.articles.class
78
+ => ActiveRecord::NamedScope::Scope
79
+ @articles.pages.first.articles.send(:scope, :find)
80
+ => {:conditions=>"published_at IS NOT NULL", :offset=>0, :limit=>2}
81
+ @articles.pages.last.articles.send(:scope, :find)
82
+ => {:conditions=>"published_at IS NOT NULL", :offset=>4, :limit=>2}
83
+ @articles.send(:scope, :find)
84
+ => {:conditions=>"published_at IS NOT NULL"}
85
+ </pre>
86
+
87
+ Yep, it's just a scope on the parent collection, with <code>:limit</code> and <code>:offset</code> added according to the page number. This is kinda important. It means that the objects in the paged collection will not load from the database until they are referenced. We can pass around page objects in view helpers and named routes and so on, without worrying about inadvertently loading the paged data.
88
+
89
+ h2. Finding a Page By Its Contents
90
+
91
+ One particularly nice feature of the library is that we can find a page by identifying an object the page contains.
92
+
93
+ <pre>
94
+ article = Article.find(3)
95
+ => #<Article id: 3, title: "Article #3">
96
+ @articles.pages.find_by_article(article)
97
+ => #<Page, for: Article, number: 2>
98
+
99
+ article = articles.find(8)
100
+ => #<Article id: 8, title: "Article #8">
101
+ @articles.pages.find_by_article(article)
102
+ => nil
103
+ @articles.pages.find_by_article!(article)
104
+ => # PagedScopes::PageNotFound: #<Article id: 8, title: "Article #8"> not found in scope
105
+ </pre>
106
+
107
+ This is really handy if you want to redirect from a resource member action to the paged of the index containing the edited object. (More on this later.)
108
+
109
+ This is implemented using the code I described in my "previous post":http://code.matthewhollingworth.net/articles/2009-06-22-indexing-activerecord-objects-in-an-ordered-collection. As a result you get a couple of freebies on your ActiveRecord objects:
110
+
111
+ <pre>
112
+ article = Article.scoped(:order => "title ASC").find(3)
113
+ => #<Article id: 3, title: "Article #3">
114
+ article.next
115
+ => #<Article id: 4, title: "Article #4">
116
+
117
+ article = Article.scoped(:order => "title DESC").find(3)
118
+ => #<Article id: 3, title: "Article #3">
119
+ article.next
120
+ => #<Article id: 2, title: "Article #2">
121
+ article.previous
122
+ => #<Article id: 4, title: "Article #4">
123
+ </pre>
124
+
125
+ In other words, you can find the <code>next</code> and <code>previous</code> objects for any object in a collection. This provides an easy way to link to neighbouring objects (e.g. older and newer posts in a blog).
126
+
127
+ h2. A Caveat
128
+
129
+ It's important to store the paged scope or association collection in a variable, rather than refer to it directly. In other words:
130
+
131
+ <pre>
132
+ # Do this:
133
+ @articles = @user.articles.published # or whatever
134
+ => [#<Article ...>, ..., #<Article ...>]
135
+ @articles.per_page = 5
136
+ => 5
137
+ @articles.per_page
138
+ => 5
139
+
140
+ # Don't do this:
141
+ @user.articles.published.per_page = 5
142
+ => 5
143
+ @user.articles.published.per_page
144
+ => nil
145
+ </pre>
146
+
147
+ This is because paged scopes and association collections return new instances each time they're called. You need to hang onto them to set the <code>per_page</code> and then get the pages.
148
+
149
+ h2. Page Routing
150
+
151
+ The most common way to represent a paginated collection in an URL is to tack on the page number as a query paramater: <code>http://www.example.com/articles?page=3</code>, for example.
152
+
153
+ I'm not a fan of this approach at all. For starters, it's a bit ugly. More importantly, it won't work with standard Rails page caching, which ignores query parameters.
154
+
155
+ I prefer to think of pagination as just another scoping of the collection. Just as we have paths like <code>/users/9/articles</code>, I prefer a paged collection to have paths like <code>/pages/2/articles</code> (or <code>/users/9/pages/2/articles</code>, for that matter).
156
+
157
+ To this end, the Paged Scopes gem adds a <code>:paged</code> option to the Rails <code>resources</code> mapper. We'll use this option to define the routes for our articles:
158
+
159
+ <pre>
160
+ ActionController::Routing::Routes.draw do |map|
161
+ map.resources :articles, :paged => true
162
+ end
163
+ </pre>
164
+
165
+ Checking our routes using <code>rake routes</code>:
166
+
167
+ <pre>
168
+ articles GET /articles(.:format) {:controller=>"articles", :action=>"index"}
169
+ POST /articles(.:format) {:controller=>"articles", :action=>"create"}
170
+ new_article GET /articles/new(.:format) {:controller=>"articles", :action=>"new"}
171
+ edit_article GET /articles/:id/edit(.:format) {:controller=>"articles", :action=>"edit"}
172
+ article GET /articles/:id(.:format) {:controller=>"articles", :action=>"show"}
173
+ PUT /articles/:id(.:format) {:controller=>"articles", :action=>"update"}
174
+ DELETE /articles/:id(.:format) {:controller=>"articles", :action=>"destroy"}
175
+ page_articles GET /pages/:page_id/articles(.:format) {:controller=>"articles", :action=>"index"}
176
+ </pre>
177
+
178
+ Just your standard set of resource routes, with one extra - the paged articles index route, last in the list. Specifying the <code>:paged</code> option in the mapping yields this extra route for use in our index actions. (Everything else remains the same.)
179
+
180
+ Want a bit more flexibility? We can pass <code>:as</code> or <code>:name</code> options to the paged option if needed:
181
+
182
+ <pre>
183
+ map.resources :articles, :paged => { :as => :pagina }
184
+ map.resources :users, :paged => { :name => :group }
185
+ </pre>
186
+
187
+ Which would produce these routes:
188
+
189
+ <pre>
190
+ page_articles GET /pagina/:page_id/articles(.:format) {:controller=>"articles", :action=>"index"}
191
+ group_users GET /groups/:group_id/users(.:format) {:controller=>"users", :action=>"index"}
192
+ </pre>
193
+
194
+ (This is likely only to be useful in rare situations. One example would be paginating more than one collection in a single view.)
195
+
196
+ h2. Controller Methods
197
+
198
+ OK, so we have our pages represented in our article index route. Let's turn to the articles controller next.
199
+
200
+ I believe there is diverging practice on this, but in controllers I always prefer to load the collection and object in before filters, typically along the lines of:
201
+
202
+ <pre>
203
+ class ArticlesController < ApplicationController
204
+ before_filter :get_articles
205
+ before_filter :get_article, :only => [ :show, :edit, :update, :destroy ]
206
+ before_filter :new_article, :only => [ :new, :create ]
207
+
208
+ # actions here ...
209
+
210
+ protected
211
+
212
+ def get_articles
213
+ @articles = @user.articles.scoped(:order => "created_at DESC") # or whatever
214
+ end
215
+
216
+ def get_article
217
+ @article = @articles.find_from_param(params[:id])
218
+ end
219
+
220
+ def new_article
221
+ @article = @articles.new(params[:article])
222
+ end
223
+ end
224
+ </pre>
225
+
226
+ It's a very consistent way to write RESTful controllers. The <code>@articles</code> collection is _always_ created, which is OK, since it's just a scope or an association and no records are actually loaded. For the member actions, the collection instance is either loaded from the collection or built from it, depending on whether the action is creating a new record (new, create) or modifying an existing once (show, edit, update, destroy).
227
+
228
+ Using this pattern, paginating the collection fits naturally as another before filter once the collection is set. To this end, Paged Scopes provides a tailored <code>paginate</code> class method to do just that:
229
+
230
+ <pre>
231
+ class ArticlesController < ApplicationController
232
+ before_filter :get_articles
233
+ before_filter :get_article, :only => [ :show, :edit, :update, :destroy ]
234
+ before_filter :new_article, :only => [ :new, :create ]
235
+
236
+ paginate :articles, :per_page => 3, :path => :page_articles_path
237
+
238
+ ...
239
+
240
+ </pre>
241
+
242
+ This <code>paginate</code> method basically adds another <code>before_filter</code> which loads the current page from the collection. As arguments, it takes an optional collection name and an options hash. If omitted, the collection name is inferred from the controller name. (Hence, in the above example, we could have omitted the <code>:artices</code> arguments and <code>@articles</code> would then be inferred from the <code>ArticlesController</code> name. Hurrah for naming conventions!)
243
+
244
+ You can pass a few options to the <code>paginate</code> method:
245
+
246
+ * A <code>:per_page</code> option sets the page size on the collection if you specify it. (This option can be omitted if <code>per_page</code> has already been set on the collection.)
247
+ * A <code>:path</code> option will set the path proc for the paginator to be the controller method you specify. In the above example we've set it to a named route (<code>page_articles_path</code>), but it could equally well be a method you've defined later in the controller. (This could be useful if you want to use a polymorphic path, for example.)
248
+ * a <code>:name</code> option is available if you want to refer to your pages by a different class name (unlikely).
249
+
250
+ Any other options will be passed through to the filter definition. So you can use filter options, such as <code>:if</code>, <code>:only</code> and <code>:except</code>, just as you would for any other filter.
251
+
252
+ Aside from setting the options you specify, the main job of the <code>paginate</code> filter is to set the page as an instance variable. Controller actions will then have a <code>@page</code> variable available to be used for pagination. The page number is determined from three locations in order of priority.
253
+
254
+ # If an object of the collection is present (an <code>@article</code>, in our example), the page containing that object is loaded (unless the object is a new record).
255
+ # Failing that, the request params are examine for a <code>:page_id</code>. If present, that page number is loaded. (This fits with the paged resource routes described earlier.)
256
+ # Failing that, the first page is loaded by default.
257
+
258
+ Loading the page for a member action (show, edit, update) might not seem useful at first. Its utility becomes apparent when we're redirecting though:
259
+
260
+ <pre>
261
+ def update
262
+ if @article.save
263
+ flash[:notice] = "Success!"
264
+ redirect_to [ @page, @article ]
265
+ else
266
+ ...
267
+ end
268
+ end
269
+ </pre>
270
+
271
+ The page is used to redirect to the index at the page containing the edited object. Very polite to users! (Views can also link back to the paged index in a similar manner.)
272
+
273
+ h2. Pagination Links
274
+
275
+ The basic idea is to render a row of numbered links for a few pages either side of the one being viewed. This is referred to as the _inner window_. An _outer window_ is often also included - this shows links for the first and last few pages at the start and end of the list. Usually, _next page_ and _previous page_ links are also sandwiched around the numbered links.
276
+
277
+ The "will_paginate rdoc":http://gitrdoc.com/mislav/will_paginate/tree/master/ has some good links to articles on pagination UI design:
278
+
279
+ * a "Yahoo Design Pattern Library article":http://developer.yahoo.com/ypatterns/parent.php?pattern=pagination describing two styles of pagination;
280
+ * a "Smashing Magazine article":http://www.smashingmagazine.com/2007/11/16/pagination-gallery-examples-and-good-practices/ with good practices and examples; and
281
+ * "another article":http://kurafire.net/log/archive/2007/06/22/pagination-101 with heaps of examples, both good and bad.
282
+
283
+ In the "will_paginate":http://wiki.github.com/mislav/will_paginate gem, the eponymous <code>will_paginate</code> view helper is provided to render these links in your view. It seems to work well, but one look at the method's options gives you an idea what you'll be up for if you want to customize the HTML structure of your pagination links. Want to render your pagination links as a list? You'll have to write your own <code>LinkRenderer</code> subclass. (Have fun with that.)
284
+
285
+ There has to be a better way. There is of course, and it comes from a less-is-more approach.
286
+
287
+ h2. Using the Window Helper
288
+
289
+ With the PagedScopes gem, each page has an associated <code>paginator</code> which provides some simple methods for generating page links. First, we need to call <code>set_path</code> to tell the paginator how to generate links for a pages:
290
+
291
+ <pre>
292
+ @page.paginator.set_path { |page| page_articles_path(page) }
293
+ </pre>
294
+
295
+ The block we supply will be used by the paginator to generate a paged URL whenever one is needed.
296
+
297
+ (Note that the controller <code>paginate</code> method I presented in the last article can also be used to set the path proc by using the <code>:path</code> option.)
298
+
299
+ Next, we use the <code>window</code> method to render the page links. We supply a block which the paginator will call for each page in the window, allowing us to render the link exactly as we want tp. Let's render that list we were talking about:
300
+
301
+ <pre>
302
+ <ul>
303
+ <% @page.paginator.window(:inner => 2, :outer => 1) do |page, path, classes| %>
304
+ <% content_tag_for :li, page, :class => classes.join(" ") do %>
305
+ <%= link_to_if path, page.number, path %>
306
+ <% end %>
307
+ <% end %>
308
+ </ul>
309
+ </pre>
310
+
311
+ Here we've specified an inner window of size 2 (meaning we want links for two pages either side of the current page) and an outer window of size 1 (meaning we want links for just the first and last pages).
312
+
313
+ The <code>window</code> helper passes a succession of pages to our block for us to render. The block arguments are:
314
+
315
+ # The page itself, from which we can get the page number.
316
+ # The path for the page, produced using the <code>set_path</code> proc we've already specified. If the page is the current page, then nil is passed as the path - this is because we shouldn't render a link for the current page. (Hence our use of <code>link_to_if</code>.)
317
+ # An optional array of classes describing the link. Possible values for the classes are <code>:selected</code> if the page is the current page, <code>:gap_before</code> if there's a gap in the numbering before the page, and <code>:gap_after</code> if there's a gap after. You can use these as you see fit, but they're intended to be passed through to your link container as classes for styling. (We've done this above with the <code>:class => classes.join(" ")</code> option.)
318
+
319
+ Within the block, the page link can be rendered as we please. In our example we're putting it inside an <code><li></code> element. For page 7, the <code>window</code> function would produce the following markup:
320
+
321
+ <pre>
322
+ <ul>
323
+ <li class="page gap_after" id="page_1">
324
+ <span><a href="/pages/1/articles">1</a></span>
325
+ </li>
326
+ <li class="page gap_before" id="page_5">
327
+ <span><a href="/pages/5/articles">5</a></span>
328
+ </li>
329
+ <li class="page" id="page_6">
330
+ <span><a href="/pages/6/articles">6</a></span>
331
+ </li>
332
+ <li class="page selected" id="page_7">
333
+ <span>7</span>
334
+ </li>
335
+ <li class="page" id="page_8">
336
+ <span><a href="/pages/8/articles">8</a></span>
337
+ </li>
338
+ <li class="page gap_after" id="page_9">
339
+ <span><a href="/pages/9/articles">9</a></span>
340
+ </li>
341
+ <li class="page gap_before" id="page_12">
342
+ <span><a href="/pages/12/articles">12</a></span>
343
+ </li>
344
+ </ul>
345
+ </pre>
346
+
347
+ h2. Styling the Output
348
+
349
+ Add some styling, using our classes to distinguish the currently selected pages and to add a separator where there are numbering gaps:
350
+
351
+ <pre>
352
+ li.page { display: inline }
353
+ li.page a { text-decoration: none }
354
+ li.page span {
355
+ border: 1px solid gray;
356
+ padding: 0.2em 0.5em }
357
+ li.page.selected span, li.page span:hover {
358
+ background: gray;
359
+ color: white }
360
+ li.page.gap_before:before { content: "..." }
361
+ </pre>
362
+
363
+ The result: a nice-looking set of page links.
364
+
365
+ [Refer to the original article at "code.matthewhollingworth.net":http://code.matthewhollingworth.net/articles/11 for correctly rendered examples!]
366
+
367
+ <notextile>
368
+ <ul class="pgex">
369
+ <li class="page gap_after" id="page_1">
370
+ <span><a href="#">1</a></span>
371
+ </li>
372
+ <li class="page gap_before" id="page_5">
373
+ <span><a href="#">5</a></span>
374
+ </li>
375
+ <li class="page" id="page_6">
376
+ <span><a href="#">6</a></span>
377
+ </li>
378
+ <li class="page selected" id="page_7">
379
+ <span>7</span>
380
+ </li>
381
+ <li class="page" id="page_8">
382
+ <span><a href="#">8</a></span>
383
+ </li>
384
+ <li class="page gap_after" id="page_9">
385
+ <span><a href="#">9</a></span>
386
+ </li>
387
+ <li class="page gap_before" id="page_12">
388
+ <span><a href="#">12</a></span>
389
+ </li>
390
+ </ul>
391
+ </notextile>
392
+
393
+ Too easy!
394
+
395
+ h2. Adding Extra Controls
396
+
397
+ How do we get add _previous_ and _next_ links? This is pretty easy, too - just specify the <code>:extras</code> we want as an option. (Choose from <code>:first</code>, <code>:previous</code>, <code>:next</code> and <code>:last</code>.) Those symbols will be passed to our block as the page when they need to be rendered.
398
+
399
+ We'll move our pagination links to a helper for clarity:
400
+
401
+ <pre>
402
+ module ArticlesHelper
403
+ MARKER = { :previous => "&lt; newer", :next => "older &gt;" }
404
+ def article_page_links
405
+ @page.paginator.window(:inner => 2, :outer => 1, :extras => [ :previous, :next ]) do |page, path, classes|
406
+ content_tag :li, :class => (classes << :page).join(" ") do
407
+ content_tag :li, link_to_if(path, MARKER[page] || page.number, path)
408
+ end
409
+ end
410
+ end
411
+ end
412
+ </pre>
413
+
414
+ Which renders as follows (for page 4 this time):
415
+
416
+ [Refer to the original article at "code.matthewhollingworth.net":http://code.matthewhollingworth.net/articles/11 for correctly rendered examples!]
417
+
418
+ <notextile>
419
+ <ul class="pgex">
420
+ <li class="page">
421
+ <span><a href="#">&lt; newer</a></span>
422
+ </li>
423
+ <li class="page">
424
+ <span><a href="#">1</a></span>
425
+ </li>
426
+ <li class="page">
427
+ <span><a href="#">2</a></span>
428
+ </li>
429
+ <li class="page">
430
+ <span><a href="#">3</a></span>
431
+ </li>
432
+ <li class="page selected">
433
+ <span>4</span>
434
+ </li>
435
+ <li class="page">
436
+ <span><a href="#">5</a></span>
437
+ </li>
438
+ <li class="page gap_after">
439
+ <span><a href="#">6</a></span>
440
+ </li>
441
+ <li class="page gap_before">
442
+ <span><a href="#">12</a></span>
443
+ </li>
444
+ <li class="page">
445
+ <span><a href="#">older &gt;</a></span>
446
+ </li>
447
+ </ul>
448
+ </notextile>
449
+
450
+ Just what we want!
451
+
452
+ Links for the <code>:first</code> and <code>:last</code> pages can also be specified as extras; these will appear outside the _previous_ and _next_ links. (If you use these extras, you'll want to omit the <code>:outer</code> window option.)
453
+
454
+ h2. Get It!
455
+
456
+ You can install the PagedScopes gem as follows:
457
+
458
+ <pre>
459
+ gem sources -a http://gems.github.com # just once
460
+ sudo gem install mholling-paged_scopes
461
+ </pre>
462
+
463
+ And in your <code>config/environment.rb</code>, if you're on Rails:
464
+
465
+ <pre>
466
+ config.gem "mholling-paged_scopes", :lib => "paged_scopes", :source => "http://gems.github.com"
467
+ </pre>
468
+
469
+ Peruse the code at "GitHub":http://github.com/mholling/paged_scopes.
470
+
471
+ Copyright (c) 2009 Matthew Hollingworth. See LICENSE for details.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 5
2
+ :patch: 0
3
3
  :major: 0
4
- :minor: 0
4
+ :minor: 1
@@ -1,17 +1,29 @@
1
1
  module PagedScopes
2
2
  module Controller
3
3
  module ClassMethods
4
- def get_page_for(collection_name, options = {})
5
- callback_method = "get_page_for_#{collection_name}"
4
+ def paginate(*args)
5
+ options = args.extract_options!
6
+ raise ArgumentError, "can't paginate multiple collections with one call" if args.many?
7
+ collection_name = args.first || default_collection_name
8
+ callback_method = "paginate_#{collection_name}"
6
9
  define_method callback_method do
7
10
  collection = instance_variable_get("@#{collection_name.to_s.pluralize}")
8
11
  raise RuntimeError, "no @#{collection_name.to_s.pluralize} collection was set" unless collection
9
12
  object = instance_variable_get("@#{collection_name.to_s.singularize}")
10
- instance_variable_set("@#{collection.pages.name.underscore}", page_for(collection, object, options))
13
+ page = page_for(collection, object, options)
14
+ instance_variable_set("@#{collection.pages.name.underscore}", page)
11
15
  end
12
16
  protected callback_method
13
17
  before_filter callback_method, options.except(:per_page, :name, :path)
14
18
  end
19
+
20
+ private
21
+
22
+ def default_collection_name
23
+ collection_name = name
24
+ raise RuntimeError, "couldn't find controller name" unless collection_name.slice!(/Controller$/)
25
+ collection_name.demodulize.tableize
26
+ end
15
27
  end
16
28
 
17
29
  def self.included(base)
@@ -19,6 +31,8 @@ module PagedScopes
19
31
  base.rescue_responses.update('PagedScopes::PageNotFound' => :not_found)
20
32
  end
21
33
 
34
+ private
35
+
22
36
  def page_for(collection, *args, &block)
23
37
  options = args.extract_options!
24
38
  collection.per_page = options[:per_page] if options[:per_page]
@@ -142,7 +142,7 @@ module PagedScopes
142
142
  end
143
143
 
144
144
  def full?
145
- !last? || page_scope.all.length == per_page
145
+ !last? || page_scope.all.length == per_page # TODO: could improve this to calculate mathematically
146
146
  end
147
147
 
148
148
  def to_param
@@ -6,6 +6,22 @@ describe "Controller" do
6
6
  it "should raise 404 on PagedScopes::PageNotFound" do
7
7
  Class.new(ActionController::Base).rescue_responses['PagedScopes::PageNotFound'].should == :not_found
8
8
  end
9
+
10
+ it "should get the default collection name from the controller name" do
11
+ [ "ArticlesController", "ArticleController", "Admin::ArticlesController" ].each do |controller_name|
12
+ @class = Class.new(ActionController::Base)
13
+ @class.stub!(:name).and_return(controller_name)
14
+ @class.send(:default_collection_name).should == "articles"
15
+ end
16
+ end
17
+
18
+ it "should not get the default collection name from a misnamed controller" do
19
+ [ "", "SomeClassName" ].each do |controller_name|
20
+ @class = Class.new(ActionController::Base)
21
+ @class.stub!(:name).and_return(controller_name)
22
+ lambda { @class.send(:default_collection_name) }.should raise_error(RuntimeError)
23
+ end
24
+ end
9
25
  end
10
26
 
11
27
  context "instance using #page_for" do
@@ -34,8 +50,8 @@ describe "Controller" do
34
50
  it "should find the page containing an object if specified" do
35
51
  in_controller @controller do
36
52
  @articles.each do |article|
37
- # page_for(@articles, article).should == @articles.pages.find_by_article(article)
38
- page_for(@articles, article).articles.all.should include(article)
53
+ page_for(@articles, article).should == @articles.pages.find_by_article(article)
54
+ # page_for(@articles, article).articles.all.should include(article)
39
55
  end
40
56
  end
41
57
  end
@@ -70,47 +86,95 @@ describe "Controller" do
70
86
  end
71
87
  end
72
88
 
73
- context "class using #get_page_for" do
89
+ context "class using #paginate" do
74
90
  before(:each) do
75
91
  @class = Class.new(ActionController::Base)
76
92
  end
77
93
 
78
- it "should add a protected get_page_for callback as a before filter when get_page_for is called" do
79
- @class.get_page_for :articles
80
- @class.before_filters.map(&:to_s).should include("get_page_for_articles")
81
- @class.protected_instance_methods.map(&:to_s).should include("get_page_for_articles")
94
+ it "should add a protected paginate callback as a before filter when paginate is called" do
95
+ @class.paginate :articles
96
+ @class.before_filters.map(&:to_s).should include("paginate_articles")
97
+ @class.protected_instance_methods.map(&:to_s).should include("paginate_articles")
98
+ end
99
+
100
+ it "should use the default collection name if no collection name is specified" do
101
+ @class.stub!(:name).and_return("ArticlesController")
102
+ @class.paginate
103
+ @class.before_filters.map(&:to_s).should include("paginate_articles")
82
104
  end
83
105
 
84
106
  it "should pass filter options except for :per_page, :name and :path on to the before filter" do
85
107
  @options = { :per_page => 3, :name => "Group", :path => :page_articles_path, :only => [ :index, :show ], :if => :test }
86
- @class.get_page_for :articles, @options
87
- @filter = @class.filter_chain.detect { |filter| filter.method.to_s == "get_page_for_articles" }
108
+ @class.paginate :articles, @options
109
+ @filter = @class.filter_chain.detect { |filter| filter.method.to_s == "paginate_articles" }
88
110
  @filter.options.keys.should_not include(:per_page, :name, :path)
89
111
  @filter.options.keys.should include(:only, :if)
90
112
  end
91
113
  end
92
114
 
93
- context "instance using get_page_for in the controller class" do
115
+ context "instance using paginate with options in the controller class" do
94
116
  before(:each) do
117
+ @options = { :per_page => 3, :name => "Group", :path => :page_articles_path, :only => [ :index, :show ], :if => :test }
95
118
  @class = Class.new(ActionController::Base)
96
- @class.get_page_for :articles, :per_page => 3
119
+ @class.paginate :articles, @options
120
+ @controller = @class.new
121
+ end
122
+
123
+ it "should set :per_page on the collection" do
124
+ in_controller @controller do
125
+ @articles = User.first.articles
126
+ paginate_articles
127
+ @articles.per_page.should == @options[:per_page]
128
+ end
129
+ end
130
+
131
+ it "should set :page_name on the collection" do
132
+ in_controller @controller do
133
+ @articles = User.first.articles
134
+ paginate_articles
135
+ @articles.page_name.should == @options[:name]
136
+ end
137
+ end
138
+
139
+ it "should set a page instance variable named accordingly" do
140
+ in_controller @controller do
141
+ @articles = User.first.articles
142
+ paginate_articles
143
+ @group.should_not be_nil
144
+ end
145
+ end
146
+
147
+ it "should set :path on the page's paginator" do
148
+ in_controller @controller do
149
+ @articles = User.first.articles
150
+ paginate_articles
151
+ self.should_receive(@options[:path])
152
+ @group.paginator.first
153
+ end
154
+ end
155
+ end
156
+
157
+ context "instance using #paginate in the controller class" do
158
+ before(:each) do
159
+ @class = Class.new(ActionController::Base)
160
+ @class.paginate :articles, :per_page => 3
97
161
  @controller = @class.new
98
162
  end
99
163
 
100
164
  it "should raise an error if no collection is set" do
101
165
  in_controller @controller do
102
- lambda { get_page_for_articles }.should raise_error(RuntimeError)
166
+ lambda { paginate_articles }.should raise_error(RuntimeError)
103
167
  end
104
168
  end
105
169
 
106
170
  it "should pass :per_page, :name and :path options on to the call to #page_for" do
107
171
  @options = { :per_page => 3, :name => "Group", :path => :page_articles_path }
108
- @class.get_page_for :articles, @options
172
+ @class.paginate :articles, @options
109
173
  @controller = @class.new
110
174
  in_controller @controller do
111
175
  @articles = User.first.articles
112
176
  self.should_receive(:page_for).with(@articles, nil, @options)
113
- get_page_for_articles
177
+ paginate_articles
114
178
  end
115
179
  end
116
180
 
@@ -125,8 +189,8 @@ describe "Controller" do
125
189
  in_controller @controller do
126
190
  @articles.each do |article|
127
191
  @article = article
128
- get_page_for_articles
129
- @page.articles.should include(@article)
192
+ paginate_articles
193
+ @page.articles.all.should include(@article)
130
194
  end
131
195
  end
132
196
  end
@@ -137,7 +201,7 @@ describe "Controller" do
137
201
  stub!(:params).and_return(:page_id => @articles.pages.last.id)
138
202
  [ nil, Article.new ].each do |article|
139
203
  @article = article
140
- get_page_for_articles
204
+ paginate_articles
141
205
  @page.should == @articles.pages.last
142
206
  end
143
207
  end
@@ -147,179 +211,24 @@ describe "Controller" do
147
211
  in_controller @controller do
148
212
  @articles.per_page = 3
149
213
  stub!(:params).and_return(:page_id => @articles.pages.last.id + 1)
150
- lambda { get_page_for_articles }.should raise_error(PagedScopes::PageNotFound)
214
+ lambda { paginate_articles }.should raise_error(PagedScopes::PageNotFound)
151
215
  end
152
216
  end
153
217
 
154
218
  it "should get the first page if the current object is a new record" do
155
219
  in_controller @controller do
156
220
  @article = @articles.new
157
- get_page_for_articles
221
+ paginate_articles
158
222
  @page.should == @articles.pages.first
159
223
  end
160
224
  end
161
225
 
162
226
  it "should otherwise default to the first page" do
163
227
  in_controller @controller do
164
- get_page_for_articles
228
+ paginate_articles
165
229
  @page.should == @articles.pages.first
166
230
  end
167
231
  end
168
232
  end
169
233
  end
170
234
  end
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
- # require 'spec_helper'
192
- #
193
- # describe "Controller" do
194
- #
195
- # context "class" do
196
- # before(:each) do
197
- # @class = Class.new(ActionController::Base)
198
- # end
199
- #
200
- # it "should raise 404 on PagedScopes::PageNotFound" do
201
- # @class.rescue_responses['PagedScopes::PageNotFound'].should == :not_found
202
- # end
203
- #
204
- # it "should add a protected get_page_for callback as a before filter when get_page_for is called" do
205
- # @class.get_page_for :articles
206
- # @class.before_filters.map(&:to_s).should include("get_page_for_articles")
207
- # @class.protected_instance_methods.map(&:to_s).should include("get_page_for_articles")
208
- # end
209
- #
210
- # it "should pass filter options except for :per_page, :name and :path on to the before filter" do
211
- # @options = { :per_page => 3, :name => "Group", :path => :page_articles_path, :only => [ :index, :show ], :if => :test }
212
- # @class.get_page_for :articles, @options
213
- # @filter = @class.filter_chain.detect { |filter| filter.method.to_s == "get_page_for_articles" }
214
- # @filter.options.keys.should_not include(:per_page, :name, :path)
215
- # @filter.options.keys.should include(:only, :if)
216
- # end
217
- #
218
- # end
219
- #
220
- # context "instance" do
221
- # before(:each) do
222
- # @controller = Class.new(ActionController::Base) do
223
- # get_page_for :articles
224
- # end.new
225
- # end
226
- #
227
- # it "should raise an error if no collection is set" do
228
- # in_controller @controller do
229
- # lambda { get_page_for_articles }.should raise_error(RuntimeError)
230
- # end
231
- # end
232
- #
233
- # context "when the collection is set" do
234
- # before(:each) do
235
- # in_controller @controller do
236
- # @articles = User.first.articles
237
- # @articles.per_page = 3
238
- # end
239
- # end
240
- #
241
- # it "should get the page from a page id in the params" do
242
- # in_controller @controller do
243
- # stub!(:params).and_return(:page_id => @articles.pages.last.id)
244
- # get_page_for_articles
245
- # @page.should == @articles.pages.last
246
- # end
247
- # end
248
- #
249
- # it "should raise PageNotFound if the page id in the params is not in range" do
250
- # in_controller @controller do
251
- # stub!(:params).and_return(:page_id => @articles.pages.last.id + 1)
252
- # lambda { get_page_for_articles }.should raise_error(PagedScopes::PageNotFound)
253
- # end
254
- # end
255
- #
256
- # it "should otherwise get the page from the current object if no page id is present in the params" do
257
- # in_controller @controller do
258
- # @article = @articles.last
259
- # get_page_for_articles
260
- # @page.should == @articles.pages.find_by_article(@article)
261
- # @page.articles.should include(@article)
262
- # end
263
- # end
264
- #
265
- # it "should get the first page if the current object is a new record" do
266
- # in_controller @controller do
267
- # @article = @articles.new
268
- # get_page_for_articles
269
- # @page.should == @articles.pages.first
270
- # end
271
- # end
272
- #
273
- # it "should otherwise get the first page" do
274
- # in_controller @controller do
275
- # get_page_for_articles
276
- # @page.should == @articles.pages.first
277
- # end
278
- # end
279
- # end
280
- # end
281
- #
282
- # context "instance when :per_page is specified in the call to #get_page_for" do
283
- # it "should set per_page on the collection" do
284
- # @controller = Class.new(ActionController::Base) do
285
- # get_page_for :articles, :per_page => 3
286
- # end.new
287
- # in_controller @controller do
288
- # @articles = User.first.articles
289
- # @articles.per_page = nil
290
- # get_page_for_articles
291
- # @articles.per_page.should == 3
292
- # @articles.pages.per_page.should == 3
293
- # end
294
- # end
295
- # end
296
- #
297
- # context "instance when :name is specified in the call to #get_page_for" do
298
- # it "should set page_name on the collection" do
299
- # @controller = Class.new(ActionController::Base) do
300
- # get_page_for :articles, :per_page => 3, :name => "Group"
301
- # end.new
302
- # in_controller @controller do
303
- # @articles = User.first.articles
304
- # get_page_for_articles
305
- # @articles.page_name.should == "Group"
306
- # @articles.pages.name.should == "Group"
307
- # end
308
- # end
309
- # end
310
- #
311
- # context "instance when :path is specified in the call to #get_page_for" do
312
- # it "should set page's pagination path to the specified controller method" do
313
- # @controller = Class.new(ActionController::Base) do
314
- # get_page_for :articles, :per_page => 3, :path => :page_articles_path
315
- # end.new
316
- # in_controller @controller do
317
- # @articles = User.first.articles
318
- # @article = @articles.first
319
- # get_page_for_articles
320
- # self.should_receive(:page_articles_path).with(@page.next)
321
- # @page.paginator.next
322
- # end
323
- # end
324
- # end
325
- # end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mholling-paged_scopes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Hollingworth
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-26 00:00:00 -07:00
12
+ date: 2009-06-28 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -30,10 +30,10 @@ extensions: []
30
30
 
31
31
  extra_rdoc_files:
32
32
  - LICENSE
33
- - README.rdoc
33
+ - README.textile
34
34
  files:
35
35
  - LICENSE
36
- - README.rdoc
36
+ - README.textile
37
37
  - Rakefile
38
38
  - VERSION.yml
39
39
  - lib/paged_scopes.rb
data/README.rdoc DELETED
@@ -1,7 +0,0 @@
1
- = paged_scopes
2
-
3
- Description goes here.
4
-
5
- == Copyright
6
-
7
- Copyright (c) 2009 Matthew Hollingworth. See LICENSE for details.