]> git.decadent.org.uk Git - maypole.git/commitdiff
CRUD tests, FromCGI that works, test to prove it ;)
authorbiopete <biopete@invalid>
Tue, 13 Jun 2006 18:49:06 +0000 (18:49 +0000)
committerbiopete <biopete@invalid>
Tue, 13 Jun 2006 18:49:06 +0000 (18:49 +0000)
git-svn-id: http://svn.maypole.perl.org/Maypole/trunk@495 48953598-375a-da11-a14b-00016c27c3ee

Makefile.PL
ex/BeerDB.pm
ex/beerdb.sql
lib/CGI/Untaint/Maypole.pm [new file with mode: 0644]
lib/Maypole/Model/CDBI.pm
lib/Maypole/Model/CDBI/AsForm.pm
lib/Maypole/Model/CDBI/FromCGI.pm [new file with mode: 0644]
t/crud.t [new file with mode: 0755]
t/templates/custom/edit [new file with mode: 0644]
t/templates/custom/view

index efe950184161d2acd924510d79428413064b1676..302189e970507b48809ec1e5728e80868efb03f1 100644 (file)
@@ -16,22 +16,22 @@ WriteMakefile(
         Class::DBI                       => 0.96,
         Class::DBI::SQLite               => 0,
         CGI::Untaint                     => 1.26,
-       CGI::Untaint::date               => 0,
-       CGI::Untaint::email              => 0,
+               CGI::Untaint::date                               => 0,
+               CGI::Untaint::email                             => 0,
         UNIVERSAL::moniker               => 0,
         UNIVERSAL::require               => 0,
         URI                              => 0,
         URI::QueryParam                  => 0,
         CGI::Simple                      => 0,
-       HTTP::Body                       => 0.5,
-       HTML::Element                    => 0,
+               HTTP::Body                       => 0.5,
+               HTML::Element                    => 0,
         HTTP::Headers                    => 1.59,
         Template                         => 0,
         Template::Plugin::Class          => 0,
         Test::MockModule                 => 0,
         Digest::MD5                     => 0,
-       File::MMagic::XS                => 0.08,
-       Class::DBI::Plugin::Type        => 0,
+               File::MMagic::XS                => 0.08,
+               Class::DBI::Plugin::Type        => 0,
     },    # e.g., Module::Name => 1.1
     (
         $] >= 5.005
