File Coverage

blib/lib/DBIx/Perform.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1 1     1   8335 use 5.6.0;
  1         4  
  1         63  
2             package DBIx::Perform;
3              
4 1     1   7 use strict;
  1         3  
  1         39  
5 1     1   6 use warnings;
  1         7  
  1         38  
6 1     1   6 use Carp;
  1         2  
  1         136  
7 1     1   3319 use Curses; # to get KEY_*
  0            
  0            
8             use Curses::Application;
9             use DBI;
10             use POSIX;
11             use DBIx::Perform::DButils;
12              
13             # Apparently it's necessary to directly "use" the derived form types one wants...
14              
15             use DBIx::Perform::Forms;
16             # use DBIx::Perform::Widgets;
17             use DBIx::Perform::Widgets::TextField;
18             use DBIx::Perform::Widgets::ButtonSet;
19              
20             use base 'Exporter';
21              
22             our $VERSION = '0.05';
23              
24             use vars qw(@EXPORT_OK %FORM %APP
25             $APP $FORM $DB $NO_MORE_ROWS);
26              
27             $NO_MORE_ROWS = "No more rows in the direction you are going.";
28              
29             @EXPORT_OK = qw(run);
30              
31             our %INSERT_RECALL = (Pg => \&Pg_refetch,
32             );
33              
34             %FORM =
35             (TABORDER => ['Buttons', 'DBForm'],
36             TYPE => '', # but from DBIx::Perform::Forms
37             # There is some bug in the alt[f]base stuff in Curses::Forms.
38             ALTFBASE => ['DBIx::Perform::Forms', 'DBIx::Perform::Widgets'],
39             ALTBASE => ['DBIx::Perform::Forms', 'DBIx::Perform::Widgets'],
40             FOCUSED => 'Buttons',
41             WIDGETS => {
42             Buttons => {
43             TYPE => 'ButtonSet',
44             BORDER => 0,
45             X => 1,
46             Y => 0,
47             LABELS => [qw(Query Next Prev. Add Update Remove Exit)],
48             LENGTH => 6,
49             FOCUSSWITCH=> "\t\nmd",
50             OnExit => \&ButtonPush,
51             },
52             },
53             );
54              
55             %APP =
56             (
57             FOREGROUND => 'white',
58             BACKGROUND => 'black',
59             MAINFORM => { Dummy => 'DummyDef' }, # changed at runtime
60             STATUSBAR => 1,
61             EXIT => 0,
62             form_name => 'Run0',
63             form_names => ['Run0'], # set later
64             form_name_indexes => { Run0 => 0 }, # also set later
65             md_mode => 'm', # master/detail mode, "m" or "d".
66             resume_command => undef, # do the specified command after switching
67             # master/detail context or screens.
68             );
69              
70              
71            
72             sub run
73             {
74             my $arg = shift;
75              
76             my $form = load($arg);
77             $DB = DBIx::Perform::DButils::open_db($form->{'db'});
78             run_form($form);
79             }
80              
81              
82             sub run_form
83             {
84             my $form = shift;
85              
86             my %appdef = %APP;
87             if (defined (my $minsize = $form->{'screen'}{'MINSIZE'})) {
88             @appdef{'MINX', 'MINY'} = @$minsize;
89             }
90             my $instrs = $appdef{'instrs'} = $form->{'instrs'};
91             my $masters = $instrs && $$instrs{'MASTERS'};
92             $appdef{'MASTERS'} = $masters; # n.b. add it even if undef'd.
93             $appdef{'BACKGROUND'} = $ENV{'BGCOLOR'}
94             if $ENV{'BGCOLOR'};
95             $APP = new Curses::Application (\%appdef)
96             or die "Unable to create application object";
97             my $mwh = $APP->mwh(); # main window handle.
98             my ($maxy, $maxx) = $APP->maxyx();
99             my $i = 0;
100             my @subformdefs = curses_formdefs($form, $maxy-2, $maxx, \%appdef);
101             my @formnames;
102             foreach my $sfd (@subformdefs) {
103             my %runformdef = %FORM;
104             my $defname = "RunDef$i";
105             my $formname = "Run$i";
106             @runformdef{qw(X Y LINES COLUMNS DERIVED SUBFORMS)} =
107             (0, 0, $maxy-1, $maxx, 1, { 'DBForm' => $sfd });
108             push (@formnames, $formname);
109             $APP->addFormDef($defname, { %runformdef });
110             $APP->createForm($formname, $defname);
111             $i++;
112             }
113             $APP->setField(MAINFORM => {Run0 => 'RunDef0'});
114             $APP->setField(form_names => [ @formnames ]);
115             $APP->setField(form_name_indexes =>
116             +{ map {($formnames[$_], $_)} 0..$#formnames });
117             $APP->draw();
118             while (! $APP->getField('EXIT')) { # run until user exits.
119             my $fname = $APP->getField('form_name');
120             local $FORM = $APP->getForm($fname);
121             my $resumecmd = $APP->getField('resume_command');
122             if ($resumecmd) {
123             &$resumecmd($FORM);
124             $APP->setField('resume_command', undef);
125             }
126             $APP->execForm($fname);
127             }
128             }
129              
130              
131             # returns a (spread) array of form defs.
132             # In a master/detail arrangement, the master form is presumed to be first.
133             sub curses_formdefs
134             {
135             my $formspec = shift; # parsed from a .per file...
136             my $maxy = shift;
137             my $maxx = shift;
138             my $appdef = shift;
139              
140             my @screens = @{$formspec->{'screens'}};
141             my $attrs = $formspec->{'attrs'};
142             my $lineoffset = 0; # used for combining screens
143             my @formdefs = ();
144             my $i = 0;
145             my $appbg = $$appdef{'BACKGROUND'};
146             my $deffldbg = $ENV{'FIELDBGCOLOR'} || 'blue';
147             foreach my $screen (@screens) {
148             my $widgets = $$screen{'WIDGETS'};
149             my $fields = $$screen{'FIELDS'};
150             my $subformfields =
151             # [ grep { $$widgets{$_} } @$fields ] ;
152             $fields; # may not need this, really.
153             my $add_taborder = # fields without NOENTRY attribute
154             [ grep { my $a = $$attrs{$_}[1]; $a && ! $$a{NOENTRY} } @$subformfields ];
155             my $update_taborder = # fields without NOUPDATE attribute
156             [ grep { my $a = $$attrs{$_}[1]; $a && ! $$attrs{$_}[1]{NOUPDATE} } @$subformfields ];
157             my $defaults =
158             +{map { my $d = $$attrs{$_}[1]{DEFAULT};
159             defined($d) ? ($_, $d) : (); } @$subformfields };
160             my %def = (
161             X => 0, Y => 1,
162             COLUMNS => $maxx,
163             LINES => $maxy,
164             DERIVED => 1,
165             ALTFBASE => 'DBIx::Perform::Forms',
166             ALTBASE => 'DBIx::Perform::Widgets',
167             TABORDER => $fields,
168             tables => $formspec->{'tables'}, # pass these two straight on
169             attrs => $attrs, # to the runtime system.
170             fields => $subformfields, # this is taborder for query mode, too.
171             add_taborder => $add_taborder,
172             update_taborder => $update_taborder,
173             defaults => $defaults,
174             md_mode => 'm', # master/detail mode.
175             editmode => '',
176             # query_save => {},
177             );
178             # Now install OnExit trampolines and more for each field...
179             foreach my $f (@$fields) {
180             my $w = $widgets->{$f};
181             my ($cols, $fattrs) = @{$$attrs{$f}};
182             # This "trampoline" function gives field name to the real OnExit fcn.
183             $w->{'OnExit'} = sub{ &OnFieldExit($f, @_); };
184             my $comments = $$fattrs{'COMMENTS'};
185             $w->{'OnEnter'} = sub{ $APP->statusbar($comments); }
186             if ($comments);
187             $w->{'FOCUSSWITCH'} = "\t\n\cp\cw\cc\ck\c[";
188             $w->{'FOCUSSWITCH_MACROKEYS'} = [KEY_UP, KEY_DOWN];
189             my $color = $fattrs->{'COLOR'} || $deffldbg;
190             $w->{'BACKGROUND'} = $color;
191             if ($color eq $appbg) {
192             # Need the open/close brackets if no color difference.
193             $$widgets{"$f.openbracket"} =
194             +{ TYPE => 'Label', COLUMNS => 1, ROWS => 1,
195             Y => $w->{'Y'}, X => $w->{'X'}-1, VALUE => "[" };
196             $$widgets{"$f.closebracket"} =
197             +{ TYPE => 'Label', COLUMNS => 1, ROWS => 1,
198             Y => $w->{'Y'}, X => $w->{'X'} + $w->{'COLUMNS'},
199             VALUE => "]" };
200             }
201             # Copy the attributes & columns out in the widgets where
202             # they may be handier.
203             $w->{'columns'} = $cols; # [ [ tbl, col ], ...]
204             $w->{'attrs'} = $fattrs; # { NOENTRY => 1, DEFAULT => '"33"',... }
205             $w->{'savevalue'} = '';
206             }
207             $def{'WIDGETS'} = { %$widgets },
208             push (@formdefs, { %def });
209             }
210             return @formdefs;
211             }
212              
213              
214             sub load
215             {
216             my $arg = shift;
217              
218             if (length($arg) < 500) {
219             # Assume filename.
220             if ($arg =~ /\.per$/) {
221             return load_per($arg);
222             }
223             elsif ($arg =~ /\.pps/) {
224             return load_file($arg);
225             }
226             else {
227             die "Unknown file extension on '$arg'";
228             }
229             }
230             else {
231             if (ref($arg) =~ /HASH/) {
232             return $arg;
233             }
234             elsif ($arg =~ /^\s*database\s/m) {
235             require "DBIx/Perform/DigestPer.pm";
236             return DBIx::Perform::DigestPer::digest_string($arg);
237             }
238             elsif ($arg =~ /^\$form\s*=/) {
239             return load_string($arg);
240             }
241             die "Unrecognized string arg.";
242             }
243             }
244              
245             # Digest it on the fly.
246             sub load_per
247             {
248             my $file = shift;
249              
250             open (PER_IN, "< $file")
251             || die "Unable to open '$file' for reading: $!";
252             require "DBIx/Perform/DigestPer.pm";
253             my $digest = DBIx::Perform::DigestPer::digest(\*PER_IN);
254             die "File did not digest to a Perl Perform Spec"
255             unless $digest =~ /\$form\s*=/;
256             return load_string($digest);
257             }
258            
259            
260              
261             sub load_file
262             {
263             my $file = shift;
264             load_internal(sub { require $file });
265             }
266              
267             sub load_string
268             {
269             my $string = shift;
270             load_internal(sub { eval $string });
271             }
272              
273              
274             sub load_internal
275             {
276             my $sub = shift;
277              
278             our $form;
279             local ($form);
280             &$sub();
281             return $form;
282             }
283              
284              
285             # Run-time functions...
286              
287             use vars '%BUTTONSUBS';
288             %BUTTONSUBS =
289             (query => \&querymode,
290             next => \&do_next,
291             'prev.' => \&do_prev,
292             add => \&addmode,
293             update => \&updatemode,
294             remove => \&do_remove,
295             exit => \&doquit,
296             );
297              
298             sub ButtonPush
299             {
300             my $form = shift;
301             my $key = shift;
302              
303             if (lc($key) =~ /[md]/) {
304             do_master_detail(lc($key), $form);
305             return;
306             }
307             my $wid = $form->getWidget('Buttons');
308             my $val = $wid->getField('VALUE');
309             my $labels = $wid->getField('LABELS');
310             my $thislabel = lc($$labels[$val]);
311             my $btnsub = $BUTTONSUBS{$thislabel};
312             if ($btnsub && ref($btnsub) eq 'CODE') {
313             &$btnsub($form);
314             }
315             else {
316             print STDERR "No button sub for '$thislabel'\n";
317             $form->setField('DONTSWITCH', 1);
318             }
319             }
320              
321             sub clear_textfields
322             {
323             my $subform = shift;
324              
325             my $fields = $subform->getField('fields');
326             return unless $fields;
327             foreach my $f (@$fields) {
328             $subform->getWidget($f)->setField('VALUE', '');
329             }
330             }
331              
332             # Hope this suffices to switch forms.
333             sub setSubform
334             {
335             my $form = shift; # top-level form.
336             my $n = shift;
337              
338             my $forms = $APP->getField('form_names');
339             my $fname = $$forms[$n];
340             if ($fname) {
341             $APP->setField('form_name', $fname);
342             $form->setField('EXIT', 1);
343             }
344             }
345              
346             use vars qw($STH @ROWS $ROWNUM $STHDONE
347             $MASTER_STH @MASTER_ROWS $MASTER_ROWNUM $MASTER_STHDONE);
348              
349              
350             sub clear_STH
351             {
352             if ($STH) {
353             eval { $STH->finish() }; # ignore errors from this.
354             undef $STH;
355             @ROWS = ();
356             $ROWNUM = -1;
357             undef $STHDONE;
358             }
359             }
360              
361             # If there are no rows, it sets DONTSWITCH and statusbars a message.
362             # Returns true if no rows.
363             sub check_rows_and_advise
364             {
365             my $form = shift;
366              
367             if ($#ROWS < 0 || !defined($ROWNUM)) {
368             $APP->statusbar("There are no rows in the current list.");
369             $form->setField('DONTSWITCH', 1);
370             return 1;
371             }
372             return undef;
373             }
374              
375             # called from button_push with the top-level form.
376             sub querymode
377             {
378             my $form = shift;
379              
380             # Shift forcibly back to master mode for query.
381             # FIX_ME? This is an incompatibility, no querying in detail mode.
382             if ($APP->getField('md_mode') ne 'm') {
383             do_master_detail('m', $form);
384             $APP->setField('resume_command', \&querymode_resume);
385             }
386             my $subform =
387             $form->getSubform('DBForm') || $form;
388             clear_textfields($subform);
389             my $to = $subform->getField('fields');
390             $subform->setField('TABORDER', $to);
391             $subform->setField('FOCUSED', $to->[0]); # first field.
392             $subform->setField('editmode', 'query');
393             $APP->statusbar("Enter fields to query. ESC queries, Ctrl-C cancels.");
394             # go ahead and switch to the form.
395             }
396              
397             # Called as a resume entry, 'cause we have to force the form into
398             # the subform since we can't rely on lack of DONTSWITCH to switch there.
399             sub querymode_resume
400             {
401             my ($form) = @_;
402             querymode(@_);
403             $form->setField('FOCUSED', 'DBForm');
404             }
405            
406              
407             sub do_master_detail
408             {
409             my $m_or_d = shift;
410             my $form = shift;
411              
412             my $masters = $APP->getField('MASTERS');
413             return ($form->setField('DONTSWITCH', 1) ,
414             $APP->statusbar('No detail table for this form.'))
415             unless $masters; # if not in a m/d form, skip it.
416             return undef
417             if ($APP->getField('md_mode') eq $m_or_d);
418             # Switch modes.
419             my $subform = $form->getSubform('DBForm');
420             $APP->setField('md_mode', $m_or_d);
421             my (@wheres, @vals);
422             if ($m_or_d eq 'd') {
423             if (@ROWS && $ROWNUM >= 0 && $ROWS[$ROWNUM]) {
424             # Do detail query...
425             # Save state of master query...
426             $MASTER_STH = $STH;
427             @MASTER_ROWS = @ROWS;
428             $MASTER_ROWNUM = $ROWNUM;
429             $MASTER_STHDONE = $STHDONE;
430             $STH = {}; # so the object doesn't get finish()'ed.
431             my $mrow = $MASTER_ROWS[$MASTER_ROWNUM];
432             my $mtable = $masters->[0][0];
433             my $dtable = $masters->[0][1];
434             my $attrs = $subform->getField('attrs');
435             # Get all the join columns...
436             my @keys = grep { scalar @{$$attrs{$_}->[0]} > 1 } keys %$attrs;
437             foreach my $k (@keys) {
438             my $f = $$attrs{$k};
439             my ($mcol) = grep { $_ ->[0] eq $mtable } @{$f->[0]};
440             my ($dcol) = grep { $_ ->[0] eq $dtable } @{$f->[0]};
441             push (@wheres, "$dcol->[1] = ?");
442             push (@vals, $mrow->{$mcol->[1]});
443             }
444             my $n = do_query_internal($dtable, \@wheres, \@vals);
445             setSubform($form, 1);
446             $APP->setField('resume_command',
447             \&do_next);
448             $n = 0 + $n; # numericize it.
449             my $p = ($n == 1 ? '' : 's');
450             $APP->statusbar("Detail: $n row$p found; row 0")
451             if $n;
452             }
453             else {
454             $APP->statusbar("No active query; not switching to detail mode.");
455             }
456             }
457             else {
458             clear_STH(); # mostly to finish the statement handle.
459             $STH = $MASTER_STH;
460             @ROWS = @MASTER_ROWS;
461             $ROWNUM = $MASTER_ROWNUM;
462             $STHDONE = $MASTER_STHDONE;
463             setSubform($form, 0);
464             display_row_fields($form, $ROWS[$ROWNUM], $ROWNUM);
465             }
466            
467             }
468              
469             # called from button_push with the top-level form.
470             sub do_next
471             {
472             my $form = shift;
473             my $switch = shift;
474              
475             $form->setField('DONTSWITCH', 1)
476             unless $switch;
477             unless ($STH) {
478             $APP->statusbar("No query is active.");
479             return;
480             }
481             my ($row, $msg);
482             if (!defined($ROWNUM) || $ROWNUM >= $#ROWS) {
483             # We're at the end of the fetched rows...
484             $row = $STH->fetchrow_hashref()
485             if !$STHDONE;
486             if ($row) {
487             push (@ROWS, $row);
488             $ROWNUM = $#ROWS;
489             }
490             else {
491             # No row was fetched...
492             $msg = $#ROWS < 0 ? "No rows found" : $NO_MORE_ROWS;
493             $APP->statusbar($msg);
494             my $newbtn = @ROWS ? 2 : 0; # FIX_ME use constants
495             $form->getWidget('Buttons')->setField('VALUE', $newbtn);
496             $STHDONE = 1;
497             if (@ROWS) {
498             # Redisplay current row
499             $row = $ROWS[$ROWNUM];
500             # display_row_fields($form, , $ROWNUM);
501             } else {
502             # Punt on the whole thing.
503             my $subform = $form->getSubform('DBForm');
504             clear_textfields($subform);
505             return;
506             }
507             }
508             }
509             else {
510             # we are marching forward through already-fetched rows...
511             $row = $ROWS[++$ROWNUM];
512             }
513             display_row_fields($form, $row, $msg ? undef : $ROWNUM);
514             }
515              
516             # called from button_push with the top-level form.
517             sub do_prev
518             {
519             my $form = shift;
520              
521             my $display_rownum = $ROWNUM;
522             $form->setField('DONTSWITCH', 1);
523             if ($ROWNUM <= 0) {
524             $APP->statusbar($NO_MORE_ROWS);
525             undef $display_rownum;
526             my $newbtn = @ROWS ? 1 : 0; # FIX_ME use constants
527             $form->getWidget('Buttons')->setField('VALUE', $newbtn);
528             }
529             else {
530             $display_rownum = --$ROWNUM;
531             }
532             display_row_fields($form, $ROWS[$ROWNUM], $display_rownum);
533             }
534              
535             # called from button_push with the top-level form.
536             sub addmode
537             {
538             my $form = shift;
539              
540             my $subform = $form->getSubform('DBForm');
541             clear_textfields($subform);
542             # go ahead and switch to form.
543            
544             my $to = $subform->getField('add_taborder');
545             $subform->setField('TABORDER', $to);
546             $subform->setField('FOCUSED', $to->[0]); # first field.
547             $subform->setField('editmode', 'add');
548             my $defs = $subform->getField('defaults');
549             foreach my $f (keys %{ $defs || {} }) {
550             my $v = $$defs{$f};
551             $v = POSIX::strftime("%Y-%m-%d", localtime())
552             if uc($v) eq 'TODAY';
553             $subform->getWidget($f)->setField('VALUE', $v);
554             }
555             $APP->statusbar("Enter row to add. ESC stores; Ctrl-C cancels the add.");
556             }
557              
558             # called from button_push with the top-level form.
559             sub updatemode
560             {
561             my $form = shift;
562              
563             return if check_rows_and_advise($form);
564             my $subform = $form->getSubform('DBForm');
565             my $fields = $subform->getField('fields');
566             my $row = $ROWS[$ROWNUM];
567             my $attrs = $subform->getField('attrs');
568             foreach my $f (@$fields) {
569             my $w = $subform->getWidget($f);
570             my $col = $attrs->{$f}[0][0][1];
571             next unless $col; # might be a lookup destination field
572             $w->setField('savevalue', $row->{$col});
573             }
574             # go ahead and switch to form.
575             my $to = $subform->getField('update_taborder');
576             $subform->setField('TABORDER', $to);
577             $subform->setField('FOCUSED', $to->[0]); # first field.
578             $subform->setField('editmode', 'update');
579             $APP->statusbar("Update the row. ESC stores; Ctrl-C cancels the update.");
580             }
581              
582             sub edit_control # Needs to be generalized to more events.
583             {
584             my $field = shift;
585             my $subform = shift;
586             my $when = lc(shift); # before or after
587              
588             my $instrs = $APP->getField('instrs');
589             my $controls = $instrs->{'CONTROLS'};
590             my $attrs = $subform->getField('attrs');
591             my ($fldtblcols, $fldattrs) = @{$attrs->{$field}};
592             my @cols = map { $#$_ >= 1 ? $_->[1] : () } @$fldtblcols;
593             my $emode = $subform->getField('editmode');
594             my $event = "edit$emode";
595             my @actions = map {$controls->{$_}{$event}{$when}} @cols;
596             @actions = map {$_ ? @$_ : () } @actions; # spread the arrayrefs.
597             foreach my $ac (@actions) {
598             my ($ac, $field, $opd1, $op, $opd2) = @$ac;
599             if ($ac eq 'nextfield'){
600             if (grep { $field eq $_ } @{$subform->getField('TABORDER')}) {
601             $subform->setField('FOCUSED', $field);
602             }
603             }
604             elsif ($ac eq 'let') {
605             ## FIX_ME *extremely* limited functionality here.
606             my $widget = $subform->getWidget($field);
607             $APP->statusbar("No field '$field' in control block."),
608             return ()
609             unless $widget;
610             $APP->statusbar("Unrecognized operator '$op' in control block."),
611             return()
612             unless $op =~ /^[-+*\/]$/;
613            
614             my $val1 = field_value_or_require_quotes($opd1, $subform);
615             my $val2 = field_value_or_require_quotes($opd2, $subform);
616             my $result = eval "$val1 $op $val2";
617             if ($@) {
618             $APP->statusbar("In control block: $@");
619             return;
620             }
621             $widget->setField('VALUE', $result);
622             $subform->setField('REDRAW', 1);
623             # $APP->redraw();
624             }
625             }
626             }
627              
628             sub perform_field_lookup
629             {
630             my $field = shift;
631             my $subform = shift;
632              
633             my $attrs = $subform->getField('attrs');
634             my $fattrs = $attrs->{$field}[1];
635             return unless my $lookup = $fattrs->{'LOOKUP'};
636             my ($destfield, $lcol, $star, $ltable, $jcol) = @$lookup;
637             my $widg = $subform->getWidget($destfield);
638             $APP->statusbar("No field '$destfield' found, processing LOOKUP attribute"), return
639             unless $widg;
640             my $valuewidg = $subform->getWidget($field);
641             my $value = $valuewidg->getField('VALUE');
642             return unless defined($value); # ignore undef'd join value.
643             my $query = "select $lcol from $ltable where $jcol = '$value'";
644             my @row = $DB->selectrow_array($query);
645             if (@row) {
646             $widg->setField('VALUE', $row[0]);
647             }
648             else {
649             my $errstr = $DBI::errstr;
650             $widg->setField('VALUE', undef);
651             $APP->statusbar("Database Error In LOOKUP: $errstr")
652             if $errstr;
653             }
654             $subform->draw($subform->{MWH}, 1);
655             }
656            
657             sub field_value_or_require_quotes # single-quote value.
658             {
659             my $opd = shift;
660             my $subform = shift;
661              
662             my $w1 = $subform->getWidget($opd);
663             if ($w1) {
664             my $val = $w1->getField('VALUE');
665             $val =~ s/\'/\\\'/;
666             return "'$val'";
667             }
668             unless ($opd =~ /^\"(.*)\"$|^(\d+(\.\d_)?)$/) {
669             $APP->statusbar("Neither field, number nor quoted string: '$opd' in control block");
670             return "''";
671             }
672             my $val = defined($1) ? $1 : $2;
673             $val =~ s/\'/\\\'/;
674             return "'$val'"; # hard-quote it lest any monkey biz happen.
675             }
676            
677              
678             # called from button_push with the top-level form.
679             sub do_remove
680             {
681             my $form = shift;
682              
683             return if check_rows_and_advise($form);
684             my $subform = $form->getSubform('DBForm');
685             my $fields = $subform->getField('fields');
686             my @wheres = ();
687             my @values = ();
688             my $tables = $subform->getField('tables');
689             my ($table) = @$tables; # only one table for now.
690             my $row = $ROWS[$ROWNUM];
691             ## FIX_ME! Do a two-table remove if necessary.
692             { # this block to be a loop someday
693             foreach my $f (@$fields) {
694             my $fieldspecs = $subform->getField('attrs')->{$f}[0];
695             foreach my $fs_n (@$fieldspecs) {
696             my ($tbl, $col) = @$fs_n[0,1];
697             next if $tbl ne $table;
698             # my $v = $subform->getWidget($f)->getField('VALUE');
699             my $v = $$row{$col}; # get value straight from source.
700             push (@wheres, defined($v) ? "$col = ?" : "$col is null");
701             push (@values, $v) if defined($v);
702             }
703             }
704             my $wheres = join ' and ', @wheres;
705             my $cmd = "delete from $table where $wheres";
706             my $rc = $DB->do($cmd, {}, @values);
707             if (!defined $rc) {
708             $APP->statusbar("Database error: $DBI::errstr");
709             }
710             else {
711             my $msg = "Row removed.";
712             splice(@ROWS, $ROWNUM, 1);
713             $ROWNUM = $#ROWS if $ROWNUM > $#ROWS;
714             clear_textfields($subform);
715             }
716             }
717             $form->setField('DONTSWITCH', 1); # in all cases.
718             }
719              
720             # called from button_push with the top-level form.
721             sub doquit
722             {
723             my $form = shift;
724             # This assumes the form is the top-level one.
725             $form->setField('EXIT', 1);
726             $APP->setField('EXIT', 1);
727             }
728              
729             # When the user hits ESC from the subform, run one of the following
730             # based on the value of the button set.
731             use vars '%MODESUBS';
732             %MODESUBS =
733             ( query => \&do_query,
734             add => \&do_add,
735             update => \&do_update,
736             );
737              
738             sub OnFieldExit
739             {
740             my ($field, $form, $key) = @_; # leaving @_ for back-patching
741              
742             my $widget = $form->getWidget($field);
743             edit_control($field, $form, 'after'); # do any AFTER control blocks.
744             perform_field_lookup($field, $form); # do a LOOKUP if field so declared.
745             if ($key eq "\t" || $key eq "\n"
746             || $key eq KEY_DOWN) { # shift to next field
747             $APP->statusbar("") # erase our comments
748             if ($widget->getField('attrs')->{COMMENTS});
749             return;
750             }
751              
752             my $dontswitch = 1;
753             # printf STDERR ("Field Exit: Field = $field; Widget = $widget; Key = %o\n", $key);
754             if ($key eq "\c[") { # Do The Mode
755             my $btns = $FORM->getWidget('Buttons');
756             my $mode = lc(($btns->getField('LABELS'))->[$btns->getField('VALUE')]);
757             my $sub = $MODESUBS{$mode};
758             if ($sub && ref($sub) eq 'CODE') {
759             $dontswitch = 0; # let the sub decide.
760             &$sub($field, $widget, $form);
761             }
762             else {
763             beep();
764             }
765             }
766             elsif ($key eq "\cw") {
767             my $msg = $widget->getField('HELPMSG');
768             $APP->statusbar($msg) if ($msg);
769             }
770             elsif ($key eq "\cp") {
771             $APP->statusbar("Current-Value-Of-This-Row not working yet");
772             }
773             elsif ($key eq "\cC") { # Ctrl-C
774             # Bailing out of Query, Update or Modify.
775             # Re-display the row as it was, if any.
776             if ($#ROWS >= 0) {
777             display_row_fields($form, $ROWS[$ROWNUM], $ROWNUM);
778             }
779             else {
780             clear_textfields($form);
781             }
782             # Back to top menu
783             $APP->statusbar("") # erase our comments
784             if ($widget->getField('attrs')->{COMMENTS});
785             $form->setField('EXIT', 1);
786             }
787             elsif ($key eq "\cK" || $key eq KEY_UP || $key eq KEY_STAB) {
788             my $taborder = $form->getField('TABORDER');
789             my %taborder = map { ($$taborder[$_], $_) } (0..$#$taborder);
790             my $i = $taborder{$form->getField('FOCUSED')};
791             $i = ($i <= 0) ? $#$taborder : $i - 1;
792             $form->setField('FOCUSED', $$taborder[$i]);
793             $APP->statusbar("") # erase our comments
794             if ($widget->getField('attrs')->{COMMENTS});
795             $dontswitch = 0;
796             }
797              
798             if ($dontswitch) {
799             $form->setField('DONTSWITCH', 1);
800             }
801             }
802              
803             # Validates a field value against applicable field attributes.
804             # If valid, returns true. If invalid, does statusbar, sets focus
805             # to the field and sets DONTSWITCH and then returns false.
806             sub validate_contents
807             {
808             my $subform = shift;
809             my $f = shift; # vield name
810             my $attrs = shift; # field's attributes hash
811             my $v = shift; # value from widget.
812              
813             my $msg;
814             $msg = "This field requires a value"
815             if ($$attrs{'REQUIRED'} && !defined($v)) ;
816             my $inc = $$attrs{'INCLUDE'};
817             my $inchash = $$attrs{'INCLUDEHASH'}; # made in curses_formdef
818             $msg ||= "Field permissible values: $inc"
819             if ($inchash && !$$inchash{$v});
820             return 1 unless $msg; # Value is OK.
821             $APP->statusbar($msg);
822             $subform->setField('FOCUSED', $f);
823             $subform->setField('DONTSWITCH', 1);
824             return undef;
825             }
826              
827              
828              
829             sub do_query
830             {
831             my $field = shift;
832             my $widget = shift;
833             my $subform = shift;
834              
835             my $masters = $APP->getField('MASTERS');
836             my $attrs = $subform->getField('attrs');
837             my @tables = @{$subform->getField('tables')};
838             my ($table, $detail);
839             if ($masters) {
840             my $mdpair = $$masters[0];
841             my $indexes = $APP->getField('form_name_indexes');
842             my $formindex = $$indexes{$APP->getField('form_name')};
843             my $mdmode = $APP->getField('md_mode');
844             my $mdindex = $mdmode eq 'm' ? 0 : $mdmode eq 'd' ? 1 : undef;
845             die "Masters exist in instructions but md_mode is '$mdmode'"
846             unless defined($mdindex);
847             $table = $$mdpair[$mdindex];
848             $detail = $mdindex != 0;
849             }
850             my @wheres = ();
851             my @vals = ();
852             my %tbls = ();
853             foreach my $f (@{$subform->getField('fields')}) {
854             my ($fldtblcols, $fldattrs) = @{$attrs->{$f}};
855             next unless $fldtblcols; # might be a lookup destination field.
856             my @fldtblcols = @$fldtblcols;
857             next if $masters &&
858             ! grep { $fldtblcols[$_]->[0] eq $table } 0..$#fldtblcols;
859             next unless $fldtblcols[0];
860             my ($tbl, $col) = @{$fldtblcols[0]};
861             next unless $tbl;
862             $tbls{$tbl} = 1;
863             if (! $masters && $#fldtblcols > 0) {
864             my ($tbl2, $col2) = @{$fldtblcols[1]}; # FIX_ME two tables only
865             push (@wheres, "$tbl.$col = $tbl2.$col2");
866             $tbls{$tbl2} = 1;
867             }
868             my $val = $subform->getWidget($f)->getField('VALUE');
869             next if ($val eq '');
870             # Non-empty field; decide what kind of comparison...
871             my $op = '=';
872             my $cval = $val;
873             if ($val =~ /[*%?]$/) {
874             $cval =~ tr/*?/%_/; # SQL wildcard characters
875             $op = 'like';
876             }
877             if ($val eq '=') {
878             $op = 'is null';
879             $cval = undef;
880             }
881             elsif ($val =~ /^(<<|>>)\s*(.*)$/) {
882             $APP->statusbar("The $1 operator is not supported yet.");
883             }
884             elsif ($val =~ /^([<>][<=>]?)\s*(.*)$/) {
885             $op = $1;
886             $cval = $2;
887             }
888             elsif ($val =~ /^(.+?):(.+)$/) {
889             $op = "between ? and ";
890             push (@vals, $1);
891             $cval = $2;
892             }
893             elsif ($val =~ /^(.+?)\|(.+)$/) {
894             $op = "= ? or $col = "; # might should use in ($1,$2)
895             push (@vals, $1);
896             $cval = $2;
897             }
898             my $where = "$tbl.$col $op" . (defined($cval) ? ' ?' : '');
899             push (@wheres, $where);
900             push (@vals, $cval) if defined($cval);
901             }
902             # my $tables = $masters ? $table : join ', ', @tables;
903             my $tables = join(', ', keys %tbls);
904             my $n = do_query_internal($tables, \@wheres, \@vals);
905             $subform->setField('EXIT', 1); # Focus back to menu if we got this far.
906             unless (defined($n)) {
907             $APP->statusbar("DB Error on execute: $DBI::errstr");
908             return;
909             }
910             $n = 0 + $n; # coerce to number.
911             do_next($FORM, 'switch');
912             $APP->statusbar("$n row" . ($n == 1 ? '' : 's') . " found; Row 0")
913             if $n > 0;
914             }
915              
916             sub do_query_internal
917             {
918             my $tables = shift;
919             my $wheres_ref = shift;
920             my $vals_ref = shift;
921            
922             my $wheres = join ' and ', @$wheres_ref;
923             my $query = "select * from $tables " . ($wheres ? "where $wheres" : '');
924             clear_STH();
925             $STH = $DB->prepare_cached($query);
926             unless($STH) {
927             $APP->statusbar("DB Error on prepare: $DBI::errstr");
928             return;
929             }
930             return $STH->execute(@$vals_ref);
931             }
932              
933             sub do_add
934             {
935             my $field = shift;
936             my $widget = shift;
937             my $subform = shift;
938              
939             my $fields = $subform->getField('fields');
940             my @cols = ();
941             my @values = ();
942             my $tables = $subform->getField('tables');
943             my ($table) = @$tables; # only one table for now.
944             my $row = {};
945             ## FIX_ME! Do a two-table add if necessary.
946             { # this block to be a loop someday
947             my %reassemblies;
948             foreach my $f (@$fields) {
949             my $fieldattrs = $subform->getField('attrs')->{$f};
950             my ($fieldspecs, $attrs) = @$fieldattrs;
951             next if $$attrs{'NOENTRY'}; # don't include in cols/vals.
952             my $fieldspec = $$fieldspecs[0];
953             next if !defined($fieldspec) || ref($fieldspec) ne 'ARRAY';
954             my ($tbl, $col, $vfy, $subfield) = @$fieldspec;
955             next if $tbl ne $table;
956             my $v = $subform->getWidget($f)->getField('VALUE');
957             # undef $v if $v eq ''; # give NULL for empty fields.
958             $v =~ s/\s*$//; # lose trailing blanks.
959             next if $v eq ''; # skip empty fields.
960             return # function below has side-effects on form.
961             unless validate_contents($subform, $f, $attrs, $v);
962             if ($subfield) {
963             push (@{$reassemblies{"$tbl.$col"}},
964             [ @$fieldspec, $v ]);
965             }
966             else {
967             push (@cols, $col);
968             push (@values, $v);
969             $$row{$col} = $v;
970             }
971             }
972             foreach my $rak (keys %reassemblies) {
973             my $value='';
974             my @subfields = sort { $a->[3][0] <=> $b->[3][0] } @{$reassemblies{$rak}};
975             my $col = $subfields[0]->[1];
976             foreach my $sf (@subfields) {
977             if (defined($$sf[4])) {
978             my ($start,$end) = @{$$sf[3]};
979             my $pos = $start - 1;
980             my $len = $end - $start + 1;
981             $value .= " " x ($pos - length($value))
982             if ($pos > length($value));
983             substr($value, $pos, $len) = $$sf[4];
984             }
985             }
986             $$row{$col} = $value;
987             push(@cols, $col);
988             push(@values, $value);
989             }
990             my $holders = join ', ', map { "?" } @cols;
991             my $cols = join ', ', @cols;
992             my $cmd = "insert into $table ($cols) values ($holders)";
993             my $sth = $DB->prepare($cmd);
994             my $rc = $sth->execute(@values);
995             if (!defined $rc) {
996             $APP->statusbar("Database error: $DBI::errstr");
997             return;
998             }
999             my $refetcher = $INSERT_RECALL{$DB->{'Driver'}->{'Name'}}
1000             || \&Default_refetch;
1001             if ($refetcher) {
1002             my $rrow = &$refetcher($sth, $table, \@cols, \@values);
1003             # push (@ROWS, $rrow); # Should do this someday
1004             # do_next($FORM, 'switch');
1005             $row = $rrow;
1006             }
1007             $APP->statusbar("Row Added.");
1008             }
1009             $subform->setField('EXIT', 1); # back to menu
1010             # Pretend it's a result of one row, so it can be removed / modified.
1011             clear_STH();
1012             @ROWS = ( $row );
1013             $ROWNUM = -1;
1014             $STH = {};
1015             $STHDONE = 1;
1016             do_next($FORM, 'switch');
1017             }
1018              
1019             sub do_update
1020             {
1021             my $field = shift;
1022             my $widget = shift;
1023             my $subform = shift;
1024              
1025             my $fields = $subform->getField('fields');
1026             my %wheres = ();
1027             my %upds = ();
1028             my $tables = $subform->getField('tables');
1029             my ($table) = @$tables; # only one table for now.
1030             my $row = {};
1031             my $attrs = $subform->getField('attrs');
1032             ## FIX_ME! Do a two-table add if necessary.
1033             { # this block to be a loop someday
1034             my %reassemblies;
1035             foreach my $f (@$fields) {
1036             my ($fieldspecs, $attrs) = @{$attrs->{$f}};
1037             foreach my $fs_n (@$fieldspecs) {
1038             my ($tbl, $col, $vfy, $subfield) = @$fs_n;
1039             next unless $tbl;
1040             next if $tbl ne $table;
1041             my $w = $subform->getWidget($f);
1042             my $v = $w->getField('VALUE');
1043             $v =~ s/\s*$//; # trim trailing whitespace.
1044             undef $v if $v eq ''; # empty field means NULL.
1045             return
1046             unless validate_contents($subform, $f, $attrs, $v);
1047             if ($subfield) {
1048             push (@{$reassemblies{"$tbl.$col"}},
1049             [ @$fs_n, $v ]);
1050             }
1051             my $sv = $w->getField('savevalue');
1052             $$row{$col} = $v;
1053             $upds{$col} = $v
1054             if (((defined($v) ne defined($sv)) ||
1055             (defined ($v) && defined($sv) && $v ne $sv))
1056             && !$$attrs{'NOUPDATE'});
1057             $wheres{$col} = $sv
1058             if !$$attrs{'NOCOMPARE'}; # not in orig. Perform.
1059             }
1060             }
1061             foreach my $rak (keys %reassemblies) {
1062             my $value='';
1063             my @subfields = sort { $a->[3][0] <=> $b->[3][0] } @{$reassemblies{$rak}};
1064             my $col = $subfields[0]->[1];
1065             foreach my $sf (@subfields) {
1066             if (defined($$sf[4])) {
1067             my ($start,$end) = @{$$sf[3]};
1068             my $pos = $start - 1;
1069             my $len = $end - $start + 1;
1070             $value .= " " x ($pos - length($value))
1071             if ($pos > length($value));
1072             substr($value, $pos, $len) = $$sf[4];
1073             }
1074             }
1075             $upds{$col} = $value;
1076             }
1077             my @updcols = keys (%upds);
1078             if (@updcols == 0) {
1079             $APP->statusbar("No fields changed.");
1080             return;
1081             }
1082             my @updvals = map { $upds{$_} } @updcols;
1083             my $sets = join(', ', map { "$_ = ?" } @updcols);
1084             my @wherecols = keys (%wheres);
1085             my @wherevals = map { my $w = $wheres{$_}; defined($w) ?
1086             ($w) : () } @wherecols;
1087             # my %whereinds = map { ($wherecols[$_], $_) } 0..$#wherecols;
1088             my %updinds = map { ($updcols[$_], $_) } 0..$#updcols;
1089             my $wheres = join(' and ', map { defined($wheres{$_}) ?
1090             "$_ = ?" :
1091             "$_ is null"
1092             } @wherecols);
1093             my $cmd = "update $table set $sets " . ($wheres && "where $wheres");
1094             my $rc = $DB->do($cmd, {}, @updvals, @wherevals);
1095             if (!defined $rc) {
1096             $APP->statusbar("Database error: $DBI::errstr");
1097             return;
1098             }
1099             else {
1100             $APP->statusbar((0+$rc) . " rows affected");
1101             my $query = "select * from $table where $wheres";
1102             # Since the new value is now in, change the where value...
1103             grep {$ROWS[$ROWNUM]->{$_} = $row->{$_} = $updvals[$updinds{$_}];}
1104             @updcols;
1105             display_row_fields($subform, $ROWS[$ROWNUM]);
1106             }
1107             }
1108             $subform->setField('EXIT', 1); # back to menu
1109             }
1110              
1111              
1112             sub display_row_fields
1113             {
1114             my $form = shift;
1115             my $row = shift;
1116             my $n = shift;
1117              
1118             my $subform =
1119             $form->getSubform('DBForm') || $form;
1120              
1121             my $fields = $subform->getField('fields');
1122             my $attrs = $subform->getField('attrs');
1123             foreach my $f (@$fields) {
1124             my $attr = $attrs->{$f}[0][0];
1125             next unless $attr; # might be a lookup destination
1126             my ($tbl, $col, $stuff, $subfield) = @$attr;
1127             next unless $col;
1128             my $val = $row->{$col};
1129             if ($subfield) {
1130             my ($start, $end) = @$subfield;
1131             $val = length($val) < $start ? '' :
1132             substr($val, $start-1, $end-$start+1);
1133             }
1134             $subform->getWidget($f)->setField('VALUE', $val);
1135             perform_field_lookup($f, $subform)
1136             if ($attrs->{$f}[1]{'LOOKUP'});
1137             }
1138             $APP->statusbar("Row number $n")
1139             if (defined($n));
1140             # $subform->draw(); ??
1141             }
1142              
1143              
1144             # Post-Add/Update refetch functions:
1145             sub Pg_refetch
1146             {
1147             my $sth = shift;
1148             my $table = shift;
1149              
1150             my $oid = $sth->{'pg_oid_status'};
1151             my $row = $DB->selectrow_hashref("select * from $table where oid='$oid'");
1152             return $row;
1153             }
1154              
1155             # When we don't know how to get the row-ID or similar marker, just query
1156             # on all the values we know...
1157             sub Default_refetch
1158             {
1159             my $sth = shift; # statement handle; ignored.
1160             my $table = shift;
1161             my $cols = shift; # columns to query
1162             my $vals = shift; # values to query
1163              
1164             my $wheres = join ' AND ', map { "$_ = ?" } @$cols;
1165             my $query = "SELECT * FROM $table WHERE $wheres";
1166             my $row = $DB->selectrow_hashref($query, {}, @$vals);
1167             return $row;
1168             }
1169              
1170              
1171             # What a kludge... required by Curses::Application
1172             package main;
1173             __DATA__