File Coverage

blib/lib/RapidApp/Module/StorCmp.pm
Criterion Covered Total %
statement 63 165 38.1
branch 16 92 17.3
condition 0 39 0.0
subroutine 15 24 62.5
pod 0 14 0.0
total 94 334 28.1


line stmt bran cond sub pod time code
1             package RapidApp::Module::StorCmp;
2              
3             # ABSTRACT: Base class for modules with a Ext.data.Store
4              
5 5     5   2640 use strict;
  5         13  
  5         132  
6 5     5   22 use warnings;
  5         11  
  5         112  
7              
8 5     5   24 use Moose;
  5         8  
  5         24  
9             extends 'RapidApp::Module::ExtComponent';
10              
11 5     5   27271 use RapidApp::Util qw(:all);
  5         13  
  5         1941  
12 5     5   35 use Clone qw(clone);
  5         9  
  5         224  
13              
14 5     5   2307 use RapidApp::Module::DatStor;
  5         16  
  5         5299  
15              
16             has 'no_datastore_plus_plugin', is => 'ro', isa => 'Bool', lazy => 1, default => 0;
17              
18             has 'TableSpec' => ( is => 'ro', isa => 'Maybe[RapidApp::TableSpec]', lazy_build => 1 );
19 11     11   256 sub _build_TableSpec { undef; }
20              
21             has 'TableSpec_applied' => ( is => 'rw', isa => 'Bool', default => 0 );
22              
23             has 'record_pk' => ( is => 'ro', default => 'id' );
24             has 'DataStore_class' => ( is => 'ro', default => 'RapidApp::Module::DatStor' );
25              
26             has 'max_pagesize' => ( is => 'ro', isa => 'Maybe[Int]', default => undef );
27              
28             has 'persist_all_immediately' => ( is => 'ro', isa => 'Bool', default => 0 );
29             has 'persist_immediately' => ( is => 'ro', isa => 'HashRef', default => sub{{
30             create => \0,
31             update => \0,
32             destroy => \0
33             }});
34              
35             # New option added in GitHub Issue #85
36             has 'dedicated_add_form_enabled', is => 'ro', isa => 'Bool', default => 1;
37              
38             # use_add_form/use_edit_form: 'tab', 'window' or undef
39             has 'use_add_form', is => 'ro', isa => 'Maybe[Str]', lazy => 1, default => undef;
40             has 'use_edit_form', is => 'ro', isa => 'Maybe[Str]', lazy => 1, default => undef;
41             has 'autoload_added_record', default => sub {
42             my $self = shift;
43             # Default to the same value as 'use_add_form'
44             return $self->use_add_form ? 1 : 0;
45             }, is => 'ro', isa => 'Bool', lazy => 1;
46              
47             has 'allow_batch_update', is => 'ro', isa => 'Bool', default => 1;
48             has 'batch_update_max_rows', is => 'ro', isa => 'Int', default => 500;
49             has 'confirm_on_destroy', is => 'ro', isa => 'Bool', default => 1;
50              
51             # not implimented yet:
52             #has 'batch_update_warn_rows', is => 'ro', isa => 'Int', default => 100;
53              
54             # If cache_total_count is true the total count query will be skipped and the value supplied
55             # by the client (if defined) will be returned instead. The code and logic to do the actual
56             # caching is in JavaScript, and utilization of this feature currently is only implemented
57             # within DbicLink2:
58             has 'cache_total_count', is => 'ro', isa => 'Bool', default => 1;
59              
60             has 'DataStore' => (
61             is => 'rw',
62             isa => 'RapidApp::Module::DatStor',
63             handles => {
64             JsonStore => 'JsonStore',
65             # store_read => 'store_read',
66             # store_read_raw => 'store_read_raw',
67             columns => 'columns',
68             column_order => 'column_order',
69             multisort_enabled => 'multisort_enabled',
70             sorters => 'sorters',
71             include_columns => 'include_columns',
72             exclude_columns => 'exclude_columns',
73             include_columns_hash => 'include_columns_hash',
74             exclude_columns_hash => 'exclude_columns_hash',
75             apply_columns => 'apply_columns',
76             column_list => 'column_list',
77             apply_to_all_columns => 'apply_to_all_columns',
78             applyIf_to_all_columns => 'applyIf_to_all_columns',
79             apply_columns_list => 'apply_columns_list',
80             set_sort => 'set_sort',
81             batch_apply_opts => 'batch_apply_opts',
82             set_columns_order => 'set_columns_order',
83             # record_pk => 'record_pk',
84             getStore => 'getStore',
85             getStore_code => 'getStore_code',
86             getStore_func => 'getStore_func',
87             store_load_code => 'store_load_code',
88             store_listeners => 'listeners',
89             apply_store_listeners => 'apply_listeners',
90             apply_store_config => 'apply_extconfig',
91             valid_colname => 'valid_colname',
92             apply_columns_ordered => 'apply_columns_ordered',
93             batch_apply_opts_existing => 'batch_apply_opts_existing',
94             delete_columns => 'delete_columns',
95             has_column => 'has_column',
96             get_column => 'get_column',
97             deleted_column_names => 'deleted_column_names',
98             column_name_list => 'column_name_list',
99             get_columns_wildcards => 'get_columns_wildcards',
100             apply_coderef_columns => 'apply_coderef_columns'
101            
102             }
103             );
104              
105              
106             has 'DataStore_build_params' => ( is => 'ro', default => undef, isa => 'Maybe[HashRef]' );
107              
108             has 'defer_to_store_module' => ( is => 'ro', isa => 'Maybe[Object]', lazy => 1, default => undef );
109              
110             around 'columns' => \&defer_store_around_modifier;
111             around 'column_order' => \&defer_store_around_modifier;
112             around 'has_column' => \&defer_store_around_modifier;
113             around 'get_column' => \&defer_store_around_modifier;
114              
115             sub defer_store_around_modifier {
116 253     253   2392 my $orig = shift;
117 253         343 my $self = shift;
118 253 50       6289 return $self->$orig(@_) unless (defined $self->defer_to_store_module);
119 0         0 return $self->defer_to_store_module->$orig(@_);
120             }
121              
122              
123             # We are doing it this way so we can hook into this exact spot with method modifiers in other places:
124             sub BUILD {}
125             before 'BUILD' => sub { (shift)->DataStore2_BUILD };
126             sub DataStore2_BUILD {
127 103     103 0 938 my $self = shift;
128            
129             # New for #85:
130 103 50       3095 $self->apply_actions( add => 'dedicated_add_form' ) if ($self->dedicated_add_form_enabled);
131            
132 103         2846 my $store_params = {
133             record_pk => $self->record_pk,
134             max_pagesize => $self->max_pagesize
135             };
136            
137 103 50       806 if ($self->can('create_records')) {
138 0 0       0 $self->apply_flags( can_create => 1 ) unless ($self->flag_defined('can_create'));
139 0 0       0 $store_params->{create_handler} = RapidApp::Handler->new( scope => $self, method => 'create_records' ) if ($self->has_flag('can_create'));
140             }
141            
142 103 50       583 if ($self->can('read_records')) {
143 103 50       3615 $self->apply_flags( can_read => 1 ) unless ($self->flag_defined('can_read'));
144 103 50       3208 $store_params->{read_handler} = RapidApp::Handler->new( scope => $self, method => 'read_records' ) if ($self->has_flag('can_read'));
145             }
146            
147 103 50       1358 if ($self->can('update_records')) {
148 0 0       0 $self->apply_flags( can_update => 1 ) unless ($self->flag_defined('can_update'));
149 0 0       0 $store_params->{update_handler} = RapidApp::Handler->new( scope => $self, method => 'update_records' ) if ($self->has_flag('can_update'));
150             }
151            
152 103 50       584 if ($self->can('destroy_records')) {
153 0 0       0 $self->apply_flags( can_destroy => 1 ) unless ($self->flag_defined('can_destroy'));
154 0 0       0 $store_params->{destroy_handler} = RapidApp::Handler->new( scope => $self, method => 'destroy_records' ) if ($self->has_flag('can_destroy'));
155             }
156            
157             $store_params = {
158             %$store_params,
159 103 100       2939 %{ $self->DataStore_build_params }
  97         2630  
160             } if (defined $self->DataStore_build_params);
161            
162 103         2872 $self->apply_modules( store => {
163             class => $self->DataStore_class,
164             params => $store_params
165             });
166 103         674 $self->DataStore($self->Module('store',1));
167            
168             #init the store with all of our flags:
169 103         2433 $self->DataStore->apply_flags($self->all_flags);
170            
171 103         569 $self->add_ONREQUEST_calls('store_init_onrequest');
172 103         622 $self->add_ONREQUEST_calls_late('apply_store_to_extconfig');
173            
174             # Init (but don't apply) TableSpec early
175 103         3001 $self->TableSpec;
176             }
177              
178              
179             after 'BUILD' => sub {
180             my $self = shift;
181              
182             $self->apply_extconfig(
183             persist_all_immediately => \scalar($self->persist_all_immediately),
184             persist_immediately => $self->persist_immediately,
185             use_add_form => $self->use_add_form,
186             use_edit_form => $self->use_edit_form,
187             autoload_added_record => $self->autoload_added_record ? \1 : \0,
188             cache_total_count => $self->cache_total_count ? \1 : \0,
189             confirm_on_destroy => $self->confirm_on_destroy ? \1 : \0
190             );
191            
192             ## Apply the TableSpec if its defined ##
193             $self->apply_TableSpec_config;
194            
195             if(defined $self->Module('store',1)->create_handler) {
196             $self->apply_actions( add_form => 'get_add_form' );
197             $self->apply_extconfig( add_form_url => $self->suburl('add_form') );
198             }
199            
200             if($self->allow_batch_update && defined $self->Module('store',1)->update_handler) {
201             $self->apply_actions( edit_form => 'get_edit_form' );
202             $self->apply_extconfig( edit_form_url => $self->suburl('edit_form') );
203            
204             $self->apply_actions( batch_update => 'batch_update' );
205             $self->apply_extconfig( batch_update_url => $self->suburl('batch_update') );
206             }
207            
208             $self->add_plugin( 'datastore-plus' ) unless ($self->no_datastore_plus_plugin);
209             };
210              
211              
212             sub apply_TableSpec_config {
213 103     103 0 225 my $self = shift;
214 103 100       2742 $self->TableSpec or return;
215 92 50       2403 $self->TableSpec_applied and return;
216            
217 92         1092 my $prop_names = [ @RapidApp::Module::DatStor::Column::attrs ];
218 92         2326 my $columns = $self->TableSpec->columns_properties_limited($prop_names);
219            
220 92         1930 $self->apply_columns($columns);
221 92         2731 $self->set_columns_order(0,$self->TableSpec->column_names_ordered);
222            
223 92 50       2591 $self->DataStore->add_onrequest_columns_mungers(
224             $self->TableSpec->all_onrequest_columns_mungers
225             ) unless ($self->TableSpec->has_no_onrequest_columns_mungers);
226            
227 92         2518 $self->TableSpec_applied(1);
228             }
229              
230              
231             sub defer_DataStore {
232 66     66 0 139 my $self = shift;
233 66 50       1689 return $self->DataStore unless (defined $self->defer_to_store_module);
234 0 0       0 return $self->defer_to_store_module->defer_DataStore if ($self->defer_to_store_module->can('defer_DataStore'));
235 0         0 return $self->defer_to_store_module;
236             }
237              
238             sub store_init_onrequest {
239 33     33 0 1517 my $self = shift;
240            
241             # Simulate direct ONREQUEST:
242 33         127 $self->Module('store');
243            
244 33         172 $self->apply_extconfig( columns => $self->defer_DataStore->column_list );
245 33         301 $self->apply_extconfig( sort => $self->defer_DataStore->get_extconfig_param('sort_spec') );
246             }
247              
248              
249             # ----
250             # NEW: use Tie::IxHash to setup the extconfig hash to be ordered with the 'store'
251             # key predeclared as the first key. Because the ExtJS client decodes and processes
252             # JSON in order, we want to make sure the store is processed before other parts
253             # which may need to reference it by storeId. This is needed after perl 5.18 because
254             # the order of hashes was changed in that version. We just happened to be lucky
255             # that the order before 5.18 just happened to have the store key showup earlier
256             # than we happened to be using it. After 5.18, its random. This solves the problem
257             # once and for all. (Note: the case where this was a problem was in cases of
258             # several nested modules maing use of defer_to_store_module feature which is not
259             # a common use-case, so this was only an issue for very specific circumstances)
260             has '+extconfig', default => sub {
261 5     5   1144 use Tie::IxHash;
  5         4849  
  5         5870  
262             my %cfg;
263             tie(%cfg, 'Tie::IxHash', store => undef );
264             return \%cfg
265             };
266             # ----
267              
268             sub apply_store_to_extconfig {
269 33     33 0 70 my $self = shift;
270            
271 33 50       804 if (defined $self->defer_to_store_module) {
272 0         0 $self->apply_extconfig( store => $self->defer_DataStore->getStore_func );
273             }
274             else {
275 33         124 $self->apply_extconfig( store => $self->Module('store')->JsonStore );
276             }
277             }
278              
279              
280             has 'add_edit_formpanel_defaults', is => 'ro', isa => 'HashRef', lazy => 1, default => sub {{
281             xtype => 'form',
282             frame => \1,
283             labelAlign => 'right',
284             labelWidth => 100,
285             plugins => ['dynamic-label-width'],
286             bodyStyle => 'padding: 25px 10px 5px 5px;',
287             cls => 'ra-datastore-add-edit-form',
288             defaults => {
289             width => 250
290             },
291             autoScroll => \1,
292             monitorValid => \1,
293             buttonAlign => 'center',
294             minButtonWidth => 100,
295            
296             # datastore-plus (client side) adds handlers based on the "name" properties 'save' and 'cancel' below
297             buttons => [
298             {
299             name => 'save',
300             text => 'Save',
301             iconCls => 'ra-icon-save-ok',
302             formBind => \1
303             },
304             {
305             name => 'cancel',
306             text => 'Cancel',
307             }
308             ]
309             }};
310              
311             sub get_add_edit_form_items {
312 0     0 0   my $self = shift;
313 0           my $mode = shift;
314 0 0 0       die '$mode should be "add" or "edit"' unless ($mode eq 'add' || $mode eq 'edit');
315            
316 0           my $allow_flag = "allow_$mode";
317            
318 0           my @items = ();
319            
320 0           foreach my $colname (@{$self->column_order}) {
  0            
321 0 0         my $Cnf = $self->columns->{$colname} or next;
322 0 0 0       next unless (defined $Cnf->{editor} and $Cnf->{editor} ne '');
323            
324 0           my $allow = jstrue($Cnf->{$allow_flag});
325             $allow = $allow || jstrue($Cnf->{allow_batchedit}) if (
326             $mode eq 'edit' &&
327             !jstrue($Cnf->{no_column})
328 0 0 0       );
      0        
329            
330             #Skip columns with 'no_column' set to true except if $allow_flag is true:
331 0 0 0       next if (jstrue($Cnf->{no_column}) && ! $allow);
332            
333             #Skip if $allow_flag is defined but set to false:
334 0 0 0       next if (defined $Cnf->{$allow_flag} && ! $allow);
335            
336 0           my $field = clone($Cnf->{editor});
337 0           $field->{name} = $colname;
338 0 0         $field->{allowBlank} = \1 unless (defined $field->{allowBlank});
339            
340             # New, extra check for newly added 'is_nullable' column attr (Github Issue #33)
341 0 0         $field->{allowBlank} = \0 unless ($Cnf->{is_nullable});
342            
343 0 0         unless (jstrue $field->{allowBlank}) {
344 0 0         $field->{labelStyle} = '' unless (defined $field->{labelStyle});
345 0           $field->{labelStyle} .= 'font-weight:bold;';
346             }
347 0 0         $field->{header} = $Cnf->{header} if(defined $Cnf->{header});
348 0 0 0       $field->{header} = $colname unless (defined $field->{header} and $field->{header} ne '');
349 0           $field->{fieldLabel} = $field->{header};
350 0           $field->{anchor} = '-20';
351            
352             # ---- Moved from DataStorePlus JS (client-side):
353             # Important: autoDestroy must be false on the store or else store-driven
354             # components (i.e. combos) will be broken as soon as the form is closed
355             # the first time
356 0 0         $field->{store}{autoDestroy} = \1 if ($field->{store});
357            
358             # Make sure that hidden fields that can't be changed don't
359             # block validation of the form if they are empty and erroneously
360             # set with allowBlank: false (common-sense failsafe):
361 0 0         $field->{allowBlank} = \1 if (jstrue $field->{hidden});
362             # ----
363            
364             # -- New: if column 'documentation' is present, render it via Ext.ux.FieldHelp plugin
365 0 0         if($Cnf->{documentation}) {
366 0   0       $field->{plugins} ||= [];
367 0           push @{$field->{plugins}}, 'fieldhelp';
  0            
368 0           $field->{helpText} = $Cnf->{documentation};
369             }
370             # --
371            
372 0           push @items, $field;
373             }
374            
375 0           return @items;
376             }
377              
378             sub get_add_form {
379 0     0 0   my $self = shift;
380             return {
381 0           %{$self->add_edit_formpanel_defaults},
  0            
382             items => [ $self->get_add_form_items ]
383             };
384             }
385              
386             sub get_add_form_items {
387 0     0 0   my $self = shift;
388 0           return $self->get_add_edit_form_items('add');
389             }
390              
391             sub get_edit_form {
392 0     0 0   my $self = shift;
393             return {
394 0           %{$self->add_edit_formpanel_defaults},
  0            
395             items => [ $self->get_edit_form_items ]
396             };
397             }
398              
399             sub get_edit_form_items {
400 0     0 0   my $self = shift;
401 0           return $self->get_add_edit_form_items('edit');
402             }
403              
404              
405              
406             sub before_batch_update {
407 0     0 0   my $self = shift;
408 0           my $editSpec = $self->param_decodeIf($self->c->req->params->{editSpec});
409 0           my $update = $editSpec->{update};
410            
411             die usererr "Invalid editSpec - record_pk found in update data!!"
412 0 0         if (exists $update->{$self->record_pk});
413            
414 0 0         my $count = $editSpec->{count} or die usererr "Invalid editSpec - no count supplied";
415            
416 0           my $max = $self->batch_update_max_rows;
417            
418 0 0 0       die usererr
419             "Too many rows for batch update ($count) - max allowed rows: $max",
420             title => "Batch Update Denied"
421             if($max && $count > $max);
422             };
423              
424             # This is expensive, but compatible with the generic DataStore2 (update) API.
425             # Should be overridden in more specific derived classes, like in DbicLink2, to
426             # perform a smarter/more efficient operation
427             sub batch_update {
428 0     0 0   my $self = shift;
429            
430             # this is called directly instead of adding a method modifier because a modifier
431             # would not get called when batch_update is overridden by another Role (such as in
432             # DbicLink2)
433 0           $self->before_batch_update;
434            
435 0           my $editSpec = $self->param_decodeIf($self->c->req->params->{editSpec});
436 0           my $read_params = $editSpec->{read_params};
437 0           my $update = $editSpec->{update};
438            
439 0           delete $read_params->{start};
440 0           delete $read_params->{limit};
441            
442             # perform a read to verify that totalCount matches the supplied/expected count
443 0           my %orig_params = %{$self->c->req->params};
  0            
444 0           %{$self->c->req->params} = %$read_params;
  0            
445 0           my $readdata = $self->read_records();
446 0           my $rows = $readdata->{rows};
447            
448             die "Actual row count (" . @$rows . ") doesn't agree with 'results' property (" . $readdata->{results} . ")"
449 0 0         unless (@$rows == $readdata->{results});
450            
451             die usererr "Update count mismatch (" .
452             $editSpec->{count} . ' vs ' . $readdata->{results} . ') ' .
453             "- This can happen if someone else modified one or more of the records in the update set.\n\n" .
454             "Reload the the grid and try again."
455 0 0         unless ($editSpec->{count} == $readdata->{results});
456            
457             # apply update data to rows:
458 0           %$_ = (%$_,%$update) for (@$rows);
459            
460 0           my $result;
461             {
462 0           local $RapidApp::Module::DatStor::BATCH_UPDATE_IN_PROGRESS = 1;
  0            
463 0           $result = $self->DataStore->update_handler->call($rows,$read_params);
464             }
465            
466 0           %{$self->c->req->params} = %orig_params;
  0            
467            
468             return $result if (
469             ref($result) eq 'HASH' and
470             defined $result->{success}
471 0 0 0       );
472            
473             return {
474 0 0         success => \1,
475             msg => 'Batch Update Succeeded'
476             } if ($result);
477            
478 0           die "Update Failed";
479             }
480              
481              
482             sub param_decodeIf {
483 0     0 0   my $self = shift;
484 0           my $param = shift;
485 0   0       my $default = shift || undef;
486            
487 0 0         return $default unless (defined $param);
488            
489 0 0         return $param if (ref $param);
490 0           return $self->json->decode($param);
491             }
492              
493              
494             # New for #85:
495             sub dedicated_add_form {
496 0     0 0   my $self = shift;
497 0 0         die "Not allowed" unless $self->dedicated_add_form_enabled;
498            
499 0           my $c = $self->c;
500 0           my $content = $self->content;
501            
502             # Just in case custom add_form_url_params are configured, merge them into the current
503             # request params so they are available as they would be if called via Ajax...
504 0   0       my $afuParams = $self->get_extconfig_param('add_form_url_params') || {};
505 0           %{$c->req->params} = ( %{$c->req->params}, %$afuParams );
  0            
  0            
506            
507 0           my $fp = $self->get_add_form;
508            
509 0   0       my $btnCfg = ($self->get_extconfig_param('store_button_cnf')||{})->{add} || {};
510            
511             return $self->render_data({
512             xtype => 'datastore-dedicated-add-form',
513            
514             source_cmp => $content,
515             formpanel => $fp,
516            
517             tabTitle => $btnCfg->{text} || '(Add ...)',
518 0   0       tabIconCls => $btnCfg->{iconCls} || 'ra-icon-add'
      0        
519            
520             });
521             }
522            
523              
524              
525             #### --------------------- ####
526              
527              
528 5     5   47 no Moose;
  5         13  
  5         38  
529             #__PACKAGE__->meta->make_immutable;
530             1;
531