File Coverage

blib/lib/DBIx/AsForm.pm
Criterion Covered Total %
statement 15 116 12.9
branch 0 36 0.0
condition 0 29 0.0
subroutine 5 13 38.4
pod 3 3 100.0
total 23 197 11.6


line stmt bran cond sub pod time code
1             package DBIx::AsForm;
2              
3 2     2   49915 use HTML::Element;
  2         56553  
  2         13  
4 2     2   2308 use Params::Validate (qw/:all/);
  2         23529  
  2         510  
5 2     2   17 use warnings;
  2         12  
  2         51  
6 2     2   10 use strict;
  2         3  
  2         65  
7              
8             =head1 NAME
9              
10             DBIx::AsForm - Generate an HTML form from a database table.
11              
12             =cut
13              
14 2     2   9 use vars qw/$VERSION/;
  2         2  
  2         3285  
15             $VERSION = '0.02_01';
16              
17             =head1 SYNOPSIS
18              
19             Generate an HTML form a database table in a flexible way.
20              
21             Setup:
22              
23              
24             use DBIx::AsForm;
25             my $daf = DBIx::AsForm->new($dbh);
26              
27             Generate an empty form:
28              
29             my @html_form = $daf->to_html_array('widgets');
30              
31             use CGI;
32             my $q = CGI->new();
33             print $q->start_form;
34             for my $href (@html_form) {
35             print "$href->{name}: ". $href->{obj}->as_HTML."
"
36             }
37             print $q->end_form;
38              
39             =head1 MOTIVATION
40              
41             This project was borne out of combined excitement and frustration with
42             L. I like the general design of the module because it
43             doesn't try to do too much. However, I don't use L as part of my
44             standard development, and I didn't want to depend on C for this tool.
45              
46             I also wanted smarter form element generation than L
47             provides. Over time I expect L to improve in this area to
48             match the advances I've made in that area.
49            
50             =head1 METHODS
51              
52             =head2 new()
53              
54             my $daf = DBIx::AsForm->new($dbh);
55              
56             Creates a new DBIx::AsForm object. The first argument must be an existing
57             database handle.
58              
59             =cut
60              
61             sub new {
62 0     0 1   my $proto = shift;
63 0   0       my $class = ref($proto) || $proto;
64 0           my ($dbh) = validate_pos( @_, 1 );
65              
66              
67 0           my $self = { dbh => $dbh };
68 0           bless ($self, $class);
69 0           return $self;
70             }
71              
72              
73             =head2 to_html_array()
74              
75             # simple syntax
76             my @html_form = $daf->to_html_array($table_name);
77              
78             # More flexible
79             my @html_form = $daf->to_html_array(
80             table => $table,
81             row_href => \%row, # optional
82             columns => \@column_names, # optional, defaults to all
83             stringify => { # optional
84             widget.id => 'widget_name'
85             },
86             );
87              
88             This returns an array of hashrefs mapping all the column names of the table to
89             HTML::Element objects representing form widgets.
90              
91             An array is used to preserve the proper ordering.
92              
93             Optionally, a hashref of data an be passed in to populate the form elements.
94              
95             A list of column names to use can be provided. The default is to use all
96             of them in the order DBI returns them.
97              
98             Finally, 'stringify'. We will detect all the "has a" foreign key relationships
99             automatically. However, usually these are ID columns when we want to display a
100             name. Use C to define another column name from the other table to display
101             in place of the ID. By default we will just display the ID. A future version will
102             support a callback here to define more complex stringification possibilities.
103              
104             =cut
105              
106             sub to_html_array {
107 0     0 1   my $self = shift;
108 0           my %p;
109 0 0         if (scalar @_ == 1) {
110 0           $p{table} = shift;
111             }
112             else {
113 0           %p = validate( @_, {
114             table => 1,
115             row_href => { type => HASHREF, default => {}, },
116             stringify => { type => HASHREF, default => {}, },
117             columns => { type => ARRAYREF, default => [], },
118             });
119             }
120              
121 0           my @col_meta;
122             # get the details for specific rows
123 0 0 0       if ((defined $p{columns}) and (scalar @{ $p{columns} })) {
  0            
124 0           for my $col (@{ $p{columns} }) {
  0            
125             # parameters are: $catalog, $schema, $table, $column
126 0   0       my $sth = $self->{dbh}->column_info( undef, undef , $p{table}, $col ) || die "column_info didn't work";
127 0           my %meta = %{ $sth->fetchrow_hashref };
  0            
128 0           push @col_meta, \%meta;
129             }
130             }
131             # get details for every row in the table.
132             else {
133 0   0       my $sth = $self->{dbh}->column_info( undef, undef , $p{table}, undef ) || die "column_info didn't work";
134 0           @col_meta = @{ $sth->fetchall_arrayref({}) };
  0            
135              
136             }
137              
138             # Find has_a relationships, but skip the grunt work if none are found
139 0           my %fk_meta;
140 0 0         if (my $sth = $self->{dbh}->foreign_key_info( undef,undef,undef,undef,undef, $p{table})) {
141 0           for my $fk (grep { defined $_->{'FK_COLUMN_NAME'} } @{ $sth->fetchall_arrayref({}) }) {
  0            
  0            
142             # I don't know why DBI uses the "UK_" prefix here, but I stick with it.
143 0           my $uk_tbl = $fk->{'UK_TABLE_NAME'};
144 0           my $uk_col = $fk->{'UK_COLUMN_NAME'};
145             # Change the column name if stringify says so
146 0 0         $uk_col = $p{stringify}{"$uk_tbl.$uk_col"} if defined $p{stringify}{"$uk_tbl.$uk_col"};
147 0           $fk_meta{ $fk->{'FK_COLUMN_NAME'} } = [ $uk_tbl, $uk_col ];
148             }
149             }
150              
151 0           for (@col_meta) {
152 0 0         $_->{value} = $p{row_href}{ $_->{COLUMN_NAME} } if defined $p{row_href}{ $_->{COLUMN_NAME } };
153 0 0         $_->{fk} = $fk_meta{ $_->{COLUMN_NAME} } if defined $fk_meta{ $_->{COLUMN_NAME} };
154             }
155              
156 0           return map { $self->_to_field($_) } @col_meta;
  0            
157             }
158              
159             =head2 to_html_href()
160              
161             The same as C, but returns the results in a single hashref.
162              
163             =cut
164              
165             sub to_html_href {
166 0     0 1   my @html_form = to_html_array(@_);
167 0           return { map { $_->{name} => $_->{obj} } @html_form };
  0            
168             }
169              
170              
171             =head2 INTERNALS
172              
173             The details are subject to change without notice and are documented
174             here solely for the benefit of contributors to this module.
175              
176             =cut
177              
178             =head2 _to_field($column_info_row_href)
179              
180             my $href = _to_field($column_info_row_href);
181            
182             # Example contents of $href
183             { name => 'widget', obj => $a };
184              
185             This maps an individual column to a form element. The input is expected
186             to be a hashref as would be returned in as an array element from a call
187             to DBI's C.
188              
189             The output is a hashref with 'name' and 'obj' keys to hold the column name and
190             a HTML::Element object.
191              
192             =cut
193              
194             sub _to_field {
195 0     0     my $self = shift;
196 0           my $col_meta = shift;
197              
198 0           my ($type,$attr) = $self->_decide_col_details($col_meta);
199              
200 0           my $type_meth = '_to_'.$type;
201             return {
202 0           name => $col_meta->{COLUMN_NAME},
203             obj => $self->$type_meth($col_meta, { %$attr, name=> $col_meta->{COLUMN_NAME} }),
204             };
205             }
206              
207             sub _to_textarea {
208 0     0     my ($self, $col, $attr) = @_;
209 0           my $a = HTML::Element->new("textarea", %$attr);
210 0           $a->push_content($attr->{value});
211 0           return $a;
212             }
213              
214             sub _to_input {
215 0     0     my ($self, $col,$attr) = @_;
216 0           my $a = HTML::Element->new("input", %$attr);
217 0 0         $a->attr("value" => $attr->{value}) if defined $attr->{value};
218 0           return $a;
219             }
220              
221             sub _to_select {
222 0     0     my ($self, $col, $attr) = @_;
223              
224 0           my $a;
225              
226 0 0         my ($tbl,$other_col_name) = @{ $col->{fk} } if (ref $col->{fk} eq 'ARRAY');
  0            
227 0 0         if (defined $other_col_name) {
    0          
228 0           $a = HTML::Element->new("select", %$attr);
229              
230 0   0       my $other_col_vals_aref = $self->{dbh}->selectcol_arrayref(
231             "SELECT $other_col_name FROM $tbl") || [];
232              
233 0           for (@{ $other_col_vals_aref }) {
  0            
234 0           my $sel = HTML::Element->new("option", value => $_);
235 0 0 0       $sel->attr("selected" => "selected") if (defined $col->{value} && $_ eq $col->{value});
236 0           $sel->push_content( $_ );
237 0           $a->push_content($sel);
238             }
239             }
240             elsif ($col->{TYPE_NAME} =~ /bool/i) {
241 0           $a = HTML::Element->new("select", %$attr);
242 0           my %bool = (
243             1 => 'Yes',
244             0 => 'No',
245             );
246 0           for (0,1) {
247 0           my $sel = HTML::Element->new("option", value => $_);
248 0 0 0       $sel->attr("selected" => "selected") if (defined $col->{value} && $_ eq $col->{value});
249 0           $sel->push_content( $bool{$_} );
250 0           $a->push_content($sel);
251             }
252             }
253             else {
254 0           die "couldn't figure out how to build select tag."
255             }
256 0           return $a;
257             }
258              
259             =head3 _decide_col_details()
260              
261             ($input_type, $attr_href ) = $self->_decide_col_details($row_href);
262              
263             Returns a suggested HTML form element type and an attribute of form tag attributes
264             based on a hashref of column meta data, as supplied by DBI's column_info().
265              
266             =cut
267              
268             sub _decide_col_details {
269 0     0     my $self = shift;
270              
271             # As returned from DBI's column_info
272 0           my $col_meta = shift;
273              
274             # use Data::Dumper;
275             # warn Dumper ($col_meta) ;
276             # # if ($col_meta->{COLUMN_NAME} eq 'textarea');
277              
278 0           my ($input_type,%attr);
279 0           my $default_field_size = 20;
280              
281 0           my $type = $col_meta->{TYPE_NAME};
282 0 0 0       if (defined $col_meta->{fk} || (defined $type && ($type =~ /bool/i)) ) {
    0 0        
    0 0        
      0        
283 0           $input_type = 'select';
284             }
285             # Should I be checking for DATA_TYPE = 12 here to be more reliable and portable?
286             elsif ((defined $type) && ($type eq 'text')) {
287 0           $input_type = 'textarea';
288 0           $attr{cols} = $default_field_size;
289 0           $attr{rows} = 4 # arbitrary;
290              
291             }
292             # We'll leave the maxlength and size alone for integers
293             elsif ((defined $type) && ($type =~ m/^(big|small)?int(eger)?$/i)) {
294 0           $input_type = 'input';
295 0           $attr{'type'} = 'text';
296 0           $attr{'size'} = $default_field_size;
297             }
298             # a text field
299             else {
300 0           my $col_size = $col_meta->{COLUMN_SIZE};
301 0 0         $col_size = $default_field_size unless defined $col_size;
302              
303             # shrink the form of the field size is smaller than our default
304 0 0         my $size = $col_size if ($col_size < $default_field_size);
305            
306             # make cells slightly larger than the data in them.
307             # this is needed to make it look "right" in some browsers.
308 0 0         if ($col_size <= $default_field_size) {
309 0           $size = $col_size+2;
310 0           $input_type = 'input';
311 0           $attr{'type'} = 'text';
312 0           $attr{'size'} = $size;
313 0           $attr{'maxlength'} = $col_size;
314             }
315             # if it's larger than the default, turn it into a textarea
316             # this prevents things like varchar(4000) from looking crazy.
317             # the textarea is specially sized to fit the length of the field
318             else {
319 0           $input_type = 'textarea';
320 0           $attr{cols} = $default_field_size;
321 0           $attr{rows} = int $col_size/$default_field_size +1;
322             }
323             }
324              
325             # we have now calculated these new values:
326 0           return ($input_type, \%attr);
327             }
328            
329             1;
330              
331             =head1 SEE ALSO
332              
333             L - The same idea, integrated with L
334              
335             L - It has similiar functionality, but is difficult to
336             use and customize if you don't want the other functionality.
337              
338             B - http://www.summersault.com/sofware/db_browser - The oldest automated
339             database to form tool I'm aware of. The code looks old school now, but still has some
340             useful nuggest of wisdom about database meta data.
341              
342             =head1 TODO
343              
344             * Testing generally isn't done
345             * Foreign key stuff is broken
346             * Test with more databases besides PostgreSQL
347             * Set a max size limit on the textareas
348             * Consider smarter date fields types, perhaps integrate
349             with one of the JavaScript calendar date-picker things.
350             * Possible tab-completion for has-a relationships with big
351             tables (via AJAX).
352             * Address underlying issue that HTML::Element doesn't always produce
353             valid HTML/XHTML
354              
355             =head1 BUGS
356              
357             please report any bugs or feature requests to
358             c, or through the web interface at
359             l.
360             i will be notified, and then you'll automatically be notified of progress on
361             your bug as i make changes.
362              
363             =head1 CONTRIBUTING
364              
365             Patches, questions and feedback are welcome. This project is managed using
366             the darcs source control system ( http://www.darcs.net/ ). My darcs archive is here:
367             http://mark.stosberg.com/darcs_hive/as_form/
368              
369             =head1 AUTHOR
370              
371             Mark Stosberg, c<< >>
372              
373             =head1 Acknowledgements
374              
375             =head1 Copyright & License
376              
377             copyright 2005 Mark Stosberg, all rights reserved.
378              
379             this program is free software; you can redistribute it and/or modify it
380             under the same terms as perl itself.
381              
382             =cut
383              
384             1;