File Coverage

blib/lib/Mojolicious/Plugin/DataTables.pm
Criterion Covered Total %
statement 45 193 23.3
branch 0 66 0.0
condition 2 35 5.7
subroutine 14 18 77.7
pod 1 1 100.0
total 62 313 19.8


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::DataTables;
2              
3 2     2   58882 use Mojo::Base 'Mojolicious::Plugin';
  2         157384  
  2         13  
4 2     2   1362 use Mojo::JSON qw(decode_json encode_json true false);
  2         17374  
  2         156  
5 2     2   417 use Mojo::Collection;
  2         3712  
  2         68  
6 2     2   415 use Mojo::DOM::HTML;
  2         3000  
  2         101  
7 2     2   14 use Mojo::ByteStream;
  2         4  
  2         73  
8 2     2   11 use Mojo::Util qw(dumper deprecated);
  2         4  
  2         100  
9              
10 2     2   11 use Carp;
  2         4  
  2         118  
11              
12 2     2   956 use Mojolicious::Plugin::DataTables::SSP::Column;
  2         5  
  2         13  
13 2     2   850 use Mojolicious::Plugin::DataTables::SSP::Params;
  2         6  
  2         14  
14 2     2   840 use Mojolicious::Plugin::DataTables::SSP::Results;
  2         5  
  2         13  
