File Coverage

blib/lib/DataTables.pm
Criterion Covered Total %
statement 135 209 64.5
branch 24 66 36.3
condition 9 31 29.0
subroutine 22 26 84.6
pod 10 11 90.9
total 200 343 58.3


line stmt bran cond sub pod time code
1             package DataTables;
2            
3 2     2   147894 use 5.008008;
  2         17  
4 2     2   11 use strict;
  2         4  
  2         56  
5 2     2   11 use warnings;
  2         5  
  2         53  
6            
7 2     2   23 use Carp;
  2         4  
  2         154  
8 2     2   1905 use CGI::Simple;
  2         32648  
  2         15  
9 2     2   3591 use DBI;
  2         37163  
  2         195  
10 2     2   1754 use JSON::XS;
  2         16163  
  2         144  
11 2     2   1339 use SQL::Abstract::Limit;
  2         63035  
  2         150  
12 2     2   1357 use JQuery::DataTables::Request;
  2         12893  
  2         14  
13            
14             our $VERSION = '0.08';
15            
16             # Preloaded methods go here.
17            
18             sub new {
19 1     1 1 15673 my $invocant = shift;
20 1   33     9 my $class = ref($invocant) || $invocant;
21 1         10 my $self = {
22             tables => undef,
23             columns => undef,
24             dbh => undef,
25             query => CGI::Simple->new,
26             data_output_format => 'column-id', # or: key-value
27             patterns => {},
28             join_clause => '',
29             where_clause => '',
30             @_, # Override previous attributes
31             };
32 1         145 return bless $self, $class;
33             }
34            
35            
36            
37            
38             sub tables {
39 6     6 1 20 my $self = shift;
40            
41 6 100       14 if (@_) {
42 1         11 my $a_ref = shift;
43 1 50       6 croak "tables must be an array ref" unless UNIVERSAL::isa($a_ref,'ARRAY');
44 1         19 $self->{tables} = $a_ref;
45             }
46 6         31 return $self->{tables};
47             }
48            
49            
50            
51            
52             sub columns {
53 10     10 1 19 my $self = shift;
54            
55 10 100       17 if (@_) {
56 1         1 my $ref = shift;
57 1 50 33     5 croak "columns_a must be an array or hash ref" unless UNIVERSAL::isa($ref,'ARRAY') or UNIVERSAL::isa($ref,'HASH');
58 1         3 $self->{columns} = $ref;
59             }
60 10         27 return $self->{columns};
61             }
62            
63            
64            
65            
66             sub dbh {
67 0     0 1 0 my $self = shift;
68            
69 0 0       0 if (@_) {
70 0         0 my $ref = shift;
71 0 0       0 croak "dbh must be a DBI object" unless UNIVERSAL::can($ref,'prepare');
72 0         0 $self->{dbh} = $ref;
73             }
74 0         0 return $self->{dbh};
75             }
76            
77            
78            
79            
80             sub data_output_format {
81 3     3 1 5 my $self = shift;
82 3         3 my $new_val = shift;
83            
84 3 50       7 if ($new_val) {
85 0 0 0     0 if( $new_val ne 'column-id' and $new_val ne 'key-value' ) {
86 0         0 croak "data_output_format must be one of: column-id, key-value; you sent: $new_val";
87             }else{
88 0         0 $self->{data_output_format} = $new_val;
89             }
90             }
91 3         8 return $self->{data_output_format};
92             }
93            
94            
95            
96            
97             sub patterns {
98 1     1 1 2 my $self = shift;
99            
100 1 50       4 if (@_) {
101 0         0 my $h_ref = shift;
102 0 0       0 croak "patterns must be a hash ref" unless UNIVERSAL::isa($h_ref,'HASH');
103 0         0 $self->{patterns} = $h_ref;
104             }
105 1         2 return $self->{patterns};
106             }
107            
108            
109            
110            
111             sub join_clause {
112 1     1 1 2 my $self = shift;
113            
114 1 50       3 if (@_) {
115 0         0 $self->{join_clause} = shift;
116             }
117 1         5 return $self->{join_clause};
118             }
119            
120            
121            
122            
123             sub where_clause {
124 1     1 1 2 my $self = shift;
125            
126 1 50       3 if (@_) {
127 0         0 $self->{where_clause} = shift;
128             }
129 1         3 return $self->{where_clause};
130             }
131            
132            
133            
134            
135             sub _columns_arr {
136 3     3   5 my $self = shift;
137 3         9 my $aColumns;
138             my $regular_columns;
139 3         0 my $as_hash;
140 3         0 my $tables_hash;
141            
142 3 50       6 if(UNIVERSAL::isa($self->columns,'HASH')) {
    50          
143 0         0 my $columns = $self->columns;
144            
145 0         0 for my $key (sort {$a <=> $b} keys %{$columns}) { #here we sort by key so columns show in the same order as they on the page
  0         0  
  0         0  
146 0         0 my $as_exists = undef;
147            
148             #if two keys, we assume user passed in AS as a key. We could check for as below in loop, but that limits users from having a column named AS
149 0 0 0     0 if(scalar(keys %{$columns->{$key}} == 2) and exists $columns->{$key}->{'AS'}) {
  0         0  
150 0         0 $as_exists = $columns->{$key}->{'AS'};
151 0         0 delete $columns->{$key}->{'AS'};
152             }
153            
154 0         0 while(my ($column,$table) = each %{$columns->{$key}}) {
  0         0  
155 0         0 my $column_name = "$table.$column";
156 0         0 push @{$aColumns}, $column_name;
  0         0  
157            
158 0 0       0 if($as_exists) {
159 0 0       0 $as_hash->{$column_name} = $as_exists if $as_exists; #add 'AS' value for this column if one exists
160 0         0 $column = $as_exists; # we want to change the column name to what it will be selected as out of database so we can do correct pattern matching
161             }
162            
163 0         0 $tables_hash->{$table} = 1;
164 0         0 push @{$regular_columns}, $column;
  0         0  
165             }
166             }
167            
168 0         0 my @tables = keys %$tables_hash;
169 0         0 $self->tables(\@tables);
170             }
171             elsif(UNIVERSAL::isa($self->columns,'ARRAY')) {
172 3         6 $aColumns = $self->columns;
173 3         5 $regular_columns = $aColumns;
174             }
175             else {
176 0         0 croak "columns must be a hash or an array ref";
177             }
178            
179 3         7 return ($aColumns,$regular_columns,$as_hash);
180             }
181            
182            
183            
184            
185             sub print_json {
186 0     0 1 0 my $self = shift;
187 0         0 my $json = $self->json;
188            
189 0         0 print "Content-type: application/json\n\n";
190 0         0 print $json;
191             }
192            
193            
194            
195            
196             sub table_data {
197 1     1 0 6 my $self = shift;
198            
199 1         4 my %all_query_parameters = $self->_get_query_parameters();
200            
201             # may croak if client_params isn't recognized as containing DataTables parameters
202 1         15 my $dt_req = $self->_create_datatables_request( \%all_query_parameters );
203            
204             # DB HANDLE
205 1         394 my $dbh = $self->{dbh};
206 1 50       6 croak "Database handle not defined" unless defined $dbh;
207            
208             #columns to use
209 1         5 my ($aColumns,$regular_columns,undef) = $self->_columns_arr;
210            
211             # check table name(s)
212 1 50       3 croak "Tables must be provided for the FROM clause" unless $self->tables;
213            
214             #filtering
215 1         4 my $where_href = $self->_generate_where_clause($dt_req);
216            
217             #ordering
218 1         5 my @order = $self->_generate_order_clause($dt_req);
219            
220             #paging
221 1   50     4 my $limit = $dt_req->length || 10;
222 1   50     23 my $offset = $dt_req->start || 0;
223            
224             #join
225 1 50       15 if($self->join_clause ne '') {
226 0         0 $where_href = $self->_add_where_clause($where_href, $self->join_clause);
227             }
228            
229             #SQL queries
230 1         16 my $sql = SQL::Abstract::Limit->new( limit_dialect => $dbh );
231            
232 1         321 my ($sQuery, @bind) = $sql->select($self->tables, $aColumns, $where_href, \@order, $limit, $offset );
233             #die("SQL: " . $sQuery);
234            
235             #get columns out of db with query we created
236 1         2006 my $result_sth = $dbh->prepare($sQuery);
237 1 50       458 $result_sth->execute(@bind) or croak "error in mysql query: $!\n$sQuery";
238            
239             # Data set length after filtering
240 1         7 my ($sQuery_cnt_filtered, @bind_cnt_filtered) = $sql->select($self->tables, 'COUNT(*)', $where_href );
241            
242 1         825 my $sth_cnt_filtered = $dbh->prepare($sQuery_cnt_filtered);
243 1 50       100 $sth_cnt_filtered->execute(@bind_cnt_filtered) or croak "mysql error: $!";
244            
245 1         14 my @aResultFilterTotal = $sth_cnt_filtered->fetchrow_array();
246 1         3 my $iFilteredTotal = $aResultFilterTotal[0];
247            
248            
249 1         2 my $num_tables = scalar(@{$self->tables});
  1         3  
250            
251 1         14 my ($sQuery_cnt_total, @bind_cnt_total) = $sql->select($self->tables, 'COUNT(*)');
252 1         787 my $sth_cnt_total = $dbh->prepare($sQuery_cnt_total);
253 1 50       85 $sth_cnt_total->execute() or croak "error in query: $!\n$sQuery";
254            
255 1         12 my @aResultTotal = $sth_cnt_total->fetchrow_array;
256 1         3 my $iTotal = $aResultTotal[0];
257            
258             # output hash
259 1         2 my %output = ();
260 1         4 my $sEcho = $dt_req->draw;
261 1         16 my $version = $dt_req->version( \%all_query_parameters );
262            
263 1         15 my $data_key_name = 'aaData'; # defaults to v1.9
264            
265 1 50       3 if( $version eq '1.10' ) {
266             # new interface
267            
268 0         0 $data_key_name = 'data';
269 0         0 %output = (
270             "draw" => int($sEcho),
271             "recordsTotal" => int($iTotal),
272             "recordsFiltered" => int($iFilteredTotal),
273             $data_key_name => [],
274             );
275            
276             }else{
277             # old interface
278            
279 1         3 $data_key_name = 'aaData';
280 1         7 %output = (
281             "sEcho" => int($sEcho),
282             "iTotalRecords" => int($iTotal),
283             "iTotalDisplayRecords" => int($iFilteredTotal),
284             $data_key_name => [],
285             );
286            
287             }
288            
289 1         2 my $count = 0;
290 1         4 my $patterns = $self->patterns;
291            
292 1         15 while(my @aRow = $result_sth->fetchrow_array) {
293 3         9 my @row = ();
294 3         9 for (my $i = 0; $i < @$aColumns; $i++) {
295 15         23 my $pat_name = $regular_columns->[$i]; #get out the name that would be used in the pattern
296 15         18 my $val = $aRow[$i];
297            
298             # apply user specified pattern for this column if one exists
299 15 50       26 if(exists $patterns->{$pat_name}) {
300 0         0 my $pattern = $patterns->{$pat_name};
301 0         0 $pattern =~ s/\[\%\s$pat_name\s\%\]/$val/g;
302 0         0 $val = $pattern;
303             }
304            
305 15         41 push @row, $val;
306             }
307            
308 3 50       14 if( $self->data_output_format eq 'column-id' ) {
309 3         8 @{$output{$data_key_name}}[$count] = [@row];
  3         7  
310             }else{
311 0         0 my %row = map { $aColumns->[$_] => $row[$_] } 0 .. $#row;
  0         0  
312 0         0 push @{$output{$data_key_name}}, \%row;
  0         0  
313             }
314            
315 3         38 $count++;
316             }
317            
318 1         42 return \%output;
319             } # /table_data
320            
321            
322            
323            
324             sub json {
325 0     0 1 0 my $self = shift;
326            
327 0         0 my $output_href = $self->table_data;
328            
329 0         0 return encode_json $output_href;
330             } # /json
331            
332            
333            
334            
335             sub _create_datatables_request {
336 1     1   3 my $self = shift;
337 1         2 my $query_params = shift;
338 1         12 return JQuery::DataTables::Request->new( client_params => $query_params );
339             }
340            
341            
342            
343            
344             sub _generate_where_clause {
345 1     1   3 my $self = shift;
346 1         2 my $dt_req = shift;
347            
348 1         3 my ($aColumns,undef,undef) = $self->_columns_arr;
349            
350 1         2 my $where_href = {};
351            
352 1 50 33     13 if( $dt_req->search && defined $dt_req->search->{value} ) {
353 0         0 my $search_string = $dt_req->search->{value}; # the global search value
354            
355             # XXX: maybe use $dt_req->columns()?
356 0         0 for( my $i = 0; $i < @$aColumns; $i++ ) {
357             # Iterate over each column and check if it is searchable.
358             # If so, add a constraint to the where clause restricting the given
359             # column. In the query, the column is identified by it's index, we
360             # need to translates the index to the column name.
361 0 0 0     0 if ( defined $dt_req->column($i) and $dt_req->column($i)->{searchable} ) {
362             # XXX: maybe use $dt_req->column($i)->{name}?
363 0         0 my $column = $aColumns->[$i];
364 0         0 push @{$where_href->{'-or'}}, { $column => {-like => '%'.$search_string.'%' } };
  0         0  
365             }
366             }
367             }
368            
369             # XXX: merge with previous loop
370             #individual column filtering
371 1         34 for (my $i = 0; $i < @$aColumns; $i++) {
372 5 50 66     164 if( defined $dt_req->column($i) and $dt_req->column($i)->{searchable}
      33        
      33        
373             and ($dt_req->column($i)->{search}->{value} and $dt_req->column($i)->{search}->{value} ne '') ) {
374 0         0 my $individual_column_search = $dt_req->column($i)->{search}->{value};
375 0         0 $where_href->{$aColumns->[$i]} = {-like => '%'.$individual_column_search.'%'};
376             }
377             }
378            
379             # add user where if given
380 1 50       16 if( $self->where_clause ) {
381 0         0 $where_href = $self->_add_where_clause($where_href, $self->where_clause);
382             }
383            
384 1         3 return $where_href;
385             } # /_generate_where_clause
386            
387            
388            
389            
390             #
391             # convert
392             # \%where = {key => value, -or => \@ }
393             # to
394             # \%where = {-and => [{key => value, -or => \@ }, $plus]}
395             #
396             # $plus can be a hashref for SQL::Abstract.
397             # $plus can also be scalarref (deprecated).
398             #
399            
400             sub _add_where_clause {
401 0     0   0 my $self = shift;
402 0 0       0 my $existing_clauses_href = shift or croak('Missing where clause');
403 0         0 my $new_clause = shift;
404            
405 0 0       0 return $existing_clauses_href unless $new_clause;
406            
407 0 0       0 if( UNIVERSAL::isa($new_clause, 'HASH') ) {
408             return {
409 0         0 -and => [
410             $existing_clauses_href,
411             $new_clause,
412             ],
413             };
414             }
415            
416             # Add arbitrary WHERE clause. This might be dangerous.
417             return {
418 0         0 -and => [
419             $existing_clauses_href,
420             \$new_clause
421             ],
422             };
423             } # /_add_where_clause
424            
425            
426            
427            
428             sub _generate_order_clause {
429 1     1   3 my $self = shift;
430 1         2 my $dt_req = shift;
431            
432 1         3 my ($aColumns,undef,undef) = $self->_columns_arr;
433            
434 1         2 my @order = ();
435            
436 1         2 foreach my $order_instruction ( @{$dt_req->orders()} ) {
  1         6  
437            
438             # build direction, must be '-asc' or '-desc' (cf. SQL::Abstract)
439             # we only get 'asc' or 'desc', so they have to be prefixed with '-'
440 0         0 my $sortable_column_nr = $order_instruction->{column};
441 0         0 my $direction = '-' . $order_instruction->{dir};
442            
443             # We only get the column index (starting from 0), so we have to
444             # translate the index into a column name.
445 0         0 my $column_name = $aColumns->[$sortable_column_nr];
446 0         0 push @order, { $direction => $column_name };
447            
448             }
449            
450 1         20 return @order;
451             } # /_generate_order_clause
452            
453            
454            
455            
456             sub _get_query_parameters {
457 1     1   3 my $self = shift;
458            
459             # CGI OBJECT
460 1         2 my $q = $self->{query};
461            
462             # TODO: available from Perl 5.20.0: get multiple key-value pairs in 1 request, e.g. my %new_hash = %hash{qw/a b/};
463             # XXX: encapsulate to make testing easier (re-use the encapsulated method in tests instead of custom code)
464 1         5 my %all_query_parameters = $q->Vars;
465            
466 1         120 return %all_query_parameters;
467             } # /_get_query_parameters
468            
469             1; # /DataTables
470            
471             __END__