File Coverage

blib/lib/CatalystX/Widget/Paginator.pm
Criterion Covered Total %
statement 62 62 100.0
branch 23 24 95.8
condition 26 33 78.7
subroutine 15 15 100.0
pod 2 3 66.6
total 128 137 93.4


line stmt bran cond sub pod time code
1             package CatalystX::Widget::Paginator;
2              
3             =head1 NAME
4              
5             CatalystX::Widget::Paginator - HTML widget for digg-style paginated DBIx::ResulSet
6              
7             =head1 VERSION
8              
9             Version 0.06
10              
11             =cut
12              
13             our $VERSION = '0.06';
14              
15 20     20   487370 use List::Util qw( min max );
  20         53  
  20         1870  
16 20     20   1790 use Moose;
  20         873432  
  20         163  
17 20     20   134722 use Moose::Util::TypeConstraints;
  20         44  
  20         226  
18 20     20   55736 use POSIX qw( ceil floor );
  20         103232  
  20         114  
19              
20             extends 'Catalyst::Plugin::Widget::Base';
21             with 'Catalyst::Plugin::Widget::WithResultSet';
22              
23              
24             =head1 DESCRIPTION
25              
26             This widget intended to solve the general problem with paginated results.
27             Assume that we have a set of objects (L<DBIx::Class::ResultSet>) and (probably)
28             the L<Catalyst::Request> parameter indicates the current page. Created widget
29             receives resultset and additional arguments, validates pagination and can be
30             queried about pagination and objects presented for current page.
31              
32             For the correct determination of the current page widget makes taking
33             the following steps:
34              
35             1. Checks for constructor arguments: C<page>, C<rows>. If specified, uses them.
36              
37             2. Checks for already paginated resultset (see L<DBIx::Class::ResultSet>
38             C<rows> and C<page> attributes for details). If specified - uses them.
39              
40             3. Uses the default value for C<rows> (10).
41              
42             4. If attribute C<page_auto> is enabled (default), try to get request parameter
43             named C<page_arg> for C<page> value.
44              
45             5. Uses the default value for C<page> (1).
46              
47             After successful identification C<page> and C<rows> attributes, the widget
48             checks their validity for a specified resultset. Processing logic for non-valid
49             attributes defined by C<invalid> attribute.
50              
51             Created instance of a widget can be queried about its attributes.
52             For example: C<last>, C<pages>, C<objects>, etc.
53              
54             Widget is converted to a string represetning the HTML table with page numbers
55             as links in the cells. Design details can be configured with C<style> and
56             C<style_prefix> attributes.
57              
58              
59             =head1 SYNOPSIS
60              
61             Typical usage pattern in the controller:
62              
63             sub index :Path :Args(0) {
64             my ( $self,$c ) = @_;
65              
66             my $pg = $c->widget( 'Paginator', rs => 'Schema::User' );
67              
68             my $current = $pg->page; # current page no
69             my $first = $pg->first; # first page no (1)
70             my $last = $pg->last; # last page no
71             my $pages = $pg->total; # total pages ($last - $first + 1)
72             my $total = $pg->total; # total objects (overall pages)
73             my $objects = $pg->objects; # objects for current page
74              
75             $c->res->body( "$pg" ); # render to nice HTML table
76             }
77              
78              
79             With L<DBIx::Class::ResultSet> instance:
80              
81             my $pg = $c->widget( 'Paginator',
82             rs => $c->model('Schema::User'),
83             rows => 3, page => 15
84             );
85              
86              
87             With paginated L<DBIx::Class::ResultSet> instance:
88              
89             my $pg = $c->widget( 'Paginator',
90             rs => $c->model('Schema::User')->search_rs( undef, { rows => 3, page => 15 )
91             );
92              
93              
94             Handling invalid page:
95              
96             use Try::Tiny;
97              
98             my $pg = try {
99             $c->widget( 'Paginator',
100             rs => 'Schema::User',
101             invalid => 'raise'
102             )
103             } except {
104             $c->detach('/error404') if /PAGE_OUT_OF_RANGE/;
105             die $_;
106             };
107              
108              
109             The same effect:
110              
111             my $pg = $c->widget( 'Paginator',
112             rs => 'Schema::User',
113             invalid => sub { $c->detach('/error404' )
114             };
115              
116             Subclassing in your application:
117              
118             package YourApp::Widget::SimplePager;
119             use Moose;
120             extends 'CatalystX::Widget::Paginator';
121            
122             has '+edges' => ( is => 'ro', default => undef );
123             has '+invalid' => ( is => 'ro', default => 'last' );
124             has '+page_arg' => ( is => 'ro', default => 'page' );
125             has '+prefix' => ( is => 'ro', default => undef );
126             has '+side' => ( is => 'ro', default => 0 );
127             has '+suffix' => ( is => 'ro', default => undef );
128            
129             __PACKAGE__->meta->make_immutable;
130             1;
131              
132             Usage subclassed widget in the controller:
133              
134             $c->widget( '~SimplePager', rs => 'Schema::User' );
135              
136             =head1 RENDERING
137              
138             Widget renders (string representated) as HTML table with single row and
139             multiple columns:
140              
141             prefix | edge | side | delim | main | delim | side | edge | suffix
142             ----------------------------------------------------------------------
143             Pages: << 1 2 ... 7 >8< 9 ... 40 41 >> Total:x
144             ----------------------------------------------------------------------
145              
146             Table has HTML class attribute with a C<style> value. Cells HTML
147             class attribute consists from C<style_prefix> and block name, where
148             the names of the blocks the same as in example above. Current page framed
149             with HTML span tag, others with links.
150              
151             =cut
152              
153              
154             # constructor
155             sub BUILD {
156 39     39 0 54089 my ( $self,$args ) = @_;
157              
158             # is page number valid?
159 39 100       1556 &{ $self->invalid } if $self->page > $self->last;
  6         240  
160             }
161              
162             #
163             # types (used internally)
164             #
165             subtype __PACKAGE__ . '::Edges'
166             => as 'ArrayRef',
167             => where { $#$_==1 }
168             ;
169             subtype __PACKAGE__ . '::Format'
170             => as 'CodeRef'
171             ;
172             coerce __PACKAGE__ . '::Format'
173             => from 'Str',
174             => via { my $x = $_; sub { sprintf $x,@_ } }
175             ;
176             subtype __PACKAGE__ . '::Invalid'
177             => as 'CodeRef'
178             ;
179             coerce __PACKAGE__ . '::Invalid'
180             => from 'Str',
181             => via {
182             return sub { my $self=shift; $self->_set_page( $self->first ) }
183             if $_ eq 'first';
184             return sub { my $self=shift; $self->_set_page( $self->last ) }
185             if $_ eq 'last';
186             return sub { die 'PAGE_OUT_OF_RANGE' }
187             if $_ eq 'raise';
188             die 'invalid value for "invalid" attribute';
189             }
190             ;
191             subtype __PACKAGE__ . '::NaturalInt'
192             => as 'Int',
193             => where { $_ >= 0 }
194             ;
195             subtype __PACKAGE__ . '::PositiveInt'
196             => as 'Int',
197             => where { $_ > 0 }
198             ;
199             coerce __PACKAGE__ . '::PositiveInt'
200             => from 'Defined',
201             => via { /^(\d+)$/ ? $1 : 1 }
202             ;
203             subtype __PACKAGE__ . '::ResultSet'
204             => as 'Object',
205             => where { $_->isa('DBIx::Class::ResultSet') }
206             ;
207             subtype __PACKAGE__ . '::Text'
208             => as 'CodeRef'
209             ;
210             coerce __PACKAGE__ . '::Text'
211             => from 'Str',
212             => via { my $x = $_; sub { $x } }
213             ;
214              
215              
216             =head1 CONSTRUCTOR
217              
218             =head2 new( rs => $name|$instance, %options )
219              
220             =head3 rs
221              
222             L<DBIx::Class::ResultSet> name or instance
223              
224             =head3 options
225              
226             =head4 delim
227              
228             Delimeter string or C<undef> (default: '...'). See L</RENDERING> for details.
229              
230             =cut
231              
232             has delim => ( is => 'ro', isa => 'Str | Undef', default => '...' );
233              
234              
235             =head4 edges
236              
237             Two element array of strings for left and right edges respectively or C<undef>
238             (default: ['<<','>>']). See L</RENDERING> for details.
239              
240             =cut
241              
242             has edges => ( is => 'ro', isa => __PACKAGE__ . '::Edges | Undef', default => sub{ ['<<','>>'] } );
243              
244              
245             =head4 invalid
246              
247             Determines the constructor behavior in the case of an invalid page.
248             Could be arbitrary code block or one of predefined words:
249              
250             =over
251              
252             =item first
253              
254             Force set C<page> to C<first> (default).
255              
256             =item last
257              
258             Force set C<page> to C<last>.
259              
260             =item raise
261              
262             Raise exception C<PAGE_OUT_OF_RANGE>.
263              
264             =back
265              
266             =cut
267              
268             has invalid => ( is => 'ro', isa => __PACKAGE__ . '::Invalid', coerce => 1, default => 'first' );
269              
270              
271             =head4 link
272              
273             Code reference for build link. Receives page number as argument and returns target URI.
274              
275             =cut
276              
277             has link => ( is => 'ro', isa => 'CodeRef', lazy => 1, builder => '_link' );
278              
279             sub _link {
280 36     36   73 my ( $self ) = @_;
281              
282 36         1172 my $c = $self->context;
283              
284             sub {
285 432         25853 $c->uri_for( $c->action, $c->req->captures, @{ $c->req->args },
286 432     432   10295 { %{ $c->req->params }, $self->page_arg => shift } );
  432         24956  
287             }
288 36         1631 }
289              
290              
291             =head4 main
292              
293             Size of 'main' pages group (default: 10). See L</RENDERING> for details.
294              
295             =cut
296              
297             has main => ( is => 'ro', isa => __PACKAGE__ . '::PositiveInt', default => 10 );
298              
299              
300             =head4 page
301              
302             Current page number.
303              
304             =cut
305              
306             has page => ( is => 'ro', isa => __PACKAGE__ . '::PositiveInt', coerce => 1, lazy => 1, builder => '_page', writer => '_set_page' );
307              
308             sub _page {
309 30     30   65 my ( $self ) = @_;
310              
311 30         1094 my $p = $self->resultset->{ attrs }{ page };
312              
313 30 100 100     42485 $p ||= $self->context->req->param( $self->page_arg )
314             if $self->page_auto;
315            
316 30 100       3473 $p || 1;
317             }
318              
319              
320             =head4 page_arg
321              
322             Name of query string parameter for page number extracting (default: 'p').
323              
324             =cut
325              
326             has page_arg => ( is => 'ro', isa => 'Str', default => 'p' );
327              
328              
329             =head4 page_auto
330              
331             Try or not to extract C<page_arg> from L<Catalyst::Request> automatically
332             (default: 1).
333              
334             =cut
335              
336             has page_auto => ( is => 'ro', isa => 'Bool', default => 1 );
337              
338              
339             =head4 prefix
340              
341             First cell content (default: 'Pages'). See L</RENDERING> for details.
342              
343             =cut
344              
345             has prefix => ( is => 'ro', isa => __PACKAGE__ . '::Text | Undef', coerce => 1, default => 'Pages:' );
346              
347              
348             =head4 rows
349              
350             Number of objects per page (default: 10).
351              
352             =cut
353              
354             has rows => ( is => 'ro', isa => __PACKAGE__ . '::PositiveInt', lazy => 1, builder => '_rows' );
355              
356             sub _rows {
357 37 100   37   1374 shift->resultset->{ attrs }{ rows } || 10;
358             }
359              
360              
361             =head4 side
362              
363             Size of 'side' pages groups (default: 2). See L</RENDERING> for details.
364              
365             =cut
366              
367             has side => ( is => 'ro', isa => __PACKAGE__ . '::NaturalInt', default => 2 );
368              
369              
370             =head4 style
371              
372             CSS class name for table tag (default: 'pages'). See L</RENDERING> for details.
373              
374             =cut
375              
376             has style => ( is => 'rw', isa => 'Str', default => 'pages' );
377              
378              
379             =head4 style_prefix
380              
381             CSS class name prefix for table cells (default: 'p_'). See L</RENDERING> for details.
382              
383             =cut
384              
385             has style_prefix => ( is => 'rw', isa => 'Str', default => 'p_' );
386              
387              
388             =head4 suffix
389              
390             Last cell content (default: 'Total: x'). See L</RENDERING> for details.
391              
392             =cut
393              
394             has suffix => ( is => 'ro', isa => __PACKAGE__ . '::Text | Undef', coerce => 1, default => sub { sub { 'Total: ' . shift->total } } );
395              
396              
397             =head4 text
398              
399             Code reference for page number formatting. Receives page number as argument and
400             returns string. Also can be just a sprintf format string (default: '%s').
401             See L</RENDERING> for details.
402              
403             =cut
404              
405             has text => ( is => 'ro', isa => __PACKAGE__ . '::Format', coerce => 1, default => '%s' );
406              
407              
408              
409             =head1 ATTRIBUTES
410              
411             =head2 first
412              
413             First page number.
414              
415             =cut
416              
417             has first => ( is => 'ro', isa => __PACKAGE__ . '::PositiveInt', init_arg => undef, default => 1 );
418              
419              
420             =head2 last
421              
422             Last page number.
423              
424             =cut
425              
426             has last => ( is => 'ro', isa => __PACKAGE__ . '::PositiveInt', init_arg => undef, lazy => 1, builder => '_last' );
427              
428             sub _last {
429 39     39   80 my ( $self ) = @_;
430            
431 39         1483 ceil $self->total / $self->rows;
432             }
433              
434              
435             =head2 objects
436              
437             Paged L<DBIx::Class::ResulSet> instance.
438              
439             =cut
440              
441             has objects => ( is => 'ro', isa => __PACKAGE__ . '::ResultSet', lazy => 1, builder => '_objects' );
442              
443             sub _objects {
444 1     1   6 my ( $self ) = @_;
445              
446 1         33 $self->resultset->search( undef, { page => $self->page, rows => $self->rows } );
447             }
448              
449              
450             =head2 pages
451              
452             Total number of pages.
453              
454             =cut
455              
456             has pages => ( is => 'ro', isa => __PACKAGE__ . ':: PositiveInt', init_arg => undef, lazy => 1, builder => '_pages' );
457              
458             sub _pages {
459 37     37   87 my ( $self ) = @_;
460              
461 37         1354 $self->last - $self->first;
462             }
463              
464              
465             =head2 total
466              
467             Total objects count (overall pages).
468              
469             =cut
470              
471             has total => ( is => 'ro', isa => 'Int', lazy => 1, builder => '_total' );
472              
473             sub _total {
474 39     39   1390 shift->resultset->search( undef, { map { $_ => undef } qw( page rows offset ) } )->count;
  117         11260  
475             }
476              
477              
478             =head1 METHODS
479              
480             =head2 format
481              
482             Formatting linked page item.
483              
484             =cut
485              
486             sub format {
487 481     481 1 805 my ( $self,$page,$text ) = @_;
488              
489 481 100 33     16220 return '<span class="' . $self->style_prefix . 'current">' . &{ $self->text }( $text || $page ) . '</span>'
  37         1342  
490             if $self->page==$page;
491            
492 444   66     620 '<a href="' . &{ $self->link }( $page ). '">' . &{ $self->text }( $text || $page ) . '</a>';
  444         14833  
  444         318288  
493             }
494              
495              
496             =head2 render
497              
498             Overriden L<Catalyst::Plugin::Widget> C<render> method.
499              
500             =cut
501              
502             sub render {
503 37     37 1 141999 my ( $self ) = @_;
504              
505 37 50       1499 return '' unless $self->pages;
506              
507             # 'main' boundaries
508 37         1415 my $ml = $self->page - floor( ($self->main - 1) / 2);
509 37         1288 my $mr = $self->page + ceil ( ($self->main - 1) / 2);
510              
511             # 'main' adjustment
512 37   66     1282 $mr-- while $mr > $self->last && $ml-- >= $self->first;
513 37   66     1353 $ml++ while $ml < $self->first && $mr++ <= $self->last;
514              
515             # 'main' range
516 37         177 my @main = $ml .. $mr;
517              
518             # 'head' range
519 37         1280 my @head = $self->first .. min( $self->first + $self->side, $main[0] ) - 1;
520              
521             # 'tail' range
522 37         1311 my @tail = max( $self->last - $self->side , $main[-1] ) + 1 .. $self->last;
523              
524             # rendering
525 37         1417 my $r = '<table class="' . $self->style . '"><tr>';
526              
527             # 'prefix'
528 37 100       1404 $r .= '<td class="' . $self->style_prefix . 'prefix">' . &{ $self->prefix }( $self ) . '</td>'
  35         1202  
529             if $self->prefix;
530              
531             # 'prev' edge
532 37 100 100     1335 $r .= '<td class="' .$self->style_prefix . 'edge">' . $self->format( $self->page - 1, $self->edges->[0] ) . '</td>'
533             if $self->page > $self->first && $self->edges;
534            
535             # 'head' side
536             $r .= '<td class="'. $self->style_prefix .'side">' . $self->format( $_ ) . '</td>'
537 37         737 for @head;
538            
539             # 'delim'
540 37 100 100     1495 $r .= '<td class="' . $self->style_prefix . 'delim">' . $self->delim . '</td>'
      66        
541             if $self->delim && @head && $main[0] - $head[-1] > 1;
542              
543             # 'main'
544             $r .= '<td class="' . $self->style_prefix . 'main">' . $self->format( $_ ) . '</td>'
545 37         1441 for @main;
546              
547             # 'delim'
548 37 100 100     1438 $r .= '<td class="' . $self->style_prefix . 'delim">' . $self->delim . '</td>'
      66        
549             if $self->delim && @tail && $tail[0] - $main[-1] > 1;
550              
551             # 'tail' side
552             $r .= '<td class="' . $self->style_prefix . 'side">' . $self->format( $_ ) . '</td>'
553 37         1264 for @tail;
554            
555             # 'next' edge
556 37 100 100     1463 $r .= '<td class="' . $self->style_prefix . 'edge">' . $self->format( $self->page + 1, $self->edges->[1] ) . '</td>'
557             if $self->page < $self->last && $self->edges;
558              
559             # 'suffix'
560 37 100       1507 $r .= '<td class="' . $self->style_prefix . 'suffix">' . &{ $self->suffix }( $self ) . '</td>'
  35         1188  
561             if $self->suffix;
562              
563             # done!
564 37         530 $r .= '</tr></table>';
565             }
566              
567              
568             =head1 AUTHOR
569              
570             Oleg A. Mamontov, C<< <oleg at mamontov.net> >>
571              
572              
573             =head1 BUGS
574              
575             Please report any bugs or feature requests to C<bug-catalystx-widget-paginator at rt.cpan.org>, or through
576             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CatalystX-Widget-Paginator>. I will be notified, and then you'll
577             automatically be notified of progress on your bug as I make changes.
578              
579              
580             =head1 SUPPORT
581              
582             You can find documentation for this module with the perldoc command.
583              
584             perldoc CatalystX::Widget::Paginator
585              
586              
587             You can also look for information at:
588              
589             =over 4
590              
591             =item * RT: CPAN's request tracker
592              
593             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=CatalystX-Widget-Paginator>
594              
595             =item * AnnoCPAN: Annotated CPAN documentation
596              
597             L<http://annocpan.org/dist/CatalystX-Widget-Paginator>
598              
599             =item * CPAN Ratings
600              
601             L<http://cpanratings.perl.org/d/CatalystX-Widget-Paginator>
602              
603             =item * Search CPAN
604              
605             L<http://search.cpan.org/dist/CatalystX-Widget-Paginator/>
606              
607             =back
608              
609              
610             =head1 LICENSE AND COPYRIGHT
611              
612             Copyright 2010 Oleg A. Mamontov.
613              
614             This program is free software; you can redistribute it and/or modify it
615             under the terms of either: the GNU General Public License as published
616             by the Free Software Foundation; or the Artistic License.
617              
618             See http://dev.perl.org/licenses/ for more information.
619              
620              
621             =cut
622              
623              
624             1;
625