15              
16             our $VERSION = '2.01';
17              
18             sub register {
19              
20 1     1 1 39 my ( $c, $app, $conf ) = @_;
21              
22 1         7 $app->helper( 'datatable_js' => \&_dt_js );
23 1         135 $app->helper( 'datatable_css' => \&_dt_css );
24 1         72 $app->helper( 'datatable.ssp' => \&_dt_ssp );
25 1         323 $app->helper( 'datatable.ssp_params' => \&_dt_ssp_params );
26 1         245 $app->helper( 'datatable.ssp_results' => \&_dt_ssp_results );
27              
28             }
29              
30             sub _dt_js {
31              
32 1     1   18547 my ( $c, $url ) = @_;
33              
34 1         2 my $dt_version = '1.10.24';
35 1   33     11 my $dt_js_url = $url || "//cdn.datatables.net/$dt_version/js/jquery.dataTables.min.js";
36              
37 1         5 return _tag( 'script', 'src' => $dt_js_url );
38              
39             }
40              
41             sub _dt_css {
42              
43 1     1   10882 my ( $c, $url ) = @_;
44              
45 1         2 my $dt_version = '1.10.24';
46 1   33     8 my $dt_js_url = $url || "//cdn.datatables.net/$dt_version/css/jquery.dataTables.min.css";
47              
48 1         10 return _tag( 'link', 'rel' => 'stylesheet', 'href' => $dt_js_url );
49              
50             }
51              
52             sub _dt_ssp {
53              
54 0     0   0 my ( $c, %args ) = @_;
55              
56 0   0     0 my $table = delete $args{table} || Carp::croak 'Missing table';
57 0         0 my $options = delete $args{options};
58 0         0 my $columns = delete $args{columns};
59 0         0 my $debug = delete $args{debug};
60 0         0 my $where = delete $args{where};
61 0         0 my $sql = delete $args{sql};
62              
63 0 0       0 if ( defined( $args{db} ) ) {
64              
65 0         0 deprecated '[Mojolicious::Plugin::DataTables] db is DEPRECATED in favor of sql';
66              
67 0         0 my $db = $args{db};
68              
69 0 0       0 $sql = $db->sqlite if ( ref $db eq 'Mojo::SQLite::Database' );
70 0 0       0 $sql = $db->pg if ( ref $db eq 'Mojo::Pg::Database' );
71 0 0       0 $sql = $db->mysql if ( ref $db eq 'Mojo::mysql::Database' );
72              
73             }
74              
75 0         0 my $log = $c->app->log->context('[DataTables]');
76              
77 0         0 my $regexp_operator = 'REGEXP'; # REGEXP operator for MySQL and SQLite
78              
79 0         0 my $sql_class = ref $sql;
80              
81             # "~" operator for PostgreSQL
82 0 0       0 if ( $sql_class =~ /Mojo::Pg/ ) {
83 0         0 $regexp_operator = '~';
84             }
85              
86 0         0 my $ssp = $c->datatable->ssp_params($options);
87              
88 0 0       0 return {} if ( !$ssp->draw );
89              
90 0         0 my @columns = $ssp->db_columns;
91              
92 0 0       0 if ($columns) {
93 0 0       0 if ( ref $columns eq 'ARRAY' ) {
94 0         0 push @columns, @{$columns};
  0         0  
95             } else {
96 0         0 push @columns, $columns;
97             }
98             }
99              
100 0         0 my $abstract = {
101             'where' => {
102             '-and' => [],
103             '-or' => [],
104             },
105             'order' => [],
106             'filter' => [],
107             };
108              
109             # Global filter
110 0 0       0 if ($where) {
111 0 0       0 if ( ref $where eq 'ARRAY' ) {
112 0         0 my ( $where_stmt, @where_bind ) = @{$where};
  0         0  
113 0         0 foreach (@where_bind) {
114 0         0 $where_stmt =~ s/\?/'$_'/; # TODO improve
115             }
116 0         0 push @{ $abstract->{where}->{'-and'} }, { '-bool' => $where_stmt };
  0         0  
117 0         0 push @{ $abstract->{filter} }, { '-bool' => $where_stmt };
  0         0  
118             } else {
119 0         0 push @{ $abstract->{where}->{'-and'} }, { '-bool' => $where };
  0         0  
120 0         0 push @{ $abstract->{filter} }, { '-bool' => $where };
  0         0  
121             }
122             }
123              
124             # Column Search
125 0         0 foreach ( @{ $ssp->columns } ) {
  0         0  
126 0 0 0     0 if ( $_->database && $_->searchable != 0 && $_->search->{value} ) {
      0        
127              
128 0 0       0 if ( $_->search->{regex} ) {
129 0         0 push @{ $abstract->{where}->{'-and'} }, { $_->database => { $regexp_operator => $_->search->{value} } };
  0         0  
130             } else {
131 0         0 push @{ $abstract->{where}->{'-and'} },
132 0         0 { $_->database => { -like => '%' . $_->search->{value} . '%' } };
133             }
134              
135             }
136             }
137              
138             # Global Search
139 0 0       0 if ( $ssp->search->{value} ) {
140 0         0 foreach ( @{ $ssp->columns } ) {
  0         0  
141 0 0 0     0 if ( $_->database && $_->searchable != 0 ) {
142              
143 0 0       0 if ( $ssp->search->{regex} ) {
144 0         0 push @{ $abstract->{where}->{'-or'} },
145 0         0 { $_->database => { $regexp_operator => $ssp->search->{value} } };
146             } else {
147 0         0 push @{ $abstract->{where}->{'-or'} },
148 0         0 { $_->database => { -like => '%' . $ssp->search->{value} . '%' } };
149             }
150             }
151             }
152             }
153              
154             # Order
155 0 0       0 if ( %{ $ssp->db_order } ) {
  0         0  
156              
157 0         0 my $order = $ssp->db_order;
158              
159 0         0 foreach my $column ( keys %{$order} ) {
  0         0  
160 0         0 my $clausole = $order->{$column};
161 0         0 push @{ $abstract->{order} }, { "-$clausole" => $column };
  0         0  
162             }
163             }
164              
165 0 0       0 delete $abstract->{where}->{'-and'} if ( scalar @{ $abstract->{where}->{'-and'} } < 1 );
  0         0  
166 0 0       0 delete $abstract->{where}->{'-or'} if ( scalar @{ $abstract->{where}->{'-or'} } < 1 );
  0         0  
167              
168 0         0 my ( $stmt, @bind ) = $sql->abstract->select( $table, \@columns, $abstract->{where}, $abstract->{order} );
169              
170 0         0 $stmt .= sprintf ' LIMIT %s OFFSET %s', $ssp->length, $ssp->start; # TODO
171              
172 0 0       0 if ($debug) {
173 0         0 $log->debug("Query: $stmt");
174 0         0 $log->debug( "Bind: " . encode_json \@bind );
175             }
176              
177 0         0 my $query = $sql->db->query( $stmt, @bind );
178              
179 0         0 my @results = ();
180              
181 0         0 while ( my $row = $query->hash ) {
182              
183 0         0 my $data = {};
184              
185 0         0 foreach my $column ( @{ $ssp->columns } ) {
  0         0  
186              
187 0         0 $column->row($row);
188              
189 0   0     0 my $col_db = $column->database || '';
190 0         0 my $col_value = $row->{$col_db};
191              
192 0 0       0 if ( ref $column->formatter eq 'CODE' ) {
193 0         0 $col_value = $column->formatter->( $col_value, $column );
194             }
195              
196 0         0 $data->{ $column->data } = $col_value;
197              
198             }
199              
200 0         0 push @results, $data;
201              
202             }
203              
204 0         0 my ( $stmt_total, @bind_total ) = $sql->abstract->select( $table, [ \'COUNT(*) AS tot' ], $abstract->{filter} );
205 0         0 my ( $stmt_filter, @bind_filter ) = $sql->abstract->select( $table, [ \'COUNT(*) AS tot' ], $abstract->{where} );
206              
207 0 0       0 if ($debug) {
208 0         0 $log->debug("Query Total: $stmt_total");
209 0         0 $log->debug( "Bind Total: " . encode_json \@bind_total );
210 0         0 $log->debug("Query Filtered: $stmt_filter");
211 0         0 $log->debug( "Bind Filtered: " . encode_json \@bind_filter );
212             }
213              
214 0         0 my $total = $sql->db->query( $stmt_total, @bind_total )->hash->{tot};
215 0         0 my $filtered = $total;
216              
217 0 0       0 if (@bind_filter) {
218 0         0 $filtered = $sql->db->query( $stmt_filter, @bind_filter )->hash->{tot};
219             }
220              
221 0         0 my $ssp_results = Mojolicious::Plugin::DataTables::SSP::Results->new(
222             draw => $ssp->draw,
223             data => \@results,
224             records_total => $total,
225             records_filtered => $filtered
226             );
227              
228 0         0 return $ssp_results;
229              
230             }
231              
232             sub _dt_ssp_params {
233              
234 0     0   0 my ( $c, $dt_options ) = @_;
235              
236 0         0 my $req_params = {};
237 0 0       0 $req_params = $c->req->query_params if ( $c->req->{method} eq 'GET' );
238 0 0       0 $req_params = $c->req->body_params if ( $c->req->{method} eq 'POST' );
239              
240 0         0 return Mojolicious::Plugin::DataTables::SSP::Params->new( %{ _decode_params( $req_params, $dt_options ) } );
  0         0  
241              
242             }
243              
244             sub _dt_ssp_results {
245 0     0   0 my ( $c, %args ) = @_;
246 0         0 return Mojolicious::Plugin::DataTables::SSP::Results->new(%args);
247             }
248              
249 2     2   8 sub _tag { Mojo::ByteStream->new( Mojo::DOM::HTML::tag_to_html(@_) ) }
250              
251             sub _decode_params {
252              
253 0     0     my ( $req_params, $dt_options ) = @_;
254              
255 0           my $dt_params = {};
256              
257 0   0       $dt_params->{draw} = $req_params->param('draw') || false;
258 0   0       $dt_params->{length} = $req_params->param('length') || 0;
259 0   0       $dt_params->{start} = $req_params->param('start') || 0;
260 0   0       $dt_params->{timestamp} = $req_params->param('_') || 0;
261 0           $dt_params->{columns} = [];
262 0           $dt_params->{order} = [];
263 0           $dt_params->{where} = undef;
264              
265 0           foreach ( @{ $req_params->names } ) {
  0            
266              
267 0           my $value = $req_params->param($_);
268 0 0         $value = true if ( $value eq 'true' );
269 0 0         $value = false if ( $value eq 'false' );
270              
271 0 0         $dt_params->{columns}[$1]->{$2} = $value if ( $_ =~ /columns\[(\d+)\]\[(data|name|searchable|orderable)\]/ );
272 0 0         $dt_params->{columns}[$1]->{search}->{$2} = $value if ( $_ =~ /columns\[(\d+)\]\[search\]\[(regex|value|)\]/ );
273 0 0         $dt_params->{order}[$1]->{$2} = $value if ( $_ =~ /order\[(\d+)\]\[(column|dir)\]/ );
274 0 0         $dt_params->{search}->{$1} = $value if ( $_ =~ /search\[(value|regex)\]/ );
275              
276             }
277              
278 0           for ( my $i = 0; $i < @{$dt_options}; $i++ ) {
  0            
279              
280 0           my $dt_option = $dt_options->[$i];
281              
282 0 0         if ( !defined $dt_option->{dt} ) {
283 0           $dt_option->{dt} = $i;
284             }
285              
286 0   0       $dt_params->{columns}[ $dt_option->{dt} ]->{database} = $dt_option->{db} || undef;
287 0   0       $dt_params->{columns}[ $dt_option->{dt} ]->{formatter} = $dt_option->{formatter} || undef;
288 0   0       $dt_params->{columns}[ $dt_option->{dt} ]->{label} = $dt_option->{label} || undef;
289              
290 0 0         if ( defined $dt_option->{searchable} ) {
291 0           $dt_params->{columns}[ $dt_option->{dt} ]->{searchable} = $dt_option->{searchable};
292             }
293 0 0         if ( defined $dt_option->{orderable} ) {
294 0           $dt_params->{columns}[ $dt_option->{dt} ]->{orderable} = $dt_option->{orderable};
295             }
296             }
297              
298 0           for ( my $i = 0; $i < @{ $dt_params->{columns} }; $i++ ) {
  0            
299 0           my $column = $dt_params->{columns}[$i];
300 0           $dt_params->{columns}[$i] = Mojolicious::Plugin::DataTables::SSP::Column->new( %{$column} );
  0            
301             }
302              
303 0           for ( my $i = 0; $i < @{ $dt_params->{order} }; $i++ ) {
  0            
304 0           $dt_params->{order}[$i]->{column} = $dt_params->{columns}[ $dt_params->{order}[$i]->{column} ];
305             }
306              
307             # $dt_params->{columns} = Mojo::Collection->new ( $dt_params->{columns} );
308             # $dt_params->{order} = Mojo::Collection->new ( $dt_params->{order} );
309              
310 0           return $dt_params;
311              
312             }
313              
314             1;
315              
316             =encoding utf8
317              
318             =head1 NAME
319              
320             Mojolicious::Plugin::DataTables - DataTables Plugin for Mojolicious
321              
322             =head1 SYNOPSIS
323              
324             # Mojolicious
325             $self->plugin('DataTables');
326              
327             # Mojolicious::Lite
328             plugin 'DataTables';
329              
330             [...]
331              
332             my $sql = Mojo::Pg->new;
333              
334             my $dt_ssp = $c->datatable->ssp(
335             table => 'users',
336             sql => $sql,
337             columns => qw/role create_date/,
338             debug => 1,
339             where => 'status = "active"'
340             options => [
341             {
342             label => 'UID',
343             db => 'uid',
344             dt => 0,
345             formatter => sub {
346             my ($value, $column) = @_;
347             return '' . $value . '';
348             }
349             },
350             {
351             label => 'e-Mail',
352             db => 'mail',
353             dt => 1,
354             },
355             {
356             label => 'Status',
357             db => 'status',
358             dt => 2,
359             },
360             ]
361             );
362              
363             return $c->render(json => $dt_ssp);
364              
365             =head1 DESCRIPTION
366              
367             L is a L plugin to add DataTables SSP (Server-Side Protocol) support in your Mojolicious application.
368              
369              
370             =head1 METHODS
371              
372             L implements the following methods.
373              
374             =head2 datatable_js
375              
376             Generate C
446              
447             Controller:
448              
449             $c->datatable->ssp(
450             table => 'users',
451             sql => $sql,
452             options => [
453             {
454             label => 'UID',
455             db => 'uid',
456             dt => 0,
457             },
458             {
459             label => 'e-Mail',
460             db => 'mail',
461             dt => 1,
462             },
463             {
464             label => 'Status',
465             db => 'status',
466             dt => 2,
467             },
468             ]
469             );
470              
471             =head2 Formatter
472              
473             The anonymous C sub accept this arguments:
474              
475             =over 4
476              
477             =item C<$value>: the column value
478              
479             =item C<$column>: A L instance
480              
481              
482             options => [
483             {
484             label => 'Name',
485             db => 'username',
486             dt => 0,
487             formatter => sub {
488             my ($value, $column) = @_;
489             my $row = $column->row;
490             return '' .$value . '';
491             }
492             },
493             {
494             ...
495             }
496             ]
497              
498             =head2 Search flag
499              
500             The C flag enable=1 or disable=0 a filter for specified column.
501              
502             options => [
503             {
504             label => 'Name',
505             db => 'username',
506             dt => 0,
507             searchable => 0,
508             },
509             {
510             ...
511             }
512             ]
513              
514             =head2 Where condition
515              
516             Use the C option to filter the table using L syntax:
517              
518             $c->datatable->ssp(
519             table => 'users',
520             sql => $sql,
521             where => { status => 'active' }
522             options => [ ... ]
523             );
524              
525             It's possible to use array (C<[ where, bind_1, bind_2, ... ]>) to bind values:
526              
527             $c->datatable->ssp(
528             table => 'users',
529             sql => $sql,
530             where => [ 'status = ?', 'active' ],
531             options => [ ... ]
532             );
533              
534              
535             =head1 SEE ALSO
536              
537             L, L, L, L, L
538             L, L, L.
539              
540              
541             =head1 SUPPORT
542              
543             =head2 Bugs / Feature Requests
544              
545             Please report any bugs or feature requests through the issue tracker
546             at L.
547             You will be notified automatically of any progress on your issue.
548              
549             =head2 Source Code
550              
551             This is open source software. The code repository is available for
552             public review and contribution under the terms of the license.
553              
554             L
555              
556             git clone https://github.com/giterlizzi/perl-Mojolicious-Plugin-DataTables.git
557              
558              
559             =head1 AUTHOR
560              
561             =over 4
562              
563             =item * Giuseppe Di Terlizzi
564              
565             =back
566              
567              
568             =head1 LICENSE AND COPYRIGHT
569              
570             This software is copyright (c) 2020-2021 by Giuseppe Di Terlizzi.
571              
572             This is free software; you can redistribute it and/or modify it under
573             the same terms as the Perl 5 programming language system itself.
574              
575             =cut
576