File Coverage

blib/lib/RapidApp/Module/StorCmp/Role/DbicLnk.pm
Criterion Covered Total %
statement 407 1116 36.4
branch 91 530 17.1
condition 38 292 13.0
subroutine 60 124 48.3
pod 0 50 0.0
total 596 2112 28.2


line stmt bran cond sub pod time code
1             package RapidApp::Module::StorCmp::Role::DbicLnk;
2              
3 5     5   1018 use strict;
  5         16  
  5         174  
4 5     5   36 use warnings;
  5         11  
  5         156  
5              
6 5     5   450 use Moose::Role;
  5         456094  
  5         47  
7             requires 'record_pk';
8              
9              
10             # Copied from (RapidApp::)Role::DbicLink2
11              
12              
13 5     5   28093 use RapidApp::Util qw(:all);
  5         16  
  5         2975  
14 5     5   2612 use RapidApp::TableSpec::DbicTableSpec;
  5         20  
  5         229  
15 5     5   50 use Clone qw(clone);
  5         12  
  5         337  
16 5     5   38 use Text::Glob qw( match_glob );
  5         14  
  5         219  
17 5     5   3817 use Text::TabularDisplay;
  5         6418  
  5         211  
18 5     5   37 use Time::HiRes qw(gettimeofday tv_interval);
  5         11  
  5         51  
19 5     5   3103 use RapidApp::Data::Dmap qw(dmap);
  5         16  
  5         283  
20 5     5   39 use URI::Escape;
  5         13  
  5         306  
21 5     5   33 use Scalar::Util qw(looks_like_number);
  5         10  
  5         198  
22 5     5   2266 use Digest::SHA1;
  5         4266  
  5         248  
23 5     5   5218 use DateTime;
  5         2183007  
  5         355  
24             require RapidApp::DBIC::Component::TableSpec;
25              
26 5     5   3014 use DBI::Const::GetInfoType '%GetInfoType';
  5         29981  
  5         20132  
