]> git.decadent.org.uk Git - maypole.git/blob - lib/Maypole/Model/CDBI/AsForm.pm
3c35dfc5fa4b23b978cb886986cd528123f1d863
[maypole.git] / lib / Maypole / Model / CDBI / AsForm.pm
1 package Maypole::Model::CDBI::AsForm;
2
3 use 5.006;
4
5 use strict;
6 use warnings;
7
8 use base 'Exporter';
9 use Data::Dumper;
10 use Class::DBI::Plugin::Type ();
11 use HTML::Element;
12
13 our $OLD_STYLE = 0;
14 # pjs  --  Added new methods to @EXPORT 
15 our @EXPORT =
16         qw(
17                 to_cgi to_field _to_textarea _to_textfield _to_select
18                 _to_foreign_inputs _to_enum_select _to_bool_select
19                 _to_select_from_many _to_select_from_related _to_select_from_objs 
20                 _to_hidden _to_link_hidden _rename_foreign_input _to_readonly
21                 _options_from_objects _options_from_arrays _options_from_hashes 
22                 _options_from_scalars
23                 _field_from_how _field_from_relationship _field_from_column
24                 _select_guts unselect_element  search_inputs make_param_foreign 
25     );
26
27 our $VERSION = '2.11';
28
29 =head1 NAME
30
31 Maypole::Model:CDBI::AsForm - Produce HTML form elements for database columns
32
33 =head1 SYNOPSIS
34
35     package Music::CD;
36     use Maypole::Model::CDBI::AsForm;
37     use base 'Class::DBI';
38     use CGI;
39     ...
40
41     sub create_or_edit {
42         my $self = shift;
43         my %cgi_field = $self->to_cgi;
44         return start_form,
45                (map { "<b>$_</b>: ". $cgi_field{$_}->as_HTML." <br>" } 
46                     $class->Columns),
47                end_form;
48     }
49
50         . . . somewhere use to_field($col, $how, $args)
51         package BeerDB::Pint;
52         __PACKAGE__->has_a('drinker', 'BeerDB::Drinker');
53         __PACKAGE__->has_a('beer',    'BeerDB::Beer');
54         package BeerDB::Drinker;
55         __PACKAGE__->has_many('pints', 'BeerDB::Pint');
56
57         # NEED to do mapping 
58         my $sel = BeerDB::Drinker->to_field('pints', 'select') # multiple
59         my $sel = $Drunk->to_field('pints', 'select'); # Already had beers selected
60
61 package Job;
62
63 __PACKAGE__->has_a('employer' => 'Employer');
64 __PACKAGE__->has_a('contact'  => 'Contact')
65
66 package Contact;
67
68 __PACKAGE__->has_a('employer_also' => 'Employer');
69 __PACKAGE__->has_many('jobs'  => 'Job',
70         { join => { employer => 'employer_also' },
71           constraint => { 'finshed' => 0  },
72           order_by   => "created ASC",
73         }
74 );
75
76 package Employer;
77
78 __PACKAGE__->has_many('jobs'  => 'Job',);
79 __PACKAGE__->has_many('contacts'  => 'Contact',
80             order_by => 'name DESC',
81 );
82
83
84   # Below gives select boxes with the multiple attribute.
85   my $select_jobs_for_new_contact =
86     Contact->to_field('jobs', 'select'); # Uses constraint and order by
87
88   my $edit_jobs_for_existing_contact =
89     $contact->to_field('jobs', 'select');
90
91
92
93   # Random uses
94
95
96 =head1 DESCRIPTION
97
98 This module helps to generate HTML forms for creating new database rows
99 or editing existing rows. It maps column names in a database table to
100 HTML form elements which fit the schema. Large text fields are turned
101 into textareas, and fields with a has-a relationship to other
102 C<Class::DBI> tables are turned into select drop-downs populated with
103 objects from the joined class.
104
105 =head1 METHODS
106
107 The module is a mix-in which adds two additional methods to your
108 C<Class::DBI>-derived class. 
109
110
111 =head2 search_inputs
112
113 Returns hashref of search inputs elements to use in cgi.
114
115 Uses fields specified in search_fields, makes foreign inputs if necessary.
116
117 =cut
118
119 # TODO -- use search_columns
120
121 sub search_inputs {
122   my ($class, $r) = @_;
123   warn "In model search_inputs " if $class->model_debug;
124   $class = ref $class || $class;
125   #my $accssr_class = { $class->accessor_classes };
126   my %cgi;
127   my $sfs = [$class->search_columns];
128
129   foreach my $field ( @$sfs ) {
130     if ( ref $field eq "HASH" ) { # foreign search fields
131       my ($accssr, $cols)  = each %$field;
132       unless (  @$cols ) {
133                                 # default to search fields for related 
134                                 #$cols =  $accssr_class->{$accssr}->search_columns;
135         die ("$class search_fields error: Must specify at least one column to search in the foreign object named '$accssr'");
136       }
137       my $fcgi  = $class->_to_foreign_inputs($accssr, $cols);
138       # unset the default values for a select box
139       foreach (keys %$fcgi) {
140         $class->unselect_element($fcgi->{$_});
141       }
142       $cgi{$accssr} = $fcgi;
143     } else {  
144       $cgi{$field} = $class->to_field($field);
145       $class->unselect_element($cgi{$field});
146     }
147   }
148   return \%cgi;
149 }
150
151
152
153 =head2 unselect_element
154
155 Unselects all options in a HTML::Element of type select.
156 It does nothing if element is not a select element.
157
158 =cut
159
160 sub unselect_element {
161         my ($self, $el) = @_;
162         #unless (ref $el eq 'HTML::Element') { 
163         #$self->_croak ('Need an HTML::Element to unselect. You gave a ' . Dumper($el)); }
164         if ($el->tag eq 'select') {
165                 foreach my $opt ($el->content_list) {
166                         $opt->attr('selected', undef) if $opt->attr('selected');
167                 }
168         }
169 }
170
171
172 =head2 a_select_box
173
174   Returns a HTML::Element representing a select box, based on the arguments
175
176 =cut
177
178 # make a select box from args
179 sub a_select_box {
180         my ($self, $name, $vals, $selected_val, $contents) = @_;
181         die "Usage: Need a name and array ref of values to make a select boxes" unless ($name && $vals);
182         $selected_val ||= "";
183         $contents ||= $vals ;
184
185         my $a = HTML::Element->new('select', 'name' => $name);
186         my $i = 0;
187         my $c;
188         foreach my $v ( @$vals ) {
189                 my $opt = HTML::Element->new('option', 'value' => $v);
190                 $opt->attr('selected' => 'selected') if $v eq $selected_val;
191                 $c = $contents->[$i++] || $v;
192                 $opt->push_content($c);
193                 $a->push_content($opt);
194         }
195         $a;
196 }
197
198
199
200 =head2 make_param_foreign
201
202 Makes a new foreign parameter out of parameter and accessor
203 Just puts accssr__FOREIGN__ in front of param name 
204
205 =cut
206
207 sub make_param_foreign {
208         my ($self, $r, $p, $accssr) = @_;
209         $r->params->{$accssr . '__FOREIGN__' . $p} = $r->params->{$p};
210 }
211
212 =head2 to_cgi
213
214 This returns a hash mapping all the column names of the class to
215 HTML::Element objects representing form widgets.
216
217 pjs -- added a columns list argument to specify which columns to make
218 inputs for.
219
220 =cut
221
222 sub to_cgi {
223         my ($class, @columns) = @_; # pjs -- added columns arg
224         @columns = $class->columns unless (@columns);
225         map { $_ => $class->to_field($_) } @columns;
226 }
227
228
229 =head2 to_field($field [, $how])
230
231 This maps an individual column to a form element. The C<how> argument
232 can be used to force the field type into one of C<textfield>, C<textarea>
233 or C<select>; you can use this is you want to avoid the automatic detection
234 of has-a relationships.
235
236 # pjs 
237    -- added support for enum and bool.   Note for enum and bool you need 
238       a better column_type method than the Plugin::Type ' s as it won't work 
239       if you are using MySQL. I have not tried others.  
240       See those method's docs below.
241    -- Foreign inputs for might_have, must_have, has_own, and has_many(i think).
242    -- Really any relationship except has_a and is_a as has_a gets a select box
243       and is_a are not considered foreign. 
244    -- Note a good column_type sub can be 
245       used to get the correct type for is_a columns.
246    -- More efficient _to_select -- no object creation.
247    -- Attempts to set default value in field for you using a "column_default" 
248       method you write yourself or your CDBI driver like mysql writes.
249    -- _to_hidden 
250
251 =cut
252
253 sub to_field {
254         my ($self, $field, @args) = @_;
255     my $how = shift @args unless ref $args[0];  
256     my $args = shift @args;  # argument hash ref  
257
258         return  $self->_field_from_how($field, $how, $args)   || 
259                 $self->_field_from_relationship($field, $args) ||
260                         $self->_field_from_column($field, $args)  ||
261                         $self->_to_textfield($field, $args);
262 }
263
264 =head2 _field_from_how($field, $how,$args)
265
266 Returns an input element based the "how" parameter or nothing at all.
267 Override at will. 
268
269 =cut
270
271 sub _field_from_how {
272         my ($self, $field, $how, $args) = @_;
273         if ($how) { 
274                 no strict 'refs';
275                 my $meth = "_to_$how";
276                 return $self->$meth($field, $args) if $self->can($meth);
277         }
278         return;
279 }
280
281 =head2 _field_from_relationship($field, $args)
282
283 Returns an input based on the relationship associated with the field or nothing.
284 Override at will.
285
286 =cut
287
288 sub _field_from_relationship {
289         my ($self, $field, $args) = @_;
290         my $meta = $self->meta_info;
291         my ($rel_type) = grep{ defined $meta->{$_}{$field} } keys %$meta;
292         $rel_type ||= ''; 
293         my $fclass = $rel_type ? $meta->{$rel_type}{$field}{foreign_class} : '';
294     $args->{class} = $fclass;
295         my $fclass_is_cdbi = $fclass ? $fclass->isa('Class::DBI') : 0;
296
297         # maybe has_a select 
298     return  $self->_to_select($field, $args) 
299                 if $rel_type eq 'has_a' and $fclass_is_cdbi;
300
301         # maybe foreign inputs 
302         my %local_cols = map { $_ => 1 } $self->columns; # includes is_a cols
303         if ($fclass_is_cdbi and (not $local_cols{$field} or $rel_type eq 'has_own'))
304         {
305                 return $self->_to_foreign_inputs($field, undef, $meta->{$rel_type}{$field});
306         }
307         return;
308 }
309
310 =head2 _field_from_column($field, $args)
311
312 Returns an input based on the column's characteristics, namely type, or nothing.
313 Override at will.
314
315 =cut
316
317 sub _field_from_column {
318   my ($self, $field, $args) = @_;
319   my $class = ref $self || $self;
320   # Get column type
321   unless ($args->{column_type}) { 
322     if ($class->can('column_type')) {
323       $args->{column_type} = $class->column_type($field);
324     } else {
325       # Right, have some of this
326       eval "package $class; Class::DBI::Plugin::Type->import()";
327       $args->{column_type} = $class->column_type($field);
328     }
329   }
330   my $type = $args->{column_type};
331
332   return $self->_to_textfield($field)
333     if $type  and $type =~ /(VAR)?CHAR/i; #common type
334   return $self->_to_textarea($field, $args)
335     if $type and $type =~ /^(TEXT|BLOB)$/i;
336   return $self->_to_enum_select($field, $args)  
337     if $type and  $type =~ /^ENUM\((.*?)\)$/i; 
338   return $self->_to_bool_select($field, $args)
339     if $type and  $type =~ /^BOOL/i; 
340   return $self->_to_readonly($field, $args)
341     if $type and $type =~ /^readonly$/i;
342   return;
343 }
344
345
346 sub _to_textarea {
347         my ($self, $col, $args) = @_;
348         # pjs added default     
349     $args ||= {};
350     my $val =  $args->{value}; 
351     
352     unless (defined $val) {
353         if (ref $self) {
354                         $val = $self->$col; 
355                 }
356                 else { 
357                         $val = eval {$self->column_default($col);}; 
358                 $val = '' unless defined $val;  
359                 }
360         }
361     my ($rows, $cols) = _box($val);
362     $rows = $args->{rows} if $args->{rows};
363     $cols = $args->{cols} if $args->{cols};;
364     my $name = $args->{name} || $col; 
365         my $a =
366                 HTML::Element->new("textarea", name => $name, rows => $rows, cols => $cols);
367         $a->push_content($val);
368         $OLD_STYLE && return $a->as_HTML;
369         $a;
370 }
371
372 sub _to_textfield {
373     my ($self, $col, $args ) = @_;
374     $args ||= {};
375     my $val = $args->{value}; 
376     my $name = $args->{name} || $col; 
377
378     unless (defined $val) {
379         if (ref $self) {
380             # Case where column inflates.
381             # Input would get stringification which could be not good.
382             #  as in the case of Time::Piece objects
383             $val = $self->$col;
384             if (ref $val) {
385                                 if (my $meta = $self->related_meta('',$col)) {
386                         if (my $code = $meta->{args}{deflate4edit} ) {
387                         $val  = ref $code ? &$code($val) : $val->$code;
388                                         }
389                                         else { 
390                                             $val  = $self->_attr($col);
391                                         }
392                 }
393                                 else {
394                                         $val  = $self->_attr($col);
395                }
396                 }
397                         
398         }
399         else {
400                 $val = eval {$self->column_default($col);};
401                 $val = '' unless defined $val;
402         }
403     }
404     my $a = HTML::Element->new("input", type => "text", name => $name);
405     $a->attr("value" => $val);
406     $OLD_STYLE && return $a->as_HTML;
407     $a;
408 }
409
410
411 # Too expensive version -- TODO
412 #sub _to_select {
413 #       my ($self, $col, $hint) = @_;
414 #       my $fclass = $hint || $self->__hasa_rels->{$col}->[0];
415 #       my @objs        = $fclass->retrieve_all;
416 #       my $a           = HTML::Element->new("select", name => $col);
417 #       for (@objs) {
418 #               my $sel = HTML::Element->new("option", value => $_->id);
419 #               $sel->attr("selected" => "selected")
420 #                       if ref $self
421 #                       and eval { $_->id eq $self->$col->id };
422 #               $sel->push_content($_->stringify_self);
423 #               $a->push_content($sel);
424 #       }
425 #       $OLD_STYLE && return $a->as_HTML;
426 #       $a;
427 #}
428
429
430
431 # pjs 
432 # -- Rewrote this to be efficient -- no object creation. 
433 # -- Added option for CDBI classes to specify a limiting clause
434 # via "has_a_select_limit". 
435 # -- Added selected argument to set a selected 
436
437 =head2 recognized arguments
438  
439   selected => $object|$id,
440   name     => $name,
441   value    => $value,
442   where    => SQL 'WHERE' clause,
443   order_by => SQL 'ORDER BY' clause,
444   limit    => SQL 'LIMIT' clause,
445   items    => [ @items_of_same_type_to_select_from ],
446   class => $class_we_are_selecting_from
447   stringify => $stringify_coderef|$method_name
448   
449   
450
451
452 # select box requirements
453 # 1. a select box for objecs of a has_a related class -- DONE 
454 =head2  1. a select box out of a has_a or has_many related class.
455   # For has_a the default behavior is to make a select box of every element in 
456   # related class and you choose one. 
457   #Or explicitly you can create one and pass options like where and order
458   BeerDB::Beer->to_field('brewery','select', {where => "location = 'Germany'");
459   
460   # For has_many the default is to get a multiple select box with all objects.
461   # If called as an object method, the objects existing ones will be selected. 
462   Brewery::BeerDB->to_field('beers','select', {where => "rating > 5"}); 
463   
464
465 =head2  2. a select box for objects of arbitrary class -- say BeerDB::Beer for fun. 
466   # general 
467   BeerDB::Beer->to_field('', 'select', $options)
468
469   BeerDB::Beer->to_field('', 'select'); # Select box of all the rows in class
470                                   # with PK as ID, $Class->to_field() same.
471   BeerDB::Beer->to_field('','select',{ where => "rating > 3 AND class like 'Ale'", order_by => 'rating DESC, beer_id ASC' , limit => 10});
472   # specify exact where clause 
473
474 =head2 3. If you already have a list of objects to select from  -- 
475
476   BeerDB:;Beer->to_field($col, 'select' , {items => $objects});
477
478 # 3. a select box for arbitrary set of objects 
479  # Pass array ref of objects as first arg rather than field 
480  $any_class_or_obj->to_field([BeerDB::Beer->search(favorite => 1)], 'select',);
481  
482
483 =cut
484
485 sub _to_select {
486     my ($self, $col, $args) = @_;
487     $args ||= {};
488 # Do we have items already ? Go no further. 
489     if ($args->{items}) {  
490         my $a = $self->_select_guts($col,  $args);
491         $OLD_STYLE && return $a->as_HTML;
492                 return $a;
493         }
494
495 # Else what are we making a select box out of ?  
496         # No Column parameter --  means making a select box of args->class or self 
497     # Using all rows from class's table
498     if (not $col) { 
499         warn "No col. $self";
500                 unless ($args->{class}) {
501                 $args->{class} = ref $self || $self;
502                         # object selected if called with one
503             $args->{selected} = { $self->id => 1} 
504                                 if not $args->{selected} and ref $self;
505                 }
506         $col = $args->{class}->primary_column;
507     }
508     # Related Class maybe ? 
509     elsif (my ($rel_type, $rel_meta) =  $self->related_meta('r:)', $col) ) {
510         $args->{class} = $rel_meta->{foreign_class};
511         # related objects pre selected if object
512                 $args->{selected} ||= [ $self->$col ] if  ref $self; 
513                                 
514                 # "Has many" --  we get multiple select 
515                 if ($rel_type =~ /has_many/i) {
516                         $args->{attr}{multiple}  = 'multiple';
517                         # TODO -- handle mapping 
518                 }
519                 my $c = $rel_meta->{args}{constraint} || {};
520                 my $j = $rel_meta->{args}{join} || {};
521                 my @join ; 
522                 if (ref $self) {
523                         @join   =  map { $_ ." = ". $self->_attr($_) } keys %$j; 
524                 }
525                 my @constr= map { "$_ = '$c->{$_}'"} keys %$c; 
526                 $args->{where}    ||= join (' AND ', (@join, @constr));
527                 $args->{order_by} ||= $rel_meta->{args}{order_by};
528                 $args->{limit}    ||= $rel_meta->{args}{limit};
529                         
530     }
531     # We could say :Col is name and we are selecting  out of class arg.
532         # DIE for now
533         else {
534                 #$args->{name} = $col;
535                 die "Usage _to_select. $col not related to any class to select from. ";
536                 
537     }
538                 
539     # Set arguments 
540         if ( $self->can('column_nullable') ) { 
541                 $args->{nullable} ||= $self->column_nullable($col);
542         }
543
544         # Get items to select from
545     $args->{items} = _select_items($args);
546     # Make select HTML element
547     $a = $self->_select_guts($col, $args);
548
549     # Return 
550     $OLD_STYLE && return $a->as_HTML;
551     $a;
552
553 }
554
555         
556 ############
557 # FUNCTION #
558 ############
559 # Get Items 
560 sub _select_items { 
561         my $args = shift;
562         my $fclass = $args->{class};
563     my @select_box_cols;
564     @select_box_cols = $fclass->columns('SelectBox');
565     @select_box_cols = $fclass->columns('Stringify')
566                                             unless @select_box_cols;
567     @select_box_cols = $fclass->_essential
568                                             unless @select_box_cols;
569     unshift @select_box_cols, $fclass->columns('Primary')
570         unless $select_box_cols[0] eq $fclass->columns('Primary');
571
572     my $sql = "SELECT " . join( ', ', @select_box_cols) . 
573                   " FROM " . $fclass->table;
574
575         $sql .= " WHERE " . $args->{where}   if $args->{where};
576         $sql .= " ORDER BY " . $args->{order_by} if $args->{order_by};
577         $sql .= " LIMIT " . $args->{limit} if $args->{limit};
578
579         return $fclass->db_Main->selectall_arrayref($sql);
580
581 }
582
583
584 # Makes a readonly input box out of column's value
585 # No args makes object to readonly
586 sub _to_readonly {
587     my ($self, $col, $val) = @_;
588     if (! $col) { # object to readonly
589         $val = $self->id;
590         $col = $self->primary_column;
591     }
592     unless (defined $val) {
593         $self->_croak("Cannot get value in _to_readonly .")
594             unless ref $self;
595         $val = $self->$col;
596     }
597     my $a = HTML::Element->new('input', 'type' => 'text', readonly => '1',
598         'name' => $col, 'value'=>$val);
599 $OLD_STYLE && return $a->as_HTML;
600     $a;
601 }
602
603
604 =head2 _to_enum_select
605
606 $sel_box = $self->_to_enum_select($column, "ENUM('Val1','Val2','Val3')");
607
608 Returns an enum select box given a column name and an enum string.
609 NOTE: The Plugin::Type does not return an enum string for mysql enum columns.
610 This will not work unless you write your own column_type method in your model.
611
612 =cut
613
614 sub _to_enum_select {
615     my ($self, $col, $type) = @_;
616     $type =~ /ENUM\((.*?)\)/i;
617     (my $enum = $1) =~ s/'//g;
618     my @enum_vals = split /\s*,\s*/, $enum;
619
620     # determine which is pre selected --
621     # if obj, the value is , otherwise use column_default which is the first
622     # value in the enum list unless it has been overridden
623     my $selected = eval { $self->$col  };
624     $selected = eval{$self->column_default($col)} unless defined $selected;
625     $selected = $enum_vals[0]               unless defined $selected;
626
627
628     my $a = HTML::Element->new("select", name => $col);
629     for ( @enum_vals ) {
630         my $sel = HTML::Element->new("option", value => $_);
631         $sel->attr("selected" => "selected") if $_ eq $selected ;
632         $sel->push_content($_);
633         $a->push_content($sel);
634     }
635     $OLD_STYLE && return $a->as_HTML;
636     $a;
637 }
638
639
640 =head2 _to_bool_select
641
642   my $sel = $self->_to_bool_select($column, $bool_string);
643
644 This  makes select input for boolean column.  You can provide a
645 bool string of form: Bool('zero','one') and those are used for option
646 content. Onthervise No and Yes are used.
647 TODO -- test without bool string.
648
649 =cut
650
651 sub _to_bool_select {
652     my ($self, $col, $type) = @_;
653         my @bool_text = ('No', 'Yes');  
654         if ($type =~ /BOOL\((.+?)\)/i) {
655                 (my $bool = $1) =~ s/'//g;
656                 @bool_text = split /,/, $bool;
657         }
658         my $one= ref $self ? eval {$self->$col;} : $self->column_default($col);
659     my $a = HTML::Element->new("select", name => $col);
660     my ($opt0, $opt1) = ( HTML::Element->new("option", value => 0),
661                                                   HTML::Element->new("option", value => 1) ); 
662     $opt0->attr("selected" => "selected") if not $one; 
663     $opt0->push_content($bool_text[0]); 
664     $opt1->attr("selected" => "selected") if $one; 
665     $opt1->push_content($bool_text[1]); 
666     $a->push_content($opt0, $opt1);
667     $OLD_STYLE && return $a->as_HTML;
668     $a;
669 }
670
671
672 =head2 _to_hidden($col, $args)
673
674 This makes a hidden html element. Give it a name and value or if name is
675 a ref it will use the PK name and value of the object.
676
677 =cut
678
679 sub _to_hidden {
680     my ($self, $name, $val) = @_;
681     my $args = {};
682     my $obj;
683     if (ref $name and $name->isa("Class::DBI")) {
684        $obj = $name;
685        $name= ($obj->primary_columns)[0]->name;
686     }
687     if (ref $val) {
688                 $args = $val;
689         $val = $args->{value};
690         $name = $args->{name} if $args->{name};
691     }
692     elsif (not $name ) { # hidding object caller
693         $self->_croak("No object available in _to_hidden") unless ref $self;
694         $name = ($self->primary_column)[0]->name;
695         $val  = $self->id;
696     }
697     return HTML::Element->new('input', 'type' => 'hidden',
698                               'name' => $name, 'value'=>$val
699     );
700 }
701
702 =head2 _to_link_hidden($col, $args) 
703
704 Makes a link with a hidden input with the id of $obj as the value and name.
705 Name defaults to the objects primary key. The object defaults to self.
706
707 =cut
708
709 sub _to_link_hidden {
710     my ($self, $accessor, $args) = @_;
711     my $r = $args->{r} || '';
712     my $url = $args->{url} || '';
713     
714     $self->_croak("_to_link_hidden called without Maypole Request object (\$r) and url. Need one or other.")
715         unless $r;
716     my ($obj, $name);
717     if (ref $self) { # hidding linking self
718          $obj  = $self;
719          $name = $args->{name} || $obj->primary_column->name;
720     }
721     else {           # hiding linking related object with id in args
722         $obj  = $self->related_class($r, $accessor)->retrieve($args->{id});
723         $name = $args->{name} || $obj->primary_column->name; # TODO make use meta data
724     }
725     $self->_croak("_to_link_hidden has no object") unless ref $obj;
726     my $href =  $url || $r->config->{uri_base} . "/". $obj->table."/view/".$obj->id;
727     my $a = HTML::Element->new('a', 'href' => $href);
728     $a->push_content("$obj");
729     $a->push_content($self->_to_hidden($name, $obj->id));
730         $OLD_STYLE && return $a->as_HTML;
731     $a;
732 }
733
734
735
736 =head2 _to_foreign_inputs
737
738 $html_els = $class_or_obj->_to_foreign_inputs($accssr, [$fields, $accssr_meta]);
739
740 Get inputs for the accessor's class.  Pass an array ref of fields to get
741 inputs for only those fields. Otherwise display_columns or all columns is used. 
742 If you have the meta info handy for the accessor you can pass that too.
743
744 TODO make AsForm know more about the request like what action we are doing
745 so it can use edit columns or search_columns
746
747 NOTE , this names the foreign inputs is a particular way so they can be
748 processed with a general routine and so there are not name clashes.
749
750 =cut
751
752 sub _to_foreign_inputs {
753         my ($self, $accssr, $fields, $accssr_meta) = @_;
754         if (!$accssr_meta) {
755                 my $class_meta = $self->meta_info;
756                 my ($rel_type) = grep { defined $class_meta->{$_}->{$accssr} }
757                         keys %$class_meta;
758                 $self->_croak( "No relationship for accessor $accssr") if not $rel_type;
759                 $accssr_meta = $class_meta->{$rel_type}->{$accssr};
760         }
761
762         my $classORobj = ref $self && ref $self->$accssr ? $self->$accssr : $accssr_meta->{foreign_class};
763         
764         unless ($fields) {      
765                 $fields = $classORobj->can('display_columns') ? 
766                         [$classORobj->display_columns] : [$classORobj->columns];
767         }
768         
769         # Ignore our fkey in them to  prevent infinite recursion 
770         my $me          = eval {$accssr_meta->{args}{foreign_column}} || '';  
771         my $constrained = $accssr_meta->{args}{constraint}; 
772         my %inputs;
773         foreach ( @$fields ) {
774                 next if $constrained->{$_} || ($_ eq $me); # don't display constrained
775                 $inputs{$_} =  $classORobj->to_field($_);
776         }
777
778         # Make hidden inputs for constrained columns unless we are editing object
779         # TODO -- is this right thing to do?
780         unless (ref $classORobj) {
781                 $inputs{$_} = $classORobj->_to_hidden($_, $constrained->{$_}) 
782                         foreach ( keys %$constrained );  
783         }
784         $self->_rename_foreign_input($accssr, \%inputs);
785         return \%inputs;
786 }
787
788
789 =head2 _hash_selected
790
791 Method to make sense out of the "selected" argument which can be in a number
792 of formats perhaps.  It returns a hashref with the the values of options to be
793 as the keys. 
794
795 Below handles these formats for the "selected" slot in the arguments hash:
796   Object (with id method)
797   Scalar (assumes it is value)
798   Array ref *OF* objects, arrays of data (0 elmnt used), hashes of data
799     (id key used), and simple scalars.
800     
801
802 =cut 
803  
804 ############
805 # FUNCTION #
806 ############
807 sub _hash_selected {
808         my ($args) = shift;
809         my $selected = $args->{selected};
810     return $selected unless $selected and ref $selected ne 'HASH'; 
811         my $type = ref $selected;
812         # Single Object 
813     if ($type and $type ne 'ARRAY') {
814        return  {$selected->id => 1};
815     }
816     # Single Scalar id 
817         elsif (not $type) {
818                 return { $selected => 1}; 
819         }
820
821         # Array of objs, arrays, hashes, or just scalalrs. 
822         elsif ($type eq 'ARRAY') {
823                 my %hashed;
824                 my $ltype = ref $selected->[0];
825                 # Objects
826                 if ($ltype and $ltype ne 'ARRAY')  {
827                         %hashed = map { $_->id  => 1 } @$selected;
828         }
829                 # Arrays of data with id first 
830             elsif ($ltype and $ltype eq 'ARRAY') {
831                         %hashed = map { $_->[0]  => 1 } @$selected; 
832                 }
833                 # Hashes using pk or id key
834                 elsif ($ltype and $ltype eq 'HASH') {
835                         my $pk = $args->{class}->primary_column || 'id';
836                         %hashed = map { $_->{$pk}  => 1 } @$selected; 
837                 }
838                 # Just Scalars
839         else { 
840                         %hashed = map { $_  => 1 } @$selected; 
841                 }
842                 return \%hashed;
843         }
844         else { warn "AsForm Could not hash the selected argument: $selected"; }
845
846                 
847
848
849
850 =head2 _select_guts 
851
852 Internal api  method to make the actual select box form elements.
853
854 3 types of lists making for -- 
855   Array of CDBI objects.
856   Array of scalars , 
857   Array or  Array refs with cols from class.
858 =cut
859
860
861
862 sub _select_guts {
863     my ($self, $col, $args) = @_; #$nullable, $selected_id, $values) = @_;
864
865     $args->{stringify} ||=  'stringify_selectbox';
866     $args->{selected} = _hash_selected($args);
867         my $name = $args->{name} || $col;
868     my $a = HTML::Element->new('select', name => $name);
869         $a->attr( %{$args->{attr}} ) if $args->{attr};
870     
871     if ($args->{nullable}) {
872                 my $null_element = HTML::Element->new('option');
873         $null_element->attr(selected => 'selected')
874                 if $args->{selected}{'null'};
875                 $null_element->push_content('-- choose or type --');
876         $a->push_content($null_element);
877     }
878
879     my $items = $args->{items};
880     my $proto = $items->[0];
881     my $type  = ref $proto || '';
882
883     # Objects 
884     if ($type and  $type !~ /ARRAY|HASH/i) {
885                 # make select  of objects
886                 $a->push_content($self->_options_from_objects($items, $args));
887         }
888     elsif ($type =~ /ARRAY/i) {
889                 $a->push_content($self->_options_from_arrays($items, $args));
890     }
891     elsif ($type =~ /HASH/i) { 
892                 $a->push_content($self->_options_from_hashes($items, $args));
893     }
894     else { 
895                 $a->push_content($self->_options_from_scalars($items, $args));
896     }
897
898     return $a;
899 }
900
901                 
902   
903   
904
905
906
907 =head2 _options_from_objects ( $objects, $args);
908
909 Private method to makes a options out of  objects. It attempts to call each
910 objects stringify method specified in $args->{stringify} as the content. Otherwise the default stringification prevails.
911
912 =cut
913 sub _options_from_objects {
914     my ($self, $items, $args) = @_;
915         my $selected = $args->{selected} || {};
916         my $stringify = $args->{stringify} || '';
917     my @res;
918         for (@$items) {
919                 my $opt = HTML::Element->new("option", value => $_->id);
920                 $opt->attr(selected => "selected") if $selected->{$_->id}; 
921                 my $content = $stringify ? $_->$stringify : "$_";
922                 $opt->push_content($content);
923                 push @res, $opt; 
924         }
925     return @res;
926 }
927
928 sub _options_from_arrays {
929     my ($self, $items, $args) = @_;
930         my $selected = $args->{selected} || {};
931     my @res;
932         my $fclass = $args->{class} || '';
933         my $stringify = $args->{stringify} || '';
934         for (@$items) {
935                 my $id = $_->[0];
936                 my $opt = HTML::Element->new("option", value => $id );
937                 #$opt->attr(selected => "selected") if $selected =~/^$id$/;
938                 $opt->attr(selected => "selected") if $selected->{$id};
939                 
940                 my $content = ($fclass and $stringify and $fclass->can($stringify)) ? 
941                               $fclass->$stringify($_) : 
942                                   join('/', @{$_});
943
944                 $opt->push_content( $content );
945         push @res, $opt; 
946     }
947     return @res;
948 }
949
950 sub _options_from_scalars {
951     my ($self, $items, $args) = @_;
952         my $selected = $args->{selected} || {};
953     my @res;
954         for (@$items) {
955                 my $opt = HTML::Element->new("option", value => $_ );
956                 #$opt->attr(selected => "selected") if $selected =~/^$id$/;
957                 $opt->attr(selected => "selected") if $selected->{$_};
958                 $opt->push_content( $_ );
959         push @res, $opt; 
960     }
961     return @res;
962 }
963
964 sub _options_from_hashes {
965     my ($self, $items, $args) = @_;
966         my $selected = $args->{selected} || {};
967         my $pk = eval {$args->{class}->primary_column} || 'id';
968         my $fclass = $args->{class} || '';
969         my $stringify = $args->{stringify} || '';
970         my @res;
971         for my $item (@$items) {
972                 my $val = $item->{$pk};
973                 my $opt = HTML::Element->new("option", value => $val );
974                 $opt->attr(selected => "selected") if $selected->{$val};
975                 my $content = ( $fclass && $stringify && $fclass->can($stringify)) ? $fclass->$stringify($item) : join(' ', @$item);
976                 $opt->push_content( $content );
977         push @res, $opt;
978     }
979     return @res;
980 }
981         
982
983 # checkboxes: if no data in hand (ie called as class method), replace
984 # with a radio button, in order to allow this field to be left
985 # unspecified in search / add forms.
986
987 # Not tested
988 # TODO  --  make this general checkboxse
989
990 #
991 sub _to_checkbox {
992     my ($self, $col, $args) = @_;
993     my $nullable = eval {self->column_nullable($col)} || 0; 
994     
995     return $self->_to_radio($col) if !ref($self) || $nullable;
996     my $value = $self->$col;
997     my $a = HTML::Element->new("input", type=> "checkbox", name => $col);
998     $a->attr("checked" => 'true') if $value eq 'Y';
999     return $a;
1000 }
1001
1002
1003 # TODO  -- make this general radio butons
1004 #
1005 sub _to_radio {
1006     my ($self, $col) = @_;
1007     my $value = ref $self && $self->$col || '';
1008     my $nullable = eval {self->column_nullable($col)} || 0; 
1009     my $a = HTML::Element->new("span");
1010     my $ry = HTML::Element->new("input", type=> "radio", name=>$col, value=>'Y' );
1011     my $rn = HTML::Element->new("input", type=> "radio", name=>$col, value=>'N' );
1012     my $ru = HTML::Element->new("input", type=> "radio", name=>$col, value=>'' ) if $nullable;
1013     $ry->push_content('Yes'); $rn->push_content('No');
1014     $ru->push_content('n/a') if $nullable;
1015     if ($value eq 'Y') { $ry->attr("checked" => 'true') }
1016     elsif ($value eq 'N') { $rn->attr("checked" => 'true') }
1017     elsif ($nullable) { $ru->attr("checked" => 'true') }
1018     $a->push_content($ry, $rn);
1019     $a->push_content($ru) if $nullable;
1020     return $a;
1021 }
1022
1023
1024
1025 ############################ HELPER METHODS ######################
1026 ##################################################################
1027
1028 =head2 _rename_foreign_input
1029
1030 _rename_foreign_input($html_el_or_hash_of_them); # changes made by reference
1031
1032 Recursively renames the foreign inputs made by to_foreign_inputs so they 
1033 can be processed generically.  The format is "accessor__AsForeign_colname". 
1034
1035 So if an Employee is a Person who has own  Address and you call 
1036
1037   Employee->to_field("person")  
1038   
1039 then you will get inputs for Address named like this: 
1040
1041   person__AsForeign__address__AsForeign__street
1042   person__AsForeign__address__AsForeign__city
1043   person__AsForeign__address__AsForeign__state  
1044   person__AsForeign__address__AsForeign__zip  
1045
1046 And the processor would know to create this address, put the address id in
1047 person address slot, create the person and put the address id in the employee
1048 before creating the employee. 
1049
1050 =cut
1051
1052 sub _rename_foreign_input {
1053         my ($self, $accssr, $input) = @_;
1054         if ( ref $input ne 'HASH' ) {
1055         #       my $new_name = $accssr . "__AsForeign__" . $input->attr('name');
1056                 $input->attr( name => $accssr . "__AsForeign__" . $input->attr('name'));
1057         }
1058         else {
1059                 $self->_rename_foreign_input($accssr, $input->{$_}) 
1060                         foreach (keys %$input);
1061         }
1062 }
1063 =head2 _box($value) 
1064
1065 This functions computes the dimensions of a textarea based on the value 
1066 or the defaults.
1067
1068 =cut
1069
1070 our ($min_rows, $max_rows, $min_cols, $max_cols) = (2 => 50, 20 => 100);
1071 sub _box
1072 {
1073     my $text = shift;
1074     if ($text) {
1075         my @rows = split /^/, $text;
1076         my $cols = $min_cols;
1077         my $chars = 0;
1078         for (@rows) {
1079             my $len = length $_;
1080             $chars += $len;
1081             $cols = $len if $len > $cols;
1082             $cols = $max_cols if $cols > $max_cols;
1083         }
1084         my $rows = @rows;
1085         $rows = int($chars/$cols) + 1 if $chars/$cols > $rows;
1086         $rows = $min_rows if $rows < $min_rows;
1087         $rows = $max_rows if $rows > $max_rows;
1088         ($rows, $cols)
1089     }
1090     else { ($min_rows, $min_cols) }
1091 }
1092
1093
1094 1;
1095
1096
1097 =head1 CHANGES
1098
1099 =head1 MAINTAINER 
1100
1101 Maypole Developers
1102
1103 =head1 ORIGINAL AUTHOR
1104
1105 Peter Speltz, Aaron Trevena 
1106
1107 =head1 TODO
1108
1109   Documenting 
1110   Testing - lots
1111   chekbox generalization
1112   radio generalization
1113   select work
1114   Make link_hidden use standard make_url stuff when it gets in Maypole
1115   How do you tell AF --" I want a has_many select box for this every time so,
1116      when you call "to_field($this_hasmany)" you get a select box
1117
1118 =head1 BUGS and QUERIES
1119
1120 Please direct all correspondence regarding this module to:
1121  Maypole list. 
1122
1123 =head1 COPYRIGHT AND LICENSE
1124
1125 Copyright 2003-2004 by Simon Cozens / Tony Bowden
1126
1127 This library is free software; you can redistribute it and/or modify
1128 it under the same terms as Perl itself.
1129
1130 =head1 SEE ALSO
1131
1132 L<Class::DBI>, L<Class::DBI::FromCGI>, L<HTML::Element>.
1133
1134 =cut
1135