X-Git-Url: https://git.decadent.org.uk/gitweb/?a=blobdiff_plain;f=lib%2FMaypole%2FModel%2FCDBI%2FAsForm.pm;h=76003ff3cf811d4ea1e44c053a98ef12897ccee7;hb=4f4bbd04570ff3c5f59f99c5cbf868d9b1a3da49;hp=da295789b8902354d6a13e8441d7c00596a5ba16;hpb=72ee4cbcfbb971c12dc6bd6d540edf96692061ca;p=maypole.git diff --git a/lib/Maypole/Model/CDBI/AsForm.pm b/lib/Maypole/Model/CDBI/AsForm.pm index da29578..76003ff 100644 --- a/lib/Maypole/Model/CDBI/AsForm.pm +++ b/lib/Maypole/Model/CDBI/AsForm.pm @@ -1,34 +1,29 @@ package Maypole::Model::CDBI::AsForm; -use 5.006; - +use Class::C3; use strict; + use warnings; use base 'Exporter'; use Data::Dumper; use Class::DBI::Plugin::Type (); use HTML::Element; +use Carp qw/cluck/; our $OLD_STYLE = 0; -# pjs -- Added new methods to @EXPORT -our @EXPORT = qw( to_cgi to_field _to_textarea _to_textfield _to_select - type_of _to_foreign_inputs _to_enum_select _to_bool_select - to_select_from_many _to_select_from_related hasmany_class - _to_hidden _rename_foreign_input _to_readonly - make_param_foreign make_hidden_elmnt make_hidden_elmnt - a_select_box unselect_element do_select search_inputs); - - - -our $VERSION = '2.41'; -# PJS VERSION .05 -# Changes : -# 08-09-05 - fixed broken has_a select box -# - fiked some docs -# - _to_foreign_inputs now takes 3 positional parameters -# (accssr, fields, accssr_meta_info) - +our @EXPORT = + qw( + to_cgi to_field foreign_input_delimiter search_inputs unselect_element + _field_from_how _field_from_relationship _field_from_column + _to_textarea _to_textfield _to_select _select_guts + _to_foreign_inputs _to_enum_select _to_bool_select + _to_hidden _to_link_hidden _rename_foreign_input _to_readonly + _options_from_objects _options_from_arrays _options_from_hashes + _options_from_array _options_from_hash + ); + +our $VERSION = '.97'; =head1 NAME @@ -43,22 +38,116 @@ Maypole::Model:CDBI::AsForm - Produce HTML form elements for database columns ... sub create_or_edit { - my $class = shift; - my %cgi_field = $class->to_cgi; + my $self = shift; + my %cgi_field = $self->to_cgi; return start_form, (map { "$_: ". $cgi_field{$_}->as_HTML."
" } $class->Columns), end_form; } - #
- # Title:
- # Artist: - # ... - #
+ + . . . + + # Somewhere else in a Maypole application about beer... + + + + + $beer->to_field('brewery', 'textfield', { + name => 'brewery_id', value => $beer->brewery, + # however, no need to set value since $beer is object + }); + + # Rate a beer + $beer->to_field(rating => select => { + items => [1 , 2, 3, 4, 5], + }); + + # Select a Brewery to visit in the UK + Brewery->to_field(brewery_id => { + items => [ Brewery->search_like(location => 'UK') ], + }); + + # Make a select for a boolean field + $Pub->to_field('open' , { items => [ {'Open' => 1, 'Closed' => 0 } ] }); + + $beer->to_field('brewery', { + selected => $beer->brewery, # again not necessary since caller is obj. + }); + + + $beer->to_field('brewery', 'link_hidden', {r => $r, uri => 'www.maypole.perl.org/brewery/view/'.$beer->brewery}); + # an html link that is also a hidden input to the object. R is required to + # make the uri unless you pass a uri + + + + ##################################################### + # Templates Usage + +
+ + ... + + + + . . . + + + + . . . + +
+ + + ##################################################### + # Advanced Usage + + # has_many select + package Job; + __PACKAGE__->has_a('job_employer' => 'Employer'); + __PACKAGE__->has_a('contact' => 'Contact') + + package Contact; + __PACKAGE__->has_a('cont_employer' => 'Employer'); + __PACKAGE__->has_many('jobs' => 'Job', + { join => { job_employer => 'cont_employer' }, + constraint => { 'finshed' => 0 }, + order_by => "created ASC", + } + ); + + package Employer; + __PACKAGE__->has_many('jobs' => 'Job',); + __PACKAGE__->has_many('contacts' => 'Contact', + order_by => 'name DESC', + ); + + + # Choose some jobs to add to a contact (has multiple attribute). + my $job_sel = Contact->to_field('jobs'); # Uses constraint and order by + + + # Choose a job from $contact->jobs + my $job_sel = $contact->to_field('jobs'); + + 1; + + + =head1 DESCRIPTION @@ -69,620 +158,1171 @@ into textareas, and fields with a has-a relationship to other C tables are turned into select drop-downs populated with objects from the joined class. -=head1 METHODS -The module is a mix-in which adds two additional methods to your -C-derived class. +=head1 ARGUMENTS HASH + +This provides a convenient way to tweak AsForm's behavior in exceptional or +not so exceptional instances. Below describes the arguments hash and +example usages. + + + $beer->to_field($col, $how, $args); + $beer->to_field($col, $args); + +Not all _to_* methods pay attention to all arguments. For example, '_to_textfield' does not look in $args->{'items'} at all. + +=over + +=item name -- the name the element will have , this trumps the derived name. + + $beer->to_field('brewery', 'readonly', { + name => 'brewery_id' + }); + +=item value -- the initial value the element will have, trumps derived value + + $beer->to_field('brewery', 'textfield', { + name => 'brewery_id', value => $beer->brewery, + # however, no need to set value since $beer is object + }); + +=item items -- array of items generally used to make select box options + +Can be array of objects, hashes, arrays, or strings, or just a hash. + + # Rate a beer + $beer->to_field(rating => select => { + items => [1 , 2, 3, 4, 5], + }); + + # Select a Brewery to visit in the UK + Brewery->to_field(brewery_id => { + items => [ Brewery->search_like(location => 'UK') ], + }); + + # Make a select for a boolean field + $Pub->to_field('open' , { items => [ {'Open' => 1, 'Closed' => 0 } ] }); + +=item selected -- something representing which item is selected in a select box + + $beer->to_field('brewery', { + selected => $beer->brewery, # again not necessary since caller is obj. + }); + +Can be an simple scalar id, an object, or an array of either + +=item class -- the class for which the input being made for field pertains to. + +This in almost always derived in cases where it may be difficult to derive, -- + # Select beers to serve on handpump + Pub->to_field(handpumps => select => { + class => 'Beer', order_by => 'name ASC', multiple => 1, + }); + +=item column_type -- a string representing column type + + $pub->to_field('open', 'bool_select', { + column_type => "bool('Closed', 'Open'), + }); + +=item column_nullable -- flag saying if column is nullable or not + +Generally this can be set to get or not get a null/empty option added to +a select box. AsForm attempts to call "$class->column_nullable" to set this +and it defaults to true if there is no shuch method. + + $beer->to_field('brewery', { column_nullable => 1 }); + +=item r or request -- the Mapyole request object + +=item uri -- uri for a link , used in methods such as _to_link_hidden + + $beer->to_field('brewery', 'link_hidden', + {r => $r, uri => 'www.maypole.perl.org/brewery/view/'.$beer->brewery}); + # an html link that is also a hidden input to the object. R is required to + # make the uri unless you pass a uri + +=item order_by, constraint, join + +These are used in making select boxes. order_by is a simple order by clause +and constraint and join are hashes used to limit the rows selected. The +difference is that join uses methods of the object and constraint uses +static values. You can also specify these in the relationship definitions. +See the relationships documentation of how to set arbitrayr meta info. + + BeerDB::LondonBeer->has_a('brewery', 'BeerDB::Brewery', + order_by => 'brewery_name ASC', + constraint => {location => 'London'}, + 'join' => {'brewery_tablecolumn => 'beer_obj_column'}, + ); + +=item no_hidden_constraints -- + +Tell AsForm not to make hidden inputs for relationship constraints. It does +this sometimes when making foreign inputs. However, i think it should not +do this and that the FromCGI 's _create_related method should do it. + +=back + +=head2 to_cgi + + $self->to_cgi([@columns, $args]); + +This returns a hash mapping all the column names to HTML::Element objects +representing form widgets. It takes two opitonal arguments -- a list of +columns and a hashref of hashes of arguments for each column. If called with an object like for editing, the inputs will have the object's values. + + $self->to_cgi(); # uses $self->columns; # most used + $self->to_cgi(qw/brewery style rating/); # sometimes + # and on rare occassions this is desireable if you have a lot of fields + # and dont want to call to_field a bunch of times just to tweak one or + # two of them. + $self->to_cgi(@cols, {brewery => { + how => 'textfield' # too big for select + }, + style => { + column_nullable => 0, + how => 'select', + items => ['Ale', 'Lager'] + } + }); + +=cut + +sub to_cgi { + my ($class, @columns) = @_; + my $args = {}; + if (not @columns) { + @columns = $class->columns; + # Eventually after stabalization, we could add display_columns + #keys map { $_ => 1 } ($class->display_columns, $class->columns); + } else { + if ( ref $columns[-1] eq 'HASH' ) { + $args = pop @columns; + } + } + map { $_ => $class->to_field($_, $args->{$_}) } @columns; +} + +=head2 to_field($field [, $how][, $args]) + +This maps an individual column to a form element. The C argument +can be used to force the field type into any you want. All that you need +is a method named "_to_$how" in your class. Your class inherits many from +AsForm already. + +If C is specified but the class cannot call the method it maps to, +then AsForm will issue a warning and the default input will be made. +You can write your own "_to_$how" methods and AsForm comes with many. +See C. You can also pass this argument in $args->{how}. + + +=cut + +sub to_field { + my ($self, $field, $how, $args) = @_; + if (ref $how) { $args = $how; $how = ''; } + unless ($how) { $how = $args->{how} || ''; } + #warn "In to_field field is $field how is $how. args ar e" . Dumper($args) . " \n"; + # Set sensible default value + if ($field and not defined $args->{default}) { + my $def = $self->column_default($field) ; + # exclude defaults we don't want actually put as value for input + if (defined $def) { + $def = $def =~ /(^0000-00-00.*$|^0[0]*$|^0\.00$|CURRENT_TIMESTAMP|NULL)/i ? '' : $def ; + $args->{default} = $def; + } + } + + return $self->_field_from_how($field, $how, $args) || + $self->_field_from_relationship($field, $args) || + $self->_field_from_column($field, $args) || + $self->_to_textfield($field, $args); +} =head2 search_inputs -Returns hashref of search inputs elements to use in cgi. + my $cgi = $class->search_inputs ([$args]); # optional $args + +Returns hash or hashref of search inputs elements for a class making sure the +inputs are empty of any initial values. +You can specify what columns you want inputs for in +$args->{columns} or +by the method "search_columns". The default is "display_columns". +If you want to te search on columns in related classes you can do that by +specifying a one element hashref in place of the column name where +the key is the related "column" (has_a or has_many method for example) and +the value is a list ref of columns to search on in the related class. + +Example: + sub BeerDB::Beer::search_columns { + return ( 'name' , 'rating', { brewery => [ 'name', 'location'] } ); + } -Uses fields specified in search_fields, makes foreign inputs if necessary. + # Now foreign inputs are made for Brewery name and location and the + # there will be no name clashing and processing can be automated. =cut + sub search_inputs { - my ($class, $r) = @_; - warn "In model search_inputs " if $class->model_debug; + my ($class, $args) = @_; $class = ref $class || $class; #my $accssr_class = { $class->accessor_classes }; my %cgi; - my $sfs = $class->search_fields; - foreach my $field ( @$sfs ) { + $args->{columns} ||= $class->can('search_columns') ?[$class->search_columns] : [$class->display_columns]; + + foreach my $field ( @{ $args->{columns} } ) { + my $base_args = { + no_hidden_constraints => 1, + column_nullable => 1, # empty option on select boxes + value => '', + }; if ( ref $field eq "HASH" ) { # foreign search fields my ($accssr, $cols) = each %$field; + $base_args->{columns} = $cols; unless ( @$cols ) { - # default to search fields for related - #$cols = $accssr_class->{$accssr}->search_fields; + # default to search fields for related + #$cols = $accssr_class->{$accssr}->search_columns; die ("$class search_fields error: Must specify at least one column to search in the foreign object named '$accssr'"); } - my $fcgi = $class->_to_foreign_inputs($accssr, $cols); + my $fcgi = $class->to_field($accssr, 'foreign_inputs', $base_args); + # unset the default values for a select box foreach (keys %$fcgi) { - $class->unselect_element($fcgi->{$_}); + my $el = $fcgi->{$_}; + if ($el->tag eq 'select') { + + $class->unselect_element($el); + my ($first, @content) = $el->content_list; + my @fc = $first->content_list; + my $val = $first ? $first->attr('value') : undef; + if ($first and (@fc > 0 or (defined $val and $val ne '')) ) { # something ( $first->attr('value') ne '' or + + # push an empty option on stactk + $el->unshift_content(HTML::Element->new('option')); + } + } + } $cgi{$accssr} = $fcgi; - #warn "Searchy inputs for field $field is " . Dumper($cgi{$accssr}); - } else { - $cgi{$field} = $class->to_field($field); - $class->unselect_element($cgi{$field}); + delete $base_args->{columns}; + } else { + $cgi{$field} = $class->to_field($field, $base_args); #{no_select => $args->{no_select}{$field} }); + my $el = $cgi{$field}; + if ($el->tag eq 'select') { + $class->unselect_element($el); + my ($first, @content) = $el->content_list; + if ($first and $first->content_list) { # something + #(defined $first->attr('value') or $first->attr('value') ne '')) + # push an empty option on stactk + $el->unshift_content(HTML::Element->new('option')); + } + } } } return \%cgi; } -=head2 do_select -Retrieves object selected from a select box and puts in $r->objects[0]. -The select box input must be named the same as the primary key. -NOTE only works with tables with single primary key for now. +=head2 unselect_element + + unselect any selected elements in a HTML::Element select list widget + +=cut +sub unselect_element { + my ($self, $el) = @_; + if (ref $el && $el->can('tag') && $el->tag eq 'select') { + foreach my $opt ($el->content_list) { + $opt->attr('selected', undef) if $opt->attr('selected'); + } + } +} + +=head2 _field_from_how($field, $how,$args) + +Returns an input element based the "how" parameter or nothing at all. +Override at will. =cut -sub do_select { - my ($self, $r) = @_; - $r->objects([ $self->retrieve($r->params->{$self->primary_column}) ]); - $r->template('view'); +sub _field_from_how { + my ($self, $field, $how, $args) = @_; + return unless $how; + $args ||= {}; + no strict 'refs'; + my $meth = "_to_$how"; + if (not $self->can($meth)) { + warn "Class can not $meth"; + return; + } + return $self->$meth($field, $args); } +=head2 _field_from_relationship($field, $args) -=head2 unselect_element +Returns an input based on the relationship associated with the field or nothing. +Override at will. -Unselects all options in a HTML::Element of type select. -It does nothing if element is not a select element. +For has_a it will give select box =cut -sub unselect_element { - my ($self, $el) = @_; - #unless (ref $el eq 'HTML::Element') { - #$self->_croak ('Need an HTML::Element to unselect. You gave a ' . Dumper($el)); } - if ($el->tag eq 'select') { - foreach my $opt ($el->content_list) { - $opt->attr('selected', undef) if $opt->attr('selected'); - } - } +sub _field_from_relationship { + my ($self, $field, $args) = @_; + return unless $field; + my $rel_meta = $self->related_meta('r',$field) || return; + my $rel_name = $rel_meta->{name}; + my $fclass = $rel_meta->foreign_class; + my $fclass_is_cdbi = $fclass ? $fclass->isa('Class::DBI') : 0; + + # maybe has_a select + if ($rel_meta->{name} eq 'has_a' and $fclass_is_cdbi) { + # This condictions allows for trumping of the has_a args + if (not $rel_meta->{args}{no_select} and not $args->{no_select}) { + $args->{class} = $fclass; + return $self->_to_select($field, $args); + } + return; + } + # maybe has many select + if ($rel_meta->{name} eq 'has_many' and $fclass_is_cdbi and ref $self) { + # This condictions allows for trumping of the has_a args + if (not $rel_meta->{args}{no_select} and not $args->{no_select}) { + $args->{class} = $fclass; + my @itms = $self->$field; # need list not iterator + $args->{items} = \@itms; + return $self->_to_select($field, $args); + } + return; + } + + # maybe foreign inputs + my %local_cols = map { $_ => 1 } $self->columns; # includes is_a cols + if ($fclass_is_cdbi and (not $local_cols{$field} or $rel_name eq 'has_own')) { + $args->{related_meta} = $rel_meta; # suspect faster to set these args + return $self->_to_foreign_inputs($field, $args); + } + return; } +=head2 _field_from_column($field, $args) -# make a select box from args -sub a_select_box { - my ($self, $name, $vals, $selected_val, $contents) = @_; - die "Usage: Need a name and array ref of values to make a select boxes" unless ($name && $vals); - $selected_val ||= ""; - $contents ||= $vals ; - - my $a = HTML::Element->new('select', 'name' => $name); - my $i = 0; - my $c; - foreach my $v ( @$vals ) { - my $opt = HTML::Element->new('option', 'value' => $v); - $opt->attr('selected' => 'selected') if $v eq $selected_val; - $c = $contents->[$i++] || $v; - $opt->push_content($c); - $a->push_content($opt); - } - $a; -} +Returns an input based on the column's characteristics, namely type, or nothing. +Override at will. +=cut + +sub _field_from_column { + my ($self, $field, $args) = @_; + # this class and pk are default class and field at this point + my $class = $args->{class} || $self; + $class = ref $class || $class; + $field ||= ($class->primary_columns)[0]; # TODO -=head2 make_hidden_elmnt + # Get column type + unless ($args->{column_type}) { + if ($class->can('column_type')) { + $args->{column_type} = $class->column_type($field); + } else { + # Right, have some of this + eval "package $class; Class::DBI::Plugin::Type->import()"; + $args->{column_type} = $class->column_type($field); + } + } + my $type = $args->{column_type}; + + return $self->_to_textfield($field, $args) + if $type and $type =~ /^(VAR)?CHAR/i; #common type + return $self->_to_textarea($field, $args) + if $type and $type =~ /^(TEXT|BLOB)$/i; + return $self->_to_enum_select($field, $args) + if $type and $type =~ /^ENUM\((.*?)\)$/i; + return $self->_to_bool_select($field, $args) + if $type and $type =~ /^BOOL/i; + return $self->_to_readonly($field, $args) + if $type and $type =~ /^readonly$/i; + return; +} -Makes a hidden HTML::Element and puts it in template_args{hidden_elements} -$model->make_hidden_elmnt($name, $val); -=cut +sub _to_textarea { + my ($self, $col, $args) = @_; + my $class = $args->{class} || $self; + $class = ref $class || $class; + $col ||= ($class->primary_columns)[0]; # TODO + # pjs added default + $args ||= {}; + my $val = $args->{value}; + + unless (defined $val) { + if (ref $self) { + $val = $self->$col; + } else { + $val = $args->{default}; + $val = '' unless defined $val; + } + } + my ($rows, $cols) = _box($val); + $rows = $args->{rows} if $args->{rows}; + $cols = $args->{cols} if $args->{cols};; + my $name = $args->{name} || $col; + my $a = + HTML::Element->new("textarea", name => $name, rows => $rows, cols => $cols); + $a->push_content($val); + $OLD_STYLE && return $a->as_HTML; + $a; +} -sub make_hidden_elmnt { - my ($self, $r, $col, $val) = @_; - my $elmnt = HTML::Element->new('input', 'type'=>'hidden', 'name'=>$col, 'value'=>$val); +sub _to_textfield { + my ($self, $col, $args ) = @_; + use Carp qw/confess/; + confess "No col passed to _to_textfield" unless $col; + $args ||= {}; + my $val = $args->{value}; + my $name = $args->{name} || $col; + + unless (defined $val) { + if (ref $self) { + # Case where column inflates. + # Input would get stringification which could be not good. + # as in the case of Time::Piece objects + $val = $self->can($col) ? $self->$col : ''; # in case it is a virtual column + if (ref $val) { + if (my $meta = $self->related_meta('',$col)) { + if (my $code = $meta->{args}{deflate4edit} || $meta->{args}{deflate} ) { + $val = ref $code ? &$code($val) : $val->$code; + } elsif ( $val->isa('Class::DBI') ) { + $val = $val->id; + } else { + #warn "No deflate4edit code defined for $val of type " . + #ref $val . ". Using the stringified value in textfield.."; + } + } else { + $val = $val->id if $val->isa("Class::DBI"); + } + } - $r->{template_args}{hidden_elements} ||= []; - push @{ $r->{template_args}{hidden_elements} }, $elmnt; + } else { + $val = $args->{default}; + $val = '' unless defined $val; + } + } + my $a; + # THIS If section is neccessary or you end up with "value" for a vaiue + # if val is + $val = '' unless defined $val; + $a = HTML::Element->new("input", type => "text", name => $name, value =>$val); + $OLD_STYLE && return $a->as_HTML; + $a; } +=head2 recognized arguments + selected => $object|$id, + name => $name, + value => $value, + where => SQL 'WHERE' clause, + order_by => SQL 'ORDER BY' clause, + constraint => hash of constraints to search + limit => SQL 'LIMIT' clause, + items => [ @items_of_same_type_to_select_from ], + class => $class_we_are_selecting_from + stringify => $stringify_coderef|$method_name -=head2 make_param_foreign -Makes a new foreign parameter out of parameter and accessor -Just puts accssr__FOREIGN__ in front of param name +=head2 1. a select box out of a has_a or has_many related class. + # For has_a the default behavior is to make a select box of every element in + # related class and you choose one. + #Or explicitly you can create one and pass options like where and order + BeerDB::Beer->to_field('brewery','select', {where => "location = 'Germany'"); -=cut + # For has_many the default is to get a multiple select box with all objects. + # If called as an object method, the objects existing ones will be selected. + Brewery::BeerDB->to_field('beers','select', {where => "rating > 5"}); -sub make_param_foreign { - my ($self, $r, $p, $accssr) = @_; - $r->params->{$accssr . '__FOREIGN__' . $p} = $r->params->{$p}; -} -=head2 to_cgi +=head2 2. a select box for objects of arbitrary class -- say BeerDB::Beer for fun. + # general + BeerDB::Beer->to_field('', 'select', $options) + + BeerDB::Beer->to_field('', 'select'); # Select box of all the rows in class + # with PK as ID, $Class->to_field() same. + BeerDB::Beer->to_field('','select',{ where => "rating > 3 AND class like 'Ale'", order_by => 'rating DESC, beer_id ASC' , limit => 10}); + # specify exact where clause + +=head2 3. If you already have a list of objects to select from -- -This returns a hash mapping all the column names of the class to -HTML::Element objects representing form widgets. + BeerDB:;Beer->to_field($col, 'select' , {items => $objects}); + +# 3. a select box for arbitrary set of objects + # Pass array ref of objects as first arg rather than field + $any_class_or_obj->to_field([BeerDB::Beer->search(favorite => 1)], 'select',); -pjs -- added a columns list argument to specify which columns to make -inputs for. =cut -sub to_cgi { - my ($class, @columns) = @_; # pjs -- added columns arg - @columns = $class->columns unless (@columns); - map { $_ => $class->to_field($_) } @columns; -} +sub _to_select { + my ($self, $col, $args) = @_; + $args ||= {}; + # Do we have items already ? Go no further. + if ($args->{items} and ref $args->{items}) { + my $a = $self->_select_guts($col, $args); + $OLD_STYLE && return $a->as_HTML; + if ($args->{multiple}) { + $a->attr('multiple', 'multiple'); + } + return $a; + } -=head2 to_field($field [, $how]) + # Proceed with work -This maps an individual column to a form element. The C argument -can be used to force the field type into one of C, C