27              
28             if($ENV{DBIC_TRACE}) {
29             debug_around 'DBIx::Class::Storage::DBI::_execute', newline => 1, stack=>20;
30             }
31              
32             our $append_exception_title = '';
33              
34             # This allows supplying custom BUILD code via a constructor:
35             has 'onBUILD', is => 'ro', isa => 'Maybe[CodeRef]', default => undef;
36              
37             has 'get_record_display' => ( is => 'ro', isa => 'CodeRef', lazy => 1, default => sub {
38             my $self = shift;
39             return $self->TableSpec->get_Cnf('row_display');
40             });
41              
42             # Useful for pages that display only the content of a single database record at a time.
43             # When set to true, rows are limited to "1" in the ResultSet in read_records and the
44             # pager is not used to perform the second query to get the total count
45             has 'single_record_fetch', is => 'ro', isa => 'Bool', default => 0;
46              
47              
48             # Colspec attrs can be specified as simple arrayrefs. Defaults to all local columns
49             has 'include_colspec' => ( is => 'ro', isa => 'ArrayRef[Str]', default => sub {['*']} );
50             has 'relation_sep' => ( is => 'ro', isa => 'Str', default => '__' );
51              
52             has 'updatable_colspec' => ( is => 'ro', isa => 'Maybe[ArrayRef[Str]]', default => undef );
53             has 'creatable_colspec' => ( is => 'ro', isa => 'Maybe[ArrayRef[Str]]', default => undef );
54              
55             # Specify a list of relspecs to enable record destroy anmd specify which related rows
56             # should also be destroyed. For the base rel only, '*', specify other rels by name
57             # NOTE: This is simular in principle, but NOT the same as the colspecs. There is currently
58             # no real logic in this, no wildcard support, etc. It is just a list of relationship names
59             # that will be followed and be deleted along with the base. BE CAREFUL! This will delete whole
60             # sets of related rows. Most of the time you'll only want to put '*' in here
61             has 'destroyable_relspec' => ( is => 'ro', isa => 'Maybe[ArrayRef[Str]]', default => undef );
62              
63             # New: List of relationship names to auto-create if they don't exist during an UPDATE
64             # TODO: make this a 'relspec' format like 'destroyable_relspec' above
65             has 'update_create_rels', is => 'ro', isa => 'ArrayRef[Str]', default => sub {[]};
66              
67             # These columns will always be fetched regardless of whether or not they were requested
68             # by the client:
69             has 'always_fetch_colspec' => ( is => 'ro', isa => 'Maybe[ArrayRef[Str]]', default => undef );
70              
71             # quicksearch_mode: either 'like' or 'exact' - see chain_Rs_req_quicksearch()
72             # currently any value other than 'exact' is treated like 'like', the default and
73             # original behavior.
74             # TODO: add 'phrases' mode to act like google searches with +/- and quotes around phrases
75             has 'quicksearch_mode', is => 'ro', isa => 'Str', default => 'like';
76              
77             # Define if the user/client is allowed to specify the quicksearch_mode:
78             has 'allow_set_quicksearch_mode', is => 'ro', isa => 'Bool', default => 1;
79              
80              
81             # If natural_column_order is true (default) columns will be ordered according to the real
82             # database/schema order, otherwise, order is based on the include_colspec
83             has 'natural_column_order', is => 'ro', isa => 'Bool', default => 1;
84              
85             # Whether or not to pull the record key from the url/args for the query.
86             # This only makes sense in the context of a single row view, not a set/grid
87             has 'allow_restful_queries', is => 'ro', isa => 'Bool', default => 0;
88              
89             # Expose the DatStor 'reload_on_save' option so the user can turn it on
90             # TODO - this is a stop-gap until the DatStor as a separate module design
91             # can be refactored to make it easier to apply store aoptions in general
92             has 'reload_on_save', is => 'ro', isa => 'Bool', default => 0;
93              
94             # Passed into TableSpec objects -- disables the default behaviour which
95             # transforms the header of related columns by appending the relationship path
96             has 'no_header_transform', is => 'ro', isa => 'Bool', default => 0;
97              
98             # If set to true, the component will try to close itself after a delete/destroy. Only
99             # makes sense in the context of a single record view, and only works with standard tabs
100             has 'close_on_destroy', is => 'ro', isa => 'Bool', traits => ['ExtProp'], default => 0;
101              
102             # If set to true, every time the component is shown (i.e. re-activating the tab) the
103             # store will reload itself to refresh data.
104             has 'reload_on_show', is => 'ro', isa => 'Bool', , traits => ['ExtProp'], default => 0;
105              
106             # Generate a param string unique to this module by URL/path. This only needs to be unique
107             # among modules whose ->content may be rendered within the same request, which is only
108             # being done for good measure
109             has '_rst_qry_param', is => 'ro', isa => 'Str', lazy => 1, default => sub {
110             my $self = shift;
111             join('_',
112             'rst_qry',
113             substr(Digest::SHA1->new->add($self->base_url)->hexdigest, 0, 5)
114             );
115             };
116             sub _appl_base_params {
117 0     0   0 my ($self, $params) = @_;
118 0         0 my $c = $self->c;
119            
120 0         0 %{$c->req->params} = ( %{$c->req->params}, %$params );
  0         0  
  0         0  
121            
122 0   0     0 my $baseParams = $self->DataStore->get_extconfig_param('baseParams') || {};
123 0         0 %$baseParams = ( %$baseParams, %$params );
124 0         0 $self->DataStore->apply_extconfig( baseParams => $baseParams );
125             }
126             sub _appl_rst_qry {
127 0     0   0 my ($self, $val) = @_;
128 0         0 $self->_appl_base_params({ $self->_rst_qry_param => $val });
129             }
130             sub _retr_rst_qry {
131 8     8   25 my $self = shift;
132 8 50       51 my $c = RapidApp->active_request_context or return undef;
133 8 50       41 my $rst_qry = $c->req->params->{ $self->_rst_qry_param } or return undef;
134            
135             # Re-apply the rst_qry now to make sure there is not a caching issue
136             # in the DataStore baseParams in case the normal rest logic doesn't
137             # do this, which is the case when launched from a foreign component
138             # by setting rest_args in the stash
139 0         0 $self->_appl_rst_qry( $rst_qry );
140            
141 0         0 $rst_qry
142             }
143              
144              
145             has 'ResultSource' => (
146             is => 'ro',
147             isa => 'DBIx::Class::ResultSource',
148             required => 1
149             );
150              
151             has 'get_ResultSet' => ( is => 'ro', isa => 'CodeRef', lazy => 1, default => sub {
152             my $self = shift;
153             return sub { $self->ResultSource->resultset };
154             });
155              
156             sub baseResultSet {
157 11     11 0 36 my $self = shift;
158 11         413 return $self->get_ResultSet->(@_);
159             }
160              
161             sub _ResultSet {
162 8     8   31 my $self = shift;
163            
164 8         43 my $p = $self->c->req->params;
165 8 0 33     791 if($p->{rs_path} && $p->{rs_method}) {
166 0 0       0 my $Module = $self->get_Module($p->{rs_path}) or die "Failed to get module at $p->{rs_path}";
167 0         0 return $Module->_resolve_rel_obj_method($p->{rs_method});
168             }
169            
170 8         58 my $Rs = $self->baseResultSet(@_);
171            
172             # the order of when this is called is vitally important:
173 8         4140 $self->prepare_rest_request;
174            
175 8 50       68 if(my $rst_qry = $self->_retr_rst_qry) {
176 0         0 my ($key,$val) = split(/\//,$rst_qry,2);
177 0         0 $Rs = $self->chain_Rs_REST($Rs,$key,$val);
178             }
179              
180 8 50       152 $Rs = $self->ResultSet($Rs) if ($self->can('ResultSet'));
181 8         31 return $Rs;
182             }
183              
184             sub chain_Rs_REST {
185 0     0 0 0 my ($self,$Rs,$key,$val) = @_;
186 0 0       0 if ($key =~ /\./) {
187             # if there is a '.' in the key name, assume it means 'rel.col', and
188             # try to add the join for 'rel':
189 0         0 my ($rel) = split(/\./,$key,2);
190 0 0       0 $Rs = $self->_chain_search_rs($Rs,undef,{ join => $rel })
191             if ($self->ResultSource->has_relationship($rel));
192             }
193             else {
194 0         0 $key = 'me.' . $key;
195             }
196 0         0 return $self->_chain_search_rs($Rs,{ $key => $val });
197             }
198              
199             has 'get_CreateData' => ( is => 'ro', isa => 'CodeRef', lazy => 1, default => sub {
200             my $self = shift;
201             return sub { {} };
202             });
203              
204             sub baseCreateData {
205 0     0 0 0 my $self = shift;
206 0         0 return $self->get_CreateData->(@_);
207             }
208              
209             sub _CreateData {
210 0     0   0 my $self = shift;
211 0         0 my $data = $self->baseCreateData(@_);
212 0 0       0 $data = $self->CreateData($data) if ($self->can('CreateData'));
213              
214 0 0       0 if(my $lock_keys = $self->_get_rs_lock_keys) {
215 0 0       0 $data = { %{ $data || {} }, %$lock_keys }
  0         0  
216             }
217              
218 0         0 return $data;
219             }
220              
221             #sub _ResultSet {
222             # my $self = shift;
223             # my $Rs = $self->ResultSource->resultset;
224             # $Rs = $self->ResultSet($Rs) if ($self->can('ResultSet'));
225             # return $Rs;
226             #}
227              
228             has 'ResultClass' => ( is => 'ro', lazy_build => 1 );
229             sub _build_ResultClass {
230 92     92   267 my $self = shift;
231 92         2994 my $source_name = $self->ResultSource->source_name;
232 92         2749 return $self->ResultSource->schema->class($source_name);
233             }
234              
235             has 'TableSpec' => ( is => 'ro', isa => 'RapidApp::TableSpec', lazy_build => 1 );
236             sub _build_TableSpec {
237 92     92   248 my $self = shift;
238              
239 92         3065 my $table = RapidApp::DBIC::Component::TableSpec::_table_name_safe($self->ResultClass->table);
240 92         3305 my %opt = (
241             name => $table,
242             relation_sep => $self->relation_sep,
243             ResultSource => $self->ResultSource,
244             include_colspec => $self->include_colspec,
245             no_header_transform => $self->no_header_transform
246             );
247            
248 92 100       3230 $opt{updatable_colspec} = $self->updatable_colspec if (defined $self->updatable_colspec);
249 92 100       3147 $opt{creatable_colspec} = $self->creatable_colspec if (defined $self->creatable_colspec);
250 92 50       3471 $opt{always_fetch_colspec} = $self->always_fetch_colspec if (defined $self->always_fetch_colspec);
251              
252 92 50 33     2612 if (!exists $opt{cache} && $self->app->rapidApp->use_cache) {
253 92         2243 $opt{cache} = $self->app->rapidApp->cache;
254             }
255              
256 92         3559 my $TableSpec = RapidApp::TableSpec::DbicTableSpec->new(%opt);
257              
258 92 50       3304 $TableSpec->apply_natural_column_order if ($self->natural_column_order);
259            
260 92         3195 return $TableSpec;
261             #return RapidApp::TableSpec->with_traits('RapidApp::TableSpec::Role::DBIC')->new(%opt);
262             }
263              
264              
265             has 'record_pk' => ( is => 'ro', isa => 'Str', default => '___record_pk' );
266             has 'primary_columns_sep' => ( is => 'ro', isa => 'Str', default => '~$~' );
267             has 'primary_columns' => ( is => 'ro', isa => 'ArrayRef[Str]', lazy => 1, default => sub {
268             my $self = shift;
269            
270             # If the db has no primary columns, then we have to use ALL the columns:
271             unless ($self->ResultSource->primary_columns > 0) {
272             my $class = $self->ResultSource->schema->class($self->ResultSource->source_name);
273             local $SIG{__WARN__} = sub {}; # GitHub Issue #167 - TODO/FIXME
274             $class->set_primary_key( $self->ResultSource->columns );
275             $self->ResultSource->set_primary_key( $self->ResultSource->columns );
276             }
277            
278             my @cols = $self->ResultSource->primary_columns;
279            
280             $self->apply_extconfig( primary_columns => [ $self->record_pk, @cols ] );
281              
282             return \@cols;
283             });
284              
285              
286             sub generate_record_pk_value {
287 10     10 0 34 my $self = shift;
288 10         22 my $data = shift;
289 10 50       52 die "generate_record_pk_value(): expected hashref arg" unless (ref($data) eq 'HASH');
290             return join(
291             $self->primary_columns_sep,
292             #map { defined $data->{$_} ? "'" . $data->{$_} . "'" : 'undef' } @{$self->primary_columns}
293 10 50       445 map { defined $data->{$_} ? $data->{$_} : 'undef' } @{$self->primary_columns}
  10         314  
  10         341  
294             );
295             }
296              
297             # reverse generate_record_pk_value:
298             sub record_pk_cond {
299 6     6 0 18 my $self = shift;
300 6         19 my $value = shift;
301            
302 6         247 my $sep = quotemeta $self->primary_columns_sep;
303 6         60 my @parts = split(/${sep}/,$value);
304            
305 6         21 my %cond = ();
306 6         17 foreach my $col (@{$self->primary_columns}) {
  6         214  
307 6         21 my $val = shift @parts;
308 6 50       29 if ($val eq 'undef') {
309 0         0 $val = undef;
310             }
311             else {
312 6         34 $val =~ s/^\'//;
313 6         22 $val =~ s/\'$//;
314             }
315             # To force an *exact* match when col is a number, have to use LIKE because of the problem described here:
316             #http://stackoverflow.com/questions/8570884/mysql-where-exact-match
317             # Otherwise '1833sdfsdf' will match just like '1833'. But LIKE is slow!!! This is lame!
318             #$cond{'me.' . $col} = { 'LIKE' => $val };
319 6         35 $cond{'me.' . $col} = $val;
320             }
321            
322 6         48 return \%cond;
323             }
324              
325              
326             # --- Handle RESTful URLs - convert 'id/1234' into '?___record_pk=1234'
327             #has 'restful_record_pk_alias', is => 'ro', isa => 'Str', default => '_id';
328             sub prepare_rest_request {
329 8     8 0 28 my $self = shift;
330 8 50       396 return unless ($self->allow_restful_queries);
331            
332             # New: allow override pf rest args from stash:
333 0         0 my $stash_args = $self->c->stash->{rest_args};
334 0 0       0 my @args = $stash_args ? @$stash_args : $self->local_args;
335            
336 0         0 $_ = uri_unescape($_) for (@args);
337            
338 0         0 my @rargs = reverse @args;
339            
340             # ignore paths that match store CRUD actions (store/create, store/read, store/update or store/destroy)
341             # (TODO: what happens on the off chance that there is a key named 'store' and a value named 'read'?)
342 0         0 my @crud = qw(create read update destroy);
343 0         0 my %crudI = map {$_=>1} @crud;
  0         0  
344             return if (
345             $rargs[0] && $rargs[1] &&
346             $rargs[1] eq 'store' &&
347 0 0 0     0 $crudI{$rargs[0]}
      0        
      0        
348             );
349            
350             # -- peel off the 'rel' (relationship) args if present:
351 0         0 my $rel;
352 0 0       0 if(scalar @args > 2) {
353 0 0 0     0 if(lc($rargs[1]) eq 'rel' || lc($rargs[1]) eq 'rs') {
354 0         0 $rel = pop @args;
355 0         0 pop @args;
356             }
357             }
358             # --
359            
360             # --- Handle and assume extra args are values containing '/'
361 0 0       0 if(scalar @args > 1) {
362 0         0 my @newargs = (shift @args);
363 0 0 0     0 if (scalar @args > 0 && $self->ResultSource->has_column($newargs[0])) {
364 0         0 push @newargs, join('/',@args);
365             }
366             else {
367 0         0 @newargs = (join('/',@newargs,@args));
368             }
369 0         0 @args = @newargs;
370             }
371             # ---
372            
373 0 0       0 return unless defined $args[0];
374 0         0 my $key = "$args[0]";
375 0         0 my $val = $args[1];
376            
377             # Ignore paths that are submodules or actions:
378 0 0 0     0 return if (exists $self->modules_obj->{$key} || $self->has_action($key));
379            
380             # if there was only 1 argument, treat it as the value and set the default key/pk:
381 0 0       0 unless (defined $val) {
382 0         0 $val = $args[0];
383 0     0   0 my $rest_key_column = try{$self->ResultClass->getRestKey};
  0         0  
384 0   0     0 $key = $rest_key_column || $self->record_pk;
385             }
386            
387             # This should never happen any more (see "Handle and assume..." above):
388 0 0       0 die usererr "Too many args in RESTful URL (" . join('/',@args) . ") - should be 2 (i.e. 'id/1234')"
389             if(scalar @args > 2);
390            
391 0 0       0 return $self->redirect_handle_rest_rel_request($key,$val,$rel) if ($rel);
392            
393             # Apply default tabTitle: (see also 'getTabTitle' in DbicRowPage)
394 0 0       0 $self->apply_extconfig( tabTitle => ($key eq $self->record_pk ? 'Id' : $key ) . '/' . $val );
395            
396             # ---
397             # Update both the params of the active request, in place, as well as updating the baseParams
398             # of the store for the subsequent read request:
399             # TODO: '___record_pk' and 'rest_query' params are handled in different places in the subsequent
400             # read request. '___record_pk' pre-dates the REST functionality and is only handled in DbicAppPropertyPage
401             # (see the req_Row and and supplied_id methods in that class) while 'rest_query' is handled by
402             # all modules with the DbicLink2 role. Need to consolidate these in DbicLink2 so this all happens in
403             # the same place
404 0 0       0 if($key eq $self->record_pk) {
405 0         0 $self->_appl_base_params({$key => $val});
406             }
407             else {
408 0         0 $self->_appl_rst_qry( join('/',$key,$val) );
409             }
410             # ---
411            
412             }
413              
414              
415             sub restGetRow {
416 0     0 0 0 my ($self,$key,$val) = @_;
417            
418 0         0 my $Rs = $self->chain_Rs_REST($self->baseResultSet,$key,$val);
419            
420             # TODO: currently duplicated in DbicAppPropertyPage... it should defer to here
421 0         0 my $count = $Rs->count;
422              
423 0 0       0 die usererr "Record not found by '$key/$val'", title => 'Record not found'
424             unless ($count);
425            
426 0 0       0 die usererr $count . " records match '$key/$val'", title => 'Multiple records match'
427             if($count > 1);
428              
429 0         0 return $Rs->first;
430             }
431              
432             # This is designed to be called from *another* module to resolve a ResultSet
433             # object via arbitrary 'rs_method' path spec
434             sub _resolve_rel_obj_method {
435 0     0   0 my ($self, $rs_method) = @_;
436            
437             # New: Parse like this in case the middle $val contains '/'
438 0         0 my @parts = split('/',$rs_method);
439 0         0 my $key = shift @parts;
440 0         0 my $rel = pop @parts;
441 0         0 my $val = join('/',@parts);
442             #my ($key,$val,$rel) = split('/',$rs_method,3);
443            
444 0         0 my $Row = $self->restGetRow($key,$val);
445 0 0       0 die usererr "No such relationship $rel at ''$rs_method''" unless ($Row->has_relationship($rel));
446 0 0       0 return wantarray ? (scalar $Row->$rel, $Row) : $Row->$rel;
447             }
448              
449             sub redirect_handle_rest_rel_request {
450 0     0 0 0 my ($self,$key,$val,$rel) = @_;
451 0         0 my $c = $self->c;
452            
453 0         0 my $mth_path = join('/',$key,$val,$rel);
454 0         0 my ($RelObj, $Row) = $self->_resolve_rel_obj_method($mth_path);
455 0         0 my $Src = $RelObj->result_source;
456 0         0 my $class = $Src->schema->class($Src->source_name);
457            
458             $c->stash->{apply_extconfig} = {
459 0         0 tabTitle => "[$key/$val] $rel"
460             };
461            
462 0 0       0 if($RelObj->isa('DBIx::Class::ResultSet')) {
463 0     0   0 my $url = try{$class->TableSpec_get_conf('open_url_multi')}
464 0 0       0 or die usererr "No path (open_url_multi) defined to render Result Class: $class";
465              
466 0         0 my $p = {
467             rs_path => $self->module_path,
468             rs_method => join('/',$key,$val,$rel)
469             };
470              
471             # ---
472             # New: For the case of a multi-relationship, attempt to resolve the reverse
473             # relationship (i.e. the belongs_to) and set the new 'rs_lock_keys' info to
474             # declare to the target Module the fk value that must be maintained and
475             # enforced for this relationship. This is then used when adding new records
476             # and editability of the linking relationship is disabled.
477 0 0       0 if(my $rev_rel_info = $Row->result_source->reverse_relationship_info($rel)) {
478 0         0 my ($rev_rel, $info) = %$rev_rel_info;
479 0 0 0     0 if($info && $info->{cond}) {
480 0         0 require RapidApp::DBIC::Component::TableSpec;
481             my $cdta = RapidApp::DBIC::Component::TableSpec
482 0   0     0 ->parse_relationship_cond($info->{cond}) || {};
483 0         0 my @pks = $Row->result_source->primary_columns;
484 0 0 0     0 if(scalar(@pks) == 1 && $cdta->{self} && $cdta->{foreign} && $pks[0] eq $cdta->{foreign}) {
      0        
      0        
485             $p->{rs_lock_keys} = $self->json->encode({
486 0         0 $cdta->{self} => $val,
487             $rev_rel => $val,
488             });
489             }
490             }
491             }
492             # ---
493              
494 0         0 %{$c->req->params} = ( %$p, base_params => $self->json->encode( $p ) );
  0         0  
495 0         0 $c->root_module_controller->approot($c,$url);
496 0         0 return $c->detach;
497             }
498             else {
499            
500             # New: here we are actually dispatching to the page for the single rel, but still
501             # within the rest URL of the rel path. Ideally, for this case we would *redirect*
502             # to the actual REST URL for thsi object, whatever it may be. In order to do this,
503             # support for redirects needs to be added to the autopanel/hashnav stuff on the
504             # client side. In the meantime, rendering the real/actual row page, albeit at an
505             # alias (but still totally valid) url path is the best choice
506            
507 0     0   0 my $url = try{$RelObj->getRestPath};
  0         0  
508 0 0       0 if($url) {
509             # Simulate the rest_args for proper handling of the remote DbicLink
510             # request to operate under the current, alias URL:
511 0         0 $self->c->stash->{rest_args} = [$RelObj->getRestKey,$RelObj->getRestKeyVal];
512 0         0 $c->root_module_controller->approot($c,$url);
513 0         0 return $c->detach;
514             }
515             else {
516             # This is just a fallback - TODO: use a better error msg...
517             die usererr rawhtml join('',
518             "Relationship at '$mth_path' is not a ResultSet, it is a Row",
519 0     0   0 try{join(''," (",
520             '<i><b style="color:darkblue;font-size:0.9em;">',
521             $RelObj->displayWithLink,'</b></i>',
522             ')')},
523 0         0 ), title => 'Not a multi relationship';
524             }
525             }
526             }
527              
528              
529             sub _get_rs_lock_keys {
530 24     24   60 my $self = shift;
531              
532 24 50       115 my $c = RapidApp->active_request_context or return undef;
533 24 50       109 my $lk_enc = $c->req->params->{rs_lock_keys} or return undef;
534 0     0   0 try{$self->json->decode($lk_enc)}
535 0         0 }
536              
537              
538             before 'store_init_onrequest' => sub {
539             my $self = shift;
540              
541             if(my $lock_keys = $self->_get_rs_lock_keys) {
542             my @cols = ();
543             for my $name (keys %$lock_keys) {
544             my $Column = $self->get_column($name) or next;
545             push @cols, $name;
546             # Set the default value of the editor to the locked key value, and then
547             # set to 'disabled' to prevent the user from changing it
548             my $editor = $Column->{editor} or next;
549             $editor->{value} = $lock_keys->{$name};
550             $editor->{disabled} = \1;
551             }
552             if(scalar(@cols) > 0) {
553             # Sets each locked fk to be the first column (top of teh add form), and not editable
554             $self->apply_columns_ordered(0, map { $_ => {
555             allow_edit => \0
556             }} @cols );
557             }
558             }
559             };
560              
561              
562             # ---
563              
564       0 0   sub BUILD {}
565             around 'BUILD' => sub { &DbicLink_around_BUILD(@_) };
566             sub DbicLink_around_BUILD {
567 92     92 0 225 my $orig = shift;
568 92         194 my $self = shift;
569            
570 92 50       462 die "FATAL: DbicLink and DbicLink2 cannot both be loaded" if ($self->does('RapidApp::Role::DbicLink'));
571            
572 92 100       75865 $self->accept_subargs(1) if ($self->allow_restful_queries);
573            
574             # Disable editing on columns that aren't updatable:
575             #$self->apply_except_colspec_columns($self->TableSpec->updatable_colspec => {
576             # editor => ''
577             #});
578            
579 92         2846 $self->apply_columns( $self->record_pk => {
580             no_column => \1,
581             no_multifilter => \1,
582             no_quick_search => \1
583             });
584            
585             # Hide any extra colspec columns that were only added for relationship
586             # columns:
587             #$self->apply_colspec_columns($self->TableSpec->added_relationship_column_relspecs,
588             # no_column => \1,
589             # no_multifilter => \1,
590             # no_quick_search => \1
591             #);
592            
593 92         731 $self->$orig(@_);
594            
595             # init primary columns:
596 92         3156 $self->primary_columns;
597            
598             # TODO: find out why this option doesn't work when applied via other, newer config mechanisms:
599 92         615 $self->apply_store_config(
600             remoteSort => \1
601             );
602            
603 92 50       3235 $self->apply_extconfig(
604             remote_columns => \1,
605             loadMask => \1,
606             quicksearch_mode => $self->quicksearch_mode,
607             allow_set_quicksearch_mode => $self->allow_set_quicksearch_mode ? \1 : \0
608             );
609            
610            
611             # This allows supplying custom BUILD code via a constructor:
612 92 50       3554 $self->onBUILD->($self) if ($self->onBUILD);
613             }
614              
615             sub apply_colspec_columns {
616 0     0 0 0 my $self = shift;
617 0         0 my $colspec = shift;
618 0 0       0 my %opt = (ref($_[0]) eq 'HASH') ? %{ $_[0] } : @_; # <-- arg as hash or hashref
  0         0  
619            
620 0         0 my @colspecs = ( $colspec );
621 0 0       0 @colspecs = @$colspec if (ref($colspec) eq 'ARRAY');
622              
623 0         0 my @columns = $self->TableSpec->get_colspec_column_names(@colspecs);
624 0         0 my %apply = map { $_ => { %opt } } @columns;
  0         0  
625 0         0 $self->apply_columns(%apply);
626             }
627              
628             # Apply to all columns except those matching colspec:
629             sub apply_except_colspec_columns {
630 0     0 0 0 my $self = shift;
631 0         0 my $colspec = shift;
632 0 0       0 my %opt = (ref($_[0]) eq 'HASH') ? %{ $_[0] } : @_; # <-- arg as hash or hashref
  0         0  
633              
634 0         0 my @colspecs = ( $colspec );
635 0 0       0 @colspecs = @$colspec if (ref($colspec) eq 'ARRAY');
636            
637 0         0 my @columns = $self->TableSpec->get_except_colspec_column_names(@colspecs);
638 0         0 my %apply = map { $_ => { %opt } } @columns;
  0         0  
639 0         0 $self->apply_columns(%apply);
640             }
641              
642             sub delete_colspec_columns {
643 0     0 0 0 my $self = shift;
644 0 0       0 my @colspecs = (ref($_[0]) eq 'ARRAY') ? @{$_[0]} : @_;
  0         0  
645            
646 0         0 my @columns = $self->TableSpec->get_colspec_column_names(@colspecs);
647 0         0 return $self->delete_columns(@columns);
648             }
649              
650             # Delete all columns except those matching colspec:
651             sub delete_except_colspec_columns {
652 0     0 0 0 my $self = shift;
653 0 0       0 my @colspecs = (ref($_[0]) eq 'ARRAY') ? @{$_[0]} : @_;
  0         0  
654            
655 0 0       0 die "delete_except_colspec_columns: no colspecs supplied" unless (@colspecs > 0);
656            
657 0         0 my @columns = $self->TableSpec->get_except_colspec_column_names(@colspecs);
658 0         0 return $self->delete_columns(@columns);
659             }
660              
661             sub apply_except_colspec_columns_ordered {
662 0     0 0 0 my $self = shift;
663 0         0 my $indx = shift;
664 0         0 my $colspec = shift;
665 0 0       0 my %opt = (ref($_[0]) eq 'HASH') ? %{ $_[0] } : @_; # <-- arg as hash or hashref
  0         0  
666              
667 0         0 my @colspecs = ( $colspec );
668 0 0       0 @colspecs = @$colspec if (ref($colspec) eq 'ARRAY');
669            
670 0         0 my @columns = $self->TableSpec->get_except_colspec_column_names(@colspecs);
671 0         0 my %apply = map { $_ => { %opt } } grep { exists $self->columns->{$_} } @columns;
  0         0  
  0         0  
672 0         0 $self->apply_columns_ordered($indx,%apply);
673             }
674              
675              
676             sub get_read_records_Rs {
677 8     8 0 29 my $self = shift;
678 8   33     35 my $params = shift || $self->c->req->params;
679              
680 8         72 my $Rs = $self->_ResultSet;
681            
682             # Apply base Attrs:
683 8         73 $Rs = $self->chain_Rs_req_base_Attr($Rs,$params);
684            
685             # Apply id_in search:
686 8         5769 $Rs = $self->chain_Rs_req_id_in($Rs,$params);
687            
688             # Apply explicit resultset:
689 8         1120 $Rs = $self->chain_Rs_req_explicit_resultset($Rs,$params);
690            
691             # Apply quicksearch:
692 8         2937 $Rs = $self->chain_Rs_req_quicksearch($Rs,$params);
693            
694             # Apply multifilter:
695 8         53 $Rs = $self->chain_Rs_req_multifilter($Rs,$params);
696            
697 8         24 return $Rs;
698             }
699              
700             sub read_records {
701 8     8 0 29 my $self = shift;
702 8   33     41 my $params = shift || $self->c->req->params;
703            
704             ## ---------
705             ## Experimental override to force all joins to be LEFT joins, since in the
706             ## context of the grid, it is never helpful to inner join which can cause
707             ## rows to not show up when the foreign key isn't found, which is never what
708             ## we want to happen - TODO: add test cases for this
709 5     5   65 no warnings 'redefine';
  5         14  
  5         52150  
710 8         37 my $orig_resolve_join = \&DBIx::Class::ResultSource::_resolve_join;
711             local *DBIx::Class::ResultSource::_resolve_join = sub {
712 0     0   0 my ($self, $join, $alias, $seen, $jpath, $parent_force_left) = @_;
713 0         0 return $orig_resolve_join->($self, $join, $alias, $seen, $jpath, 1)
714 8         179 };
715             # for more info, see the thread/convo on github:
716             # https://github.com/vanstyn/RapidApp/commit/cab4a6732
717             ## ---------
718            
719            
720 8         69 my $Rs = $self->get_read_records_Rs($params);
721            
722             # -- Github Issue #10 - SQLite-specific fix --
723             local $Rs->result_source->storage->dbh
724 8         56 ->{sqlite_see_if_its_a_number} = 1;
725             # --
726            
727 8 50       4594 $Rs = $self->_chain_search_rs($Rs,{},{rows => 1}) if ($self->single_record_fetch);
728            
729             # don't use Row objects
730 8         57 my $Rs2 = $self->_chain_search_rs($Rs,undef, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' });
731            
732 8         54427 my $rows;
733             try {
734 8     8   504 my $start = [gettimeofday];
735            
736             # -----
737 8         80 $rows = [ $self->rs_all($Rs2) ];
738             #Hard coded munger for record_pk:
739 8         37 foreach my $row (@$rows) { $row->{$self->record_pk} = $self->generate_record_pk_value($row); }
  7         60  
740 8         64 $self->apply_first_records($Rs2,$rows,$params);
741             # -----
742            
743 8         58 my $elapsed = tv_interval($start);
744 8         376 $self->c->stash->{query_time} = sprintf('%.2f',$elapsed) . 's';
745             }
746             catch {
747 0     0   0 my $err = shift;
748 0         0 $self->handle_dbic_exception($err);
749 8         118 };
750            
751             # Now calculate a total, for the grid to display the number of available pages
752 8         1051 my $total;
753             try {
754 8     8   433 $total = $self->rs_count($Rs2,$params);
755             }
756             catch {
757 0     0   0 my $err = shift;
758 0         0 local $append_exception_title = '(total count)';
759 0         0 $self->handle_dbic_exception($err);
760 8         80 };
761              
762 8         830 my $ret = {
763             rows => $rows,
764             results => $total,
765             query_time => $self->query_time
766             };
767            
768 8 50       285 $self->calculate_column_summaries($ret,$Rs,$params) unless($self->single_record_fetch);
769            
770 8         51 return $ret;
771             }
772              
773              
774             # If optional param 'first_records_cond' was supplied, a second query (sub-set of the original)
775             # is ran and matching rows are moved to the top of the list of rows
776             sub apply_first_records {
777 8     8 0 35 my ($self,$Rs,$rows,$params) = @_;
778 8 50 33     75 return unless ($params && $params->{first_records_cond});
779            
780 0         0 my $cond = $self->param_decodeIf($params->{first_records_cond},{});
781 0 0       0 return undef unless (keys %$cond > 0);
782            
783 0         0 my $first_rows = [ $self->_chain_search_rs($Rs,$cond)->all ];
784            
785             #Hard coded munger for record_pk:
786 0         0 foreach my $row (@$first_rows) {
787 0         0 $row->{$self->record_pk} = $self->generate_record_pk_value($row);
788             }
789            
790             # concat both sets of rows together, with first_rows first:
791 0         0 push @$first_rows, @$rows;
792            
793             # Remove duplicates:
794 0         0 my %seen = ();
795 0         0 @$first_rows = grep { !$seen{$_->{$self->record_pk}}++ } @$first_rows;
  0         0  
796            
797             # Shorten (truncate) to original length and replace original list with new list:
798 0         0 @$rows = splice(@$first_rows, 0,@$rows);
799             }
800              
801             sub rs_all {
802 8     8 0 31 my ($self, $Rs) = @_;
803 8         25 my $want = wantarray;
804              
805             # ----- GitHub Issue #165
806             # NEW: extract the nested select refs from the special ''/-as structure for
807             # the query, throwing away the outer layer and the -as. This is being done
808             # for MSSQL specificially because this was causing 'AS' being added twice
809             # in the generated query. We are now doing this here, after the fact, to
810             # avoid having to refactor a lot of existing code which expects and looks for
811             # these ''/-as structures (but this is a TODO to revisit). The only ramification
812             # of stripping this structure appears to be in sorting; we can no longer sort
813             # according to the '-as' name for virtual columns (see also the change further down
814             # regarding sorting, also tagged as #165). So, istead we have to sort
815             # on the select ref again. We already had to give up on using predeclared names
816             # for HAVING because Pg didn't like them (#51), and it seems MSSQL doesn't like
817             # it for sorting either. So we are falling back to the broadest compatability.
818             # TODO: optimize cases for each different backend
819 8 50       59 if(my $sels = $Rs->{attrs}{select}) {
820 8         58 @$sels = map { $self->_extract_hash_inner_AS($_) } @$sels
  46         130  
821             }
822             # -----
823              
824 8         31 my @ret = ();
825             try {
826 8 50   8   407 @ret = $want ? $Rs->all : scalar $Rs->all
827             }
828             catch {
829 0     0   0 my $err = shift;
830              
831 0         0 my $dbh = $Rs->result_source->schema->storage->dbh;
832 0   0     0 my $LRL = $dbh->{LongReadLen} || 80;
833              
834 0 0 0     0 if($LRL == 80 && "$err" =~ /or LongReadLen too small/) {
835 0         0 local $dbh->{LongReadLen} = 1024*256;
836 0         0 warn join("\n",'','',
837             ' Caught DBI LongTruncOk/LongReadLen exception and LongReadLen not configured --',
838             " Trying over with really large LongReadLen : $dbh->{LongReadLen}",
839             ' You need to set this to a real/appropriate value for your database','',''
840             );
841 0 0       0 @ret = $want ? $self->rs_all($Rs) : scalar $self->rs_all($Rs)
842             }
843             else {
844 0         0 die $err
845             }
846 8         95 };
847              
848 8 50       24177 $want ? @ret : $ret[0]
849             }
850              
851              
852             sub _is_special_AS_hash {
853 46     46   82 my ($self, $h) = @_;
854 46 50 100     414 (ref($h)||'') eq 'HASH' && exists $h->{''} && exists $h->{-as} && scalar(keys %$h) == 2
      66        
      33        
855             }
856              
857             # extract the nested select ref from the special ''/-as structure
858             sub _extract_hash_inner_AS {
859 46     46   99 my ($self, $select) = @_;
860 46 100       108 $self->_is_special_AS_hash($select) ? $select->{''} : $select
861             }
862              
863              
864             has '_count_col', is => 'ro', lazy => 1, default => sub {
865             my $self = shift;
866            
867             # We only want to use this optimization for PostgreSQL, since it is
868             # known to perform poorly with the standard count(*) method, which
869             # is uniquely a Pg issue....
870             my $sqlt_type = $self->ResultSource->schema->storage->sqlt_type || '';
871             $sqlt_type eq 'PostgreSQL' or return undef;
872              
873             my @pris = $self->ResultSource->primary_columns;
874             if(scalar(@pris) == 1) {
875             return $pris[0];
876             }
877             return undef;
878              
879             }, isa => 'Maybe[Str]';
880              
881             sub rs_count {
882 8     8 0 218 my $self = shift;
883 8         21 my $Rs2 = shift;
884 8   50     35 my $params = shift || {};
885            
886 8 50       312 return 1 if ($self->single_record_fetch);
887 8 50       74 return undef if ($params->{no_total_count});
888            
889             # Optionally return the client supplied cached total:
890             return $params->{cached_total_count}
891 8 50 33     310 if($self->cache_total_count && exists $params->{cached_total_count});
892            
893 8         65 $self->c->stash->{query_count_start} = [gettimeofday];
894            
895             #return $self->rs_count_manual($Rs2);
896            
897             #return $self->rs_count_via_pager($Rs2);
898             #return $self->rs_count_manual($Rs2);
899            
900 8 50       914 if(my $col = $self->_count_col) {
901             return try {
902 0     0   0 $Rs2->search_rs(undef,{
903             page => undef, rows => undef, order_by => undef,
904             select => { count => join('.',$Rs2->current_source_alias,$col) },
905             as => 'count'
906             })
907             ->get_column('count')
908             ->first
909             }
910             catch {
911 0     0   0 warn RED . "\n\n" . $self->extract_db_error_from_exception($_) . CLEAR;
912 0         0 warn RED.BOLD . "\n\n" .
913             'COUNT VIA _count_col FAILED, FAILING BACK TO PAGER COUNT' .
914             "\n\n" . CLEAR;
915 0         0 return $self->rs_count_with_fallbacks($Rs2);
916 0         0 };
917             }
918            
919 8         94 return $self->rs_count_with_fallbacks($Rs2);
920             }
921              
922             sub rs_count_via_pager {
923 0     0 0 0 my $self = shift;
924 0         0 my $Rs2 = shift;
925 0         0 return $Rs2->pager->total_entries;
926             }
927              
928             # -- Alternate way to calculate the total count. The logic in 'pager->total_entries' just
929             # isn't entirely reliable still. Have been going back and forth between these two
930             # approaches, this time, I am leaving both in as separates functions (after writing this
931             # same code for the 3rd time at least!). The latest problem with the pager breaks with multiple
932             # having conditions on the same virtual column. The DBIC pager/total_entries code is
933             # putting in the same sub-select, with AS, for each condition into the select which throws a
934             # duplicate column db exception (MySQL).
935             # UPDATE: added options to fine-tune behaviors:
936             sub rs_count_manual {
937 0     0 0 0 my $self = shift;
938 0         0 my $Rs2 = shift;
939 0         0 my %opts = @_;
940            
941 0         0 my $attr = {
942             page => undef,
943             rows => undef,
944             order_by => undef
945             };
946            
947 0 0       0 unless($opts{no_strip_colums}) {
948 0         0 my $cur_select = $Rs2->{attrs}->{select};
949 0         0 my $cur_as = $Rs2->{attrs}->{as};
950            
951             # strip all columns except virtual columns (show as hashrefs)
952 0         0 my ($select,$as) = ([],[]);
953 0         0 for my $i (0..$#$cur_select) {
954 0 0       0 next unless (ref $cur_select->[$i]);
955 0         0 push @$select, $cur_select->[$i];
956 0         0 push @$as, $cur_as->[$i];
957             }
958            
959 0         0 $attr = { %$attr,
960             select => $select,
961             as => $as
962             };
963             }
964            
965 0         0 $Rs2 = $self->_chain_search_rs($Rs2,{},$attr);
966 0 0       0 $Rs2 = $Rs2->as_subselect_rs unless ($opts{no_subselect});
967            
968 0 0       0 return $Rs2->count_literal if ($opts{count_literal});
969 0         0 return $Rs2->count;
970             }
971              
972             # 3rd alternative for getting the rs_count, tries several methods. This is not currently
973             # active, even though it is arguably the more reliable approach, because we don't want
974             # to hide problems by stopping the app from breaking. This is here mostly for future
975             # reference and for debugging
976             sub rs_count_with_fallbacks {
977 8     8 0 24 my $self = shift;
978 8         21 my $Rs2 = shift;
979            
980             return try {
981             try {
982 8         352 $Rs2->pager->total_entries
983             } catch {
984 0         0 warn RED . "\n\n" . $self->extract_db_error_from_exception($_) . CLEAR;
985 0         0 warn RED.BOLD . "\n\n" .
986             'COUNT VIA PAGER FAILED, FAILING BACK TO MANUAL COUNT' .
987             "\n\n" . CLEAR;
988 0         0 my $opt = {};
989             try {
990 0         0 $self->rs_count_manual($Rs2,%$opt)
991             } catch {
992 0         0 $opt->{no_strip_colums} = 1;
993 0         0 warn RED . "\n\n" . $self->extract_db_error_from_exception($_) . CLEAR;
994 0         0 warn RED.BOLD . "\n\n" .
995             'COUNT VIA MANUAL FAILED, TRYING AGAIN WITHOUT STRIPPING COLUMNS ' . Dumper($opt) .
996             "\n" . CLEAR;
997             try {
998 0         0 $self->rs_count_manual($Rs2,%$opt)
999             } catch {
1000 0         0 $opt->{count_literal} = 1;
1001 0         0 warn RED . "\n\n" . $self->extract_db_error_from_exception($_) . CLEAR;
1002 0         0 warn RED.BOLD . "\n\n" .
1003             'COUNT VIA MANUAL FAILED, TRYING AGAIN WITH COUNT_LITERAL ' . Dumper($opt) .
1004             "\n" . CLEAR;
1005 0         0 $self->rs_count_manual($Rs2,%$opt)
1006             }
1007 0         0 };
  0         0  
1008 8     8   435 };
1009             } catch {
1010 0     0   0 warn RED . "\n\n" . $self->extract_db_error_from_exception($_) . CLEAR;
1011 0         0 warn RED.BOLD . "\n\n" .
1012             'FAILED TO GET TOTAL COUNT, GIVING UP' .
1013             "\n\n" . CLEAR;
1014 0         0 die $_;
1015 8         98 };
1016             }
1017              
1018             # --
1019              
1020             after rs_count => sub {
1021             my $self = shift;
1022             my $start = $self->c->stash->{query_count_start} || return undef;
1023             my $elapsed = tv_interval($start);
1024             $self->c->stash->{query_count_time} = sprintf('%.2f',$elapsed) . 's';
1025             };
1026              
1027              
1028             sub query_time {
1029 8     8 0 42 my $self = shift;
1030 8   50     32 my $qt = $self->c->stash->{query_time} || return undef;
1031 8 50       597 $qt .= '/' . $self->c->stash->{query_count_time} if ($self->c->stash->{query_count_time});
1032 8         535 return $qt;
1033             }
1034              
1035              
1036             has '_dbh_driver', is => 'ro', lazy => 1, default => sub {
1037             my $self = shift;
1038            
1039             my $dbh = $self->ResultSource->schema->storage->dbh;
1040            
1041             my $driver = $dbh->{Driver}->{Name};
1042            
1043             if($driver eq 'ODBC') {
1044             my $dbms_name = $dbh->get_info($GetInfoType{SQL_DBMS_NAME});
1045             $driver = 'MSSQL' if ($dbms_name eq 'Microsoft SQL Server');
1046             }
1047            
1048             return $driver
1049             }, isa => 'Str';
1050              
1051             has '_named_column_summaries', is => 'ro', lazy => 1, default => sub {
1052             my $self = shift;
1053            
1054             my $d = {
1055             sum => 'sum',
1056             max => 'max',
1057             min => 'min',
1058             count => 'count',
1059             count_uniq => 'count(distinct({x}))',
1060             avg => 'round(avg({x}),2)',
1061             };
1062            
1063             if($self->_dbh_driver eq 'mysql') {
1064             %$d = ( %$d,
1065             oldest_days => 'CONCAT(DATEDIFF(NOW(),min({x})),\' days\')',
1066             youngest_days => 'CONCAT(DATEDIFF(NOW(),max({x})),\' days\')',
1067             age_range_days => 'CONCAT(DATEDIFF(max({x}),min({x})),\' days\')',
1068             );
1069             }
1070             elsif($self->_dbh_driver eq 'SQLite') {
1071             # TODO ...
1072            
1073             }
1074             elsif($self->_dbh_driver eq 'Pg') {
1075             # TODO ...
1076            
1077             }
1078             elsif($self->_dbh_driver eq 'MSSQL') {
1079             # TODO ...
1080            
1081             }
1082            
1083             return $d
1084              
1085             }, isa => 'HashRef';
1086              
1087             sub calculate_column_summaries {
1088 8     8 0 32 my ($self,$ret,$Rs,$params) = @_;
1089 8 50 33     78 return unless ($params && $params->{column_summaries});
1090            
1091 0         0 my $sums = $self->param_decodeIf($params->{column_summaries},{});
1092 0 0       0 return unless (keys %$sums > 0);
1093            
1094             # -- Filter out summaries for cols that weren't requested in 'columns':
1095 0         0 my $req_cols = $self->c->stash->{req_columns}; #<-- previously calculated in get_req_columns():
1096 0 0 0     0 if($req_cols && @$req_cols > 0) {
1097 0         0 my %limit = map {$_=>1} @$req_cols;
  0         0  
1098 0         0 %$sums = map {$_=>$sums->{$_}} grep {$limit{$_}} keys %$sums;
  0         0  
  0         0  
1099             }
1100             # --
1101            
1102 0         0 my $select = [];
1103 0         0 my $as = [];
1104            
1105 0         0 my %extra = ();
1106            
1107             #foreach my $col (@{$Rs->{attrs}->{as}}) {
1108 0         0 foreach my $col (keys %$sums) {
1109 0         0 my $sum = $sums->{$col};
1110 0 0       0 if($sum) {
1111 0         0 my $dbic_name = $self->resolve_dbic_render_colname($col);
1112 0         0 local $self->{_get_col_summary_select_msg} = undef;
1113 0         0 my $sel = $self->get_col_summary_select($dbic_name,$sum);
1114 0 0       0 if($sel) {
1115 0         0 push @$select, $sel;
1116 0         0 push @$as, $col;
1117             }
1118             else {
1119 0   0     0 $extra{$col} = $self->{_get_col_summary_select_msg} || 'BadFunc!';
1120             }
1121             }
1122             }
1123            
1124             try {
1125 0 0   0   0 if(scalar(@$select) > 0) {
1126 0 0       0 my $agg = $self->_chain_search_rs($Rs,undef,{
1127             rows => 1, page => undef, order_by => undef,
1128             select => $select, as => $as,
1129             result_class => 'DBIx::Class::ResultClass::HashRefInflator'
1130             })->first or return;
1131              
1132 0         0 $ret->{column_summaries} = { %$agg, %extra };
1133             }
1134             else {
1135 0         0 $ret->{column_summaries} = \%extra;
1136             }
1137             }
1138             catch {
1139 0     0   0 $self->c->log->error("$_");
1140 0         0 $ret->{column_summaries} = { map {$_=>'FuncError!'} keys %$sums };
  0         0  
1141 0         0 };
1142             };
1143              
1144             sub get_col_summary_select {
1145 0     0 0 0 my $self = shift;
1146 0         0 my $col = shift;
1147 0         0 my $func = shift;
1148            
1149             # Lookup by predefined name if starts with '&'
1150 0 0       0 if($func =~ /^\&(.+)$/) {
1151 0         0 my $name = $1;
1152 0         0 $func = $self->_named_column_summaries->{$name};
1153 0 0       0 unless($func) {
1154 0 0       0 $self->{_get_col_summary_select_msg} = 'Unsupported' if(exists $self->{_get_col_summary_select_msg});
1155 0         0 return undef;
1156             }
1157             }
1158             else {
1159             # TODO: check to enforce allow_custom_summary_functions is true or die
1160            
1161             }
1162            
1163             # ---
1164             # NEW: Look for and extract the SQL literal from the special ''/-as structure
1165             # which is how virtual columns appear. This is the structure needed for the
1166             # select, we need to strip this wrapper out for use here
1167             $col = $col->{''} if (
1168             ref($col) && ref($col) eq 'HASH'
1169             && scalar(keys %$col) == 2
1170 0 0 0     0 && $col->{''} && $col->{'-as'}
      0        
      0        
      0        
1171             );
1172             # Normalize to a ScalarRef from several known nested structures (multi-rel vs virtual)
1173 0 0       0 if(ref($col)) {
1174 0 0       0 $col = $$col if (ref($col) eq 'REF');
1175 0 0       0 $col = $col->[0] if (ref($col) eq 'ARRAY');
1176 0 0       0 $col = \"$col" unless (ref $col);
1177             }
1178             # ---
1179              
1180 0         0 $func =~ s/^\s+//;
1181 0         0 $func =~ s/\s+$//;
1182            
1183             # Simple function name
1184 0 0       0 return { uc($func) => $col } if ($func =~ /^[a-zA-Z]+$/);
1185            
1186 0 0 0     0 $col = $$col if (ref($col) && ref($col) eq 'SCALAR');
1187              
1188             # Replace macro string '{x}' with the column name
1189 0         0 $func =~ s/\{x\}/${col}/g;
1190            
1191 0         0 return \[ $func ];
1192            
1193 0         0 return undef;
1194             }
1195              
1196              
1197              
1198             # Applies base request attrs to ResultSet:
1199             sub chain_Rs_req_base_Attr {
1200 8     8 0 27 my $self = shift;
1201 8   33     58 my $Rs = shift || $self->_ResultSet;
1202 8   33     39 my $params = shift || $self->c->req->params;
1203            
1204 8         113 $params = {
1205             start => 0,
1206             limit => 10000000,
1207             dir => 'asc',
1208             %$params
1209             };
1210            
1211             my $attr = {
1212             'select' => [],
1213             'as' => [],
1214             join => {},
1215             page => int($params->{start}/$params->{limit}) + 1,
1216             rows => $params->{limit}
1217 8         108 };
1218            
1219 8         60 my $columns = $self->get_req_columns;
1220            
1221 8         24 my $used_aliases = {};
1222              
1223 8         26 for my $col (@$columns) {
1224 46         185 my $dbic_name = $self->resolve_dbic_colname($col,$attr->{join});
1225            
1226 46 100       51207 unless (ref $dbic_name) {
1227 17         87 my ($alias,$field) = split(/\./,$dbic_name);
1228 17         94 my $prefix = $col;
1229            
1230 17         355 $prefix =~ s/${field}$//;
1231 17 100       95 $used_aliases->{$alias} = {} unless ($used_aliases->{$alias});
1232 17 50       57 $used_aliases->{$alias}->{$prefix}++ unless($alias eq 'me');
1233 17         36 my $count = scalar(keys %{$used_aliases->{$alias}});
  17         52  
1234             # automatically set alias for duplicate joins:
1235 17 50       90 $dbic_name = $alias . '_' . $count . '.' . $field if($count > 1);
1236             }
1237            
1238 46         93 push @{$attr->{'select'}}, $dbic_name;
  46         149  
1239 46         84 push @{$attr->{'as'}}, $col;
  46         145  
1240             }
1241              
1242             my @sorts = defined $params->{sorters}
1243 0         0 ? @{$self->param_decodeIf($params->{sorters},[])}
1244             : $params->{sort}
1245             ? ({ field => $params->{sort}, direction => (
1246 8 0       66 $params->{dir} eq 'DESC' ? 'DESC' : 'ASC'
    50          
    50          
1247             )})
1248             : ();
1249              
1250 8         32 for my $sort (@sorts) {
1251 0         0 my $field = $sort->{field};
1252 0         0 my $sort_name = $self->resolve_dbic_render_colname($field,$attr->{join});
1253              
1254             # ----- GitHub Issue #165
1255             # (See also comments tagged with #165 in rs_all further up...)
1256 0 0       0 $sort_name = ref($sort_name) ? $self->_extract_virtual_subselect_ref($sort_name) : $sort_name;
1257             # we can no longer use the '-as' name for sorting with virtual columns because
1258             # MSSQL doesn't like it. So we're just using the actual select/ref again, which
1259             # is probably slower, but works the same across different backends.
1260             #if (ref $sort_name eq 'HASH') {
1261             # die "Can't sort by column if it doesn't have an SQL alias"
1262             # unless exists $sort_name->{-as};
1263             # $sort_name= $sort_name->{-as};
1264             #}
1265             # -----
1266             my @order_by = ref $attr->{order_by} eq 'HASH'
1267             ? ($attr->{order_by})
1268             : ref $attr->{order_by} eq 'ARRAY'
1269 0 0       0 ? @{$attr->{order_by}}
  0 0       0  
1270             : ();
1271 0         0 push @order_by, { '-' . lc($sort->{direction}) => $sort_name };
1272 0         0 $attr->{order_by} = \@order_by;
1273             }
1274              
1275 8         89 return $self->_chain_search_rs($Rs,{},$attr);
1276             }
1277              
1278             sub resolve_dbic_colname {
1279 46     46 0 89 my $self = shift;
1280 46         1685 return $self->TableSpec->resolve_dbic_colname(@_);
1281             }
1282              
1283              
1284             sub resolve_dbic_render_colname {
1285 0     0 0 0 my $self = shift;
1286 0         0 my $name = shift;
1287 0   0     0 my $join = shift || {};
1288            
1289 0   0     0 $self->c->stash->{dbic_render_colnames} = $self->c->stash->{dbic_render_colnames} || {};
1290 0         0 my $h = $self->c->stash->{dbic_render_colnames};
1291            
1292 0         0 my $get_render_col = 1;
1293 0   0     0 $h->{$name} = $h->{$name} || $self->resolve_dbic_colname($name,$join,$get_render_col);
1294            
1295 0         0 return $h->{$name};
1296             }
1297              
1298             has 'always_fetch_columns', is => 'ro', lazy => 1, default => sub {
1299             my $self = shift;
1300             return [] unless ($self->always_fetch_colspec);
1301             return [ $self->TableSpec->get_colspec_column_names(
1302             $self->TableSpec->always_fetch_colspec->colspecs
1303             )];
1304             }, isa => 'ArrayRef';
1305 8     8 0 24 sub all_always_fetch_columns { uniq( @{(shift)->always_fetch_columns} ) }
  8         299  
1306              
1307             sub get_req_columns {
1308 8     8 0 24 my $self = shift;
1309 8   33     66 my $params = shift || $self->c->req->params;
1310 8   50     786 my $param_name = shift || 'columns';
1311            
1312 8         22 my $columns = $params;
1313 8 50       84 $columns = $self->param_decodeIf($params->{$param_name},[]) if (ref($params) eq 'HASH');
1314            
1315 8 50       51 die "get_req_columns(): bad options" unless(ref($columns) eq 'ARRAY');
1316            
1317 8         58 $self->c->stash->{req_columns} = [@$columns];
1318            
1319             # ---
1320             # If no columns were supplied by the client, add all the columns from
1321             # include_relspec
1322             # TODO: move column request logic that's currently only in AppGrid2 to a
1323             # plugin/store where it can be used by other js modules like dataview
1324 8 50       738 unless(@$columns > 0) {
1325             # new/simple way:
1326 0         0 @$columns = grep { $_ ne $self->record_pk } $self->column_name_list;
  0         0  
1327             # old, more complex (and slow) approach:
1328             #push @$columns, $self->TableSpec->get_colspec_column_names(
1329             # $self->TableSpec->include_colspec->colspecs
1330             #);
1331             ## Limit to current real/valid columns according to DataStore2:
1332             #my %cols_indx = map {$_=>1} $self->column_name_list;
1333             #@$columns = grep { $cols_indx{$_} } @$columns;
1334             }
1335             # ---
1336            
1337 8         66 push @$columns, $self->all_always_fetch_columns;
1338            
1339             # Make sure the supplied sort column is included (needed for sorting on a *hidden* virtual
1340             # columns, including mutli and m2m relationship columns)
1341 8 50       44 push @$columns, $params->{sort} if ($params->{sort});
1342            
1343 8         352 my @exclude = ( $self->record_pk, 'loadContentCnf' );
1344            
1345 8         28 push @$columns, @{$self->primary_columns};
  8         288  
1346            
1347 8         25 my @req_fetch = ();
1348 8         31 foreach my $col (grep {defined $self->columns->{$_}} @$columns) {
  54         211  
1349 54 50       196 my $req = $self->columns->{$col}->required_fetch_columns or next;
1350 54         154 push @req_fetch, grep { defined $self->columns->{$_} } @$req;
  22         97  
1351             }
1352 8         68 push @$columns, @req_fetch;
1353            
1354 8         30 foreach my $col (@$columns) {
1355 76         212 my $column = $self->columns->{$col};
1356 76 50       234 push @exclude, $col if ($column->{no_fetch});
1357             }
1358            
1359 8         44 uniq($columns);
1360 8         24 my %excl = map { $_ => 1 } @exclude;
  16         62  
1361 8         23 @$columns = grep { !$excl{$_} } @$columns;
  46         117  
1362            
1363 8         27 return $columns;
1364             }
1365              
1366              
1367             # Applies id_in filter to ResultSet:
1368             sub chain_Rs_req_id_in {
1369 8     8 0 25 my $self = shift;
1370 8   33     820 my $Rs = shift || $self->_ResultSet;
1371 8   33     35 my $params = shift || $self->c->req->params;
1372            
1373 8 100       62 my $id_in = $self->param_decodeIf($params->{id_in}) or return $Rs;
1374            
1375 3 50 33     27 return $Rs if (ref $id_in and ! ref($id_in) eq 'ARRAY');
1376 3 50       16 $id_in = [ $id_in ] unless (ref $id_in);
1377            
1378             # TODO: second form below doesn't work, find out why...
1379 3         13 return $self->_chain_search_rs($Rs,{ '-or' => [ map { $self->record_pk_cond($_) } @$id_in ] });
  3         22  
1380            
1381             ## If there is more than one primary column, we have to construct the condition completely
1382             ## different:
1383             #return $self->_chain_search_rs($Rs,{ '-or' => [ map { $self->record_pk_cond($_) } @$id_in ] })
1384             # if (@{$self->primary_columns} > 1);
1385             #
1386             ## If there is really only one primary column we can use '-in' :
1387             #my $col = $self->TableSpec->resolve_dbic_colname($self->primary_columns->[0]);
1388             #return $self->_chain_search_rs($Rs,{ $col => { '-in' => $id_in } });
1389             }
1390              
1391              
1392             # Applies additional explicit resultset cond/attr to ResultSet:
1393             sub chain_Rs_req_explicit_resultset {
1394 8     8 0 25 my $self = shift;
1395 8   33     46 my $Rs = shift || $self->_ResultSet;
1396 8   33     37 my $params = shift || $self->c->req->params;
1397            
1398 8         41 my $cond = $self->param_decodeIf($params->{resultset_condition},{});
1399 8         40 my $attr = $self->param_decodeIf($params->{resultset_attr},{});
1400            
1401            
1402             ##
1403             ## TODO: make this code handle more cases
1404             ## This code converts [[ 'foo' ]] into \[ 'foo' ] and is needed because the later cannot
1405             ## be expressed in JSON. This allows the client to send a literal col name
1406 8 50       41 if(ref($attr->{select}) eq 'ARRAY') {
1407 0         0 my @new;
1408 0         0 foreach my $sel (@{$attr->{select}}) {
  0         0  
1409 0 0 0     0 if(ref($sel) eq 'ARRAY' and scalar @$sel == 1 and ref($sel->[0]) eq 'ARRAY') {
      0        
1410 0         0 push @new, \[ $sel->[0]->[0] ];
1411             }
1412             else {
1413 0         0 push @new,$sel;
1414             }
1415             }
1416 0         0 @{$attr->{select}} = @new;
  0         0  
1417             }
1418             ##
1419             ##
1420            
1421 8         34 return $self->_chain_search_rs($Rs,$cond,$attr);
1422             }
1423              
1424              
1425             # Applies Quick Search to ResultSet:
1426             sub chain_Rs_req_quicksearch {
1427 8     8 0 28 my $self = shift;
1428 8   33     52 my $Rs = shift || $self->_ResultSet;
1429 8   33     50 my $params = shift || $self->c->req->params;
1430              
1431 8 50 33     64 delete $params->{qs_query} if (defined $params->{qs_query} and $params->{qs_query} eq '');
1432 8 50       50 my $query = $params->{qs_query} or return $Rs;
1433              
1434 0         0 my $fields = $self->param_decodeIf($params->{qs_fields},[]);
1435 0 0       0 return $Rs unless (@$fields > 0);
1436              
1437 0         0 my $attr = { join => {} };
1438              
1439 0   0     0 my $mode = $params->{quicksearch_mode} || $self->quicksearch_mode;
1440 0 0       0 $mode = $self->quicksearch_mode unless ($self->allow_set_quicksearch_mode);
1441              
1442 0         0 my @search = ();
1443 0         0 foreach my $field (@$fields) {
1444             my $cond = $self->_resolve_quicksearch_condition(
1445             $field, $query, { mode => $mode, joinref => $attr->{join} }
1446 0 0       0 ) or next; #<-- skip for undef (see below)
1447 0         0 push @search, $cond;
1448             }
1449              
1450             # If no search conditions have been populated at all it means the query
1451             # failed pre-validation for all active columns. We need to simulate
1452             # a condition which will return no rows
1453 0 0       0 unless(scalar(@search) > 0) {
1454             # Simple dummy condition that will always be false to force 0 results
1455 0         0 return $Rs->search_rs(\'1 = 2');
1456             }
1457              
1458 0         0 return $self->_chain_search_rs($Rs,{ '-or' => \@search },$attr);
1459             }
1460              
1461              
1462             sub _resolve_quicksearch_condition {
1463 0     0   0 my ($self, $field, $query, $opt) = @_;
1464              
1465 0 0       0 my $cnf = $self->get_column($field) or die "field/column '$field' not found!";
1466 0 0       0 my $join = $opt->{joinref} or die "missing opt/ref 'joinref'";
1467 0 0       0 my $mode = $opt->{mode} or die "missing opt 'mode'";
1468              
1469             # Force to exact mode via optional TableSpec column cnf override:
1470             $mode = 'exact' if (
1471             exists $cnf->{quick_search_exact_only}
1472             && jstrue($cnf->{quick_search_exact_only})
1473 0 0 0     0 );
1474              
1475 0   0     0 my $dtype = $cnf->{broad_data_type} || 'text';
1476 0         0 my $dbicname = $self->_extract_hash_inner_AS( $self->resolve_dbic_colname($field,$join) );
1477              
1478             # For numbers, force to 'exact' mode and discard (return undef) for queries
1479             # which are not numbers (since we already know they will not match anything).
1480             # This is also now safe for PostgreSQL which complains when you try to search
1481             # on a numeric column with a non-numeric value:
1482 0 0       0 if ($dtype eq 'integer') {
    0          
1483 0 0       0 return undef unless $query =~ /^[+-]*[0-9]+$/;
1484 0         0 $mode = 'exact';
1485             }
1486             elsif ($dtype eq 'number') {
1487             return undef unless (
1488 0 0       0 looks_like_number( $query )
1489             );
1490 0         0 $mode = 'exact';
1491             }
1492              
1493             # Special-case: pre-validate enums (Github Issue #56)
1494 0         0 my $enumVh = $cnf->{enum_value_hash};
1495 0 0       0 if ($enumVh) {
1496 0 0       0 return undef unless ($enumVh->{$query});
1497 0         0 $mode = 'exact';
1498             }
1499              
1500             # New for GitHub Issue #97
1501 0         0 my $strf = $cnf->{search_operator_strf};
1502 0 0   0   0 my $s = $strf ? sub { sprintf($strf,shift) } : sub { shift };
  0         0  
  0         0  
1503              
1504             # 'text' is the only type which can do a LIKE (i.e. sub-string)
1505 0 0       0 return $mode eq 'like'
1506             ? $self->_op_fuse($dbicname => { $s->('like') => join('%','',$query,'') })
1507             : $self->_op_fuse($dbicname => { $s->('=') => $query });
1508             }
1509              
1510              
1511              
1512             our ($needs_having,$dbf_active_conditions);
1513              
1514             # Applies multifilter search to ResultSet:
1515             sub chain_Rs_req_multifilter {
1516 8     8 0 21 my $self = shift;
1517 8   33     42 my $Rs = shift || $self->_ResultSet;
1518 8   33     38 my $params = shift || $self->c->req->params;
1519            
1520 8         43 my $multifilter = $self->param_decodeIf($params->{multifilter},[]);
1521 8         38 my $multifilter_frozen = $self->param_decodeIf($params->{multifilter_frozen},[]);
1522 8         34 @$multifilter = (@$multifilter_frozen,@$multifilter);
1523            
1524 8 50       47 return $Rs unless (scalar @$multifilter > 0);
1525            
1526             # Localize HAVING tracking global variables. These will be set within the call chain
1527             # from 'multifilter_to_dbf' called next;
1528 0         0 local $needs_having = 0;
1529 0         0 local $dbf_active_conditions = [];
1530            
1531 0         0 my $attr = { join => {} };
1532 0   0     0 my $cond = $self->multifilter_to_dbf($multifilter,$attr->{join}) || {};
1533            
1534 0 0       0 return $self->_chain_search_rs($Rs,$cond,$attr) unless ($needs_having);
1535              
1536             # If we're here, '$needs_having' was set to true and we need to convert the
1537             # *entire* query to use HAVING instead of WHERE to be sure we correctly handle
1538             # any possible interdependent hierachy of '-and'/'-or' conditions. This means that
1539             # one or more of our columns are virtual, and one or more of them are contained
1540             # within the multifilter search, which effects the entire multifilter query.
1541             #
1542             # To convert from WHERE to HAVING we need to convert ALL columns to act like
1543             # virtual columns with '-as' and then transform the conditions to reference those
1544             # -as/alias names. Also, we need to make sure that each condition exists in the
1545             # SELECT clause of the query for it to be able to work as a HAVING condition,
1546             # because HAVING references the declared AS name from the SELECT, while WHERE is
1547             # based on real/existing column names of the schema. Note that we're doing this
1548             # because we have to; when there are no virtual columns in the condition we do
1549             # a nomal WHERE which provides better performance.
1550             #
1551             # TODO: investigate teasing out exactly which conditions really have to use HAVING
1552             # and which others could continue to use WHERE without harming the overall effective
1553             # result set. This could get very complicated because the condition data structure
1554             # supports an arbitrary structure. It is doable, but it depends on the real-world
1555             # performance differences to determine how important that extra layer of logic would
1556             # really be.
1557            
1558             #
1559             # Step 1/3 - add missing selects
1560             #
1561            
1562             # sort virtual selects to the end for priority in name collisions
1563             # (can happen with multi-rels with the same name as a column):
1564 0         0 @$dbf_active_conditions = sort { !(ref $b->{select}) cmp (ref $a->{select}) } @$dbf_active_conditions;
  0         0  
1565            
1566              
1567             # Collapse uniq needed col/cond names into a Hash:
1568 0         0 my %needed_selects = map { $_->{name} => $_ } @$dbf_active_conditions;
  0         0  
1569              
1570             # ---- Hack/fix for searching non-active virtual columns:
1571             # the problem with this $dbf_active_conditions global/local design is that
1572             # it will not contain *virtual* columns that are not being selected in
1573             # active columns. This breaks virtual columns from being able to be filtered
1574             # while not active. To solve this we just need to manually resolve the column
1575             # into its proper dbic column select name:
1576 0         0 for my $fname (keys %needed_selects) {
1577 0         0 my $hash = $needed_selects{$fname};
1578 0         0 $hash->{select} = $self->resolve_dbic_colname($hash->{field},{});
1579             }
1580             # ----
1581              
1582 0         0 my $cur_select = $Rs->{attrs}->{select};
1583 0         0 my $cur_as = $Rs->{attrs}->{as};
1584            
1585             # prune out all columns that are already being selected:
1586             exists $needed_selects{$_} and delete $needed_selects{$_}
1587 0 0 0 0   0 for (map { try{$_->{-as}} || $_ } @$cur_select);
  0         0  
  0         0  
1588            
1589             # Add all leftover needed selects. These are column/cond/select names that are being
1590             # used in one or more conditions but are not already being selected. Unlike WHERE, all
1591             # HAVING conditions must be contained in the SELECT clause:
1592             push(@$cur_select,$needed_selects{$_}->{select})
1593 0   0     0 and push(@$cur_as,$needed_selects{$_}->{field}) for (keys %needed_selects);
1594            
1595             #
1596             # Step 2/3 - transform selects:
1597             #
1598            
1599 0         0 my %virtuals = (); #<-- new for Github Issue #51
1600 0         0 my %map = ();
1601 0         0 my ($select,$as) = ([],[]);
1602 0         0 for my $i (0..$#$cur_select) {
1603 0 0       0 delete $needed_selects{$cur_select->[$i]} if (exists $needed_selects{$cur_select->[$i]});
1604 0         0 push @$as, $cur_as->[$i];
1605 0 0       0 if (ref $cur_select->[$i]) {
1606             # Already a virtual column, no changes:
1607 0         0 push @$select, $cur_select->[$i];
1608             # new for Github Issue #51:
1609 0         0 $virtuals{$cur_as->[$i]} = $self->_extract_virtual_subselect_ref($cur_select->[$i]);
1610 0         0 next;
1611             }
1612            
1613 0         0 push @$select, { '' => $cur_select->[$i], '-as' => $cur_as->[$i] };
1614            
1615             # Track the mapping so we can walk/replace the cond in the next step:
1616 0         0 $map{$cur_select->[$i]} = $cur_as->[$i];
1617             }
1618            
1619             #
1620             # Step 3/3 - transform conditions:
1621             #
1622            
1623             # Deep remap all condition values from WHERE type to HAVING (AS) type:
1624             my ($having) = dmap { ref $_ eq 'HASH' ?
1625             # wtf? dmap doesn't act on keys, so we have to do it ourselves.
1626             # We only care about keys, anyway
1627 0 0 0 0   0 { map { defined $_ && exists $map{$_} ? $map{$_} : $_ } %$_ } :
  0 0       0  
1628             $_
1629 0         0 } $cond;
1630            
1631             # ---
1632             # Temporary implementation for Github Issue #51
1633             # Here we are doing yet another transformation step, which is to duplicate the full sub-select
1634             # for our virtual columns within the condition. This was needed for PostgreSQL support which
1635             # was discussed at length within the comments of Github Issue #51. Since we're doing it this
1636             # way now, we can use a normal WHERE clause instead of a HAVING clause. I'm still not certain
1637             # this represents the final implementation, and there are lots of entanglements and potential
1638             # points-of-failure (which are not yet under test coverage) so for now this is being done using
1639             # the least code changes possible. If this is finalized, a refactor pass will remove a *lot* of
1640             # code and machinery that serves no purpose if we are not transforming into a HAVING at all...
1641             #
1642 0         0 my $virtual_where = 1; #<-- set to 0 to revert to HAVING codepath
1643 0 0       0 if ($virtual_where) {
1644 0         0 $cond = $self->_recurse_transform_condition(clone($cond),\%virtuals);
1645 0         0 return $self->_chain_search_rs($Rs,{},{ %$attr,
1646             where => $cond,
1647             select => $select,
1648             as => $as
1649             });
1650             }
1651             else {
1652             # This is the old code which uses HAVING via alias identifiers. This is being left in for
1653             # now as an active code block (rather than removed/commented out) to make it easier to
1654             # come back to later. We may want to still do this for RDBMS'es which support this (at
1655             # least MySQL and SQLite do, and at least PostgreSQL does not). But, the question will be
1656             # to ask if there is even a performance advantage of doing this, and if so, when, how, etc
1657             return $self->_chain_search_rs($Rs,{},{ %$attr,
1658 0         0 group_by => [ map { 'me.' . $_ } @{$self->primary_columns} ], #<-- safe group_by
  0         0  
  0         0  
1659             having => $having,
1660             select => $select,
1661             as => $as
1662             });
1663             }
1664             # ---
1665             }
1666              
1667              
1668             # This machinery was added for Github Issue #51 (see earlier comments)
1669             sub _extract_virtual_subselect_ref {
1670 0     0   0 my ($self, $el) = @_;
1671 0 0       0 my $val = $el->{''} or die "Expected empty-string hashkey";
1672             # We're handling just 2 cases which know about in advance, virtual columns
1673             # and multi-relationship columns:
1674 0 0       0 $val = ref($val) eq 'ARRAY' ? $val->[0] : $val;
1675 0 0       0 return ref $val ? $val : \$val;
1676             }
1677              
1678 0     0 0 0 sub sql_maker { (shift)->ResultSource->schema->storage->sql_maker }
1679              
1680             sub _recurse_transform_condition {
1681 0     0   0 my ($self, $val, $remap) = @_;
1682              
1683 0 0 0     0 return $val unless ($val && ref $val);
1684              
1685 0 0       0 if(ref($val) eq 'ARRAY') {
    0          
1686             @$val = map {
1687 0         0 $self->_recurse_transform_condition($_,$remap)
  0         0  
1688             } @$val;
1689             }
1690             elsif(ref($val) eq 'HASH') {
1691 0 0       0 if(scalar(keys %$val) == 1) {
1692 0         0 my ($k,$v) = (%$val);
1693             # This is the location where we are actually
1694             # changing something in the structure:
1695             return &_binary_op_fuser(
1696             $self->sql_maker,
1697             $remap->{$k},
1698             $self->_recurse_transform_condition($v,$remap)
1699 0 0       0 ) if($remap->{$k});
1700             }
1701              
1702             %$val = map {
1703 0         0 $_ => $self->_recurse_transform_condition($val->{$_},$remap)
  0         0  
1704             } (keys %$val);
1705             }
1706              
1707 0         0 return $val;
1708             }
1709              
1710             # -- Function (and disclaimer) provided by ribasushi for Github Issue #51 --
1711             ###############################################################
1712             # DO NOT COPY THIS UNDER ANY CIRCUMSTANCES
1713             # THIS IS A TEMPORARY HACK AND WILL BE BROKEN BY THE MAINTAINERS
1714             # POSSIBLY BEFORE THE END OF THIS YEAR
1715             ###############################################################
1716             sub _binary_op_fuser {
1717 10     10   120032 my ($sm, $l, $r) = @_;
1718              
1719 10         38 my ($lsql, @lbind) = $sm->_recurse_where($l);
1720              
1721 10         506 local $sm->{_nested_func_lhs} = {};
1722 10         37 my ($rsql, @rbind) = $sm->_recurse_where({ "\0" => $r });
1723              
1724 10         3263 my ($ql, $qr) = $sm->_quote_chars;
1725 10         187 $rsql =~ s/ (\Q$ql\E)? \0 (\Q$qr\E)? //gx;
1726              
1727 10         44 $rsql =~ s/ \A \s* \( (.+?) \) \s* \z /$1/sx;
1728              
1729             return \[
1730 10         86 "$lsql $rsql",
1731             @lbind,
1732             @rbind
1733             ];
1734             }
1735             ###############################################################
1736             # DO NOT COPY THIS UNDER ANY CIRCUMSTANCES
1737             # THIS IS A TEMPORARY HACK AND WILL BE BROKEN BY THE MAINTAINERS
1738             # POSSIBLY BEFORE THE END OF THIS YEAR
1739             ###############################################################
1740             # --
1741              
1742              
1743             # Common proxy for calls to $Rs->search_rs(...)
1744             sub _chain_search_rs {
1745 27     27   75 my ($self, $Rs, $cond, $attr) = @_;
1746              
1747             # --
1748             # Convert {} joins to undef - this prevents ResultSet unititialized warnings when:
1749             # join => { rel1 => { rel2 => {} } }
1750             # becomes:
1751             # join => { rel1 => { rel2 => undef } }
1752             # (See DBIx::Class::ResultSet::_calculate_score() and related code)
1753             $attr = {
1754             %$attr,
1755             join => $self->_recurse_clean_empty_hashrefs($attr->{join})
1756 27 100       147 } if ($attr->{join});
1757             # --
1758              
1759 27         124 $Rs->search_rs($cond,$attr)
1760             }
1761              
1762             sub _recurse_clean_empty_hashrefs {
1763 8     8   28 my ($self, $val) = @_;
1764              
1765 8 50 33     63 if($val && ref($val) eq 'HASH') {
1766             return (scalar keys(%$val) > 0)
1767 8 50       67 ? { map { $_ => $self->_recurse_clean_empty_hashrefs($val->{$_}) } keys(%$val) }
  0         0  
1768             : undef
1769             }
1770             else {
1771 0         0 return $val
1772             }
1773             }
1774              
1775              
1776             sub multifilter_to_dbf {
1777 0     0 0 0 my $self = shift;
1778 0         0 my $multi = clone(shift);
1779 0   0     0 my $join = shift || {};
1780            
1781 0 0       0 return $self->multifilter_to_dbf({ '-and' => $multi },$join) if (ref($multi) eq 'ARRAY');
1782            
1783 0 0 0     0 die RED.BOLD."Invalid multifilter:\n" . Dumper($multi).CLEAR unless (
1784             ref($multi) eq 'HASH' and
1785             keys %$multi == 1
1786             );
1787            
1788 0         0 my ($f,$cond) = (%$multi);
1789 0 0 0     0 if($f eq '-and' or $f eq '-or') {
1790 0 0       0 die "-and/-or must reference an ARRAY/LIST" unless (ref($cond) eq 'ARRAY');
1791 0         0 my @list = map { $self->multifilter_to_dbf($_,$join) } @$cond;
  0         0  
1792 0         0 return { $f => \@list };
1793             }
1794            
1795             # -- relationship column:
1796             my $is_cond = (
1797             ref($cond) eq 'HASH' and
1798             exists $cond->{is}
1799 0 0 0     0 ) ? 1 : 0;
1800            
1801 0   0     0 my $column = $self->get_column($f) || {};
1802 0   0     0 $f = $column->{query_search_use_column} || $f;
1803 0 0 0     0 $f = $column->{query_id_use_column} || $f if ($is_cond);
1804             # --
1805            
1806 0 0       0 my $dbfName = $self->resolve_dbic_colname($f,$join)
1807             or die "Client supplied Unknown multifilter-field '$f' in Ext Query!";
1808            
1809             # Set the localized '$needs_having' flag to tell our caller to convert
1810             # from WHERE to HAVING if this condition is based on a virtual column:
1811             $needs_having = 1 if(
1812             ref $dbfName eq 'HASH' and
1813             exists $dbfName->{-as} and
1814 0 0 0     0 exists $dbfName->{''}
      0        
1815             );
1816            
1817 0         0 return $self->multifilter_translate_cond($cond,$dbfName,$f);
1818             }
1819              
1820              
1821              
1822             my %mf_op_alias = (
1823             'is' => '=',
1824             'equal to' => '=',
1825             'is equal to' => '=',
1826             'exactly' => '=',
1827             'before' => '<',
1828             'less than' => '<',
1829             'greater than' => '>',
1830             'after' => '>',
1831             'not equal to' => '!=',
1832             'is not equal to' => '!=',
1833             "doesn't contain" => 'not_contain',
1834             'starts with' => 'starts_with',
1835             'ends with' => 'ends_with',
1836             "doesn't start with" => 'not_starts_with',
1837             "doesn't end with" => 'not_ends_with',
1838             'ends with' => 'ends_with',
1839              
1840             'is null' => 'is_null',
1841             'is empty' => 'is_empty',
1842             'is not null' => 'not_null',
1843             'is not empty' => 'not_empty',
1844             'is null or empty' => 'null_or_empty',
1845             'is not null or empty' => 'not_null_or_empty',
1846              
1847             'null/empty status' => 'null_empty_status'
1848             );
1849             # This will deep recurse if there there a circular refs in %mf_op_alias above
1850             sub _mf_resolve_op {
1851 0     0   0 my ($self, $op) = @_;
1852 0 0       0 $mf_op_alias{$op} ? $self->_mf_resolve_op($mf_op_alias{$op}) : $op;
1853             }
1854              
1855             sub _mf_get_cond {
1856 0     0   0 my ($self,$select,$op,$val,$strf) = @_;
1857              
1858 0         0 $op = $self->_mf_resolve_op($op);
1859              
1860             # New for GitHub Issue #97
1861 0 0   0   0 my $s = $strf ? sub { sprintf($strf,shift) } : sub { shift };
  0         0  
  0         0  
1862              
1863 0         0 my $cond;
1864              
1865 0 0       0 if($op eq 'contains') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1866 0         0 $cond = $self->_op_fuse($select => { $s->('like') => join('','%',$val,'%') });
1867             }
1868             elsif($op eq 'starts_with') {
1869 0         0 $cond = $self->_op_fuse($select => { $s->('like') => join('',$val,'%') });
1870             }
1871             elsif($op eq 'ends_with') {
1872 0         0 $cond = $self->_op_fuse($select => { $s->('like') => join('','%',$val) });
1873             }
1874             elsif($op eq 'not_contain') {
1875 0         0 $cond = { -or => [ # NOT LIKE -OR- NULL
1876             $self->_op_fuse($select => { $s->('not like') => join('','%',$val,'%') }),
1877             # Note: we do not pass the operator for undef through the strf because it
1878             # is treated special by SQLA - becomes "IS NULL" etc... (#97)
1879             $self->_op_fuse($select => { '=' => undef }),
1880             ]};
1881             }
1882             elsif($op eq 'not_starts_with') {
1883 0         0 $cond = { -or => [ # NOT LIKE -OR- NULL
1884             $self->_op_fuse($select => { $s->('not like') => join('',$val,'%') }),
1885             $self->_op_fuse($select => { '=' => undef }),
1886             ]};
1887             }
1888             elsif($op eq 'not_ends_with') {
1889 0         0 $cond = { -or => [ # NOT LIKE -OR- NULL
1890             $self->_op_fuse($select => { $s->('not like') => join('','%',$val) }),
1891             $self->_op_fuse($select => { '=' => undef }),
1892             ]};
1893             }
1894             elsif($op eq 'is_null') {
1895 0         0 $cond = $self->_op_fuse($select => { '=' => undef });
1896             }
1897             elsif($op eq 'is_empty') {
1898 0         0 $cond = $self->_op_fuse($select => { $s->('=') => '' });
1899             }
1900             elsif($op eq 'not_null') {
1901 0         0 $cond = $self->_op_fuse($select => { '!=' => undef });
1902             }
1903             elsif($op eq 'not_empty') {
1904 0         0 $cond = $self->_op_fuse($select => { $s->('!=') => '' });
1905             }
1906             elsif($op eq 'null_or_empty') {
1907 0         0 $cond = { -or => [
1908             $self->_op_fuse($select => { '=' => undef }),
1909             $self->_op_fuse($select => { $s->('=') => '' })
1910             ]};
1911             }
1912             elsif($op eq 'not_null_or_empty') {
1913 0         0 $cond = { -and => [
1914             $self->_op_fuse($select => { '!=' => undef }),
1915             $self->_op_fuse($select => { $s->('!=') => '' })
1916             ]};
1917             }
1918             elsif($op eq 'null_empty_status') {
1919             # Re-call with with the val as the op:
1920 0         0 $cond = $self->_mf_get_cond($select, $val);
1921             }
1922             else {
1923 0         0 $cond = $self->_op_fuse($select => { $op => $val });
1924             }
1925              
1926 0         0 $cond
1927             }
1928              
1929             sub _op_fuse {
1930 0     0   0 my $self = shift;
1931 0         0 &_binary_op_fuser($self->sql_maker, @_)
1932             }
1933              
1934              
1935             # -- multifilter_translate_cond()
1936             # - refactored for #88 and #89
1937             # - previously modified for #69 and #51
1938             sub multifilter_translate_cond {
1939 0     0 0 0 my $self = shift;
1940 0         0 my $cond = shift;
1941 0         0 my $dbfName = shift;
1942 0         0 my $field = shift;
1943 0   0 0   0 my $column = try{$self->get_column($field)} || {};
  0         0  
1944              
1945             # If we're a virtual column:
1946             my ($select,$as) = ((ref $dbfName||'') eq 'HASH' && $dbfName->{-as} && $dbfName->{''})
1947             ? ($dbfName->{''} => $dbfName->{-as} )
1948 0 0 0     0 : ($dbfName => $dbfName );
1949              
1950             # -- TODO - this is legacy and needs to be investigated and removed
1951             # Track in localized global:
1952 0         0 push @$dbf_active_conditions, {
1953             name => $as,
1954             field => $field,
1955             select => clone($dbfName)
1956             };
1957             # --
1958            
1959             # There should be exactly 1 key/value:
1960 0 0       0 die "invalid multifilter condition: must have exactly 1 key/value pair:\n" . Dumper($cond)
1961             unless (keys %$cond == 1);
1962            
1963 0         0 my ($k,$v) = (%$cond);
1964            
1965             $v = $self->inflate_multifilter_date($v) if(
1966             $column->{multifilter_type} &&
1967 0 0 0     0 $column->{multifilter_type} =~ /^date/
1968             );
1969              
1970             # New for GitHub #97 - pass in optional new search_operator_strf param
1971 0         0 return $self->_mf_get_cond($select, $k, $v,$column->{search_operator_strf});
1972             }
1973              
1974              
1975              
1976             sub multifilter_date_getKeywordDt {
1977 0     0 0 0 my $self = shift;
1978 0         0 my $keyword = shift;
1979              
1980 0         0 $keyword =~ s/\s*//g; #<-- stip whitespace from the keyword
1981 0         0 $keyword = lc($keyword); #<-- lowercase it
1982              
1983 0         0 my $dt = DateTime->now( time_zone => 'local' );
1984              
1985 0         0 my $kw = $keyword;
1986 0 0       0 if($kw eq 'now') { return $dt }
  0 0       0  
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1987            
1988 0         0 elsif($kw eq 'thisminute') { return DateTime->new(
1989             year => $dt->year,
1990             month => $dt->month,
1991             day => $dt->day,
1992             hour => $dt->hour,
1993             minute => $dt->minute,
1994             second => 0,
1995             time_zone => 'local'
1996             )}
1997            
1998 0         0 elsif($kw eq 'thishour') { return DateTime->new(
1999             year => $dt->year,
2000             month => $dt->month,
2001             day => $dt->day,
2002             hour => $dt->hour,
2003             minute => 0,
2004             second => 0,
2005             time_zone => 'local'
2006             )}
2007            
2008 0         0 elsif($kw eq 'thisday') { return DateTime->new(
2009             year => $dt->year,
2010             month => $dt->month,
2011             day => $dt->day,
2012             hour => 0,
2013             minute => 0,
2014             second => 0,
2015             time_zone => 'local'
2016             )}
2017            
2018             # same as thisday:
2019 0         0 elsif($kw eq 'today') { return DateTime->new(
2020             year => $dt->year,
2021             month => $dt->month,
2022             day => $dt->day,
2023             hour => 0,
2024             minute => 0,
2025             second => 0,
2026             time_zone => 'local'
2027             )}
2028            
2029             elsif($kw eq 'thisweek') {
2030 0         0 my $day = $dt->day_of_week;
2031             #$day++; $day = 1 if ($day > 7); #<-- shift day 1 from Monday to Sunday
2032 0         0 $dt = $dt->subtract( days => ($day - 1) );
2033 0         0 return DateTime->new(
2034             year => $dt->year,
2035             month => $dt->month,
2036             day => $dt->day,
2037             hour => 0,
2038             minute => 0,
2039             second => 0,
2040             time_zone => 'local'
2041             );
2042             }
2043            
2044 0         0 elsif($kw eq 'thismonth') { return DateTime->new(
2045             year => $dt->year,
2046             month => $dt->month,
2047             day => 1,
2048             hour => 0,
2049             minute => 0,
2050             second => 0,
2051             time_zone => 'local'
2052             )}
2053            
2054             elsif($kw eq 'thisquarter') {
2055 0         0 my $month = $dt->month;
2056 0         0 my $subtract = 0;
2057 0 0 0     0 if($month > 0 && $month <= 3) {
    0 0        
    0 0        
2058 0         0 $subtract = $month - 1;
2059             }
2060             elsif($month > 3 && $month <= 6) {
2061 0         0 $subtract = $month - 4;
2062             }
2063             elsif($month > 6 && $month <= 9) {
2064 0         0 $subtract = $month - 7;
2065             }
2066             else {
2067 0         0 $subtract = $month - 10;
2068             }
2069            
2070 0         0 $dt = $dt->subtract( months => $subtract );
2071 0         0 return DateTime->new(
2072             year => $dt->year,
2073             month => $dt->month,
2074             day => 1,
2075             hour => 0,
2076             minute => 0,
2077             second => 0,
2078             time_zone => 'local'
2079             );
2080             }
2081            
2082 0         0 elsif($kw eq 'thisyear') { return DateTime->new(
2083             year => $dt->year,
2084             month => 1,
2085             day => 1,
2086             hour => 0,
2087             minute => 0,
2088             second => 0,
2089             time_zone => 'local'
2090             )}
2091            
2092 0         0 return undef;
2093             }
2094              
2095             # This is a clone of the JavaScript logic in the function parseRelativeDate() in the plugin
2096             # class Ext.ux.RapidApp.Plugin.RelativeDateTime. While it is not ideal to have to reproduce
2097             # this and have to maintain in both Perl and JavaScript simultaneously, this is the most
2098             # straightforward way to achive the desired functionality. This is because these relative
2099             # dates have to be inflated at query/request time, and MultiFilters wasn't designed with that
2100             # in mind. To do this in the client side, multifilters would need significant modifications
2101             # to get it to munge its filters on every request, which is was not designed to do.
2102             sub inflate_multifilter_date {
2103 0     0 0 0 my $self = shift;
2104 0         0 my $v = shift;
2105            
2106 0         0 my $dt = $self->multifilter_date_getKeywordDt($v);
2107 0 0       0 return $dt->ymd . ' ' . $dt->hms if ($dt);
2108            
2109 0         0 my $orig_v = $v;
2110              
2111 0         0 my @parts = split(/[\-\+]/,$v);
2112 0 0 0     0 if(scalar @parts > 1 && length $parts[0] > 0) {
2113             #If we are here then it means a custom start keyword was specified:
2114 0         0 my $keyword = $parts[0];
2115 0         0 $v =~ s/^${keyword}//; #<-- strip out the keyword from the string value
2116 0         0 $keyword =~ s/\s*//g; #<-- stip whitespace from the keyword
2117 0         0 $keyword = lc($keyword); #<-- lowercase it
2118            
2119 0         0 $dt = $self->multifilter_date_getKeywordDt($keyword);
2120             }
2121             else {
2122 0         0 $dt = $self->multifilter_date_getKeywordDt('now');
2123             }
2124            
2125 0         0 my $sign = substr($v,0,1);
2126 0 0 0     0 return $orig_v unless ($dt && ($sign eq '-' || $sign eq '+'));
      0        
2127            
2128 0         0 my $str = substr($v,1);
2129            
2130             # Strip whitespace and commas:
2131 0         0 $str =~ s/[\s\,]*//g;
2132            
2133 0         0 $str = lc($str);
2134            
2135 0         0 @parts = ();
2136 0         0 while(length $str) {
2137 0         0 my ($num,$unit);
2138 0         0 my $match;
2139 0         0 ($match) = ($str =~ /^(\d+)/); $str =~ s/^(\d+)//; $num = $match;
  0         0  
  0         0  
2140 0         0 ($match) = ($str =~ /^(\D+)/); $str =~ s/^(\D+)//; $unit = $match;
  0         0  
  0         0  
2141            
2142             #custom support for "weeks":
2143 0 0 0     0 if($unit eq 'w' || $unit eq 'week' || $unit eq 'weeks' || $unit eq 'wk' || $unit eq 'wks') {
      0        
      0        
      0        
2144 0         0 $unit = 'days';
2145 0         0 $num = $num * 7;
2146             }
2147            
2148             #custom support for "quarters":
2149 0 0 0     0 if($unit eq 'q' || $unit eq 'quarter' || $unit eq 'quarters' || $unit eq 'qtr' || $unit eq 'qtrs') {
      0        
      0        
      0        
2150 0         0 $unit = 'months';
2151 0         0 $num = $num * 3;
2152             }
2153            
2154 0 0 0     0 push @parts, { num => $num, unit => $unit } if ($num && $unit);
2155             }
2156            
2157 0 0       0 return $v unless (@parts > 0);
2158            
2159 0 0       0 my $method = ($sign eq '-') ? 'subtract' : 'add';
2160            
2161 0         0 my $map = $self->inflate_multifilter_date_unit_map;
2162 0         0 my $count = 0;
2163 0         0 foreach my $part (@parts) {
2164 0 0       0 my $interval = $map->{$part->{unit}} or next;
2165 0 0       0 my $newDt = $dt->$method( $interval => $part->{num} ) or next;
2166 0         0 $count++;
2167 0         0 $dt = $newDt;
2168             }
2169            
2170 0 0       0 return $orig_v unless ($count);
2171            
2172 0         0 return $dt->ymd . ' ' . $dt->hms;
2173             }
2174              
2175             # Equiv to Ext.ux.RapidApp.Plugin.RelativeDateTime.unitMap
2176             has 'inflate_multifilter_date_unit_map', is => 'ro', default => sub {{
2177            
2178             y => 'years',
2179             year => 'years',
2180             years => 'years',
2181             yr => 'years',
2182             yrs => 'years',
2183            
2184             m => 'months',
2185             mo => 'months',
2186             month => 'months',
2187             months => 'months',
2188            
2189             d => 'days',
2190             day => 'days',
2191             days => 'days',
2192             dy => 'days',
2193             dys => 'days',
2194            
2195             h => 'hours',
2196             hour => 'hours',
2197             hours => 'hours',
2198             hr => 'hours',
2199             hrs => 'hours',
2200            
2201             i => 'minutes',
2202             mi => 'minutes',
2203             min => 'minutes',
2204             mins => 'minutes',
2205             minute => 'minutes',
2206             minutes => 'minutes',
2207            
2208             s => 'seconds',
2209             sec => 'seconds',
2210             secs => 'seconds',
2211             second => 'seconds',
2212             second => 'seconds'
2213             }};
2214              
2215             has 'is_virtual_source', is => 'ro', lazy => 1, default => sub {
2216             my $self = shift;
2217             return (
2218             $self->ResultClass->result_source_instance->can('is_virtual') &&
2219             $self->ResultClass->result_source_instance->is_virtual
2220             );
2221             }, isa => 'Bool';
2222              
2223             has 'DataStore_build_params' => ( is => 'ro', isa => 'HashRef', default => sub {{}} );
2224             before DataStore2_BUILD => sub {
2225             my $self = shift;
2226            
2227             my @store_fields = map {{ name => $_ }} uniq(
2228             $self->TableSpec->updated_column_order,
2229             'loadContentCnf', #<-- specific to AppGrid2
2230             $self->record_pk
2231             );
2232            
2233             my $store_params = {
2234             store_autoLoad => 1,
2235             reload_on_save => $self->reload_on_save,
2236             remoteSort => \1,
2237             store_fields => \@store_fields
2238             };
2239            
2240             $store_params->{create_handler} = RapidApp::Handler->new( scope => $self, method => '_dbiclink_create_records' ) if (
2241             defined $self->creatable_colspec and
2242             not $self->can('create_records')
2243             );
2244            
2245             $store_params->{update_handler} = RapidApp::Handler->new( scope => $self, method => '_dbiclink_update_records' ) if (
2246             defined $self->updatable_colspec and
2247             not $self->can('update_records')
2248             );
2249            
2250             $store_params->{destroy_handler} = RapidApp::Handler->new( scope => $self, method => '_dbiclink_destroy_records' ) if (
2251             defined $self->destroyable_relspec and
2252             not $self->can('destroy_records')
2253             );
2254            
2255             # New: Override to globally disable create/destroy for virtual sources:
2256             if($self->is_virtual_source) {
2257             exists $store_params->{create_handler} && delete $store_params->{create_handler};
2258             exists $store_params->{destroy_handler} && delete $store_params->{destroy_handler};
2259             $self->apply_flags( can_create => 0 );
2260             $self->apply_flags( can_destroy => 0 );
2261             }
2262            
2263             # merge this way to make sure the opts get set, but yet still allow
2264             # the opts to be specifically overridden DataStore_build_params attr
2265             # is defined but with different params
2266             %{$self->DataStore_build_params} = ( %$store_params, %{$self->DataStore_build_params} );
2267             };
2268              
2269              
2270              
2271             # convenience method: prints the primary keys of a Row object
2272             # just used to print info to the screen during CRUD ops below
2273             sub get_Row_Rs_label {
2274 3     3 0 9 my $self = shift;
2275 3         7 my $Row = shift;
2276 3         8 my $verbose = shift;
2277            
2278 3 50       25 if($Row->isa('DBIx::Class::ResultSet')) {
2279 0         0 my $Rs = $Row;
2280 0         0 my $str = ref($Rs) . ' [' . $Rs->count . ' rows]';
2281 0 0       0 return $str unless ($verbose);
2282 0         0 $str .= ':';
2283 0         0 $str .= "\n " . $self->get_Row_Rs_label($_) for ($Rs->all);
2284 0         0 return $str;
2285             }
2286              
2287 3         15 my $Source = $Row->result_source;
2288 3         32 my @keys = $Source->primary_columns;
2289 3         33 my $data = { $Row->get_columns };
2290            
2291 3         15 my $str = ref($Row) . ' [ ';
2292 3         25 $str .= $_ . ': ' . $data->{$_} . ' ' for (@keys);
2293 3         11 $str .= ']';
2294            
2295 3         16 return $str;
2296             }
2297              
2298             # Gets programatically added as a method named 'update_records' (see BUILD modifier method above)
2299             #
2300             # This first runs updates on each supplied (and allowed) relation.
2301             # It then re-runs a read_records to tell the client what the new values are.
2302             #
2303             sub _dbiclink_update_records {
2304 3     3   10 my $self = shift;
2305 3         8 my $params = shift;
2306            
2307 3         7 my $limit_columns;
2308 3         14 my $declared_columns = $self->param_decodeIf($self->c->req->params->{columns});
2309 3 50       20 $limit_columns = { map {$_=>1} @$declared_columns } if ($declared_columns);
  19         86  
2310            
2311             # -- current real/valid columns according to DataStore2:
2312 3         44 my %cols_indx = map {$_=>1} $self->column_name_list;
  39         97  
2313             # --
2314            
2315 3         13 my $arr = $params;
2316 3 50       17 $arr = [ $params ] if (ref($params) eq 'HASH');
2317            
2318             #my $Rs = $self->ResultSource->resultset;
2319 3         28 my $Rs = $self->baseResultSet;
2320            
2321 3         1207 my @updated_keyvals = ();
2322 3         15 my %keyval_changes = ();
2323            
2324             # FIXME!!
2325             # There is a logic problem with update. The comparisons are done iteratively, and so when
2326             # update is called on one row, and then the backend logic changes another row that is
2327             # encountered later on in the update process, it can appear that rows were changed, when in fact they
2328             # were the original values, and it can change the data in an inconsistent/non-atomic way.
2329             # would be good to find a way to do this just like in create. What really needs to happen is
2330             # at least the column_data_alias remapping needs to be atomic (like create).
2331             # this currently only breaks in edge-cases (and where an incorrect/non-sensible set of colspecs
2332             # was supplied to begin with, but still needs to be FIXED). Needs to be thought about...
2333             # -- ^^^ --- UPDATE: I believe that I have solved this problem by now pushing rows into
2334             # a queue and then running updates at the end. Need to spend a bit more
2335             # time thinking about it though, so I am not removing the above comment yet
2336            
2337             try {
2338             $self->ResultSource->schema->txn_do(sub {
2339 3         1587 foreach my $data (@$arr) {
2340 3         116 my $pkVal= $data->{$self->record_pk};
2341 3 50       16 defined $pkVal or die ref($self)."->update_records: Record is missing primary key '".$self->record_pk."'";
2342 3 50       28 my $BaseRow = $Rs->search($self->record_pk_cond($pkVal))->next or die usererr "Failed to find row by record_pk: $pkVal";
2343            
2344             # -- Filter out the supplied data packet according to the supplied 'columns' parameter
2345             # if the client has supplied a column list, filter out fieldnames that aren't in it.
2346             # The Ext store currently sends all of its configured store fields, including ones it never
2347             # loaded from the database. If we don't do this filtering, those fields will appear to have
2348             # changed.
2349             #
2350             # FIXME: handle this on the client/js side so these fields aren't submitted at all
2351 3 50       9466 if($limit_columns) {
2352 3         507 %$data = map { $_ => $data->{$_} } grep { $limit_columns->{$_} } keys %$data;
  3         19  
  6         18  
2353             }
2354             # --
2355            
2356 3 50       12 my @columns = grep { $_ ne $self->record_pk && $_ ne 'loadContentCnf' } keys %$data;
  3         126  
2357            
2358 3         115 @columns = $self->TableSpec->filter_updatable_columns(@columns);
2359              
2360             # -- Limit to current real/valid columns according to DataStore2:
2361 3         21 @columns = grep { $cols_indx{$_} } @columns;
  3         16  
2362             # --
2363            
2364 3         45 my @update_queue = $self->prepare_record_updates($BaseRow,\@columns,$data);
2365            
2366             # Update all the rows at the end:
2367 3         29 $self->process_update_queue(@update_queue);
2368            
2369             # Get the new record_pk for the row (it probably hasn't changed, but it could have):
2370 3         18 my $newPkVal = $self->generate_record_pk_value({ $BaseRow->get_columns });
2371 3         18 push @updated_keyvals, $newPkVal;
2372 3 50       40 $keyval_changes{$newPkVal} = $pkVal unless ("$pkVal" eq "$newPkVal");
2373             }
2374 3     3   243 });
2375             }
2376             catch {
2377 0     0   0 my $err = shift;
2378 0         0 $self->handle_dbic_exception($err);
2379             #die usererr rawhtml $self->make_dbic_exception_friendly($err), title => 'Database Error';
2380 3         58 };
2381            
2382             # --
2383             # Perform a fresh lookup of all the records we just updated and send them back to the client:
2384             delete $self->c->req->params->{ $self->_rst_qry_param } if (
2385             # clear any existing rst_qry to prevent polluting the read
2386 3 50       39431 exists $self->c->req->params->{ $self->_rst_qry_param }
2387             );
2388             my $newdata = $self->DataStore->read({
2389 3         131 columns => [ keys %{ $arr->[0] } ],
  3         54  
2390             id_in => \@updated_keyvals
2391             });
2392             # --
2393            
2394             ## ----------------
2395             # NEW: We need to make sure the order of the returned rows matches the supplied rows;
2396             # Ext's data store uses the order rather than the record ids to match. If we don't do
2397             # this it could mix up the rows and cause subsequent updates to change the wrong rows!!
2398             {
2399 3         13 my %pkRowMap = map { $_->{$self->record_pk} => $_ } @{$newdata->{rows}};
  3         10  
  3         106  
  3         13  
2400 3         13 my $supplied_count = scalar @updated_keyvals;
2401 3         13 my $returned_count = scalar keys %pkRowMap;
2402 3 50       15 die "Supplied/returned row mismatch. Expected $supplied_count rows, got $returned_count. "
2403             unless ($supplied_count == $returned_count);
2404            
2405             # Manually set the correct order
2406 3         12 @{$newdata->{rows}} = map { $pkRowMap{$_} } @updated_keyvals;
  3         15  
  3         14  
2407             }
2408             ## ----------------
2409            
2410            
2411             # -- Restore the original record_pk, if it changed, and put the new value in another key.
2412             # This is needed to make sure the client can keep track of which row is which. Code in datastore-plus
2413             # then detects this and updates the idProperty in the record to the new value so it will be used
2414             # in subsequent requests. THIS APPLIES ONLY IF THE PRIMARY KEYS ARE EDITABLE, WHICH ONLY HAPPENS
2415             # IN RARE SITUATIONS:
2416 3         9 foreach my $row (@{$newdata->{rows}}) {
  3         12  
2417 3         92 my $newPkVal = $row->{$self->record_pk};
2418 3 50       20 my $oldPkVal = $keyval_changes{$newPkVal} or next;
2419 0         0 $row->{$self->record_pk . '_new'} = $row->{$self->record_pk};
2420 0         0 $row->{$self->record_pk} = $oldPkVal;
2421             }
2422             # --
2423            
2424             return {
2425 3         37 %$newdata,
2426             success => \1,
2427             msg => 'Update Succeeded'
2428             };
2429             }
2430              
2431             sub process_update_queue {
2432 3     3 0 9 my $self = shift;
2433 3         9 my @update_queue = @_;
2434            
2435 3         21 my $lock_keys = $self->_get_rs_lock_keys;
2436 3 50       357 my @excl = $lock_keys ? keys %$lock_keys : ();
2437              
2438 3         18 foreach my $upd (@update_queue) {
2439 3 50       15 if(my $chg = $upd->{change}) {
    0          
2440             # We simply exclude the lock_keys from the update instead of changing them, because
2441             # this is safer. They should already be the same, but if they aren't, it is more
2442             # likely taht the client has outdated data than the server is somehow wrong
2443 3   0     12 exists $chg->{$_} and delete $chg->{$_} for (@excl);
2444 3         30 $upd->{row}->update($chg);
2445             }
2446             elsif($upd->{rel_update}) {
2447             # Special handling for updates to relationship columns
2448             #(which aren't real columns):
2449 0         0 $self->apply_virtual_rel_col_update($upd->{row},$upd->{rel_update});
2450             }
2451             }
2452             }
2453              
2454             # currently this just handles updates to m2m relationship columns, but, this is
2455             # also where other arbitrary update logic could go for other kinds of virtual
2456             # columns that may be added in the future
2457             sub apply_virtual_rel_col_update {
2458 0     0 0 0 my $self = shift;
2459 0         0 my $UpdRow = shift;
2460 0         0 my $update = shift;
2461            
2462 0         0 my $Source = $UpdRow->result_source;
2463            
2464 0         0 foreach my $colname (keys %$update) {
2465             ## currently ignore everything but m2m relationship columns:
2466 0 0       0 my $info = $Source->relationship_info($colname) or next;
2467 0 0       0 my $m2m_attrs = $info->{attrs}->{m2m_attrs} or next;
2468            
2469             # This method should have been setup by the call to "many_to_many":
2470 0         0 my $method = 'set_' . $colname;
2471 0 0       0 $UpdRow->can($method) or die "Row '" . ref($UpdRow) .
2472             "' missing expected many_to_many method '$method' - cannot update m2m data for '$colname'!";
2473            
2474 0         0 my @ids = split(/\s*,\s*/,$update->{$colname});
2475            
2476 0         0 my $Rs = $Source->schema->source($m2m_attrs->{rrinfo}->{source})->resultset;
2477 0         0 my $keycol = $m2m_attrs->{rrinfo}->{cond_info}->{foreign};
2478            
2479 0         0 my @rrows = $self->_chain_search_rs($Rs,{ $keycol => { '-in' => \@ids }})->all;
2480 0         0 my $count = scalar @rrows;
2481            
2482 0 0       0 scream_color(WHITE.ON_BLUE.BOLD," --> Setting '$colname' m2m links (count: $count)")
2483             if($self->c->debug);
2484            
2485 0         0 $UpdRow->$method(\@rrows);
2486             }
2487             }
2488              
2489              
2490             # moved/generalized out of _dbiclink_update_records to also be used by batch_update:
2491             sub prepare_record_updates {
2492 3     3 0 11 my $self = shift;
2493 3         9 my $BaseRow = shift;
2494 3         6 my $columns = shift;
2495 3         8 my $data = shift;
2496 3         7 my $ignore_current = shift;
2497            
2498 3         10 my @update_queue = ();
2499            
2500             $self->TableSpec->walk_columns_deep(sub {
2501 3     3   8 my $TableSpec = shift;
2502 3         11 my @columns = @_;
2503            
2504 3   33     28 my $Row = $_{return} || $BaseRow;
2505 3 50       19 return ' ' if ($Row eq ' ');
2506            
2507 3         10 my $rel = $_{rel};
2508 3 50       11 my $UpdRow = $rel ? $Row->$rel : $Row;
2509            
2510            
2511             # ---- New partial/preliminary auto create relationship support
2512             #
2513             # 1st-level relationships that don't already exist that are listed in the
2514             # 'update_create_rels' attr will be automatically created (as blank so they
2515             # can be updated in the subsequent update process)
2516             #
2517             # TODO: support any depth via an alternate 'update_create_relspec' attr and
2518             # create with supplied column values instead of blank (1 step instead of 2)
2519             #
2520 3         10 my %ucrls = map {$_=>1} @{$self->update_create_rels};
  0         0  
  3         129  
2521 3 0 33     16 if($rel && !$UpdRow && $ucrls{$rel} && $_{depth} == 1){
      33        
      0        
2522 0         0 $UpdRow = $Row->create_related($rel,{})->get_from_storage;
2523 0         0 my $msg = 'Auto CREATED RELATED -> ' . $self->get_Row_Rs_label($UpdRow) . "\n";
2524 0 0       0 scream_color(WHITE.ON_GREEN.BOLD,$msg) if($self->c->debug);
2525             }
2526             #
2527             # ----
2528            
2529            
2530 3         10 my %update = map { $_ => $data->{ $_{name_map}->{$_} } } keys %{$_{name_map}};
  3         16  
  3         16  
2531            
2532             # --- Need to do a map and a grep here; map to remap the values, and grep to prevent
2533             # the new values from being clobbered by identical key names from the original data:
2534 3         12 my $alias = { %{ $TableSpec->column_data_alias } };
  3         106  
2535             # -- strip out aliases that are identical to the original value. This will happen in the special
2536             # case of an update to a rel col that is ALSO a local col when 'priority_rel_columns' is on.
2537             # It shouldn't happen other times, but if it does, this is the right way to handle it, regardless:
2538 3   0     14 $_ eq $alias->{$_} and delete $alias->{$_} for (keys %$alias);
2539             # --
2540 3         12 my %revalias = map {$_=>1} values %$alias;
  0         0  
2541 3 50       10 %update = map { $alias->{$_} ? $alias->{$_} : $_ => $update{$_} } grep { !$revalias{$_} } keys %update;
  3         22  
  3         15  
2542             # ---
2543            
2544 3 50       14 unless (defined $UpdRow) {
2545 0 0       0 scream('NOTICE: Relationship/row "' . $rel . '" is not defined',\@columns)
2546             if($self->c->debug);
2547            
2548             # New: Throw an error when trying to update a column through a missing relationship so
2549             # the user knows instead of silenting ignoring those columns.
2550             # TODO: make this an option and alternatively *create* the missing relationship based on
2551             # settings of the relationship (needs an API/design to be thought up)
2552 0 0       0 if($rel) {
2553 0         0 my $relf = '<span style="font-weight:bold;color:navy;">' . $rel . '</span>';
2554 0         0 my $cols = '<span style="font-family:monospace;font-size:.85em;">' . join(', ',keys %update) . '</span>';
2555 0         0 my $html = '<span style="font-size:1.3em;">' .
2556             "Cannot update related field(s) of $relf ($cols) because there is no $relf set for this record. " .
2557             "<br><br>This probably just means you need to add or select a $relf first.</span>";
2558 0         0 die usererr rawhtml $html, title => "Can't update fields of non-existant related '$rel' ";
2559             }
2560             }
2561            
2562             # This should throw an error to the user, too:
2563 3 50       39 if ($UpdRow->isa('DBIx::Class::ResultSet')) {
2564 0 0       0 scream('NOTICE: Skipping multi relationship "' . $rel . '"')
2565             if($self->c->debug);
2566 0         0 return ' ';
2567             }
2568              
2569            
2570             # --- pull out updates to virtual relationship columns
2571 3         22 my $Source = $UpdRow->result_source;
2572 3         35 my $relcol_updates = {};
2573             (!$Source->has_column($_) && $Source->has_relationship($_)) and
2574 3   66     27 $relcol_updates->{$_} = delete $update{$_} for (keys %update);
      33        
2575             # add to the update queue with a special attr 'rel_update' instead of 'change'
2576 3 50       75 push @update_queue,{ row => $UpdRow, rel_update => $relcol_updates }
2577             if (keys %$relcol_updates > 0);
2578             # ---
2579            
2580 3         13 my $change = \%update;
2581            
2582 3 50       16 unless($ignore_current) {
2583            
2584 3         66 my %current = $UpdRow->get_columns;
2585            
2586 3         15 $change = {};
2587 3         12 foreach my $col (keys %update) {
2588 5     5   74 no warnings 'uninitialized';
  5         15  
  5         13119  
2589 3 50       19 next unless (exists $current{$col});
2590 3 0 33     14 next if (! defined $update{$col} and ! defined $current{$col});
2591 3 50       12 next if ($update{$col} eq $current{$col});
2592 3         13 $change->{$col} = $update{$col};
2593             }
2594            
2595 3         28 my $msg = 'Will UPDATE -> ' . $self->get_Row_Rs_label($UpdRow) . "\n";
2596 3 50       17 if (keys %$change > 0){
2597 3         43 my $t = Text::TabularDisplay->new(qw(column old new));
2598 3         467 $t->add($_,print_trunc(60,$current{$_}),print_trunc(60,$change->{$_})) for (keys %$change);
2599 3         267 $msg .= $t->render;
2600             }
2601             else {
2602 0         0 $msg .= 'No Changes';
2603             }
2604 3 50       565 scream_color(WHITE.ON_BLUE.BOLD,$msg) if($self->c->debug);
2605             }
2606            
2607 3         32 push @update_queue,{ row => $UpdRow, change => $change };
2608              
2609 3         26 return $UpdRow;
2610 3         153 },@$columns);
2611            
2612 3         98 return @update_queue;
2613             }
2614              
2615             # Works with the hashtree supplied to create_records to recursively
2616             # remap columns according to supplied TableSpec column_data_aliases
2617             sub hashtree_col_alias_map_deep {
2618 0     0 0 0 my $self = shift;
2619 0         0 my $hash = shift;
2620 0         0 my $TableSpec = shift;
2621            
2622             # Recursive:
2623 0         0 foreach my $rel (grep { ref($hash->{$_}) eq 'HASH' } keys %$hash) {
  0         0  
2624 0 0       0 my $rel_TableSpec = $TableSpec->related_TableSpec->{$rel} or next;
2625 0         0 $hash->{$rel} = $self->hashtree_col_alias_map_deep($hash->{$rel},$rel_TableSpec);
2626             }
2627            
2628             # -- Need to do a map and a grep here; map to remap the values, and grep to prevent
2629             # the new values from being clobbered by identical key names from the original data:
2630 0         0 my $alias = $TableSpec->column_data_alias;
2631 0         0 my %revalias = map {$_=>1} grep {!exists $hash->{$_}} values %$alias;
  0         0  
  0         0  
2632 0 0       0 %$hash = map { $alias->{$_} ? $alias->{$_} : $_ => $hash->{$_} } grep { !$revalias{$_} } keys %$hash;
  0         0  
  0         0  
2633             # --
2634            
2635             # --- remap special m2m relationship column values:
2636             # see apply_virtual_rel_col_update() above for the 'update' version
2637 0         0 my $Source = $TableSpec->ResultSource;
2638 0         0 foreach my $col (keys %$hash) {
2639 0 0       0 next if ($Source->has_column($col));
2640 0 0       0 my $info = $Source->relationship_info($col) or next;
2641 0 0       0 my $m2m_attrs = $info->{attrs}->{m2m_attrs} or next;
2642 0         0 my $keycol = $m2m_attrs->{rrinfo}->{cond_info}->{self};
2643            
2644 0         0 my @ids = split(/\s*,\s*/,$hash->{$col});
2645            
2646             # Convert the value into a valid "has_many" create packet:
2647 0         0 $hash->{$col} = [ map { { $keycol => $_ } } @ids ];
  0         0  
2648             }
2649             # ---
2650            
2651 0         0 return $hash;
2652             }
2653              
2654              
2655             # Gets programatically added as a method named 'create_records' (see BUILD modifier method above)
2656             sub _dbiclink_create_records {
2657 0     0   0 my $self = shift;
2658 0         0 my $params = shift;
2659            
2660 0         0 my $arr = $params;
2661 0 0       0 $arr = [ $params ] if (ref($params) eq 'HASH');
2662            
2663             #my $Rs = $self->ResultSource->resultset;
2664 0         0 my $Rs = $self->baseResultSet;
2665            
2666             # create_columns turned off in 080-DataStore.js - 2014-11-24 by HV
2667             #my @req_columns = $self->get_req_columns(undef,'create_columns');
2668 0         0 my @req_columns = $self->get_req_columns;
2669            
2670             # -- current real/valid columns according to DataStore2:
2671 0         0 my %cols_indx = map {$_=>1} $self->column_name_list;
  0         0  
2672             # --
2673            
2674 0         0 my @updated_keyvals = ();
2675              
2676             try {
2677             $self->ResultSource->schema->txn_do(sub {
2678 0         0 foreach my $data (@$arr) {
2679              
2680             # Apply optional base/hard coded data:
2681 0         0 %$data = ( %$data, %{$self->_CreateData} );
  0         0  
2682 0         0 my @columns = uniq(keys %$data,@req_columns);
2683 0 0       0 @columns = grep { $_ ne $self->record_pk && $_ ne 'loadContentCnf' } @columns;
  0         0  
2684 0         0 @columns = $self->TableSpec->filter_creatable_columns(@columns);
2685            
2686             # -- Limit to current real/valid columns according to DataStore2:
2687 0         0 @columns = grep { $cols_indx{$_} } @columns;
  0         0  
2688             # --
2689            
2690 0         0 my $relspecs = $self->TableSpec->columns_to_relspec_map(@columns);
2691            
2692 0         0 my $create_hash = {};
2693            
2694 0         0 foreach my $rel (keys %$relspecs) {
2695 0 0       0 $create_hash->{$rel} = {} unless (defined $create_hash->{$rel});
2696             exists $data->{$_->{orig_colname}} and $create_hash->{$rel}->{$_->{local_colname}} = $data->{$_->{orig_colname}}
2697 0   0     0 for (@{$relspecs->{$rel}});
  0         0  
2698             }
2699            
2700 0   0     0 my $create = delete $create_hash->{''} || {};
2701 0         0 $create = { %$create_hash, %$create };
2702            
2703             # -- Recursively remap column_data_alias:
2704 0         0 $create = $self->hashtree_col_alias_map_deep($create,$self->TableSpec);
2705             # --
2706            
2707 0         0 my $msg = 'CREATE -> ' . ref($Rs) . "\n";
2708 0 0       0 if (keys %$create > 0){
2709 0         0 my $t = Text::TabularDisplay->new(qw(column value));
2710             #$t->add($_,ref $create->{$_} ? Dumper($create->{$_}) : $create->{$_} ) for (keys %$create);
2711             #$t->add($_,disp(sub{ ref $_ ? Dumper($_) : undef },$create->{$_}) ) for (keys %$create);
2712 0         0 $t->add($_,print_trunc(60,$create->{$_})) for (keys %$create);
2713 0         0 $msg .= $t->render;
2714             }
2715             else {
2716 0         0 $msg .= 'Empty Record';
2717             }
2718 0 0       0 scream_color(WHITE.ON_GREEN.BOLD,$msg) if($self->c->debug);
2719 0         0 my $Row = $Rs->create($create);
2720            
2721 0         0 push @updated_keyvals, $self->generate_record_pk_value({ $Row->get_columns });
2722             }
2723 0     0   0 });
2724             }
2725             catch {
2726 0     0   0 my $err = shift;
2727 0         0 $self->handle_dbic_exception($err);
2728             #die usererr rawhtml $self->make_dbic_exception_friendly($err), title => 'Database Error';
2729 0         0 };
2730            
2731             # --
2732             # Perform a fresh lookup of all the records we just updated and send them back to the client:
2733             delete $self->c->req->params->{ $self->_rst_qry_param } if (
2734             # clear any existing rst_qry to prevent polluting the read
2735 0 0       0 exists $self->c->req->params->{ $self->_rst_qry_param }
2736             );
2737 0         0 my $newdata = $self->DataStore->read({
2738             columns => \@req_columns,
2739             id_in => \@updated_keyvals
2740             });
2741             # --
2742            
2743             die usererr rawhtml "Unknown error; no records were created",
2744 0 0 0     0 title => 'Create Failed' unless ($newdata && $newdata->{results});
2745            
2746             return {
2747 0         0 %$newdata,
2748             success => \1,
2749             msg => 'Create Succeeded',
2750             use_this => 1
2751             };
2752             }
2753              
2754             # Gets programatically added as a method named 'destroy_records' (see BUILD modifier method above)
2755             sub _dbiclink_destroy_records {
2756 0     0   0 my $self = shift;
2757 0         0 my $params = shift;
2758            
2759 0         0 my $arr = $params;
2760 0 0       0 $arr = [ $params ] if (not ref($params));
2761            
2762             #my $Rs = $self->ResultSource->resultset;
2763 0         0 my $Rs = $self->baseResultSet;
2764            
2765             try {
2766             $self->ResultSource->schema->txn_do(sub {
2767 0         0 my @Rows = ();
2768 0         0 foreach my $pk (@$arr) {
2769 0 0       0 my $Row = $Rs->search($self->record_pk_cond($pk))->next or die usererr "Failed to find row by record_pd: $pk";
2770            
2771 0         0 foreach my $rel (reverse sort @{$self->destroyable_relspec}) {
  0         0  
2772             next unless(
2773 0 0 0     0 $rel =~ /^[a-zA-Z0-9\-\_]+$/
2774             and $Row->can($rel)
2775             );
2776            
2777 0         0 my $relObj = $Row->$rel;
2778            
2779 0 0       0 scream_color(WHITE.ON_RED.BOLD,'DbicLink2 DESTROY --> ' . ref($Row) . '->' . $rel . ' --> ' .$self->get_Row_Rs_label($relObj,1) . "\n") if($self->c->debug);
2780 0 0       0 $relObj->can('delete_all') ? $relObj->delete_all : $relObj->delete;
2781             }
2782 0 0       0 scream_color(WHITE.ON_RED.BOLD,'DbicLink2 DESTROY --> ' . $self->get_Row_Rs_label($Row,1) . "\n")
2783             if($self->c->debug);
2784 0         0 $Row->delete;
2785             }
2786 0     0   0 });
2787             }
2788             catch {
2789 0     0   0 my $err = shift;
2790 0         0 $self->handle_dbic_exception($err);
2791             #die usererr rawhtml $self->make_dbic_exception_friendly($err), title => 'Database Error';
2792 0         0 };
2793            
2794 0         0 return 1;
2795             }
2796              
2797              
2798              
2799             sub extract_db_error_from_exception {
2800 0     0 0 0 my $self = shift;
2801 0         0 my $exception = shift;
2802 0 0       0 die $exception if (ref($exception) =~ /^RapidApp\:\:Responder/);
2803            
2804 0         0 warn $exception;
2805            
2806 0         0 my $msg = "" . $exception . "";
2807            
2808 0         0 my @parts = split(/DBD\:\:.+\:\:st execute failed\:\s*/,$msg);
2809 0 0       0 return undef unless (scalar @parts > 1);
2810            
2811 0         0 $msg = $parts[1];
2812 0         0 @parts = split(/\s*\[/,$msg);
2813            
2814 0         0 return $parts[0];
2815             }
2816              
2817              
2818             sub handle_dbic_exception {
2819 0     0 0 0 my $self = shift;
2820 0         0 my $exception = shift;
2821            
2822 0         0 my $msg = $self->extract_db_error_from_exception($exception);
2823 0 0       0 $msg = $msg ? "$msg\n\n----------------\n" : '';
2824              
2825 0         0 my $html = '<pre>' . $msg . $exception . "</pre>";
2826            
2827 0         0 die usererr rawhtml $html, title => "Database Error $append_exception_title";
2828            
2829             #die $exception if (ref($exception) =~ /^RapidApp\:\:Responder/);
2830             #die usererr rawhtml $self->make_dbic_exception_friendly($exception), title => 'DbicLink2 Database Error';
2831             }
2832              
2833              
2834             sub make_dbic_exception_friendly {
2835 0     0 0 0 my $self = shift;
2836 0         0 my $exception = shift;
2837            
2838 0         0 warn $exception;
2839            
2840 0         0 my $msg = "" . $exception . "";
2841            
2842            
2843             #### Fix me!!!! ####
2844             # Randomly getting this DBIx exception when throwing a customprompt object within CRUD operations
2845             # no idea silently pass it up for now
2846 0 0       0 die infostatus msg => "Bizarre copy of HASH in aassign", status => 500 if ($msg =~/Bizarre copy of HASH in aassign/);
2847            
2848            
2849            
2850 0         0 my @parts = split(/DBD\:\:mysql\:\:st execute failed\:\s*/,$msg);
2851 0 0       0 return $exception unless (scalar @parts > 1);
2852            
2853 0         0 $msg = $parts[1];
2854            
2855 0         0 @parts = split(/\s*\[/,$msg);
2856              
2857 0         0 return '<center><pre>' . $parts[0] . "</pre></center>";
2858 0         0 return $parts[0];
2859             }
2860              
2861              
2862             sub param_decodeIf {
2863 51     51 0 400 my $self = shift;
2864 51         112 my $param = shift;
2865 51   100     167 my $default = shift || undef;
2866            
2867 51 100       183 return $default unless (defined $param);
2868            
2869 14 100       71 return $param if (ref $param);
2870 11         26 my $decoded;
2871             try {
2872 11     11   968 $decoded = $self->json->decode($param);
2873             }
2874             catch {
2875 0     0   0 my $err = shift;
2876 0         0 confess "$err \n\nINPUT STRING: '$param'\n\n";
2877 11         148 };
2878 11         9062 return $decoded;
2879             }
2880              
2881              
2882             # This is a DbicLink2-specific implementation of batch_update. Overrides generic method
2883             # in DataStore2. It is able to perform much better with large batches
2884             sub batch_update {
2885 0     0 0   my $self = shift;
2886            
2887             # See DataStore2:
2888 0           $self->before_batch_update;
2889            
2890 0           my $editSpec = $self->param_decodeIf($self->c->req->params->{editSpec});
2891 0           my $read_params = $editSpec->{read_params};
2892 0           my $update = $editSpec->{update};
2893            
2894 0           delete $read_params->{start};
2895 0           delete $read_params->{limit};
2896            
2897 0           my %orig_params = %{$self->c->req->params};
  0            
2898 0           %{$self->c->req->params} = %$read_params;
  0            
2899 0           my $Rs = $self->get_read_records_Rs($read_params);
2900 0           %{$self->c->req->params} = %orig_params;
  0            
2901            
2902             # Remove select/as so the columns are normal (these select/as attrs only apply to read_records)
2903 0           delete $Rs->{attrs}->{select};
2904 0           delete $Rs->{attrs}->{as};
2905            
2906 0           my $total = $Rs->pager->total_entries;
2907            
2908             die usererr "Update count mismatch (" .
2909             $editSpec->{count} . ' vs ' . $total . ') ' .
2910             "- This can happen if someone else modified one or more of the records in the update set.\n\n" .
2911             "Reload the the grid and try again."
2912 0 0         unless ($editSpec->{count} == $total);
2913            
2914 0 0         my @columns = grep { $_ ne $self->record_pk && $_ ne 'loadContentCnf' } keys %$update;
  0            
2915 0           @columns = $self->TableSpec->filter_updatable_columns(@columns);
2916            
2917             try {
2918             $self->ResultSource->schema->txn_do(sub {
2919            
2920 0           my $ignore_current = 1;
2921 0           my @update_queue = ();
2922             push(@update_queue, $self->prepare_record_updates($_,\@columns,$update,$ignore_current))
2923 0           for ($Rs->all);
2924            
2925             # Update all the rows at the end:
2926 0           $self->process_update_queue(@update_queue);
2927 0     0     });
2928             }
2929             catch {
2930 0     0     my $err = shift;
2931 0           $self->handle_dbic_exception($err);
2932 0           };
2933            
2934 0           return 1;
2935             }
2936              
2937              
2938             1;