Add pagination
authorVincent Tondellier <tonton+hg@team1664.org>
Sun, 03 Aug 2014 17:40:13 +0200
changeset 29 006e82a1bcd0
parent 28 0df70b8735e3
child 30 f65708dc1be1
Add pagination
CrashTest.pl
lib/CrashTest/Storage/FileSystem.pm
lib/CrashTest/Storage/Sql.pm
lib/Mojolicious/Plugin/TagHelpers/BootstrapPagination.pm
templates/index.html.ep
--- a/CrashTest.pl	Sun Aug 03 17:35:01 2014 +0200
+++ b/CrashTest.pl	Sun Aug 03 17:40:13 2014 +0200
@@ -23,6 +23,8 @@
 my @valid_params = qw/Add-ons Distributor ProductName ReleaseChannel StartupTime UserID Version BuildID CrashTime Comments/;
 my $config = plugin 'Config';
 
+plugin 'TagHelpers::BootstrapPagination';
+
 app->attr(storage => sub {
     my $self = shift;
     eval "require $config->{Storage}->{Type}" or die "Loading module failed $@";
@@ -78,7 +80,14 @@
 
 get '/' => sub {
     my $self = shift;
-    $self->stash(files => $self->app->storage->index());
+    my $page = 1;
+    $self->validation->required('page')->like(qr/^[0-9]+$/);
+    $page = scalar $self->validation->param("page") if $self->validation->is_valid('page');
+
+    my $result = $self->app->storage->index($page, 20);
+
+    $self->stash(files => $result->{crashs});
+    $self->stash(pager => $result->{pager});
     $self->render('index');
 } => 'index';
 
--- a/lib/CrashTest/Storage/FileSystem.pm	Sun Aug 03 17:35:01 2014 +0200
+++ b/lib/CrashTest/Storage/FileSystem.pm	Sun Aug 03 17:40:13 2014 +0200
@@ -14,6 +14,7 @@
 package CrashTest::Storage::FileSystem;
 use Mojo::Base -strict;
 use DateTime;
+use Data::Page;
 
 sub new {
     my ($class, $config) = @_;
@@ -24,32 +25,57 @@
 }
 
 sub index {
-    my ($self) = @_;
+    my ($self, $page, $nperpage) = @_;
+
     my @files;
     my $dh;
     opendir($dh, $self->{data_path}) or die $!;
     my @allfiles = readdir $dh;
-    foreach(@allfiles)
-    {
-        if($_ =~ /(.*)\.json$/)
-        {
+    foreach(@allfiles) {
+        if($_ =~ /(.*)\.json$/) {
             my $filename = File::Spec->catfile($self->{data_path}, $_);
-            push @files, {
-                file        => $filename,
-                uuid        => $1,
-                product     => "",
-                version     => "",
-                user        => "",
-                date        => DateTime->from_epoch(epoch => ((stat $filename)[9])),
-            };
+            if(-f $filename) {
+                push @files, {
+                    file        => $filename,
+                    uuid        => $1,
+                    product     => "",
+                    version     => "",
+                    user        => "",
+                    date        => DateTime->from_epoch(epoch => ((stat $filename)[9])),
+                };
+            }
         }
     }
     closedir $dh;
 
-    my @sorted_files = ( sort { $b->{date} <=> $a->{date} } @files );
-    @sorted_files = @sorted_files[0..19] if scalar(@sorted_files) > 20;
+    my @sorted_files = sort {
+        $b->{date} <=> $a->{date}
+    } @files;
+
+    my $pager = Data::Page->new();
+    $pager->total_entries(scalar @files);
+    $pager->entries_per_page($nperpage);
+    $pager->current_page($page);
 
-    return \@sorted_files;
+    if($page <= $pager->last_page) {
+        @sorted_files = @sorted_files[($pager->first - 1)..($pager->last - 1)];
+    } else {
+        @sorted_files = ();
+    }
+
+    foreach(@sorted_files)
+    {
+        $_->{product} = $self->get_processed_data($_->{uuid})->{client_info}->{ProductName};
+        $_->{version} = $self->get_processed_data($_->{uuid})->{client_info}->{Version};
+        $_->{user} = $self->get_processed_data($_->{uuid})->{client_info}->{UserID};
+    }
+
+    my $results = {
+        pager   => $pager,
+        crashs  => \@sorted_files,
+    };
+
+    return $results;
 }
 
 sub get_processed_data {
--- a/lib/CrashTest/Storage/Sql.pm	Sun Aug 03 17:35:01 2014 +0200
+++ b/lib/CrashTest/Storage/Sql.pm	Sun Aug 03 17:40:13 2014 +0200
@@ -28,21 +28,25 @@
 }
 
 sub index {
-    my ($self) = @_;
+    my ($self, $page, $nperpage) = @_;
 
     my $dbcrashs = $self->{schema}->resultset('CrashReport')->search(
         undef,
         {
-            prefetch => [ 'product', 'crash_user' ],
-            order_by => 'crash_time',
-            rows => 50,
+            prefetch    => [ 'product', 'crash_user' ],
+            order_by    => 'crash_time',
+            page        => $page,
+            rows        => $nperpage,
         },
     );
 
-    my @results;
+    my $results = {
+        pager   => $dbcrashs->pager,
+        crashs  => [],
+    };
     for my $crash($dbcrashs->all) {
         my $filename = File::Spec->catfile($self->{data_path}, $crash->uuid);
-        push @results, {
+        push $results->{crashs}, {
             file        => $filename,
             uuid        => $crash->uuid,
             product     => $crash->product->name,
@@ -52,7 +56,7 @@
         }
     }
 
-    return \@results;
+    return $results;
 }
 
 sub get_processed_data {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/Mojolicious/Plugin/TagHelpers/BootstrapPagination.pm	Sun Aug 03 17:40:13 2014 +0200
@@ -0,0 +1,432 @@
+# Based on Mojolicious-Plugin-TagHelpers-Pagination and slightly modified for bootstrap
+# Under Artistic License 2.0
+
+package Mojolicious::Plugin::TagHelpers::BootstrapPagination;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::ByteStream 'b';
+use Scalar::Util 'blessed';
+use POSIX 'ceil';
+
+our $VERSION = 0.01;
+
+our @value_list =
+  qw/prev
+     next
+     current_start
+     current_end
+     page_start
+     page_end
+     separator
+     ellipsis
+     placeholder/;
+
+# Register plugin
+sub register {
+  my ($plugin, $mojo, $param) = @_;
+
+  $param ||= {};
+
+  # Load parameter from Config file
+  if (my $config_param = $mojo->config('TagHelpers-Pagination')) {
+    $param = { %$config_param, %$param };
+  };
+
+  foreach (@value_list) {
+    $plugin->{$_} = $param->{$_} if defined $param->{$_};
+  };
+
+  # Set 'current_start' and 'current_end' symbols,
+  # if 'current' template is available.
+  # Same for 'page'.
+  foreach (qw/page current/) {
+    if (defined $param->{$_}) {
+      @{$plugin}{$_ . '_start', $_ . '_end'} = split("{$_}", $param->{$_});
+      $plugin->{$_ . '_end'} ||= '';
+    };
+  };
+
+  # Default current start and current end symbols
+  for ($plugin) {
+    $_->{current_start} //= '';
+    $_->{current_end}   //= '';
+    $_->{page_start}    //= '';
+    $_->{page_end}      //= '';
+    $_->{prev}          //= '&lt;';
+    $_->{next}          //= '&gt;';
+    $_->{separator}     //= '';
+    $_->{ellipsis}      //= '<li><span>...</span></li>';
+    $_->{placeholder}   //= 'page';
+  };
+
+  # Establish pagination helper
+  $mojo->helper(
+    pagination => sub {
+      shift; # Controller
+      return b( $plugin->pagination( @_ ) );
+    });
+};
+
+
+# Pagination helper
+sub pagination {
+  my $self = shift;
+
+  # $_[0] = current page
+  # $_[1] = page count
+  # $_[2] = template or Mojo::URL
+
+  return '' unless $_[0] || $_[1];
+
+  # No valid count given
+  local $_[1] = !$_[1] ? 1 : ceil($_[1]);
+
+  # New parameter hash
+  my %values =
+    map { $_ => $self->{$_} } @value_list;
+
+  # Overwrite plugin defaults
+  if ($_[3] && ref $_[3] eq 'HASH') {
+    my $overwrite = $_[3];
+    foreach (@value_list) {
+      $values{$_}  = $overwrite->{$_} if defined $overwrite->{$_};
+    };
+
+    foreach (qw/page current/) {
+      if (defined $overwrite->{$_}) {
+	@values{$_ . '_start', $_ . '_end'} = split("{$_}", $overwrite->{$_});
+	$values{$_ . '_end'} ||= '';
+      };
+    };
+  };
+
+  # Establish string variables
+  my ($p, $n, $cs, $ce, $ps, $pe, $s, $el, $ph) = @values{@value_list};
+  # prev next current_start current_end
+  # page_start page_end separator ellipsis placeholder
+
+  # Template
+  my $t = $_[2];
+  if (blessed $t && blessed $t eq 'Mojo::URL') {
+    $t = $t->to_string;
+    $t =~ s/\%7[bB]$ph\%7[dD]/{$ph}/g;
+  };
+
+
+  my $sub = sublink_gen($t,$ps,$pe,$ph);
+
+  # Pagination string
+  my $e;
+  my $counter = 1;
+
+  if ($_[1] >= 7){
+
+    # < [1] #2 #3
+    if ($_[0] == 1){
+      $e .= $sub->(undef, [$p, 'prev']) . $s .
+	    $sub->(undef, [$cs . 1  . $ce, 'self']) . $s .
+	    $sub->('2') . $s .
+	    $sub->('3') . $s;
+    }
+
+    # < #1 #2 #3
+    elsif (!$_[0]) {
+      $e .= $sub->(undef, [$p, 'prev']) . $s;
+      $e .= $sub->($_) . $s foreach (1 .. 3);
+    }
+
+    # #< #1
+    else {
+      $e .= $sub->(($_[0] - 1), [$p, 'prev']) . $s .
+            $sub->('1') . $s;
+    };
+
+    # [2] #3
+    if ($_[0] == 2){
+      $e .= $sub->(undef, [$cs . 2 . $ce, 'self']) . $s .
+	    $sub->('3') . $s;
+    }
+
+    # ...
+    elsif ($_[0] > 3){
+      $e .= $el . $s;
+    };
+
+    # #x-1 [x] #x+1
+    if (($_[0] >= 3) && ($_[0] <= ($_[1] - 2))){
+      $e .= $sub->($_[0] - 1) . $s .
+	    $sub->(undef, [$cs .$_[0] . $ce, 'self']) . $s .
+	    $sub->($_[0] + 1) . $s;
+    };
+
+    # ...
+    if ($_[0] < ($_[1] - 2)){
+      $e .= $el . $s;
+    };
+
+    # number is prefinal
+    if ($_[0] == ($_[1] - 1)){
+      $e .= $sub->($_[1] - 2) . $s .
+	    $sub->(undef, [$cs . $_[0] . $ce, 'self']) . $s;
+    };
+
+    # Number is final
+    if ($_[0] == $_[1]){
+      $e .= $sub->($_[1] - 1) . $s .
+            $sub->(undef, [$cs . $_[1] . $ce, 'self']) . $s .
+	    $sub->(undef, [$n, 'next']);
+    }
+
+    # Number is anywhere in between
+    else {
+      $e .= $sub->($_[1]) . $s .
+            $sub->(($_[0] + 1), [$n,'next']);
+    };
+  }
+
+  # Counter < 7
+  else {
+
+    # Previous
+    if ($_[0] > 1){
+      $e .= $sub->(($_[0] - 1), [$p, 'prev']) . $s;
+    } else {
+      $e .= $sub->(undef, [$p, 'prev']) . $s;
+    };
+
+    # All numbers in between
+    while ($counter <= $_[1]){
+      if ($_[0] != $counter) {
+        $e .= $sub->($counter) . $s;
+      }
+
+      # Current
+      else {
+        $e .= $sub->(undef, [$cs . $counter . $ce, 'self']) . $s;
+      };
+
+      $counter++;
+    };
+
+    # Next
+    if ($_[0] != $_[1]){
+      $e .= $sub->(($_[0] + 1), [$n, 'next']);
+    }
+
+    else {
+      $e .= $sub->(undef, [$n, 'next']);
+    };
+  };
+
+  # Pagination string
+  $e;
+};
+
+# Sublink function generator
+sub sublink_gen {
+  my ($url, $ps, $pe, $ph) = @_;
+
+  my $s = 'sub {';
+  # $_[0] = number
+  # $_[1] = number_shown
+
+  # Url is template
+  if ($url) {
+    $s .= 'my $url=' . b($url)->quote . ';';
+    $s .= 'if($_[0]){$url=~s/\{' . $ph . '\}/$_[0]/g}else{$url=undef};';
+  }
+
+  # No template given
+  else {
+    $s .= 'my $url = $_[0];';
+  };
+
+  $s .= 'my$n=$_[1]||' . b($ps)->quote . '.$_[0].' . b($pe)->quote . ';';
+  $s .= q{my $liclass='';};
+  $s .= q{if(ref $n && $n->[1] eq "self"){$liclass = "active"};};
+  $s .= q{my $rel='';};
+  $s .= q{if(ref $n){$rel=' rel="'.$n->[1].'"';$n=$n->[0]};};
+  $s .= q!if($url){$url=~s/&/&amp;/g;!;
+  $s .= q{$url=~s/</&lt;/g;};
+  $s .= q{$url=~s/>/&gt;/g;};
+  $s .= q{$url=~s/"/&quot;/g;};
+  $s .= q!$url=~s/'/&#39;/g;};!;
+
+  # Create sublink
+  $s .= q{return '<li'.($liclass?' class="'.$liclass.'"':'').'><a'.($url?' href="'.$url.'"':'').$rel.'>' . $n . '</a></li>';};
+  $s .= '}';
+
+  my $x = eval($s);
+
+  warn $@ if $@;
+
+  $x;
+};
+
+
+1;
+
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Mojolicious::Plugin::TagHelpers::Pagination - Pagination Helper for Mojolicious
+
+
+=head1 SYNOPSIS
+
+  # Mojolicious
+  $app->plugin('TagHelpers::Pagination' => {
+    separator => ' ',
+    current => '<strong>{current}</strong>'
+  });
+
+  # Mojolicious::Lite
+  plugin 'TagHelpers::Pagination' => {
+    separator => ' ',
+    current   =>  '<strong>{current}</strong>'
+  };
+
+  # In Templates
+  %= pagination(2, 4, '?page={page}' => { separator => "\n" })
+  # <a href="?page=1" rel="prev">&lt;</a>
+  # <a href="?page=1">1</a>
+  # <a rel="self"><strong>2</strong></a>
+  # <a href="?page=3">3</a>
+  # <a href="?page=4">4</a>
+  # <a href="?page=3" rel="next">&gt;</a>
+
+=head1 DESCRIPTION
+
+L<Mojolicious::Plugin::TagHelpers::Pagination> helps you to create
+pagination elements in your templates, like this:
+
+L<E<lt>|/#> L<1|/#> ... L<5|/#> B<[6]> L<7|/#> ... L<18|/#> L<E<gt>|/#>
+
+=head1 METHODS
+
+L<Mojolicious::Plugin::TagHelpers::Pagination> inherits all methods from
+L<Mojolicious::Plugin> and implements the following new one.
+
+
+=head2 register
+
+  # Mojolicious
+  $app->plugin('TagHelpers::Pagination' => {
+    separator => ' ',
+    current => '<strong>{current}</strong>'
+  });
+
+  # Or in your config file
+  {
+    'TagHelpers-Pagination' => {
+      separator => ' ',
+      current => '<strong>{current}</strong>'
+    }
+  }
+
+Called when registering the plugin.
+
+All L<parameters|/PARAMETERS> can be set either on registration or
+as part of the configuration file with the key C<TagHelpers-Pagination>.
+
+
+=head1 HELPERS
+
+=head2 pagination
+
+  # In Templates:
+  %= pagination(4, 6 => '/page-{page}.html');
+  % my $url = Mojo::URL->new->query({ page => '{page}'});
+  %= pagination(4, 6 => $url);
+  %= pagination(4, 6 => '/page/{page}.html', { current => '<b>{current}</b>' });
+
+Generates a pagination string.
+Expects at least two numeric values: the current page number and
+the total count of pages.
+Additionally it accepts a link pattern and a hash reference
+with parameters overwriting the default plugin parameters for
+pagination.
+The link pattern can be a string using a placeholder in curly brackets
+(defaults to C<page>) for the page number it should link to.
+It's also possible to give a
+L<Mojo::URL> object containing the placeholder.
+The placeholder can be used multiple times.
+
+
+=head1 PARAMETERS
+
+For the layout of the pagination string, the plugin accepts the
+following parameters, that are able to overwrite the default
+layout elements. These parameters can again be overwritten in
+the pagination helper.
+
+=over 2
+
+=item current
+
+Pattern for current page number. The C<{current}> is a
+placeholder for the current number.
+Defaults to C<[{current}]>.
+Instead of a pattern, both sides of the current number
+can be defined with C<current_start> and C<current_end>.
+
+
+=item ellipsis
+
+Placeholder symbol for hidden pages. Defaults to C<...>.
+
+
+=item next
+
+Symbol for next pages. Defaults to C<&gt;>.
+
+
+=item page
+
+Pattern for page number. The C<{page}> is a
+placeholder for the page number.
+Defaults to C<{page}>.
+Instead of a pattern, both sides of the page number
+can be defined with C<page_start> and C<page_end>.
+
+
+=item placeholder
+
+String representing the placeholder for the page number in the URL
+pattern. Defaults to C<page>.
+
+
+=item prev
+
+Symbol for previous pages. Defaults to C<&lt;>.
+
+
+=item separator
+
+Symbol for the separation of pagination elements.
+Defaults to C<&nbsp;>.
+
+=back
+
+
+=head1 DEPENDENCIES
+
+L<Mojolicious>.
+
+
+=head1 AVAILABILITY
+
+  https://github.com/Akron/Mojolicious-Plugin-TagHelpers-Pagination
+
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2012-2014, L<Nils Diewald|http://nils-diewald.de/>.
+
+This program is free software, you can redistribute it
+and/or modify it under the terms of the Artistic License version 2.0.
+
+=cut
--- a/templates/index.html.ep	Sun Aug 03 17:35:01 2014 +0200
+++ b/templates/index.html.ep	Sun Aug 03 17:40:13 2014 +0200
@@ -11,14 +11,20 @@
   </tr>
 </thead>
 % foreach my $crash(@$files) {
-<tr>
-  %= t td => $crash->{product}
-  %= t td => $crash->{version}
-  %= t td => $crash->{user}
-  %= t td => (style => "font-family:monospace;") => begin
-    %= link_to $crash->{uuid} => url_for('report', uuid => $crash->{uuid})
+  %= t tr => begin
+    %= t td => $crash->{product}
+    %= t td => $crash->{version}
+    %= t td => $crash->{user}
+    %= t td => (style => "font-family:monospace;") => begin
+      %= link_to $crash->{uuid} => url_for('report', uuid => $crash->{uuid})
+    % end
+    %= t td => $crash->{date}->strftime("%F %T")
   % end
-  %= t td => $crash->{date}->strftime("%F %T")
-</tr>
 % }
 % end
+% if($pager->first_page != $pager->last_page) {
+  %= t ul => (class => "pagination") => begin
+    % my $url = Mojo::URL->new->query({ page => '{page}'});
+    %= pagination($pager->current_page, $pager->last_page => $url);
+  % end
+% }