@@ -87,7 +87,7 @@ create table beer (
     style integer,
     name varchar(30),
     url varchar(120),
-#    tasted date,
+    tasted date,
     score integer(2),
     price varchar(12),
     abv varchar(10),
index db4ec1c66ce5556790ee37913e90c6187ef71b82..e0b2894de09ebf11c3428675e8352d60c972262f 100644 (file)
@@ -30,8 +30,10 @@ BeerDB->config->application_name('The Beer Database');
 BeerDB->config->uri_base( $ENV{BEERDB_BASE} || "http://localhost/beerdb/" );
 
 # Change this to the htdoc root for your maypole application.
-BeerDB->config->template_root( $ENV{BEERDB_TEMPLATE_ROOT} ) if $ENV{BEERDB_TEMPLATE_ROOT};
 
+my @root=  ('t/templates'); 
+push @root,$ENV{BEERDB_TEMPLATE_ROOT} if ($ENV{BEERDB_TEMPLATE_ROOT});
+BeerDB->config->template_root( [@root] ); 
 # Specify the rows per page in search results, lists, etc : 10 is a nice round number
 BeerDB->config->rows_per_page(10);
 
@@ -42,10 +44,16 @@ BeerDB::Style->untaint_columns( printable => [qw/name notes/] );
 BeerDB::Beer->untaint_columns(
     printable => [qw/abv name price notes url/],
     integer => [qw/style brewery score/],
-    date =>[ qw/date/],
+    date =>[ qw/tasted/],
 );
 BeerDB::Pub->untaint_columns(printable => [qw/name notes url/]);
 
+# Required Fields
+BeerDB->config->{brewery}{required_cols} = [qw/name/];
+BeerDB->config->{style}{required_cols} = [qw/name/];
+BeerDB->config->{beer}{required_cols} = [qw/brewery name price/];
+BeerDB->config->{pub}{required_cols} = [qw/name/];
+
 BeerDB->config->{loader}->relationship($_) for (
     "a brewery produces beers",
     "a style defines beers",
index b5d7d7c5d6160c4411f6977744c222dca3152564..0c6a0df4d50e77b891259c84977dee453824c3e6 100644 (file)
@@ -26,7 +26,8 @@ CREATE TABLE beer (
     score integer(2),
     price varchar(12),
     abv varchar(10),
-    notes text
+    notes text,
+       tasted date
 );
 
 CREATE TABLE brewery (
diff --git a/lib/CGI/Untaint/Maypole.pm b/lib/CGI/Untaint/Maypole.pm
new file mode 100644 (file)
index 0000000..d096c27
--- /dev/null
@@ -0,0 +1,124 @@
+package CGI::Untaint::Maypole;
+
+use strict;
+use warnings;
+our $VERSION = '0.01';
+use base 'CGI::Untaint';
+use Carp;
+
+=head1 NAME 
+
+CGI::Untaint::Maypole - Use instead of CGI::Untaint. Based on CGI::Untaint
+
+=head1 SYNOPSIS
+
+  use CGI::Untaint::Maypole;
+  my $h = CGI::Untaint::Maypole->new($params);
+  $value = $h->extract(-as_printable => 'name);
+
+  if ($h->error =~ /No input for/) {
+       # caught empty input now handle it
+               ....
+  }
+  if ($h->raw_data->{$field} eq $object->$field) {
+    # Raw data same as database data. Perhaps we should not update field
+       ...
+  }
+
+=head1 DESCRIPTION
+
+This patches some issues I have with CGI::Untaint. You still need it installed
+and you install handlers the same.
+
+1) Instead of passing the empty string to the untaint handlers and relying on
+them to handle it to everyone's liking, it seems better 
+to have CGI::Untaint just say "No input for field" if the field is blank.
+
+2) It  adds the method C<raw_data> to the get back the parameters the handler
+was created with. 
+
+=cut
+
+
+sub raw_data { 
+       return shift->{__data};
+}
+
+# offending method ripped from base and patched
+sub _do_extract {
+       my $self = shift;
+
+       my %param = @_;
+
+       #----------------------------------------------------------------------
+       # Make sure we have a valid data handler
+       #----------------------------------------------------------------------
+       my @as = grep /^-as_/, keys %param;
+       croak "No data handler type specified"        unless @as;
+       croak "Multiple data handler types specified" unless @as == 1;
+
+       my $field      = delete $param{ $as[0] };
+       my $skip_valid = $as[0] =~ s/^(-as_)like_/$1/;
+       my $module     = $self->_load_module($as[0]);
+
+       #----------------------------------------------------------------------
+       # Do we have a sensible value? Check the default untaint for this
+       # type of variable, unless one is passed.
+       #----------------------------------------------------------------------
+
+       ################# PETER'S PATCH #####################
+       my $raw = $self->{__data}->{$field} ;
+       die "No parameter for '$field'\n" if !defined($raw);
+       die "No input for '$field'\n" if $raw eq '';
+    #####################################################
+
+
+       my $handler = $module->_new($self, $raw);
+
+       my $clean = eval { $handler->_untaint };
+       if ($@) {    # Give sensible death message
+               die "$field ($raw) is in invalid format.\n"
+                       if $@ =~ /^Died at/;
+               die $@;
+       }
+
+       #----------------------------------------------------------------------
+       # Are we doing a validation check?
+       #----------------------------------------------------------------------
+       unless ($skip_valid) {
+               if (my $ref = $handler->can('is_valid')) {
+                       die "$field ($raw) is in invalid format.\n"
+                               unless $handler->is_valid;
+               }
+       }
+
+       return $handler->untainted;
+}
+
+=head1 BUGS
+
+None known yet.
+
+=head1 SEE ALSO
+
+L<perlsec>. L<CGI::Untaint>.
+
+=head1 AUTHOR
+
+Peter Speltz.
+
+=head1 BUGS and QUERIES
+
+Please direct all correspondence regarding this module to:
+   bug-Maypole@rt.cpan.org
+
+=head1 COPYRIGHT and LICENSE
+
+Copyright (C) 2006 Peter Speltz.  All rights reserved.
+
+This module is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
+
+1;
index ae11c4dc1fc50d2d498d5515c93e9a16e262cccd..d8d5ace5ea3d18f76e6be064701b421597256593 100644 (file)
@@ -25,9 +25,18 @@ will instead use Class::DBI classes provided.
 
 use base qw(Maypole::Model::Base Class::DBI);
 use Maypole::Model::CDBI::AsForm;
-use CGI::Untaint;
+
+use Maypole::Model::CDBI::FromCGI; 
+use CGI::Untaint::Maypole;
+our $Untainter = 'CGI::Untaint::Maypole';
+
+# or if you like bugs 
+
+#use Class::DBI::FromCGI;
+#use CGI::Untaint;
+#our $Untainter = 'CGI::Untaint';
+
 use Class::DBI::Plugin::Type;
-use Class::DBI::FromCGI;
 use Class::DBI::Loader;
 use Class::DBI::AbstractSearch;
 use Class::DBI::Plugin::RetrieveAll;
@@ -61,7 +70,7 @@ sub do_edit : Exported {
   my $config   = $r->config;
   my $table    = $r->table;
 
-  # handle cancel button hits
+  # handle cancel button hit
   if ( $r->{params}->{cancel} ) {
     $r->template("list");
     $r->objects( [$self->retrieve_all] );
@@ -119,10 +128,19 @@ sub _do_update_or_create {
   my $fatal;
   my $creating = 0;
 
-  my $h = CGI::Untaint->new( %{$r->params} );
+  my $h = $Untainter->new( %{$r->params} );
 
   # update or create
   if ($obj) {
+               # 1: Required fields for update are different than create. Its only required
+         #              if it is in the parameters
+
+#              my @real_required = ();
+#              my %required = map { $_ => 1 } @$required_cols;
+#    foreach (keys %{$r->params}) {
+#                      push @real_required, $_ if $required{$_};
+#        }
+                       
     # We have something to edit
     eval { $obj->update_from_cgi( $h => {
                                         required => $required_cols,
@@ -132,20 +150,14 @@ sub _do_update_or_create {
         };
     $fatal = $@;
   } else {
-    eval {
-      $obj = $self->create_from_cgi( $h => {
+       eval {
+       $obj = $self->create_from_cgi( $h => {
                                            required => $required_cols,
                                            ignore => $ignored_cols,
-                                          } )
-    };
-
-    if ($fatal = $@) {
-      warn "FATAL ERROR: $fatal" if $r->debug;
-#      $self->dbi_rollback;
-    } else {
-#      $self->dbi_commit;
-    }
-    $creating++;
+                                          } );
+       };
+       $fatal = $@;
+       $creating++;
   }
 
   return $obj, $fatal, $creating;
index 6ca422a0c8bdea4088f6f4a28647be8a5a667c60..47b0cbd424ded13c1a2a961b1bc40424e76e0cf7 100644 (file)
@@ -1092,7 +1092,6 @@ sub _select_guts {
     #$args->{stringify} ||=  'stringify_selectbox';
 
     $args->{selected} = _hash_selected($args) if defined $args->{selected};
-       warn "*** Dumpe of selected " . Dumper( $args->{selected} );
        my $name = $args->{name} || $col;
     my $a = HTML::Element->new('select', name => $name);
        $a->attr( %{$args->{attr}} ) if $args->{attr};
diff --git a/lib/Maypole/Model/CDBI/FromCGI.pm b/lib/Maypole/Model/CDBI/FromCGI.pm
new file mode 100644 (file)
index 0000000..8718f9b
--- /dev/null
@@ -0,0 +1,524 @@
+package Maypole::Model::CDBI::FromCGI; 
+use strict;
+use warnings;
+
+# The base base model class for apps --
+# provides good search and create functions
+
+use base qw(Exporter); 
+use CGI::Untaint;
+use Maypole::Constants;
+use CGI::Untaint::Maypole;
+our $Untainter = 'CGI::Untaint::Maypole';
+
+our @EXPORT = qw/update_from_cgi create_from_cgi untaint_columns
+    cgi_update_errors untaint_type _validate validate_all _do_update_all 
+       _do_create_all classify_form_inputs/;
+
+
+
+use Data::Dumper; # for debugging
+
+
+sub untaint_columns {
+    die "untaint_columns() needs a hash" unless @_ % 2;
+    my ($class, %args) = @_;
+    $class->mk_classdata('__untaint_types')
+        unless $class->can('__untaint_types');
+    my %types = %{ $class->__untaint_types || {} };
+    while (my ($type, $ref) = each(%args)) {
+        $types{$type} = $ref;
+    }
+    $class->__untaint_types(\%types);
+}
+
+# get/set untaint_type for a column
+sub untaint_type {
+    my ($class, $field, $new_type) = @_;
+    my %handler = __PACKAGE__->_untaint_handlers($class);
+    return $handler{$field} if $handler{$field};
+    my $handler = eval {
+        local $SIG{__WARN__} = sub { };
+        my $type = $class->column_type($field) or die;
+        _column_type_for($type);
+    };
+    return $handler || undef;
+}
+
+sub cgi_update_errors { %{ shift->{_cgi_update_error} || {} } }
+
+
+
+###################
+# create_from_cgi #
+###################
+
+# Creates  multiple objects  from a  cgi form. 
+# Errors are returned in cgi_update_errors
+# 
+# simple usage: $beer->create_from_cgi($r);
+#
+# The last arg is flag to say whether to classify inputs or not.
+# TODO : make 100% backward compatible 
+#
+
+sub create_from_cgi { 
+       my ($self, $r, $params, $no_classify) = @_;
+       $self->_croak( "create_from_cgi can only be called as a class method")
+               if ref $self;
+                
+       my ($errors, $validated); 
+       #print "*** create_from_cgi ***\n\n";
+       # FromCGI interface compatibility
+    # params are ($h, $wanted)
+       if ($r->isa('CGI::Untaint')) {
+               #print "*** raw data ***" . Dumper($r->raw_data);
+               #print "*** wanted data ***" . Dumper($params);
+               
+       ($errors, $validated) = $self->_validate($r,$params); 
+               #print "*** validated data ***" . Dumper($validated);
+               #print "*** errors data ***" . Dumper($errors);
+       }
+       else {
+               $params ||= $r->params;
+               my $classified = $no_classify ? {%$params}:$self->classify_form_inputs($params);
+               ($errors, $validated) = $self->validate_all($r, $classified);
+       }
+
+       if (keys %$errors) {
+               return bless { _cgi_update_error => $errors }, $self;
+       }
+       
+       # Insert all the data
+       my ($obj, $err ) = $self->_do_create_all($validated); 
+    if ($err) {
+               return bless { _cgi_update_error => $err }, $obj ;
+       }
+       return $obj;
+}
+
+
+###################
+# update_from_cgi #
+###################
+
+# returns 1 or nothing if errors
+                                                                               
+
+sub update_from_cgi {
+       my ($self, $r, $params, $no_classify) = @_;
+       $self->_croak( "update_from_cgi can only be called as an object method")        
+       unless ref $self;
+       my ($errors, $validated, $wanted); 
+       $self->{_cgi_update_error} = {};
+    
+       #print "*** update_from_cgi talking ***\n\n";
+       # FromCGI interface compatibility params are ($h, $wanted)
+       if ($r->isa('CGI::Untaint')) {
+               # REHASH the $wanted for updating:
+               # 1: we ignore any fields we dont have parmeter for. (safe ?)
+               # 2: we dont want to update fields unless they change
+
+               my ($h, $wanted) = ($r, $params);
+               my @ignore = @{$wanted->{ignore} || []};
+               push @ignore, $self->primary_column->name;
+               my $raw = $h->raw_data;
+        #print "*** raw data ****" . Dumper($raw);
+        foreach my $field ($self->columns) {
+                       #print "*** field is $field ***\n";
+                       if (not defined $raw->{$field}) {
+                               push @ignore, $field->name; 
+                               #print "*** ignoring $field because it is not present ***\n";
+                next;
+                               
+                       }
+                       # stupid inflation , cant get at raw db value easy, must call
+                       # deflate ***FIXME****
+                       my $cur_val = ref $self->$field ? $self->$field->id : $self->$field;
+                       if ($raw->{$field} eq $cur_val) {
+                               #print "*** ignoring $field because unchanged ***\n";
+                               push @ignore, $field->name; 
+                       }
+        }
+                       
+               $wanted->{ignore} = \@ignore;
+        #print "*** wanted  ****" . Dumper($wanted);
+       ($errors, $validated) = $self->_validate($h,$wanted,1); 
+        #print "*** validated data  ****" . Dumper($validated);
+        #print "*** errors   ****" . Dumper($errors);
+       }
+       else {
+               $params ||= $r->params;
+               my $classified = $no_classify ? {%$params}:$self->classify_form_inputs($params);
+               ($errors, $validated) = $self->validate_all($r, $classified,1);
+        #print "*** errors for validate all   ****" . Dumper($errors);
+       }
+
+       if (keys %$errors) {
+        #print "*** we have errors   ****" . Dumper($errors);
+               $self->{_cgi_update_error} = $errors;
+               return;
+       }
+       
+       # Update all the data
+       my ($obj, $err ) = $self->_do_update_all($validated); 
+    if ($err) {
+               $self->{_cgi_update_error} = $err;
+               return; 
+       }
+       return 1; 
+}
+       
+=head2 validate_all 
+
+Validates (untaints) a hash of possibly mixed table params. It returns hashrefs
+of errors and validated data. 
+
+=cut
+
+sub validate_all {
+       my ($self, $r, $classified,  $updating) = @_;
+       
+    # Base case - validate this classes data
+       my $all    = eval{ $r->config->{$self->table}{all_cols} }               ||
+                    [$self->columns('All')];
+       my $req    = eval{ $r->config->{$self->table}{required_cols} }  ||
+                        [];
+       my $ignore = eval{ $r->config->{$self->table}{ignore_cols} }    || 
+                            [];
+       push @$ignore, $self->primary_column->name if $updating;
+       # ignore hashes of foreign inputs
+       foreach (keys %$classified) {
+               push @$ignore, $_ if  ref $classified->{$_} eq 'HASH'; 
+       }
+       my $h = $Untainter->new($classified);
+       my ($errs, $validated) = $self->_validate(
+               $h, {all => $all, required => $req, ignore => $ignore},$updating
+    );
+       
+       #warn "Validated dump is " . Dumper($validated);
+       #warn "classified dump is " . Dumper($classified);
+       # Validate all foreign input
+    foreach my $field (keys %$classified) {
+               if (ref $classified->{$field} eq "HASH") {
+                       my $data = $classified->{$field};
+#                      warn "Dump of fdata for $field: " . Dumper($data) if $r->debug;
+                       my @usr_entered_vals = ();
+                       my %required = map { $_ => 1 } 
+                               @{$r->config->{$self->table}{required_cols}};
+                       foreach ( values %$data ) {
+                               push @usr_entered_vals, $_  if $_  ne '';
+                       }
+
+                       # filled in values
+                       # IF we have some inputs for the related
+                   if ( @usr_entered_vals )  {
+#                          warn "user entered vals . " . Dumper(\@usr_entered_vals) if $r->debug;
+                               my ($ferrs, $valid) = $self->related_class($r, $field)->validate_all($r, $classified->{$field}, $updating );    
+                               $errs->{$field} = $ferrs if $ferrs;
+                               $validated->{$field} = $valid;
+                       }
+                       else { 
+                               # Check its not requeired
+                               if ($required{$field}) {
+                                       $errs->{$field}{FATAL} = "This is required. Please enter the required fields in this section." 
+                               }
+                               
+                       }
+                       
+           }
+       }
+       undef $errs unless keys %$errs;
+    return ($errs, $validated);        
+}
+
+
+sub _validate {
+       my ($self, $h, $wanted, $updating) = @_;
+       my %required = map { $_ => 1 } @{$wanted->{required}};
+       my %seen;
+       $seen{$_}++ foreach @{$wanted->{ignore}};
+       my $errors      = {}; 
+       my $fields      = {};
+       $wanted->{all} = [ $self->columns ] unless @{$wanted->{all} || [] } ;
+       foreach my $field (@{$wanted->{required}}, @{$wanted->{all}}) {
+               next if $seen{$field}++;
+               my $type = $self->untaint_type($field) or 
+                       do { warn "No untaint type for $self 's field $field. Ignoring.";
+                           next;
+                          };
+               my $value = $h->extract("-as_$type" => $field);
+               my $err = $h->error;
+
+               # Required field error 
+               if ($required{$field} and !ref($value) and $err =~ /^No input for/) {
+                               #($value eq '' or !defined $value)) 
+                       $errors->{$field} = "You must supply '$field'" 
+                       #unless ($updating and$self->field;
+               } elsif ($err) {
+
+                       # 1: No inupt entered
+                       if ($err =~ /^No input for/) 
+                       {
+                               # A : Updating -- set the field to undef or '' 
+                               if ($updating) { 
+                                       $fields->{$field} = eval{$self->column_nullable($field)} ? 
+                                                           undef : ''; 
+                               }
+                               # B : Creating -- dont set a value and RDMS will put default
+                       }
+
+                       # 2: A real untaint error -- just set the error 
+                       elsif ($err !~ /^No parameter for/) 
+                       {
+                               $errors->{$field} =  $err;
+                       }
+               } else {
+                       $fields->{$field} = $value
+               }
+       }
+       undef $errors unless keys %$errors;
+       return ($errors, $fields);
+}
+
+
+
+
+##################
+# _do_create_all #
+##################
+
+# Untaints and Creates objects from hashed params.
+# Returns parent object and errors.  
+sub _do_create_all {
+       my ($self, $validated) = @_;
+       my $class = ref $self  || $self;
+       my ($errors, $accssr); 
+
+       # Separate out related objects' data from main hash 
+       my %related;
+       foreach (keys %$validated) {
+               $related{$_}= delete $validated->{$_} if ref $validated->{$_} eq 'HASH';
+       }
+       # Make has_own/a rel type objects and put id in parent's data hash 
+       foreach $accssr (keys %related) {
+               my $rel_meta = $self->related_meta('r', $accssr); 
+               $self->_croak("No relationship found for $accssr to $class.")
+                       unless $rel_meta;
+               my $rel_type   = $rel_meta->{name};
+               if ($rel_type =~ /(^has_own$|^has_a$)/) {
+                       my $fclass= $rel_meta->{foreign_class};
+                       my ($rel_obj, $errs) = $fclass->_do_create_all($related{$accssr});
+                       # put id in parent's data hash 
+                       if (not keys %$errs) { $validated->{$accssr} = $rel_obj->id; }
+                       else {  $errors->{$accssr} = $errs; }
+                       delete $related{$accssr}; # done with this 
+               }
+       }
+
+       # Make main object -- base case
+       my $me_obj  = eval { $self->insert($validated) };
+       if ($@) { 
+               warn "Just failed making a " . $self. " FATAL Error is $@";  
+               $errors->{FATAL} = $@; 
+               return (undef, $errors);
+       }
+       
+       if (eval{$self->model_debug}) {
+               if ($me_obj) {
+                       warn "Just made a $self : $me_obj ( " . $me_obj->id . ")";
+               }
+               else {
+                       warn "Just failed making a " . $self. " FATAL Error is $@" if not $me_obj;
+               }
+       }
+
+       # Make other related (must_have, might_have, has_many , etc )
+       foreach $accssr ( keys %related )
+       {
+               my ($rel_obj, $errs) = 
+                        $me_obj->_create_related($accssr, $related{$accssr});
+               $errors->{$accssr} = $errs if $errs;
+       }
+
+       undef $errors unless keys %$errors;
+       return ($me_obj, $errors);
+}
+
+
+##################
+# _do_update_all #
+##################
+
+#  Updates objects from hashed untainted data 
+# Returns 1 
+
+sub _do_update_all {
+       my ($self, $validated) = @_;
+       my ($errors, $accssr); 
+
+       #  Separate out related objects' data from main hash 
+       my %related;
+       foreach (keys %$validated) {
+               $related{$_}= delete $validated->{$_} if ref $validated->{$_} eq 'HASH';
+       }
+       # Update main obj 
+       # set does not work with IsA right now so we set each col individually
+       #$self->set(%$validated);
+       my $old = $self->autoupdate(0); 
+       for (keys %$validated) {
+               $self->$_($validated->{$_});
+       }
+       $self->update;
+       $self->autoupdate($old);
+
+       # Update related
+       foreach $accssr (keys %related) {
+               my $fobj = $self->$accssr;
+               my $validated = $related{$accssr};
+               if ($fobj) {
+                       my $old = $fobj->autoupdate(0); 
+                       for (keys %$validated) {
+                               $fobj->$_($validated->{$_});
+                       }
+                       $fobj->update;
+                       $fobj->autoupdate($old);
+               }
+               else { 
+                       $fobj = $self->_create_related($accssr, $related{$accssr});
+               }       
+       }
+       return 1;
+}
+       
+
+###################
+# _create_related #
+###################
+
+# Creates and automatically relates newly created object to calling object 
+# It returns related object and possibly  errors
+
+sub _create_related {
+       # self is object or class, accssr is accssr to relationship, params are 
+       # data for relobject, and created is the array ref to store objs we 
+       # create (optional).
+       my ( $self, $accssr, $params, $created )  = @_;
+       $self->_croak ("Can't make related object without a parent $self object") 
+               unless ref $self;
+       $created      ||= [];
+       my  $rel_meta = $self->related_meta('r',$accssr);
+    if (!$rel_meta) {
+               $self->_croak("No relationship for $accssr in " . ref($self));
+       }
+       my $rel_type  = $rel_meta->{name};
+       my $fclass    = $rel_meta->{foreign_class};
+
+       my ($rel, $errs); 
+       if ($rel_type ne 'has_own' or $rel_type ne 'has_a') {
+               # set up params for might_have, has_many, etc
+               $params->{ $rel_meta->{args}{foreign_column} } = $self->id;
+               %$params = ( %$params, %{$rel_meta->{args}->{constraint} || {}} );
+           ($rel, $errs) =  $fclass->_do_create_all($params, $created);
+       }
+       else { 
+           ($rel, $errs) =  $fclass->_do_create_all($params, $created);
+               unless ($errs) {
+                       $self->$accssr($rel->id);
+                       $self->update;
+               }
+       }
+       return ($rel, $errs);
+}
+
+
+
+               
+
+########################
+# classify_form_inputs #
+########################
+################################################################################
+#   Foreign inputs are inputs that have data for a related table.
+#   We must name them so we can tell which related class they belong to.
+#   This assumes the form : $accessor . $delimeter . $column.
+#   
+#    Example Customer must_have   person which is a
+#       CstmrPrsn which has_a Person;
+#
+#   Customer->must_have('cstmrprsn' => 'CstmrPrsn');
+#   CstmrPrsn->has_own('prsn_id' => 'Person');
+# 
+#      If you say: Customer->to_field('cstmrprsn'); 
+#   AsForm makes inputs for CstmrPrsn which leads to inputs for Person (first
+#   _name, last_name, etc);
+#      We need to keep track that the Person inputs are not related to Customer 
+#      directly but to the CstmrPrsn object which is related to Customer.
+#
+#      Input Names end up like so:
+#                      cstmr_type                                      # Customer column
+#              cstmrprsn__AF__role                                     # CstmrPrsn column
+#              cstmrprsn__AF__person__AF__first_name   # Person column
+#              cstmrprsn__AF__person__AF__last_name    # Person column
+#
+#
+# So our job is to rehash the inputs into a multi level hash keyed on 
+# column or virtual column (accessor) names.
+#
+#
+###############################################################################
+sub classify_form_inputs {
+       my ($self, $params, $delimiter) = @_;
+       my %hashed = ();
+       my $bottom_level;
+       $delimiter ||= $self->foreign_input_delimiter;
+       # Put forminputs in own hashes by accessor (class they belong too)
+       # AsForm makes "$accessor__AF__columnname" form for foeign inputs
+       foreach my $input_name (keys %$params) {
+               my @accssrs  = split /$delimiter/, $input_name;
+               my $col_name = pop @accssrs;    
+               $bottom_level = \%hashed;
+               while ( my $a  = shift @accssrs ) {
+                       $bottom_level->{$a} ||= {};
+                       $bottom_level = $bottom_level->{$a};  # point to bottom level
+               }
+               # now insert parameter at bottom level keyed on col name
+               $bottom_level->{$col_name} = $params->{$input_name};
+       }
+       return  \%hashed;
+}
+
+sub _untaint_handlers {
+    my ($me, $them) = @_;
+    return () unless $them->can('__untaint_types');
+    my %type = %{ $them->__untaint_types || {} };
+    my %h;
+    @h{ @{ $type{$_} } } = ($_) x @{ $type{$_} } foreach keys %type;
+    return %h;
+}
+
+sub _column_type_for {
+    my $type = lc shift;
+    $type =~ s/\(.*//;
+    my %map = (
+        varchar   => 'printable',
+        char      => 'printable',
+        text      => 'printable',
+        tinyint   => 'integer',
+        smallint  => 'integer',
+        mediumint => 'integer',
+        int       => 'integer',
+        bigint    => 'integer',
+        year      => 'integer',
+        date      => 'date',
+    );
+    return $map{$type} || "";
+}
+       
+
+
+
+1;
+
+
diff --git a/t/crud.t b/t/crud.t
new file mode 100755 (executable)
index 0000000..2406ad7
--- /dev/null
+++ b/t/crud.t
@@ -0,0 +1,112 @@
+#!/usr/bin/perl -w
+use Test::More;
+use lib 'ex'; # Where BeerDB should live
+BEGIN {
+    $ENV{BEERDB_DEBUG} = 2;
+
+    eval { require BeerDB };
+    Test::More->import( skip_all =>
+        "SQLite not working or BeerDB module could not be loaded: $@"
+    ) if $@;
+
+    plan tests =>21;
+    
+}
+use Maypole::CLI qw(BeerDB);
+use Maypole::Constants;
+$ENV{MAYPOLE_TEMPLATES} = "t/templates";
+
+isa_ok( (bless {},"BeerDB") , "Maypole");
+
+
+
+# Test create missing required 
+like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit?name=&url=www.sammysmiths.com&notes=Healthy Brew"), qr/name' => 'This field is required/, "Required fields necessary to create ");
+
+# Test create with all  required
+like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit?name=Samuel Smiths&url=www.sammysmiths.com&notes=Healthy Brew"), qr/^# view/, "Created a brewery");
+     
+($brewery,@other) = BeerDB::Brewery->search(name=>'Samuel Smiths'); 
+
+
+SKIP: {
+    skip "Could not create and retrieve Brewery", 8 unless $brewery;
+       like(eval {$brewery->name}, qr/Samuel Smiths/, "Retrieved Brewery, $brewery, we just created");
+
+       #-------- Test updating printable fields ------------------   
+
+    # TEST clearing out  required printable column 
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id."?name="), qr/name' => 'This field is required/, "Required printable field can not be cleared on update");
+
+       # Test cgi update errors hanging around from last request 
+       unlike(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id), qr/name' => 'This field is required/, "cgi_update_errors did not persist"); 
+
+       # Test update no columns 
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id), qr/^# view/, "Updated no columns"); 
+       
+       # Test only updating one non required column
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id."?notes="), qr/^# view/, "Updated a single non required column"); 
+
+       # TEST empty input for non required  printable 
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id."?notes=&name=Sammy Smiths"), qr/^# view/, "Updated brewery" );
+
+       # TEST update actually cleared out a printable field
+       $val  = $brewery->notes ;
+    if ($val eq '') { $val = undef }; 
+       is($val, undef, "Verified non required printable field was cleared");
+
+       # TEST update did not change a field not in parameter list
+       is($brewery->url, 'www.sammysmiths.com', "A field not in parameter list is not updated.");
+};
+
+#-----------------  Test other types of  fields --------------
+
+$style = BeerDB::Style->insert({name => 'Stout', notes => 'Rich, dark, creamy, mmmmmm.'});
+
+# TEST create with integer, date, printable fields
+like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&score=5&notes=Healthy Brew&price=5.00&tasted=2000-12-01"),  qr/^# view/, "Created a beer with date, integer and printable fields");
+
+($beer, @other) = BeerDB::Beer->search(name=>'Oatmeal Stout');
+
+SKIP: {
+       skip "Could not create and retrieve Beer", 7 unless $beer;
+
+       # TEST wiping out an integer field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&score=&notes=Healthy Brew&price=5.00"),  qr/^# view/, "Updated a beer");
+
+       # TEST update actually cleared out a the integer field
+       $val  = $beer->score ;
+    if ($val eq '') { $val = undef }; 
+       is($val, undef, "Verified non required integer field was cleared");
+
+       
+       # TEST invalid integer field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=Stout&price=5.00"),  qr/style' => 'Please provide a valid value/, "Integer field invalid");
+
+       # TEST update with empty  date field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&tasted=&notes=Healthy Brew&price=5.00"),  qr/^# view/, "Updated a beer");
+
+       # TEST update actually cleared out a  date field
+       $tasted = $beer->tasted ;
+    if ($tasted eq '') { $tasted = undef }; 
+       is($tasted, undef, "Verified non required date field was cleared.");
+
+       # TEST invalid date 
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&tasted=baddate&notes=Healthy Brew&price=5.00"),  qr/tasted' => 'Please provide a valid value/, "Date field invalid");
+
+       # TEST  negative value allowed for required field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&price=-5.00"),  qr/^# view/, "Negative values allowed for required field");
+       
+       # TEST negative value actually got stored
+       is($beer->price, '-5.00', "Negative value for required field stored in database") 
+};
+$beer_id = $beer->id;
+$beer->delete;
+
+# TEST delete
+$beer = BeerDB::Beer->retrieve($beer_id);
+is($beer, undef, "Deleted Beer");
+
+$brewery->delete;
+$style->delete;
diff --git a/t/templates/custom/edit b/t/templates/custom/edit
new file mode 100644 (file)
index 0000000..ceee5d2
--- /dev/null
@@ -0,0 +1,11 @@
+# edit
+[%     
+
+       USE  dumper;
+"# errors dump"; 
+       dumper.dump(errors);
+"# parameters dump"; 
+       dumper.dump(request.params);
+%]
+
+# End errors dump 
index c5f9229538ec798aed1a2ab2c085f541ccd9f3db..ab110c62c8a31586d0dc771e73242ee45dec282d 100644 (file)
@@ -1,3 +1,4 @@
+# view 
 # Begin object list
 [% FOR obj = objects %]
 - [% obj.name %]