File Coverage

blib/lib/App/AutoCRUD/Controller/Table.pm
Criterion Covered Total %
statement 146 229 63.7
branch 27 62 43.5
condition 8 25 32.0
subroutine 22 26 84.6
pod 3 10 30.0
total 206 352 58.5


line stmt bran cond sub pod time code
1             package App::AutoCRUD::Controller::Table;
2              
3 2     2   1068 use 5.010;
  2         5  
4 2     2   9 use strict;
  2         2  
  2         40  
5 2     2   8 use warnings;
  2         4  
  2         44  
6              
7 2     2   8 use Moose;
  2         3  
  2         13  
8             extends 'App::AutoCRUD::Controller';
9 2     2   10108 use SQL::Abstract::More 1.27;
  2         37  
  2         88  
10 2     2   9 use List::MoreUtils qw/mesh firstval/;
  2         4  
  2         20  
11 2     2   1415 use JSON::MaybeXS ();
  2         1187  
  2         38  
12 2     2   9 use URI;
  2         4  
  2         50  
13              
14 2     2   59 use namespace::clean -except => 'meta';
  2         4  
  2         19  
15              
16             #----------------------------------------------------------------------
17             # entry point to the controller
18             #----------------------------------------------------------------------
19             sub serve {
20 21     21 1 30 my ($self) = @_;
21              
22 21         533 my $context = $self->context;
23              
24             # extract from path : table name and method to dispatch to
25 21 50       69 my ($table, $meth_name) = $context->extract_path_segments(2)
26             or die "URL too short, missing table and method name";
27 21 50       94 my $method = $self->can($meth_name)
28             or die "no such method: $meth_name";
29              
30             # set default template and title
31 21         597 $context->set_template("table/$meth_name.tt");
32 21         487 $context->set_title($context->title . "-" . $table);
33              
34             # dispatch to method
35 21         61 return $self->$method($table);
36             }
37              
38              
39             #----------------------------------------------------------------------
40             # published methods
41             #----------------------------------------------------------------------
42              
43             sub descr {
44 11     11 1 19 my ($self, $table) = @_;
45              
46 11         50 my $datasource = $self->datasource;
47 11         50 my $descr = $datasource->config(tables => $table => 'descr');
48              
49             # datastructure describing this table
50 11         206 return {table => $table,
51             colgroups => $datasource->colgroups($table),
52             primary_key => [$datasource->primary_key($table)],
53             descr => $descr};
54              
55             }
56              
57              
58             sub list {
59 3     3 1 6 my ($self, $table) = @_;
60              
61 3         85 my $context = $self->context;
62 3         81 my $req_data = $context->req_data;
63 3         81 my $datasource = $context->datasource;
64              
65             # the "message" arg is sent once from inserts/updates/deletes; not to
66             # be repeated in links to other queries
67 3         11 my $message = delete $req_data->{-message};
68              
69             # dashed args are set apart
70 3         9 my %where_args = %$req_data; # need a clone because of deletes below
71 3         90 my %dashed_args = $context->view->default_dashed_args($context);
72 3         84 foreach my $arg (grep {/^-/} keys %where_args) {
  1         4  
73 0         0 $dashed_args{$arg} = delete $where_args{$arg};
74             }
75              
76             # some dashed args are treated here (not sent to the SQL request)
77 3         10 my $with_count = delete $dashed_args{-with_count};
78 3         9 my $template = delete $dashed_args{-template};
79 3 50       10 $context->set_template($template) if $template;
80              
81             # select from database
82 3   50     97 my $criteria = $datasource->query_parser->parse(\%where_args) || {};
83 3         5054 my $statement = $datasource->schema->db_table($table)->select(
84             -where => $criteria,
85             %dashed_args,
86             -result_as => 'statement',
87             );
88 3         22846 my $rows = $statement->select();
89              
90             # recuperate SQL for logging / informational purposes
91 3         3769 my ($sql, @bind) = $statement->sql;
92 3         33 my $show_sql = join " / ", $sql, @bind;
93 3         37 $self->logger({level => 'debug', message => $show_sql});
94              
95             # assemble results
96 3         71 my $data = $self->descr($table);
97 3         45 $data->{rows} = $rows;
98 3         8 $data->{message} = $message;
99 3         9 $data->{criteria} = $show_sql;
100 3 50       10 if ($with_count) {
101 0         0 $data->{row_count} = $statement->row_count;
102 0         0 $data->{page_count} = $statement->page_count;
103             }
104              
105             # links to prev/next pages
106             $self->_add_links_to_other_pages($data, $req_data,
107             $dashed_args{-page_index},
108 3         22 $dashed_args{-page_size});
109              
110             # link to update/delete forms
111             $data->{where_args} = $self->_query_string(
112 3         9 map { ("where.$_" => $where_args{$_}) } keys %where_args,
  1         6  
113             );
114              
115 3         100 return $data;
116             }
117              
118              
119             sub _add_links_to_other_pages {
120 3     3   6 my ($self, $data, $req_data, $page_index, $page_size) = @_;
121              
122 3 50 33     23 return unless defined $page_index && defined $page_size;
123              
124 3         10 $data->{page_index} = $page_index;
125 3         12 $data->{offset} = ($page_index - 1) * $page_size + 1;
126 3         18 $data->{similar_query} = $self->_query_string(%$req_data,
127             -page_index => 1);
128             $data->{next_page} = $self->_query_string(%$req_data,
129             -page_index => $page_index+1)
130 3 50       222 unless @{$data->{rows}} < $page_size;
  3         18  
131 3 50       12 $data->{prev_page} = $self->_query_string(%$req_data,
132             -page_index => $page_index-1)
133             unless $page_index <= 1;
134             }
135              
136              
137              
138             sub id {
139 6     6 0 11 my ($self, $table) = @_;
140              
141 6         20 my $data = $self->descr($table);
142              
143 6         79 my $pk = $data->{primary_key};
144 6         160 my @vals = $self->context->extract_path_segments(scalar(@$pk));
145 6         33 my %criteria = mesh @$pk, @vals;
146              
147             # get row from database
148 6         23 my $row = $self->datasource->schema->db_table($table)->fetch(@vals);
149              
150             # assemble results
151 6         6464 $data->{row} = $row;
152 6         19 $data->{pk_val} = join "/", @vals;
153              
154             # links
155 6         18 my %where_pk = map { ("where_pk.$_" => $criteria{$_}) } keys %criteria;
  6         25  
156 6         23 $data->{where_pk} = $self->_query_string(%where_pk);
157              
158 6         25 return $data;
159             }
160              
161              
162             sub search {
163 2     2 0 4 my ($self, $table) = @_;
164              
165 2         48 my $context = $self->context;
166 2         47 my $req_data = $context->req_data;
167              
168 2 100       48 if ($context->req->method eq 'POST') {
169 1   50     12 my $output = delete $req_data->{-output} || "";
170 1 50       2 my $cols = [keys %{delete $req_data->{col} || {}}];
  1         6  
171 1         5 $req_data->{-columns} = join ",", @$cols;
172 1         6 $self->redirect("list$output?" . $self->_query_string(%$req_data));
173             }
174             else {
175             # display the search form
176 1   50     18 my @cols = split /,/, (delete $req_data->{-columns} || "");
177 1         3 $req_data->{"col.$_"} = 1 foreach @cols;
178 1         4 my $data = $self->descr($table);
179 1         19 $data->{init_form} = $self->_encode_json($req_data);
180 1         3 return $data;
181             }
182             }
183              
184              
185             sub update {
186 5     5 0 11 my ($self, $table) = @_;
187              
188 5         16 $self->_check_canmodify;
189              
190 3 50       68 if ($self->context->req->method eq 'POST') {
191 3         22 $self->_do_update_data($table);
192             }
193             else {
194 0         0 $self->_display_update_form($table);
195             }
196             }
197              
198             sub _check_canmodify {
199 12     12   13 my ($self) = @_;
200            
201 12 100       324 if ($self->context->app->readonly) {
202 4         36 die 'readonly mode';
203             }
204             }
205              
206             sub _do_update_data {
207 3     3   4 my ($self, $table) = @_;
208              
209 3         5 $self->_check_canmodify;
210              
211 3         67 my $context = $self->context;
212 3         66 my $req_data = $context->req_data;
213 3         72 my $datasource = $context->datasource;
214              
215             # columns to update
216 3   50     8 my $to_set = $req_data->{set} || {};
217 3         7 foreach my $key (keys %$to_set) {
218 3         4 my $val = $to_set->{$key};
219 3 50       9 delete $to_set->{$key} if ! length $val;
220 3 50       9 $to_set->{$key} = undef if $val eq 'Null';
221             }
222 3 50       8 keys %$to_set or die "nothing to update";
223              
224             # build filtering criteria
225 3 100       15 my $where = $req_data->{where} or die "update without any '-where' clause";
226 2         52 my $criteria = $datasource->query_parser->parse($where);
227 2 100 66     713 $criteria and keys %$criteria or die "update without any '-where' criteria";
228              
229             # perform the update
230 1         29 my $db_table = $datasource->schema->db_table($table);
231 1         103 my $n_updates = $db_table->update(-set => $to_set, -where => $criteria);
232              
233             # redirect to a list to display the results
234 1 50       613 my $message = ($n_updates == 1) ? "1 record was updated"
235             : "$n_updates records were updated";
236             # TODO: $message could repeat the $to_set pairs
237 1         6 my $query_string = $self->_query_string(%$where, -message => $message);
238 1         7 $self->redirect("list?$query_string");
239             }
240              
241             sub _display_update_form {
242 0     0   0 my ($self, $table) = @_;
243              
244 0         0 $self->_check_canmodify;
245              
246 0         0 my $context = $self->context;
247 0         0 my $req_data = $context->req_data;
248 0         0 my $datasource = $context->datasource;
249 0         0 my $data = $self->descr($table);
250              
251 0 0       0 if (my $where_pk = delete $req_data->{where_pk}) {
252             # we got the primary key of one single record
253 0         0 $data->{where_pk} = $where_pk;
254 0         0 $req_data->{where} = $where_pk;
255              
256             # fetch current values so that we can display them on page
257 0         0 my $criteria = $datasource->query_parser->parse($where_pk);
258 0         0 my $db_table = $datasource->schema->db_table($table);
259 0         0 $req_data->{curr} = $db_table->select(-where => $criteria,
260             -result_as => 'firstrow');
261             }
262             else {
263             # we got criteria that may touch several records
264 0         0 $self->_mark_multicols_keys($data);
265             }
266              
267             # fields that should not be updatable
268 0 0       0 if (my $noupd = delete $req_data->{_noupd}) {
269 0         0 $data->{noupd}{$_} = 1 foreach split qr[/], $noupd;
270             }
271              
272             # initial values for the form
273 0         0 $data->{init_form} = $self->_encode_json($req_data);
274              
275 0         0 return $data;
276             }
277              
278              
279             sub delete {
280 3     3 0 5 my ($self, $table) = @_;
281              
282 3         10 $self->_check_canmodify;
283              
284 2         47 my $context = $self->context;
285 2         44 my $req_data = $context->req_data;
286 2         45 my $datasource = $context->datasource;
287              
288 2 50       43 if ($context->req->method eq 'POST') { # POST => delete in database
289             # build filtering criteria
290 2 100       24 my $where = $req_data->{where} or die "delete without any '-where' clause";
291 1         26 my $criteria = $datasource->query_parser->parse($where);
292 1 50 33     47 $criteria and keys %$criteria or die "delete without any '-where' criteria";
293              
294             # perform the delete
295 0         0 my $db_table = $datasource->schema->db_table($table);
296 0         0 my $n_deletes = $db_table->delete(-where => $criteria);
297              
298             # redirect to a list to display the results
299 0 0       0 my $message = ($n_deletes == 1) ? "1 record was deleted"
300             : "$n_deletes records were deleted";
301 0         0 my $query_string = $self->_query_string(%$where, -message => $message);
302 0         0 $self->redirect("list?$query_string");
303             }
304             else { # GET => display the delete form
305             # display the delete form
306 0         0 my $data = $self->descr($table);
307 0 0       0 if (my $where_pk = delete $req_data->{where_pk}) {
308             # we got the primary key of one single record
309 0         0 $data->{where_pk} = $where_pk;
310 0         0 $req_data->{where} = $where_pk;
311             }
312             else {
313             # we got criteria that may touch several records
314 0         0 $self->_mark_multicols_keys($data);
315             }
316              
317             # initial values for the form
318 0         0 $data->{init_form} = $self->_encode_json($req_data);
319              
320 0         0 return $data;
321             }
322             }
323              
324              
325              
326              
327             sub clone {
328 0     0 0 0 my ($self, $table) = @_;
329              
330 0         0 $self->_check_canmodify;
331              
332 0         0 my $context = $self->context;
333 0 0       0 $context->req->method eq 'GET'
334             or die "the /clone URL only accepts GET requests";
335              
336             # get primary key
337 0         0 my $data = $self->descr($table);
338 0         0 my $pk = $data->{primary_key};
339 0         0 my %is_pk = map {$_ => 1} @$pk;
  0         0  
340 0         0 my @vals = $context->extract_path_segments(scalar(@$pk));
341              
342             # get row from database
343 0         0 my $row = $self->datasource->schema->db_table($table)->fetch(@vals);
344              
345             # populate req_data before calling insert()
346 0         0 my $req_data = $context->req_data;
347 0         0 foreach my $col (keys %$row) {
348 0         0 my $val = $row->{$col};
349 0 0 0     0 $req_data->{$col} = $val if $val and !$is_pk{$col};
350             }
351              
352             # cheat with path (simulating a call to insert())
353 0         0 my $path = $context->path;
354 0         0 $path =~ s/clone$/insert/;
355 0         0 $context->set_path($path);
356 0         0 $context->set_template('table/insert.tt');
357              
358             # forward to insert()
359 0         0 $self->insert($table);
360             }
361              
362              
363             sub insert {
364 1     1 0 3 my ($self, $table) = @_;
365              
366 1         4 $self->_check_canmodify;
367              
368 0         0 my $context = $self->context;
369 0         0 my $req_data = $context->req_data;
370 0         0 my $datasource = $context->datasource;
371              
372 0 0       0 if ($context->req->method eq 'POST') {
373             # perform the insert
374 0         0 my $db_table = $datasource->schema->db_table($table);
375 0         0 my @pk = $db_table->insert($req_data);
376              
377             # redirect to a list to display the results
378 0         0 my $message = "1 record was inserted";
379 0         0 my $query_string = $self->_query_string(-message => $message);
380 0         0 $self->redirect(join("/", "id", @pk) . "?$query_string");
381             }
382             else {
383             # display the insert form
384 0         0 my $data = $self->descr($table);
385 0         0 $data->{init_form} = $self->_encode_json($req_data);
386              
387 0         0 return $data;
388             }
389             }
390              
391              
392              
393             sub count_where { # used in Ajax mode by update and delete forms
394 0     0 0 0 my ($self, $table) = @_;
395              
396 0         0 my $context = $self->context;
397 0         0 my $req_data = $context->req_data;
398 0         0 my $datasource = $context->datasource;
399              
400 0         0 my $n_records = -1;
401              
402 0 0       0 if (my $where = $req_data->{where}) {
403 0         0 my $criteria = $datasource->query_parser->parse($where);
404 0 0 0     0 if ($criteria and keys %$criteria) {
405 0         0 my $db_table = $datasource->schema->db_table($table);
406 0         0 my $result = $db_table->select(
407             -columns => 'COUNT(*)',
408             -where => $criteria,
409             -result_as => 'flat_arrayref',
410             );
411 0         0 $n_records = $result->[0];
412             }
413             }
414              
415 0         0 return {n_records => $n_records};
416             }
417              
418              
419              
420              
421             #----------------------------------------------------------------------
422             # auxiliary methods
423             #----------------------------------------------------------------------
424              
425              
426             sub _query_string {
427 14     14   31 my ($self, %params) = @_;
428 14         19 my @fragments;
429             KEY:
430 14         37 foreach my $key (sort keys %params) {
431 14         19 my $val = $params{$key};
432 14 100       41 length $val or next KEY;
433              
434             # cheap URI escape (for chars '=', '&', ';' and '+')
435 13         67 s/=/%3D/g, s/&/%26/g, s/;/%3B/g, s/\+/%2B/g for $key, $val;
436              
437 13         34 push @fragments, "$key=$val";
438             }
439              
440 14         59 return join "&", @fragments;
441             }
442              
443              
444             sub _encode_json {
445 1     1   2 my ($self, $data) = @_;
446              
447             # utf8-encoding is done in the view, so here we turn it off
448 1         10 my $json_maker = JSON::MaybeXS->new(allow_blessed => 1,
449             convert_blessed => 1,
450             utf8 => 0);
451 1         40 return $json_maker->encode($data);
452             }
453              
454              
455             sub _mark_multicols_keys {
456 0     0     my ($self, $data) = @_;
457              
458 0 0         if (my $sep = $self->datasource->schema->sql_abstract->multicols_sep) {
459             # in case of multi-columns keys, the form needs to add special fields
460             # and to ignore regular fields for those columns
461 0   0       my $where = $self->context->req_data->{where} || {};
462 0           my @multi_cols_keys = grep m[$sep], keys %$where;
463 0           $data->{multi_cols_keys} = \@multi_cols_keys;
464 0           $data->{ignore_col}{$_} = 1 foreach map {split m[$sep]} @multi_cols_keys;
  0            
465             }
466             }
467              
468              
469             1;
470              
471             __END__
472              
473             =head1 NAME
474              
475             App::AutoCRUD::Controller::Table - Table controller
476              
477             =head1 DESCRIPTION
478              
479             This controller provides methods for searching and describing
480             a given table within some datasource.
481              
482             =head1 METHODS
483              
484             =head2 serve
485              
486             Entry point to the controller; from the URL, it extracts the table
487             name and the name of the method to dispatch to (the URL is expected
488             to be of shape C<< table/{table_name}/{$method_name}?{arguments} >>).
489             It also sets the default template to C<< table/{method_name}.tt >>.
490              
491             =head2 descr
492              
493             Returns a hashref describing the table, with keys C<descr>
494             (description information from the config), C<table> (table name),
495             C<colgroups> (datastructure as returned from
496             L<App::AutoCRUD::DataSource/colgroups>), and
497             C<primary_key> (arrayref of primary key columns).
498              
499             =head2 list
500              
501             Returns a list of records from the table, corresponding to the query
502             parameters specified in the URL.
503             [TODO: EXPLAIN MORE -- in particular the "-template" arg ]
504              
505              
506              
507              
508