File Coverage

lib/CatalystX/CRUD/Model/Utils.pm
Criterion Covered Total %
statement 100 116 86.2
branch 28 54 51.8
condition 11 39 28.2
subroutine 10 11 90.9
pod 3 3 100.0
total 152 223 68.1


line stmt bran cond sub pod time code
1             package CatalystX::CRUD::Model::Utils;
2 4     4   11219 use strict;
  4         10  
  4         126  
3 4     4   30 use warnings;
  4         8  
  4         117  
4 4     4   22 use base qw( CatalystX::CRUD Class::Accessor::Fast );
  4         7  
  4         2370  
5 4     4   17541 use Sort::SQL;
  4         1889  
  4         122  
6 4     4   2009 use Data::Pageset;
  4         16535  
  4         57  
7 4     4   2176 use Search::QueryParser::SQL;
  4         43630  
  4         124  
8 4     4   29 use Carp;
  4         17  
  4         6503  
9              
10             __PACKAGE__->mk_accessors(qw( use_ilike use_lower ne_sign ));
11              
12             our $VERSION = '0.58';
13              
14             =head1 NAME
15              
16             CatalystX::CRUD::Model::Utils - helpful methods for your CRUD Model class
17              
18             =head1 SYNOPSIS
19              
20             package MyApp::Model::Foo;
21             use base qw(
22             CatalystX::CRUD::Model
23             CatalystX::CRUD::Model::Utils
24             );
25             # ...
26             1;
27            
28             =head1 DESCRIPTION
29              
30             CatalystX::CRUD::Model::Utils provides helpful, non-essential methods
31             for CRUD Model implementations. Stick it in your @ISA to help reduce the
32             amount of code you have to write yourself.
33              
34             =head1 METHODS
35              
36             =head2 use_ilike( boolean )
37              
38             Convenience accessor to flag requests in params_to_sql_query()
39             to use ILIKE instead of LIKE SQL command.
40              
41             =head2 ne_sign( I<string> )
42              
43             What string to use for 'not equal' in params_to_sql_query().
44             Defaults to '!='.
45              
46             =head2 treat_like_int
47              
48             Should return a hashref of column names to treat as integers
49             instead of text strings when parsing wildcard request params. Example
50             might be all date/timestamp columns.
51              
52             =cut
53              
54             =head2 make_sql_query( [ I<field_names> ] )
55              
56             Returns a hashref suitable for passing to a SQL-oriented model.
57              
58             I<field_names> should be an array of valid column names.
59             If false or missing, will call $c->controller->field_names().
60              
61             The following reserved request param names are implemented:
62              
63             =over
64              
65             =item cxc-order
66              
67             Sort order. Should be a SQL-friendly string parse-able by Sort::SQL.
68              
69             =item cxc-sort
70              
71             Instead of cxc-order, can pass one column name to sort by.
72              
73             =item cxc-dir
74              
75             With cxc-sort, pass the direction in which to sort.
76              
77             =item cxc-page_size
78              
79             For the Data::Pageset pager object.
80             Defaults to page_size(). An upper limit of 200
81             is implemented by default to reduce the risk of
82             a user [unwittingly] creating a denial
83             of service situation.
84              
85             =item cxc-page
86              
87             What page the current request is coming from. Used to set the offset value
88             in the query. Defaults to C<1>.
89              
90             =item cxc-offset
91              
92             Pass explicit row to offset from in query. If not present, deduced from
93             cxc-page and cxc-page_size.
94              
95             =item cxc-no_page
96              
97             Ignore cxc-page_size, cxc-page and cxc-offset and do not return a limit
98             or offset value.
99              
100             =item cxc-op
101              
102             If set to C<OR> then the query columns will be marked as OR'd together,
103             rather than AND'd together (the default).
104              
105             =item cxc-query
106              
107             The query string to use. This overrides any param values set for
108             field names.
109              
110             =item cxc-query-fields
111              
112             Which field names to set as 'default_column' in the Search::QueryParser::SQL
113             parser object. The default is all I<field_names>. B<NOTE> this param is only
114             checked if C<cxc-query> has a value.
115              
116             =item cxc-fuzzy
117              
118             If set to a true value triggers the 'fuzzify' feature in
119             Search::QueryParser::SQL.
120              
121             =item cxc-fuzzy2
122              
123             If set to a true value, overrides cxc-fuzzy and triggers the 'fuzzify2'
124             feature in Search::QueryParser::SQL.
125              
126             =back
127              
128             =cut
129              
130             sub _which_sort {
131 6     6   30 my ( $self, $c ) = @_;
132 6         14 my $params = $c->req->params;
133              
134             # backwards compat
135 6         456 for my $p (qw( cxc-order _order )) {
136 11 100       41 return $params->{$p} if defined $params->{$p};
137             }
138              
139             # use explicit param
140 5         13 for my $p (qw( cxc-sort _sort )) {
141             my $dir = $params->{'cxc-dir'}
142 10   33     34 || $params->{'_dir'};
143             return join( ' ', $params->{$p}, uc($dir) )
144 10 50 33     25 if defined( $params->{$p} ) && defined($dir);
145             }
146              
147 5         16 my $pks = $c->controller->primary_key;
148 5 50       1254 return join( ' ', map { $_ . ' DESC' } ref $pks ? @$pks : ($pks) );
  5         45  
149             }
150              
151             sub make_sql_query {
152 6     6 1 2559 my $self = shift;
153 6         32 my $c = $self->context;
154             my $field_names
155             = shift
156 6   0     41 || $c->req->params->{'cxc-query-fields'}
157             || $c->controller->field_names($c)
158             || $self->throw_error("field_names required");
159              
160             # if present, param overrides default of form->field_names
161             # passed by base controller.
162 6 0 33     20 if ( exists $c->req->params->{'cxc-query-fields'}
163             && exists $c->req->params->{'cxc-query'} )
164             {
165 0         0 $field_names = $c->req->params->{'cxc-query-fields'};
166             }
167              
168 6 50       578 if ( !ref($field_names) ) {
169 0         0 $field_names = [$field_names];
170             }
171              
172 6         33 my $p2q = $self->params_to_sql_query($field_names);
173 6         36 my $params = $c->req->params;
174 6         549 my $sp = Sort::SQL->string2array( $self->_which_sort($c) );
175 6         281 my $s = join( ', ', map { join( ' ', %$_ ) } @$sp );
  7         36  
176 6   33     32 my $offset = $params->{'cxc-offset'} || $params->{'_offset'};
177             my $page_size
178             = $params->{'cxc-page_size'}
179 6   33     63 || $params->{'_page_size'}
180             || $c->controller->page_size
181             || $self->page_size;
182              
183             # don't let users DoS us. unless they ask to (see _no_page).
184 6 50       1515 $page_size = 200 if $page_size > 200;
185              
186 6   50     99 my $page = $params->{'cxc-page'} || $params->{'_page'} || 1;
187              
188 6 50       27 if ( !defined($offset) ) {
189 6         17 $offset = ( $page - 1 ) * $page_size;
190             }
191              
192             # normalize since some ORMs require UPPER case
193 6         28 $s =~ s,\b(asc|desc)\b,uc($1),eg;
  0         0  
194              
195             my %query = (
196             query => $p2q->{sql},
197             sort_by => $s,
198             limit => $page_size,
199             offset => $offset,
200             sort_order => $sp,
201             plain_query => $p2q->{query_hash},
202             plain_query_str => (
203             $p2q->{query}
204             ? $p2q->{query}->stringify
205             : ''
206             ),
207             query_obj => $p2q->{query},
208 6 50       35 );
209              
210             # undo what we've done if asked.
211 6 50       9592 if ( $params->{'cxc-no_page'} ) {
212 0         0 delete $query{limit};
213 0         0 delete $query{offset};
214             }
215              
216 6         29 return \%query;
217              
218             }
219              
220             =head2 params_to_sql_query( I<field_names> )
221              
222             Convert request->params into a SQL-oriented
223             query.
224              
225             Returns a hashref with three key/value pairs:
226              
227             =over
228              
229             =item sql
230              
231             Arrayref of ORM-friendly SQL constructs.
232              
233             =item query_hash
234              
235             Hashref of column_name => raw_values_as_arrayref.
236              
237             =item query
238              
239             The Search::QueryParser::SQL::Query object used
240             to generate C<sql>.
241              
242             =back
243              
244             Called internally by make_sql_query().
245              
246             =cut
247              
248             sub params_to_sql_query {
249 6     6 1 19 my ( $self, $field_names ) = @_;
250 6 50 33     47 croak "field_names ARRAY ref required"
251             unless defined $field_names
252             and ref($field_names) eq 'ARRAY';
253 6         20 my $c = $self->context;
254 6         20 my ( @sql, %pq );
255 6   50     126 my $ne = $self->ne_sign || '!=';
256 6 50       168 my $like = $self->use_ilike ? 'ilike' : 'like';
257 6 50       75 my $treat_like_int
258             = $self->can('treat_like_int') ? $self->treat_like_int : {};
259 6         21 my $params = $c->req->params;
260 6   50     520 my $oper = $params->{'cxc-op'} || $params->{'_op'} || 'AND';
261 6   50     34 my $fuzzy = $params->{'cxc-fuzzy'} || $params->{'_fuzzy'} || 0;
262 6   50     19 my $fuzzy2 = $params->{'cxc-fuzzy2'} || 0;
263              
264 6         12 my %columns;
265 6         15 for my $fn (@$field_names) {
266 12 50       34 $columns{$fn} = exists $treat_like_int->{$fn} ? 'int' : 'char';
267             }
268              
269 6         13 my ( @param_query, @default_columns );
270              
271             # if cxc-query is present, prefer that.
272             # otherwise, any params matching those in $field_names
273             # are each parsed as individual queries, serialized and joined
274             # with $oper.
275             # cxc-query should be free from sql-injection attack as
276             # long as Models use 'sql' or 'query'->dbi in returned hashref
277 6 100       12 if ( exists $params->{'cxc-query'} ) {
278             my $q
279             = ref $params->{'cxc-query'}
280             ? $params->{'cxc-query'}->[0]
281 2 50       8 : $params->{'cxc-query'};
282              
283 2 50       7 if ( exists $params->{'cxc-query-fields'} ) {
284             @default_columns
285             = ref $params->{'cxc-query-fields'}
286 0         0 ? @{ $params->{'cxc-query-fields'} }
287 0 0       0 : ( $params->{'cxc-query-fields'} );
288              
289             }
290              
291 2         7 @param_query = ($q);
292 2         5 $pq{'cxc-query'} = \@param_query;
293              
294             }
295             else {
296 4         11 for (@$field_names) {
297 8 100       1437 next unless exists $params->{$_};
298             my @v
299 7 100       30 = ref $params->{$_} ? @{ $params->{$_} } : ( $params->{$_} );
  4         17  
300              
301 7         14 grep {s/\+/ /g} @v; # TODO URI + for space -- is this right?
  11         33  
302              
303 7         25 $pq{$_} = \@v;
304              
305 7 50       18 next unless grep {m/\S/} @v;
  11         51  
306              
307             # we don't want to "double encode" $like
308             # or $use_lower because it will
309             # be re-parsed as a word not an op, so we have a modified
310             # parser for per-field queries.
311 7         60 my %args = (
312             like => '=',
313             fuzzify => $fuzzy,
314             columns => \%columns,
315             strict => 1,
316             rxOp => qr/==|<=|>=|!=|=~|!~|=|<|>|~/,
317             );
318 7 50       23 if ($fuzzy2) {
319 0         0 delete $args{fuzzify};
320 0         0 $args{fuzzify2} = 1;
321             }
322 7         51 my $parser = Search::QueryParser::SQL->new(%args);
323              
324 7         3372 my $query;
325 7         11 eval {
326 7         61 $query = $parser->parse( "$_ = (" . join( ' ', @v ) . ')' );
327             };
328 7 50       2155 return $self->throw_error($@) if $@;
329              
330 7         30 push @param_query, $query->stringify;
331             }
332             }
333              
334             #Carp::carp Data::Dump::dump \@param_query;
335              
336 6         995 my $joined_query = join( ' ', @param_query );
337 6         20 my $sql = [];
338 6         11 my $query = '';
339              
340 6 50       24 if ( length $joined_query ) {
341              
342 6 50       154 my %args = (
343             like => $like,
344             fuzzify => $fuzzy,
345             lower => $self->use_lower,
346             columns => \%columns,
347             default_column => (
348             @default_columns
349             ? \@default_columns
350             : [ keys %columns ]
351             ),
352             strict => 1,
353             rxOp => qr/==|<=|>=|!=|=~|!~|=|<|>|~/,
354              
355             );
356 6 50       128 if ($fuzzy2) {
357 0         0 delete $args{fuzzify};
358 0         0 $args{fuzzify2} = 1;
359             }
360 6         36 my $parser = Search::QueryParser::SQL->new(%args);
361              
362             # must eval and re-throw since we run under strict
363 6         2323 eval { $query = $parser->parse( $joined_query, uc($oper) eq 'AND' ); };
  6         24  
364 6 50       2476 return $self->throw_error($@) if $@;
365              
366 6         28 $sql = $query->rdbo;
367              
368             }
369              
370             #Carp::carp Data::Dump::dump $sql;
371              
372             return {
373 6         3036 sql => $sql,
374             query => $query,
375             query_hash => \%pq,
376             };
377             }
378              
379             =head2 make_pager( I<total> )
380              
381             Returns a Data::Pageset object using I<total>,
382             either the C<_page_size> param or the value of page_size(),
383             and the C<_page> param or C<1>.
384              
385             If the C<_no_page> request param is true, will return undef.
386             B<NOTE:> Model authors should check (and respect) the C<_no_page>
387             param when constructing queries.
388              
389             =cut
390              
391             sub make_pager {
392 0     0 1   my ( $self, $count ) = @_;
393 0           my $c = $self->context;
394 0           my $params = $c->req->params;
395 0 0 0       return if ( $params->{'cxc-no_page'} or $params->{'_no_page'} );
396             return Data::Pageset->new(
397             { total_entries => $count,
398             entries_per_page => $params->{'cxc-page_size'}
399             || $params->{'_page_size'}
400             || $c->controller->page_size
401             || $self->page_size,
402             current_page => $params->{'cxc-page'}
403 0   0       || $params->{'_page'}
      0        
404             || 1,
405             pages_per_set => 10, #TODO make this configurable?
406             mode => 'slide',
407             }
408             );
409             }
410              
411             1;
412              
413             __END__
414              
415             =head1 AUTHOR
416              
417             Peter Karman, C<< <perl at peknet.com> >>
418              
419             =head1 BUGS
420              
421             Please report any bugs or feature requests to
422             C<bug-catalystx-crud at rt.cpan.org>, or through the web interface at
423             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CatalystX-CRUD>.
424             I will be notified, and then you'll automatically be notified of progress on
425             your bug as I make changes.
426              
427             =head1 SUPPORT
428              
429             You can find documentation for this module with the perldoc command.
430              
431             perldoc CatalystX::CRUD
432              
433             You can also look for information at:
434              
435             =over 4
436              
437             =item * AnnoCPAN: Annotated CPAN documentation
438              
439             L<http://annocpan.org/dist/CatalystX-CRUD>
440              
441             =item * CPAN Ratings
442              
443             L<http://cpanratings.perl.org/d/CatalystX-CRUD>
444              
445             =item * RT: CPAN's request tracker
446              
447             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=CatalystX-CRUD>
448              
449             =item * Search CPAN
450              
451             L<http://search.cpan.org/dist/CatalystX-CRUD>
452              
453             =back
454              
455             =head1 COPYRIGHT & LICENSE
456              
457             Copyright 2007 Peter Karman, all rights reserved.
458              
459             This program is free software; you can redistribute it and/or modify it
460             under the same terms as Perl itself.
461              
462             =cut