]> git.decadent.org.uk Git - maypole.git/commitdiff
Merge commit '2.11+2.111' into HEAD
authorBen Hutchings <ben@decadent.org.uk>
Tue, 4 Nov 2008 03:31:30 +0000 (03:31 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Tue, 4 Nov 2008 03:31:30 +0000 (03:31 +0000)
93 files changed:
AUTHORS
Changes
MANIFEST
META.yml
Makefile.PL
README
ex/BeerDB.pm
ex/BeerDB/Base.pm [new file with mode: 0644]
ex/BeerDB/Beer.pm [new file with mode: 0644]
ex/beerdb.sql [new file with mode: 0644]
ex/fancy_example/BeerDB.pm [new file with mode: 0644]
ex/fancy_example/BeerDB/Base.pm [new file with mode: 0644]
ex/fancy_example/BeerDB/Beer.pm [new file with mode: 0644]
ex/fancy_example/BeerDB/Brewery.pm [new file with mode: 0644]
ex/fancy_example/BeerDB/Drinker.pm [new file with mode: 0644]
ex/fancy_example/beerdb.sql [new file with mode: 0644]
ex/fancy_example/templates/custom/addnew [new file with mode: 0644]
ex/fancy_example/templates/custom/display_inputs [new file with mode: 0644]
ex/fancy_example/templates/custom/display_search_inputs [new file with mode: 0644]
ex/fancy_example/templates/custom/edit [new file with mode: 0644]
ex/fancy_example/templates/custom/header [new file with mode: 0644]
ex/fancy_example/templates/custom/maypole.css [new file with mode: 0644]
ex/fancy_example/templates/custom/metadata [new file with mode: 0644]
ex/fancy_example/templates/custom/search_form [new file with mode: 0644]
lib/Apache/MVC.pm
lib/CGI/Maypole.pm
lib/CGI/Untaint/Maypole.pm [new file with mode: 0644]
lib/Maypole.pm
lib/Maypole/Application.pm
lib/Maypole/Config.pm
lib/Maypole/Constants.pm
lib/Maypole/Headers.pm
lib/Maypole/Manual.pod
lib/Maypole/Manual/About.pod
lib/Maypole/Manual/Beer.pod
lib/Maypole/Manual/Cookbook.pod [new file with mode: 0644]
lib/Maypole/Manual/Inheritance.pod [new file with mode: 0644]
lib/Maypole/Manual/Install.pod [new file with mode: 0644]
lib/Maypole/Manual/Model.pod
lib/Maypole/Manual/Plugins.pod
lib/Maypole/Manual/Request.pod [deleted file]
lib/Maypole/Manual/StandardTemplates.pod
lib/Maypole/Manual/View.pod
lib/Maypole/Model/Base.pm
lib/Maypole/Model/CDBI.pm
lib/Maypole/Model/CDBI/AsForm.pm [new file with mode: 0644]
lib/Maypole/Model/CDBI/FromCGI.pm [new file with mode: 0644]
lib/Maypole/Model/CDBI/Plain.pm
lib/Maypole/Session.pm
lib/Maypole/View/Base.pm
lib/Maypole/View/TT.pm
lib/Maypole/templates/beer/addnew [new file with mode: 0644]
lib/Maypole/templates/factory/addnew [new file with mode: 0644]
lib/Maypole/templates/factory/edit [new file with mode: 0644]
lib/Maypole/templates/factory/footer [new file with mode: 0644]
lib/Maypole/templates/factory/frontpage [new file with mode: 0644]
lib/Maypole/templates/factory/header [new file with mode: 0644]
lib/Maypole/templates/factory/list [new file with mode: 0644]
lib/Maypole/templates/factory/login [new file with mode: 0644]
lib/Maypole/templates/factory/macros [new file with mode: 0644]
lib/Maypole/templates/factory/maypole [new file with mode: 0644]
lib/Maypole/templates/factory/maypole.css [new file with mode: 0644]
lib/Maypole/templates/factory/navbar [new file with mode: 0644]
lib/Maypole/templates/factory/pager [new file with mode: 0644]
lib/Maypole/templates/factory/search_form [new file with mode: 0644]
lib/Maypole/templates/factory/title [new file with mode: 0644]
lib/Maypole/templates/factory/view [new file with mode: 0644]
t/01.httpd-basic.t [new file with mode: 0644]
t/01basics.t
t/03podcoverage.t
t/apache_mvc.t
t/cgi_maypole.t
t/constants.t
t/db_colinfo.t [new file with mode: 0755]
t/maypole.t
t/pathtools.t [new file with mode: 0644]
t/templates/custom/view
templates/beer/addnew [deleted file]
templates/factory/addnew [deleted file]
templates/factory/edit [deleted file]
templates/factory/footer [deleted file]
templates/factory/frontpage [deleted file]
templates/factory/header [deleted file]
templates/factory/list [deleted file]
templates/factory/login [deleted file]
templates/factory/macros [deleted file]
templates/factory/maypole [deleted file]
templates/factory/navbar [deleted file]
templates/factory/pager [deleted file]
templates/factory/search_form [deleted file]
templates/factory/title [deleted file]
templates/factory/view [deleted file]
templates/maypole.css [deleted file]

diff --git a/AUTHORS b/AUTHORS
index 36510339de5f13a3a2569c73224129074243c0f9..34ec9499d273106db05c7cd22326cfbd6a5a11d0 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -3,17 +3,20 @@ AUTHORS
 
 The following people have written and documented Maypole:
 
-Simon Cozens - Original Author
-Sebastian Riedel - Maintainer
-Simon Flack - Maintainer
-Aaron Trevena - Maintainer
-Dave Howarth - Contributor
+Simon Cozens - Author Emeritus
+Sebastian Riedel - Maintainer (1.x to 2.x)
+Simon Flack - Maintainer ( 2.x to 2.9 )
+Aaron Trevena - Maintainer (2.10 to present)
+Dave Howorth - Developer
+David Baird - Developer
 
 Thanks also to for fixes and other contributions:
 
 Randal Schwartz
 Jester
-David Baird
-David Howorth
 Marcus Ramberg
 Steven Simms
+Kevin Connor
+Dagfinn Ilmari MannsÃ¥ker
+Danijel Milicevic
+Dave Slack
diff --git a/Changes b/Changes
index 030e9ffd46da0029754f90458543c5e67a1ff186..3f69490a406c9789826ef0bdb2969e43199dc40b 100644 (file)
--- a/Changes
+++ b/Changes
@@ -2,6 +2,126 @@ This file documents the revision history for Perl extension Maypole.
 For information about current developments and future releases, see:
     http://maypole.perl.org/?TheRoadmap
 
+2.111 Sat 21 April 2007
+
+Fixes :
+   Fixed typo in edit form template
+   Fixed extra html filter in link macro in factory templates
+   Fixed typo in _do_update_or_create (bug 26495)
+   fix to display_line macro in factory templates (bug 22920)
+   fixed template path with array refs
+   fixed redirect_request
+   fixed db_colinfo.t test when no mysql
+
+
+2.11 Mon 31 July 2006
+
+SVN revision 519
+
+Deprecated:
+    Directly accessing the attributes of the request object, or the parameters 
+    in $r->params, or anything else, is DEPRECATED and likely to break in future 
+    releases. Be good, and use proper method calls.
+
+    Maypole no longer uses Class::DBI::AsForm, instead Maypole::Model::CDBI::AsForm
+    has replaced it.
+
+    $config->{$table}{required_cols} is deprecated, please use $class->required_columns instead
+
+
+Incompatible API changes:
+    Maypole
+        - is_applicable() deprecated for is_model_applicable(). is_applicable is
+         an alias for is_model_applicable now.
+    Maypole::Constants
+        - ERROR constant now 500, previously -1 (bug #18901)
+    Maypole::Model
+       - delete and search actions are now deprecated - use do_search and do_delete
+         instead
+    Maypole::View
+       - table name rather than moniker is used to find templates
+
+API additions and enhancements:
+    Maypole::Application:
+       - -Init flag (wishlist 14123)
+        - recognises Maypole::HTTPD and installs Maypole::HTTPD::Frontend
+            as its frontend
+    Maypole::Headers:
+       add() alias to push() (wishlist 14142)
+    Maypole:
+        - get_session() method (no-op)
+        - get_user() method (no-op)
+        - get_session() is called during handler_guts() before authenticate()
+        - new preprocess_path() method added and called by parse_path(), 
+               parse_path() will leave any properties set by preprocess_path() in 
+            place
+        - start_request_hook() added
+        - status() attribute added (though only used by start_request_hook() 
+            so far)
+        - setup() split into setup(), setup_model(), and load_model_subclass()
+        - added new path processing methods for ssl and default table/action
+        - added make_path() 
+        - added make_uri()
+       - improved exception handling
+       - now uses File::MMagic::XS to guess mime type of output unless already set
+       - new component method provides Maypole::Component functionality
+       - new object method gets/sets first/only object in objects
+     Maypole::Model
+       - do_delete, do_search in place of delete/search actions
+     Maypole::View::TT:
+       - new report_error method
+       - new embedded error report page in __DATA__
+     Templates:
+        - Improved pager macro/include
+       - Improved factory templates
+        - added the status() attribute, although it's not used in many places 
+            yet
+       - Changed factory edit/view to use object instead of objects
+     Maypole::Model::CDBI
+       - improved error messages in do_edit action
+        - new required_columns mutator method
+        - new column_required accessor method
+
+Bug fixes:
+    Fix to cgi_maypole.t (bug 11346)
+    Fix to TT error reporting (bug 13991)
+    Template xhtml validation (bug 13975)
+    Apache2 fixes in Apache::MVC (bug 13888)
+    Fixed inheritance issues in Mp::Application - Mp::App now manipulates the 
+        caller's @ISA directly, and doesn't inject itself into the chain (bugs 
+        12923 & 14120)
+    Improved Template error reporting (14133)
+    Maypole::Session::generate_unique_id() now returns the id (bug 14124)
+    Moved ar accessor to Apache::MVC (bug 14014)
+    Refactored core to support further development in 2.11 and onwards
+    Fixed related_class() method (bug 14566)
+    Added a cgi() attribute in Maypole::CGI
+    Factory templates now less vulnerable to XSS (bug 16659)
+    Reduced risk of XSS in factory templates (bug 16659)
+    model search/delete methods in model and subclassing the cdbi mode (bug 16661)
+    fixed problems with stringify_self and untaint missing ignore columns (bug 15678)
+    fixed Maypole::Model::CDBI::Plain to JustWork(TM) with plain CDBI Classes (bug 16977)
+    some silent death scenarios resolved
+       Now initializes template_args, stash, parmas, objects and others to correct data type. (bug 15147)
+
+Documentation:
+    Fix to documentation for CGI::Maypole (bug 7263)
+    Simplified Net::Amazon example (bug 14073)
+    Numerous major and minor updates to docs.
+    Renamed Maypole::Manual::Request to Maypole::Manual::Cookbook
+       Added Maypole::Manual::Install, with material removed from 
+           Maypole::Manual::About
+       Added Maypole::Manual::Inheritance
+       Added Maypole::Manual::Terminology
+        - updated Maypole::Manual::View
+        - updated Maypole::View:TT
+    Examples of fancy forms and templates using new features
+
+Requirements:
+   HTTP::Body now required
+   CGI::Untaint >= 1.26 now required rather than >= 0
+
+
 2.10 Tue 19 Jul 2005
     Multiple Template Paths added ( http://rt.cpan.org/NoAuth/Bug.html?id=13447 )
     Small fix to templates/factory/frontpage ( http://rt.cpan.org/NoAuth/Bug.html?id=11236 )
index 6647351a206ed61612a1bcae8cd04bd5e8af4f4d..d5515b9fd6253edc1a6f2e2ef9e84d8e3330e105 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,7 +1,25 @@
 Changes
 ex/BeerDB.pm
+ex/BeerDB/Base.pm
+ex/BeerDB/Beer.pm
+ex/beerdb.sql
+ex/fancy_example/BeerDB.pm
+ex/fancy_example/beerdb.sql
+ex/fancy_example/BeerDB/Base.pm
+ex/fancy_example/BeerDB/Beer.pm
+ex/fancy_example/BeerDB/Brewery.pm
+ex/fancy_example/BeerDB/Drinker.pm
+ex/fancy_example/templates/custom/addnew  
+ex/fancy_example/templates/custom/display_inputs  
+ex/fancy_example/templates/custom/display_search_inputs  
+ex/fancy_example/templates/custom/edit  
+ex/fancy_example/templates/custom/header  
+ex/fancy_example/templates/custom/maypole.css  
+ex/fancy_example/templates/custom/metadata  
+ex/fancy_example/templates/custom/search_form
 lib/Apache/MVC.pm
 lib/CGI/Maypole.pm
+lib/CGI/Untaint/Maypole.pm
 lib/Maypole.pm
 lib/Maypole/Application.pm
 lib/Maypole/CLI.pm
@@ -11,11 +29,13 @@ lib/Maypole/Headers.pm
 lib/Maypole/Session.pm
 lib/Maypole/Manual.pod
 lib/Maypole/Manual/About.pod
+lib/Maypole/Manual/Install.pod
 lib/Maypole/Manual/Beer.pod
 lib/Maypole/Manual/BuySpy.pod
 lib/Maypole/Manual/Flox.pod
 lib/Maypole/Manual/Model.pod
-lib/Maypole/Manual/Request.pod
+lib/Maypole/Manual/Cookbook.pod
+lib/Maypole/Manual/Inheritance.pod
 lib/Maypole/Manual/StandardTemplates.pod
 lib/Maypole/Manual/View.pod
 lib/Maypole/Manual/Workflow.pod
@@ -23,15 +43,18 @@ lib/Maypole/Manual/Plugins.pod
 lib/Maypole/Model/Base.pm
 lib/Maypole/Model/CDBI.pm
 lib/Maypole/Model/CDBI/Plain.pm
+lib/Maypole/Model/CDBI/AsForm.pm
+lib/Maypole/Model/CDBI/FromCGI.pm
 lib/Maypole/View/Base.pm
 lib/Maypole/View/TT.pm
 Makefile.PL
 MANIFEST
 MANIFEST.SKIP
-META.yml                       Module meta-data (added by MakeMaker)
+META.yml
 README
 AUTHORS
 t/01basics.t
+t/01.httpd-basic.t
 t/02pod.t
 t/03podcoverage.t
 t/apache_mvc.t
@@ -39,23 +62,25 @@ t/cgi_maypole.t
 t/constants.t
 t/headers.t
 t/maypole.t
+t/pathtools.t
+t/db_colinfo.t
 t/templates/custom/classdata
 t/templates/custom/frontpage
 t/templates/custom/list
 t/templates/custom/view
-templates/beer/addnew
-templates/factory/addnew
-templates/factory/edit
-templates/factory/footer
-templates/factory/frontpage
-templates/factory/header
-templates/factory/list
-templates/factory/login
-templates/factory/macros
-templates/factory/maypole
-templates/factory/navbar
-templates/factory/pager
-templates/factory/search_form
-templates/factory/title
-templates/factory/view
-templates/maypole.css
+lib/Maypole/templates/beer/addnew
+lib/Maypole/templates/factory/addnew
+lib/Maypole/templates/factory/edit
+lib/Maypole/templates/factory/footer
+lib/Maypole/templates/factory/frontpage
+lib/Maypole/templates/factory/header
+lib/Maypole/templates/factory/list
+lib/Maypole/templates/factory/login
+lib/Maypole/templates/factory/macros
+lib/Maypole/templates/factory/maypole
+lib/Maypole/templates/factory/navbar
+lib/Maypole/templates/factory/pager
+lib/Maypole/templates/factory/search_form
+lib/Maypole/templates/factory/title
+lib/Maypole/templates/factory/view
+lib/Maypole/templates/factory/maypole.css
index 02da7a517a229586fcc2c68adcace8cd6c27ce8b..6fd72b67a3c696ee1f67eea72cb5eae60943e6c8 100644 (file)
--- a/META.yml
+++ b/META.yml
@@ -1,29 +1,34 @@
 # http://module-build.sourceforge.net/META-spec.html
 #XXXXXXX This is a prototype!!!  It will change in the future!!! XXXXX#
 name:         Maypole
-version:      2.10
+version:      2.111
 version_from: lib/Maypole.pm
 installdirs:  site
 requires:
     CGI::Simple:                   0
-    CGI::Untaint:                  0
+    CGI::Untaint:                  1.26
+    CGI::Untaint::date:            0
+    CGI::Untaint::email:           0
     Class::DBI:                    0.96
     Class::DBI::AbstractSearch:    0
-    Class::DBI::AsForm:            2.2
-    Class::DBI::FromCGI:           0.94
     Class::DBI::Loader:            0.02
     Class::DBI::Loader::Relationship: 0
     Class::DBI::Pager:             0
     Class::DBI::Plugin::RetrieveAll: 0
-    Class::DBI::SQLite:            0
+    Class::DBI::Plugin::Type:      0
+    Class::DBI::SQLite:            0.08
     Digest::MD5:                   0
+    File::MMagic::XS:              0.08
+    HTML::Element:                 0
+    HTTP::Body:                    0.5
     HTTP::Headers:                 1.59
     Template:                      0
     Template::Plugin::Class:       0
     Test::MockModule:              0
     UNIVERSAL::moniker:            0
     UNIVERSAL::require:            0
+    URI:                           0
     URI::QueryParam:               0
 
 distribution_type: module
-generated_by: ExtUtils::MakeMaker version 6.17
+generated_by: ExtUtils::MakeMaker version 6.30
index dbb6545947117a6f1ed480cadc5c2e040a84c16a..935c677a0410d2c81b1ad0bd46b23786bcfa8832 100644 (file)
@@ -11,28 +11,33 @@ WriteMakefile(
         Class::DBI::AbstractSearch       => 0,
         Class::DBI::Pager                => 0,
         Class::DBI::Plugin::RetrieveAll  => 0,
-        Class::DBI::AsForm               => 2.2,
-        Class::DBI::FromCGI              => 0.94,
         Class::DBI::Loader::Relationship => 0,
         Class::DBI                       => 0.96,
-        Class::DBI::SQLite               => 0,
-        CGI::Untaint                     => 0,
+        Class::DBI::SQLite               => 0.08,
+        CGI::Untaint                     => 1.26,
+       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::Headers                    => 1.59,
         Template                         => 0,
         Template::Plugin::Class          => 0,
         Test::MockModule                 => 0,
-       Digest::MD5                      => 0,
+        Digest::MD5                     => 0,
+       File::MMagic::XS                 => 0.08,
+       Class::DBI::Plugin::Type         => 0,
     },    # e.g., Module::Name => 1.1
     (
         $] >= 5.005
         ?    ## Add these new keywords supported since 5.005
           (
             ABSTRACT_FROM => 'lib/Maypole.pm',   # retrieve abstract from module
-            AUTHOR => 'Simon flack <simonflk#cpan.org>'
+            AUTHOR => 'Aaron TEEJAY Trevena <aaron@aarontrevena.co.uk>'
           )
         : ()
     ),
@@ -81,7 +86,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),
diff --git a/README b/README
index a8f509c84e5f87cb84862ec99909a7ff0265e4be..c4721a4504bff8098d21430086b7d85909519bea 100644 (file)
--- a/README
+++ b/README
@@ -3,9 +3,9 @@ NAME
 
 DESCRIPTION
     Maypole is a Perl framework for MVC-oriented web applications, similar
-    to Jakarta's Struts. Maypole is designed to minimize coding requirements
-    for creating simple web interfaces to databases, while remaining flexible
-    enough to support enterprise web applications.
+    to Jakarta's Struts or Ruby on Rails. Maypole is designed to minimize 
+    coding requirements for creating simple web interfaces to databases, 
+    while remaining flexible enough to support enterprise web applications.
 
 QUICK START
     Maypole ships with a basic demo application, the Beer Database.
@@ -29,7 +29,7 @@ LINKS
     http://maypole.perl.org - Maypole's home. tips & tricks, mailing list
 
 AUTHOR
-    Maypole is currently maintained by Simon Flack, C<simonflk#cpan.org>
+    Maypole is currently maintained by Aaron Trevena, C<aaron.trevena#gmail.com>
 
 AUTHOR EMERITUS
     Simon Cozens, C<simon#cpan.org>
index 778185a0b7e657336dabe63b8c55a2e7cea3eefa..e0b2894de09ebf11c3428675e8352d60c972262f 100644 (file)
@@ -2,12 +2,13 @@ package BeerDB;
 use Maypole::Application;
 use Class::DBI::Loader::Relationship;
 
-sub debug { $ENV{BEERDB_DEBUG} }
+sub debug { $ENV{BEERDB_DEBUG} || 0 }
 # This is the sample application.  Change this to the path to your
 # database. (or use mysql or something)
 use constant DBI_DRIVER => 'SQLite';
 use constant DATASOURCE => $ENV{BEERDB_DATASOURCE} || 't/beerdb.db';
 
+
 BEGIN {
     my $dbi_driver = DBI_DRIVER;
     if ($dbi_driver =~ /^SQLite/) {
@@ -29,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);
 
@@ -41,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",
diff --git a/ex/BeerDB/Base.pm b/ex/BeerDB/Base.pm
new file mode 100644 (file)
index 0000000..75ed338
--- /dev/null
@@ -0,0 +1,7 @@
+package BeerDB::Base;
+use strict;
+use warnings;
+
+sub floob {}
+
+1;
diff --git a/ex/BeerDB/Beer.pm b/ex/BeerDB/Beer.pm
new file mode 100644 (file)
index 0000000..d7de346
--- /dev/null
@@ -0,0 +1,10 @@
+package BeerDB::Beer;
+use strict;
+use warnings;
+
+# do this to test we get the expected @ISA after setup_model()
+use base 'BeerDB::Base';
+
+sub fooey : Exported {}
+
+1;
diff --git a/ex/beerdb.sql b/ex/beerdb.sql
new file mode 100644 (file)
index 0000000..0c6a0df
--- /dev/null
@@ -0,0 +1,38 @@
+CREATE TABLE style (
+    id integer primary key auto_increment,
+    name varchar(60),
+    notes text
+);
+
+CREATE TABLE pub (
+    id integer primary key auto_increment,
+    name varchar(60),
+    url varchar(120),
+    notes text
+);
+
+CREATE TABLE handpump (
+    id integer primary key auto_increment,
+    beer integer,
+    pub integer
+);
+
+CREATE TABLE beer (
+    id integer primary key auto_increment,
+    brewery integer,
+    style integer,
+    name varchar(30),
+    url varchar(120),
+    score integer(2),
+    price varchar(12),
+    abv varchar(10),
+    notes text,
+       tasted date
+);
+
+CREATE TABLE brewery (
+    id integer  primary key auto_increment,
+    name varchar(30),
+    url varchar(50),
+    notes text
+);
diff --git a/ex/fancy_example/BeerDB.pm b/ex/fancy_example/BeerDB.pm
new file mode 100644 (file)
index 0000000..cb72574
--- /dev/null
@@ -0,0 +1,78 @@
+package BeerDB;
+use Maypole::Application;
+use Class::DBI::Loader::Relationship;
+
+sub debug { $ENV{BEERDB_DEBUG} || 0 }
+# This is the sample application.  Change this to the path to your
+# database. (or use mysql or something)
+use constant DBI_DRIVER => 'SQLite';
+use constant DATASOURCE => '/home/peter/Desktop/maypolebeer/beerdb'; 
+
+BeerDB->config->model('BeerDB::Base'); 
+
+BeerDB->setup("dbi:mysql:beerdb",'root', '');
+
+# Give it a name.
+BeerDB->config->application_name('The Beer Database');
+
+# Change this to the root of the web site for your maypole application.
+BeerDB->config->uri_base( $ENV{BEERDB_BASE} || "http://localhost/beerdb/" );
+
+# Change this to the htdoc root for your maypole application.
+
+my @root=  ('/home/peter/Desktop/maypolebeer/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);
+
+# Let TT templates recursively include  themselves
+BeerDB->config->{view_options} = { RECURSION => 1, };
+
+# Handpumps should not show up.
+BeerDB->config->display_tables([qw[beer brewery pub style drinker pint person]]);
+# Access handpumps if want
+BeerDB->config->ok_tables([ @{BeerDB->config->display_tables}, qw[handpump]]);
+
+BeerDB::Brewery->untaint_columns( printable => [qw/name notes url/] );
+BeerDB::Style->untaint_columns( printable => [qw/name notes/] );
+BeerDB::Beer->untaint_columns(
+    printable => [qw/abv name price notes/],
+    integer => [qw/style brewery score/],
+    date =>[ qw/tasted/],
+);
+BeerDB::Pub->untaint_columns(printable => [qw/name notes url/]);
+BeerDB::Drinker->untaint_columns( printable => [qw/handle created/] );
+BeerDB::Pint->untaint_columns( printable => [qw/date_and_time/]);
+
+
+# Required Fields
+BeerDB->config->{brewery}{required_cols} = [qw/name url/];
+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->{drinker}{required_cols} = [qw/handle person/];
+BeerDB->config->{pint}{required_cols} = [qw/drinker handpump/]; 
+BeerDB->config->{person}{required_cols} = [qw/first_name sur_name dob email/];
+
+# Columns to display 
+sub BeerDB::Handpump::display_columns { qw/pub beer/ }
+
+BeerDB->config->{loader}->relationship($_) for (
+    "a brewery produces beers",
+    "a style defines beers",
+    "a pub has beers on handpumps",
+    "a handpump defines pints",
+    "a drinker drinks pints",);
+
+# For testing classmetadata
+#sub BeerDB::Beer::classdata :Exported {};
+sub BeerDB::Beer::list_columns  { return qw/score name price style brewery/};
+
+sub BeerDB::Handpump::stringify_self { 
+       my $self = shift; 
+       return $self->beer . " @ " . $self->pub;
+}
+
+
+1;
diff --git a/ex/fancy_example/BeerDB/Base.pm b/ex/fancy_example/BeerDB/Base.pm
new file mode 100644 (file)
index 0000000..aaafce1
--- /dev/null
@@ -0,0 +1,51 @@
+package BeerDB::Base;
+use base qw/Maypole::Model::CDBI/;
+use strict;
+use warnings;
+use Data::Dumper;
+
+# Overide list to add display_columns to cgi  
+# Perhaps do this in AsForm?
+sub list : Exported {
+       use Data::Dumper;
+       my ($self, $r) = @_;
+       $self->SUPER::list($r);
+       my %cols =  map { $_ => 1 } $self->columns, $self->display_columns;
+       my @cols = keys %cols;
+       $r->template_args->{classmetadata}{cgi} = { $self->to_cgi(@cols) }; 
+}
+
+# Override view to make inputs and process form to add to related 
+sub view : Exported {
+    my ($self, $r, $obj) = @_;
+    $self->_croak( "Object method only") unless $obj;
+
+    if ($r->params->{submit}) {
+        my @related  = $obj->add_to_from_cgi($r, { required => [$self->related ]});
+        if (my $errs = $obj->cgi_update_errors) {
+            $r->template_args->{errors} = $errs;
+        }
+    }
+
+    # Inputs to add to related on the view page
+       # Now done on the view template 
+       # my %cgi = $self->to_cgi($self->related);
+       #$r->template_args->{classmetadata}{cgi} =  \%cgi ;
+}
+
+
+# Template switcheroo bug bit me -- was seeing view page but the view action was never 
+# being executed after an edit.
+sub do_edit : Exported {
+       my ($self, $r) = (shift, shift);
+       $self->SUPER::do_edit($r, @_);
+       if (my $obj = $r->object) {
+               my $url = $r->config->uri_base . "/" . $r->table . "/view/" . $obj->id;
+               $r->redirect_request(url => $url);
+       }
+}
+
+sub metadata: Exported {}
+       
+
+1;
diff --git a/ex/fancy_example/BeerDB/Beer.pm b/ex/fancy_example/BeerDB/Beer.pm
new file mode 100644 (file)
index 0000000..d7de346
--- /dev/null
@@ -0,0 +1,10 @@
+package BeerDB::Beer;
+use strict;
+use warnings;
+
+# do this to test we get the expected @ISA after setup_model()
+use base 'BeerDB::Base';
+
+sub fooey : Exported {}
+
+1;
diff --git a/ex/fancy_example/BeerDB/Brewery.pm b/ex/fancy_example/BeerDB/Brewery.pm
new file mode 100644 (file)
index 0000000..ad99483
--- /dev/null
@@ -0,0 +1,10 @@
+package BeerDB::Brewery;
+use strict;
+use warnings;
+
+use Data::Dumper;
+
+sub display_columns { qw/name url beers/ } # note has_man beers
+sub list_columns { qw/name url/ } 
+
+1;
diff --git a/ex/fancy_example/BeerDB/Drinker.pm b/ex/fancy_example/BeerDB/Drinker.pm
new file mode 100644 (file)
index 0000000..db798fd
--- /dev/null
@@ -0,0 +1,48 @@
+package BeerDB::Drinker;
+use strict;
+use warnings;
+
+use Data::Dumper;
+
+__PACKAGE__->columns('Stringify' => qw/handle/);
+
+# A drinker is a person but we do not want to select who that person is 
+# from a list because this is a 1:1 relationship rather than a M:1. 
+# The no_select option tells AsForm not to bother making a select box
+
+__PACKAGE__->has_a(person => 'BeerDB::Person', no_select => 1);
+
+# Drinker drinks many beers at pubs if they are lucky. I like to specify the
+# name of the foreign key unless i can control the order that the
+# cdbi classes are created. CDBI does not guess very well the fk column.
+
+#__PACKAGE__->has_many(pints => 'BeerDB::Pint', 'drinker');
+
+# When we create a drinker we want to create a person as well
+# So tell AsForm to display the person inputs too.
+
+sub display_columns { qw/person handle/ }
+sub list_columns { qw/person handle/ }
+# AsForm and templates may check for search_colums when making 
+#sub search_columns { qw/person handle/ }
+
+# We need to tweak the cgi inputs a little. 
+# Since list is where addnew is, override that.
+# Person is a has_a rel and AsForm wont make foreign inputs automatically so
+# we manually do it.
+
+sub list : Exported {
+       my ($self, $r) = @_;
+       $self->SUPER::list($r);
+       my %cgi = $self->to_cgi;
+       $cgi{person} = $self->to_field('person', 'foreign_inputs');
+       $r->template_args->{classmetadata}{cgi} = \%cgi;
+       #$r->template_args->{classmetadata}{search_cgi} = $self->search_inputs;
+}
+
+
+       
+
+#sub foreign_input_delimiter { '__IMODDD__'}
+
+1;
diff --git a/ex/fancy_example/beerdb.sql b/ex/fancy_example/beerdb.sql
new file mode 100644 (file)
index 0000000..6089c94
--- /dev/null
@@ -0,0 +1,67 @@
+CREATE TABLE style (
+    id integer primary key auto_increment,
+    name varchar(60),
+    notes text
+);
+
+CREATE TABLE pub (
+    id integer primary key auto_increment,
+    name varchar(60),
+    url varchar(120),
+    notes text
+);
+
+CREATE TABLE handpump (
+    id integer primary key auto_increment,
+    beer integer,
+    pub integer
+);
+
+CREATE TABLE beer (
+    id integer primary key auto_increment,
+    brewery integer,
+    style integer,
+    name varchar(30),
+    score integer(2),
+    price varchar(12),
+    abv varchar(10),
+    notes text,
+       tasted date
+);
+
+CREATE TABLE brewery (
+    id integer  primary key auto_increment,
+    name varchar(30),
+    url varchar(50),
+    notes text
+);
+
+CREATE TABLE drinker (
+  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
+  person INTEGER UNSIGNED NOT NULL,
+  handle VARCHAR(20) NOT NULL,
+  created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY(id),
+  INDEX drinker_FKIndex1(person)
+);
+
+CREATE TABLE person (
+  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
+  first_name VARCHAR(50) NULL,
+  sur_name VARCHAR(50) NULL,
+  dob DATE NULL,
+  username VARCHAR(20) NULL,
+  password VARCHAR(20) NULL,
+  email VARCHAR(255) NULL,
+  PRIMARY KEY(id)
+);
+
+CREATE TABLE pint (
+  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
+  drinker INTEGER UNSIGNED NOT NULL,
+  handpump INTEGER UNSIGNED NOT NULL,
+  date_and_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY(id)
+);
+
+
diff --git a/ex/fancy_example/templates/custom/addnew b/ex/fancy_example/templates/custom/addnew
new file mode 100644 (file)
index 0000000..7053240
--- /dev/null
@@ -0,0 +1,24 @@
+[%#
+
+=head1 addnew
+
+This is the interface to adding a new instance of an object. (or a new
+row in the database, if you want to look at it that way) It displays a
+form containing a list of HTML components for each of the columns in the
+table.
+
+=cut
+
+#%]
+[% tbl = classmetadata.table; %]
+
+<div id="addnew">
+<form method="post" action="[% base %]/[% classmetadata.table %]/do_edit/">
+<fieldset>
+<legend>Add a new [% config.TABLES.$tbl.singular || tbl | ucfirst | replace('_',' '); %] </legend>
+       [% INCLUDE display_inputs; %]
+    <input type="submit" name="create" value="create" />
+    <input type="hidden" name="__form_id" value="[% request.make_random_id %]" />
+</fieldset>
+</form>
+</div>
diff --git a/ex/fancy_example/templates/custom/display_inputs b/ex/fancy_example/templates/custom/display_inputs
new file mode 100644 (file)
index 0000000..6baf703
--- /dev/null
@@ -0,0 +1,114 @@
+[%# 
+
+=head1 display_inputs
+
+This *RECURSIVELY* displays inputs for a hash of html elements
+
+Vars it needs: 
+   classmetadata-- the hash of bunch of data:
+   cgi -- inputs keyed on column names
+   table -- table inputs are for
+   columns -- list  in order want displayed inputs
+   colnames -- hash of what to label inputs
+   
+errors          -- hash of errors keyed on columns 
+
+
+TODO -- make it recognize a general submit button for redisplaying
+values on errors
+
+=cut
+
+#
+%]
+
+[% # some variables
+   foreign    = []; 
+   names      = [];
+   # get hash of related classes keyed on accessor for Foreign Inputs
+   USE this   = Class(classmetadata.name); 
+   tbl = classmetadata.table;
+   required = { }; 
+   FOR c IN request.config.$tbl.required_cols;
+        required.$c = 1;
+   END;
+
+%]     
+
+[% 
+SET heading_shown = 0; 
+FOR col = classmetadata.columns;  
+       NEXT IF !classmetadata.cgi.$col;
+       NEXT IF col == "id" OR col == classmetadata.table _ "_id";
+       # Display foreign inputs last 
+       IF (mykeys = classmetadata.cgi.$col.keys); 
+                       foreign.push(col);
+                       names.push(classmetadata.colnames.$col);
+                       NEXT;
+       END;
+    IF ! heading_shown; 
+       heading = classmetadata.moniker | ucfirst; 
+               "<h4> $heading </h4>";
+       SET heading_shown = 1; 
+    END;
+%]
+
+[%  # Base case starts here 
+
+       SET elem = classmetadata.cgi.$col; #.clone; # not sure why clone
+       IF elem.type == 'hidden'; 
+               elem.as_XML;
+               NEXT;
+       ELSIF  cgi_params;
+               param_col = col_prefix _ col;
+               IF elem.tag == "textarea";
+                       elem = elem.push_content(cgi_params.$param_col);
+               ELSIF elem.tag == "select";
+                       oldval = set_selected(elem, cgi_params.$col);
+               ELSE;
+                       oldval = elem.attr("value", cgi_params.$param_col);
+               END;
+       END;
+%]
+
+       <label>
+               [% indicator = '';
+           SET indicator = '*' IF (required.$col); 
+        %]
+               <span class="field">
+                   [% indicator _ classmetadata.colnames.$col || 
+                       col | replace('_',' ') | ucfirst  %] 
+               </span>
+               [% elem.as_XML; %]
+       </label>
+
+       [% IF errors.$col %]
+               <span class="error">[% errors.$col | html  %]</span>
+       [% END %]
+[% END; %]
+
+<!-- Display the differnt component inputs --> 
+
+[%     USE this = Class(classmetadata.name); 
+       FOR col IN foreign; 
+               # has_many mapping throws a stick in our spokes because related_class returns the mapped 
+               # class. Sometimes we just want the has_many class. 
+
+        # In case of Pub Handpumps maps to Beer and we want to add Handpump to Pub, we dont 
+               # want the mapped data .
+        # In case of "Create New Handpump" -- we want the mapped data probably so we get
+        # Beer inputs and Pub select box.
+
+               fclass_rel_meta = this.related_meta(request, col);
+        fclass = fclass_rel_meta.foreign_class; # ignor args.mapping
+               fclass_meta = this.get_classmetadata(fclass);
+               fclass_meta.cgi = classmetadata.cgi.$col;
+ #       USE Dumper; Dumper.dump(fclass_meta);
+               INCLUDE display_inputs
+                       col_prefix = col _ "__AF__" _ col_prefix
+                       errors = errors.$col
+                       heading = names.shift
+                       classmetadata = fclass_meta; # localize 
+       END;
+%]
+
diff --git a/ex/fancy_example/templates/custom/display_search_inputs b/ex/fancy_example/templates/custom/display_search_inputs
new file mode 100644 (file)
index 0000000..9985bfb
--- /dev/null
@@ -0,0 +1,63 @@
+[%# 
+
+=head1 display_search_inputs
+
+This displays inputs for search page.  Override in individual class template
+directories as needed. 
+
+Vars it needs: 
+classmetadata-- the hash of inputs keyed on column names
+errors          -- hash of errors keyed on columns 
+=cut
+
+#%]
+
+[% IF errors.FATAL; "FATAL ERROR: "; errors.FATAL; "<br>"; END %]
+
+[%     USE this = Class(classmetadata.name);
+       SET srch_fields = classmetadata.search_columns || 
+                         classmetadata.columns; 
+       SET cgi         = classmetadata.cgi; 
+       SET delimiter = this.foreign_input_delimiter;
+       FOR field IN srch_fields;
+               NEXT IF !cgi.$field;
+               # Recursivly call this tmeplate if we have foreign field 
+        # (hash of foreign inputs should come with it) 
+               IF (  cgi.$field.keys ); 
+               fclass = this.related_class(request, field); 
+                       fclass_meta = this.get_classmetadata(fclass);
+                       fclass_meta.cgi = cgi.$field; 
+                       tbl   = fclass_meta.table;
+                       INCLUDE display_search_inputs
+                               col_prefix    = col _ delimiter _ col_prefix
+                               classmetadata = fclass_meta;
+                       NEXT;
+               END;
+
+       NEXT IF field == 'id' OR field == classmetadata.table _ 'id';
+       SET element = cgi.$field; 
+%]
+
+<label>
+               <span class="field">
+               [% 
+                       classmetadata.colnames.$field || field | ucfirst | replace('_',' '); %]
+               </span>
+               [%      IF element.tag == "select";
+                               # set the previous value 
+                               IF cgi_params.exists(field);
+                                       set_selected(element, cgi_params.$field);
+                               END;
+                                       
+                       END;
+            IF element.tag == "input";  # wipe out any default value
+                         old_val =  element.attr('value', '');
+               END;
+       
+
+               element.as_XML; 
+               %]
+</label>
+[% END; %]
+       
+
diff --git a/ex/fancy_example/templates/custom/edit b/ex/fancy_example/templates/custom/edit
new file mode 100644 (file)
index 0000000..dae8c42
--- /dev/null
@@ -0,0 +1,72 @@
+[%#
+
+=head1 edit
+
+This is the edit page. It edits the passed-in object, by displaying a
+form similar to L<addnew> but with the current values filled in.
+
+=cut
+
+#%]
+[% PROCESS macros %]
+[% INCLUDE header %]
+[% INCLUDE title %]
+
+[% IF request.action == 'edit' %]
+[% INCLUDE navbar %]
+[% END %]
+
+[% IF objects.size %]
+<div id="title">Edit a [% classmetadata.moniker %]</div>
+[% FOR item = objects; %]
+<form action="[% base %]/[% item.table %]/do_edit/[% item.id %]" method="post">
+<fieldset>
+<legend>Edit [% item.name %]</legend>
+[% FOR col = classmetadata.columns;
+    NEXT IF col == "id" OR col == classmetadata.table _ "_id";
+    '<label><span class="field">';
+    classmetadata.colnames.$col || col | ucfirst | replace('_',' '); ":</span>";
+    item.to_field(col).as_XML;
+    "</label>";
+    IF errors.$col; 
+       '<span class="error">'; errors.$col;'</span>';
+    END;
+    END %]
+    <input type="submit" name="edit" value="edit"/>
+    <input type="hidden" name="__form_id" value="[% request.make_random_id %]">
+    </fieldset></form>
+    
+    [% END %]
+[% ELSE %]
+
+<div id="addnew">
+<form method="post" action="[% base %]/[% classmetadata.table %]/do_edit/">
+<fieldset>
+<legend>Add a new [% classmetadata.moniker %]</legend>
+    [% FOR col = classmetadata.columns %]
+        [% NEXT IF col == "id" %]
+            <label><span class="field">[% classmetadata.colnames.$col %]</span>
+            [% 
+            SET elem = classmetadata.cgi.$col.clone;
+            IF request.action == 'do_edit';
+                IF elem.tag == "textarea";
+                    elem = elem.push_content(request.param(col));
+                ELSE;
+                    elem.attr("value", request.param(col));
+                END;
+            END;
+            elem.as_XML; %]
+           </label>
+        [% IF errors.$col %]
+           <span class="error">[% errors.$col | html  %]</span>
+        [% END %]
+
+    [% END; %]
+    <input type="submit" name="create" value="create" />
+    <input type="hidden" name="__form_id" value="[% request.make_random_id %]" />
+</fieldset>
+</form>
+</div>
+
+[% END %]
+[% INCLUDE footer %]
diff --git a/ex/fancy_example/templates/custom/header b/ex/fancy_example/templates/custom/header
new file mode 100644 (file)
index 0000000..c21fff7
--- /dev/null
@@ -0,0 +1,16 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <title>
+            [%
+              title || config.application_name ||
+                "A poorly configured Maypole application"
+            %]
+        </title>
+        <meta http-equiv="Content-Type" content="text/html; charset=[% request.document_encoding %]" />
+       <base href="[% config.uri_base%]"/>
+        <link title="Maypole" href="maypole.css" type="text/css" rel="stylesheet" />
+   </head>
+    <body>
+        <div class="content">
diff --git a/ex/fancy_example/templates/custom/maypole.css b/ex/fancy_example/templates/custom/maypole.css
new file mode 100644 (file)
index 0000000..b13b4f1
--- /dev/null
@@ -0,0 +1,382 @@
+html {
+    padding-right: 0px;
+    padding-left: 0px; 
+    padding-bottom: 0px; 
+    margin: 0px; 
+    padding-top: 0px
+}
+body {
+    font-family: sans-serif;
+    padding-right: 0px; 
+    padding-left: 0px; 
+    padding-bottom: 0px;
+    margin: 0px; padding-top: 0px;
+    background-color: #fff;
+}
+#frontpage_list {
+    position: absolute;
+    z-index: 5;
+    padding: 0px 100px 0px 0px;
+    margin:0 0.5%;     
+    margin-bottom:1em; 
+    margin-top: 1em;
+    background-color: #fff;
+}
+
+#frontpage_list a:hover {
+    background-color: #d0d8e4;
+}
+
+#frontpage_list ul {
+    list-style-type: square;
+}
+
+.content {
+    padding: 12px;
+    margin-top: 1px;  
+    margin-bottom:0px;
+    margin-left: 15px; 
+    margin-right: 15px;
+    border-color: #000000;
+    border-top: 0px;
+    border-bottom: 0px;
+    border-left: 1px;
+    border-right: 1px;
+}
+
+A { 
+    text-decoration: none; 
+    color:#225 
+}
+A:hover { 
+    text-decoration: underline; 
+    color:#222 
+}
+
+#title {
+    z-index: 6;
+    width: 100%;
+    height: 18px;
+    margin-top: 10px;
+    font-size: 90%;
+    border-bottom: 1px solid #ddf;
+    text-align: left;
+}
+
+#subtitle {
+    postion: absolute;
+    z-index: 6;
+    padding: 10px;
+    margin-top: 2em;
+    height: 18px;
+    text-align: left;
+    background-color: #fff;
+}
+
+input[type=text] {
+    height: 16px;
+    width: 136px;
+    font-family: sans-serif;
+    font-size: 11px;
+    color: #2E415A;
+    padding: 0px;
+    margin-bottom: 5px;
+}
+
+input[type=submit] {
+    height: 18px;
+    width: 60px;
+    font-family: sans-serif;
+    font-size: 11px;
+    border: 1px outset;
+    background-color: #fff;
+    padding: 0px 0px 2px 0px;
+    margin-bottom: 5px;
+}
+
+input:hover[type=submit] {
+    color: #fff;
+    background-color: #7d95b5;
+}
+
+textarea {
+    width: 136px;
+    font-family: sans-serif;
+    font-size: 11px;
+    color: #2E415A;
+    padding: 0px;
+    margin-bottom: 5px;
+}
+
+select {
+    height: 16px;
+    width: 140px;
+    font-family: sans-serif;
+    font-size: 12px;
+    color: #202020;
+    padding: 0px;
+    margin-bottom: 5px;
+}
+
+.deco1 {
+    font-size: 0px;
+    z-index:1;
+    border:0px;
+    border-style:solid;
+    border-color:#4d6d99;
+    background-color:#4d6d99;
+}
+
+.deco2 {
+    z-index:2;
+    border:0px;
+    border-style:solid;
+    border-color:#627ea5;
+    background-color:#627ea5;
+}
+
+
+.deco3 {
+    z-index:3;
+    border:0px;
+    border-style:solid;
+    border-color:#7d95b5;
+    background-color:#7d95b5;
+}
+                   
+.deco4 {
+    z-index:4;
+    border:0px;
+    border-style:solid;
+    border-color:#d0d8e4;
+    background-color:#d0d8e4;
+}
+                   
+
+table { 
+    border: 0px solid; 
+    background-color: #ffffff;
+}
+
+#matrix { width: 100%; }
+
+#matrix th {
+    background-color: #b5cadc;
+    border: 1px solid #778;
+    font: bold 12px Verdana, sans-serif;
+}
+
+#matrix #actionth {
+    width: 1px; 
+    padding: 0em 1em 0em 1em;
+}
+
+#matrix tr.alternate { background-color:#e3eaf0; }
+#matrix tr:hover { background-color: #b5cadc; }
+#matrix td { font: 12px Verdana, sans-serif; }
+
+#navlist {
+    padding: 3px 0;
+    margin-left: 0;
+    margin-top:3em;
+    border-bottom: 1px solid #778;
+    font: bold 12px Verdana, sans-serif;
+}
+
+#navlist li {
+    list-style: none;
+    margin: 0;
+    display: inline;
+}
+
+#navlist li a {
+    padding: 3px 0.5em;
+    margin-left: 3px;
+    border: 1px solid #778;
+    border-bottom: none;
+    background: #b5cadc;
+    text-decoration: none;
+}
+
+#navlist li a:link { color: #448; }
+#navlist li a:visited { color: #667; }
+
+#navlist li a:hover {
+    color: #000;
+    background: #eef;
+    border-top: 4px solid #7d95b5;
+    border-color: #227;
+}
+
+#navlist #active a {
+    background: white;
+    border-bottom: 1px solid white;
+    border-top: 4px solid;
+}
+
+td { font: 12px Verdana, sans-serif; }
+
+
+fieldset {
+    margin-top: 1em;
+    padding: 1em;
+    background-color: #f3f6f8;
+    font:80%/1 sans-serif;
+    border:1px solid #ddd;
+}
+
+legend {
+    padding: 0.2em 0.5em;
+    background-color: #fff;
+    border:1px solid #aaa;
+    font-size:90%;
+    text-align:right;
+}
+
+label {
+    display:block;
+}
+
+label.error {
+    display:block;
+       border-color: red;
+       border-width: 1px;
+}
+
+label .field {
+    float:left;
+    width:25%;
+    margin-right:0.5em;
+    padding-top:0.2em;
+    text-align:right;
+    font-weight:bold;
+}
+
+#vlist {
+    padding: 0 1px 1px;
+    margin-left: 0;
+    font: bold 12px Verdana, sans-serif;
+    background: gray;
+    width: 13em;
+}
+
+#vlist li {
+    list-style: none;
+    margin: 0;
+    border-top: 1px solid gray;
+    text-align: left;
+}
+
+#vlist li a {
+    display: block;
+    padding: 0.25em 0.5em 0.25em 0.75em;
+    border-left: 1em solid #7d95b5;
+    background: #d0d8e4;
+    text-decoration: none;
+}
+
+#vlist li a:hover { 
+    border-color: #227;
+}
+
+.view .field {
+    background-color: #f3f6f8;
+    border-left: 1px solid #7695b5;
+    border-top: 1px solid #7695b5;
+    padding: 1px 10px 0px 2px;
+}
+
+#addnew {
+    width: 50%;
+    float: left;
+}
+
+#search {
+    width: 50%;
+    float:right;
+}
+
+.error { color: #d00; }
+
+.action {
+    border: 1px outset #7d95b5;
+    style:block;
+}
+
+.action:hover {
+    color: #fff;
+    text-decoration: none;
+    background-color: #7d95b5;
+}
+
+.actionform {
+    display: inline;
+}
+
+.actionbutton {
+    height: 16px;
+    width: 40px;
+    font-family: sans-serif;
+    font-size: 10px;
+    border: 1px outset;
+    background-color: #fff;
+    margin-bottom: 0px;
+}
+
+.actionbutton:hover {
+    color: #fff;
+    background-color: #7d95b5;
+}
+
+.actions {
+    white-space: nowrap;
+}
+
+.field {
+    display:inline;
+}
+
+#login { width: 400px; }
+
+#login input[type=text] { width: 150px; }
+#login input[type=password] { width: 150px; }
+
+.pager {
+    font: 11px Arial, Helvetica, sans-serif;
+    text-align: center;
+    border: solid 1px #e2e2e2;
+    border-left: 0;
+    border-right: 0;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    margin: 0px;
+    background-color: #f3f6f8;
+}
+
+.pager a {
+    padding: 2px 6px;
+    border: solid 1px #ddd;
+    background: #fff;
+    text-decoration: none;
+}
+
+.pager a:visited {
+    padding: 2px 6px;
+    border: solid 1px #ddd;
+    background: #fff;
+    text-decoration: none;
+}
+
+.pager .current-page {
+    padding: 2px 6px;
+    font-weight: bold;
+    vertical-align: top;
+}
+
+.pager a:hover {
+    color: #fff;
+    background: #7d95b5;
+    border-color: #036;
+    text-decoration: none;
+}
+
diff --git a/ex/fancy_example/templates/custom/metadata b/ex/fancy_example/templates/custom/metadata
new file mode 100644 (file)
index 0000000..e15fb6a
--- /dev/null
@@ -0,0 +1,5 @@
+<h3> Class::DBI meta info for [% classmetadata.name %] </h3> 
+[%
+   USE this = Class(classmetadata.name);
+   USE Dumper; Dumper.dump(this.meta_info);
+%]
diff --git a/ex/fancy_example/templates/custom/search_form b/ex/fancy_example/templates/custom/search_form
new file mode 100644 (file)
index 0000000..5d540fb
--- /dev/null
@@ -0,0 +1,9 @@
+<div id="search">
+<form method="get" action="[% base %]/[% classmetadata.table %]/search/">
+<fieldset>
+<legend>Search</legend>
+    [% INCLUDE display_search_inputs; %] 
+    <input type="submit" name="search" value="search"/>
+</fieldset>
+</form>
+</div>
index c7bfdef74a2ffd6ba17a89279627baf507eae320..4d32dc43f6d260048ae47e5e41eb719728c8d9ac 100644 (file)
@@ -1,95 +1,46 @@
 package Apache::MVC;
 
-our $VERSION = '2.09';
+our $VERSION = '2.11';
 
 use strict;
 use warnings;
 
+use URI;
+use URI::QueryParam;
+
 use base 'Maypole';
-use mod_perl;
 use Maypole::Headers;
-
-use constant APACHE2 => $mod_perl::VERSION >= 1.99;
-
-if (APACHE2) {
-    require Apache2;
-    require Apache::RequestIO;
-    require Apache::RequestRec;
-    require Apache::RequestUtil;
-    require APR::URI;
-}
-else { require Apache }
-require Apache::Request;
-
-sub get_request {
-    my ( $self, $r ) = @_;
-    $self->{ar} = Apache::Request->new($r);
-}
-
-sub parse_location {
-    my $self = shift;
-
-    # Reconstruct the request headers
-    $self->headers_in(Maypole::Headers->new);
-    my %headers;
-    if (APACHE2) { %headers = %{$self->{ar}->headers_in};
-    } else { %headers = $self->{ar}->headers_in; }
-    for (keys %headers) {
-        $self->headers_in->set($_, $headers{$_});
-    }
-
-    $self->{path} = $self->{ar}->uri;
-    my $loc = $self->{ar}->location;
-    no warnings 'uninitialized';
-    $self->{path} .= '/' if $self->{path} eq $loc;
-    $self->{path} =~ s/^($loc)?\///;
-    $self->parse_path;
-    $self->parse_args;
-}
-
-sub parse_args {
-    my $self = shift;
-    $self->{params} = { $self->_mod_perl_args( $self->{ar} ) };
-    $self->{query}  = { $self->_mod_perl_args( $self->{ar} ) };
-}
-
-sub send_output {
-    my $r = shift;
-    $r->{ar}->content_type(
-          $r->{content_type} =~ m/^text/
-        ? $r->{content_type} . "; charset=" . $r->{document_encoding}
-        : $r->{content_type}
-    );
-    $r->{ar}->headers_out->set(
-        "Content-Length" => do { use bytes; length $r->{output} }
-    );
-
-    foreach ($r->headers_out->field_names) {
-        next if /^Content-(Type|Length)/;
-        $r->{ar}->headers_out->set($_ => $r->headers_out->get($_));
+use Maypole::Constants;
+
+__PACKAGE__->mk_accessors( qw( ar ) );
+
+our $MODPERL2;
+our $modperl_version;
+
+BEGIN {
+    $MODPERL2  = ( exists $ENV{MOD_PERL_API_VERSION} and
+                        $ENV{MOD_PERL_API_VERSION} >= 2 );
+    if ($MODPERL2) {
+     eval 'use mod_perl2; $modperl_version = $mod_perl2::VERSION;';
+     if ($@) {
+      $modperl_version = $Apache2::RequestRec::VERSION;
+     }
+     require Apache2::RequestIO;
+     require Apache2::RequestRec;
+     require Apache2::RequestUtil;
+     eval 'use Apache2::Const -compile => qw/REDIRECT/;'; # -compile 4 no import
+     require APR::URI;
+     require HTTP::Body;
+    } else {
+     eval ' use mod_perl; ';
+     require Apache;
+     require Apache::Request;
+     eval 'use Apache::Constants -compile => qw/REDIRECT/;';
+     $modperl_version = 1;
     }
 
-    APACHE2 || $r->{ar}->send_http_header;
-    $r->{ar}->print( $r->{output} );
 }
 
-sub get_template_root {
-    my $r = shift;
-    $r->{ar}->document_root . "/" . $r->{ar}->location;
-}
-
-sub _mod_perl_args {
-    my ( $self, $apr ) = @_;
-    my %args;
-    foreach my $key ( $apr->param ) {
-        my @values = $apr->param($key);
-        $args{$key} = @values == 1 ? $values[0] : \@values;
-    }
-    return %args;
-}
-
-1;
-
 =head1 NAME
 
 Apache::MVC - Apache front-end to Maypole
@@ -97,15 +48,7 @@ Apache::MVC - Apache front-end to Maypole
 =head1 SYNOPSIS
 
     package BeerDB;
-    use base 'Apache::MVC';
-    BeerDB->setup("dbi:mysql:beerdb");
-    BeerDB->config->uri_base("http://your.site/");
-    BeerDB->config->display_tables([qw[beer brewery pub style]]);
-    # Now set up your database:
-    # has-a relationships
-    # untaint columns
-
-    1;
+    use Maypole::Application;
 
 =head1 DESCRIPTION
 
@@ -115,7 +58,7 @@ L<Maypole::Application>.
 
 =head1 INSTALLATION
 
-Create a driver module like the one above.
+Create a driver module like the one illustrated in L<Maypole::Application>.
 
 Put the following in your Apache config:
 
@@ -124,15 +67,13 @@ Put the following in your Apache config:
         PerlHandler BeerDB
     </Location>
 
-Copy the templates found in F<templates/factory> into the
-F<beer/factory> directory off the web root. When the designers get
-back to you with custom templates, they are to go in
-F<beer/custom>. If you need to do override templates on a
-database-table-by-table basis, put the new template in
-F<beer/I<table>>. 
+Copy the templates found in F<templates/factory> into the F<beer/factory>
+directory off the web root. When the designers get back to you with custom
+templates, they are to go in F<beer/custom>. If you need to override templates
+on a database-table-by-table basis, put the new template in F<beer/I<table>>.
 
-This will automatically give you C<add>, C<edit>, C<list>, C<view> and
-C<delete> commands; for instance, a list of breweries, go to 
+This will automatically give you C<add>, C<edit>, C<list>, C<view> and C<delete>
+commands; for instance, to see a list of breweries, go to
 
     http://your.site/beer/brewery/list
 
@@ -141,31 +82,210 @@ see L<Maypole>.
 
 =head1 Implementation
 
-This class overrides a set of methods in the base Maypole class to provide it's
+This class overrides a set of methods in the base Maypole class to provide its
 functionality. See L<Maypole> for these:
 
 =over
 
 =item get_request
 
-=item get_template_root
+=cut
 
-=item parse_args
+sub get_request {
+    my ($self, $r) = @_;
+    my $ar;
+    if ($MODPERL2) {
+       $ar = eval {require Apache2::Request} ? Apache2::Request->new($r) : $r;
+       }
+    else { $ar = Apache::Request->instance($r); }
+    $self->ar($ar);
+}
 
 =item parse_location
 
+=cut
+
+sub parse_location {
+    my $self = shift;
+
+    # Reconstruct the request headers
+    $self->headers_in(Maypole::Headers->new);
+    my %headers;
+    if ($MODPERL2) { %headers = %{$self->ar->headers_in};
+    } else { %headers = $self->ar->headers_in; }
+    for (keys %headers) {
+        $self->headers_in->set($_, $headers{$_});
+    }
+    my $path = $self->ar->uri;
+    my $loc  = $self->ar->location;
+    {
+        no warnings 'uninitialized';
+        $path .= '/' if $path eq $loc;
+        $path =~ s/^($loc)?\///;
+    }
+    $self->path($path);
+    $self->parse_path;
+    $self->parse_args;
+}
+
+=item parse_args
+
+=cut
+
+sub parse_args {
+    my $self = shift;
+    $self->params( { $self->_mod_perl_args( $self->ar ) } );
+    $self->query( $self->params );
+}
+
+=item redirect_request
+
+=cut
+
+sub redirect_request {
+  my $r = shift;
+  my $redirect_url = $_[0];
+  my $status = $MODPERL2 ? eval 'Apache2::Const::REDIRECT;' :
+          eval 'Apache::Constants::REDIRECT;'; # why have to eval this?
+  if ($_[1]) {
+    my %args = @_;
+    if ($args{url}) {
+      $redirect_url = $args{url};
+    } else {
+      my $path = $args{path} || $r->path;
+      my $host = $args{domain} || $r->ar->hostname;
+      my $protocol = $args{protocol} || $r->get_protocol;
+
+      $redirect_url = URI->new;
+         $redirect_url->scheme($protocol);
+         $redirect_url->host($host);
+         $redirect_url->path($path);
+    }
+    $status = $args{status} if ($args{status});
+  }
+
+  $r->ar->status($status);
+  $r->ar->headers_out->set('Location' => $redirect_url);
+  return OK;
+}
+
+=item get_protocol
+
+=cut
+
+sub get_protocol {
+  my $self = shift;
+  my $protocol = ( $self->ar->protocol =~ m/https/i ) ? 'https' : 'http' ;
+  return $protocol;
+}
+
 =item send_output
 
+=cut
+
+sub send_output {
+    my $r = shift;
+    $r->ar->content_type(
+          $r->content_type =~ m/^text/
+        ? $r->content_type . "; charset=" . $r->document_encoding
+        : $r->content_type
+    );
+    $r->ar->headers_out->set(
+        "Content-Length" => do { use bytes; length $r->output }
+    );
+
+    foreach ($r->headers_out->field_names) {
+        next if /^Content-(Type|Length)/;
+        $r->ar->headers_out->set($_ => $r->headers_out->get($_));
+    }
+
+    $MODPERL2 || $r->ar->send_http_header;
+    $r->ar->print( $r->output );
+}
+
+=item get_template_root
+
+=cut
+
+sub get_template_root {
+    my $r = shift;
+    $r->ar->document_root . "/" . $r->ar->location;
+}
+
 =back
 
+=cut
+
+#########################################################
+# private / internal methods and subs
+
+
+sub _mod_perl_args {
+    my ( $self, $apr ) = @_;
+    my %args;
+    if ($apr->isa('Apache::Request')) {
+      foreach my $key ( $apr->param ) {
+        my @values = $apr->param($key);
+        $args{$key} = @values == 1 ? $values[0] : \@values;
+      }
+    } else {
+      my $body = $self->_prepare_body($apr);
+      %args = %{$body->param};
+      my $uri = URI->new($self->ar->unparsed_uri);
+      foreach my $key ($uri->query_param) {
+       if (ref $args{$key}) {
+         push (@{$args{$key}}, $uri->query_param($key));
+       } else {
+         if ($args{$key}) {
+           $args{$key} = [ $args{$key}, $uri->query_param($key) ];
+         } else {
+           my @args = $uri->query_param($key);
+           if (scalar @args > 1) {
+             $args{$key} = [ $uri->query_param($key) ];
+           } else {
+             $args{$key} = $uri->query_param($key);
+           }
+         }
+       }
+      }
+    }
+    return %args;
+}
+
+sub _prepare_body {
+    my ( $self, $r ) = @_;
+
+    unless ($self->{__http_body}) {
+        my $content_type   = $r->headers_in->get('Content-Type');
+        my $content_length = $r->headers_in->get('Content-Length');
+        my $body   = HTTP::Body->new( $content_type, $content_length );
+        my $length = $content_length;
+        while ( $length ) {
+            $r->read( my $buffer, ( $length < 8192 ) ? $length : 8192 );
+            $length -= length($buffer);
+            $body->add($buffer);
+        }
+       $self->{__http_body} = $body;
+    }
+    return $self->{__http_body};
+}
+
+
+
 =head1 AUTHOR
 
 Simon Cozens, C<simon@cpan.org>
+
+=head1 CREDITS
+
+Aaron Trevena
 Marcus Ramberg, C<marcus@thefeed.no>
-Screwed up by Sebastian Riedel, C<sri@oook.de>
+Sebastian Riedel, C<sri@oook.de>
 
 =head1 LICENSE
 
 You may distribute this code under the same terms as Perl itself.
 
 =cut
+
+1;
index f8c122962502a7d5c4a9454009e8bf4354050196..b8a0a48d3d40d6351f00d4533c0ca16e2953ab59 100644 (file)
@@ -5,73 +5,11 @@ use strict;
 use warnings;
 use CGI::Simple;
 use Maypole::Headers;
+use Maypole::Constants;
 
-our $VERSION = '2.09';
+our $VERSION = '2.11';
 
-sub run {
-    my $self = shift;
-    return $self->handler();
-}
-
-sub get_request {
-    shift->{cgi} = CGI::Simple->new();
-}
-
-
-sub parse_location {
-    my $self = shift;
-    my $cgi = $self->{cgi};
-
-    # Reconstruct the request headers (as far as this is possible)
-    $self->headers_in(Maypole::Headers->new);
-    for my $http_header ($cgi->http) {
-        (my $field_name = $http_header) =~ s/^HTTPS?_//;
-        $self->headers_in->set($field_name => $cgi->http($http_header));
-    }
-
-    $self->{path} = $cgi->url( -absolute => 1, -path_info => 1 );
-    my $loc = $cgi->url( -absolute => 1 );
-    no warnings 'uninitialized';
-    $self->{path} .= '/' if $self->{path} eq $loc;
-    $self->{path} =~ s/^($loc)?\///;
-    $self->parse_path;
-    $self->parse_args;
-}
-
-sub parse_args {
-    my $self = shift;
-    my (%vars) = $self->{cgi}->Vars;
-    while ( my ( $key, $value ) = each %vars ) {
-        my @values = split "\0", $value;
-        $vars{$key} = @values <= 1 ? $values[0] : \@values;
-    }
-    $self->{params} = {%vars};
-    $self->{query}  = {%vars};
-}
-
-sub send_output {
-    my $r = shift;
-
-    # Collect HTTP headers
-    my %headers = (
-        -type            => $r->{content_type},
-        -charset         => $r->{document_encoding},
-        -content_length  => do { use bytes; length $r->{output} },
-    );
-    foreach ($r->headers_out->field_names) {
-        next if /^Content-(Type|Length)/;
-        $headers{"-$_"} = $r->headers_out->get($_);
-    }
-
-    print $r->{cgi}->header(%headers), $r->{output};
-}
-
-sub get_template_root {
-    my $r = shift;
-    $r->{cgi}->document_root . "/" . $r->{cgi}->url( -relative => 1 );
-}
-
-1;
+__PACKAGE__->mk_accessors( qw/cgi/ );
 
 =head1 NAME
 
@@ -80,16 +18,7 @@ CGI::Maypole - CGI-based front-end to Maypole
 =head1 SYNOPSIS
 
      package BeerDB;
-     use base 'CGI::Maypole';
-     BeerDB->setup("dbi:mysql:beerdb");
-     BeerDB->config->uri_base("http://your.site/cgi-bin/beer.cgi/");
-     BeerDB->config->display_tables([qw[beer brewery pub style]]);
-     BeerDB->config->template_root("/var/www/beerdb/");
-     # Now set up your database:
-     # has-a relationships
-     # untaint columns
-
-     1;
+     use Maypole::Application;
 
      ## example beer.cgi:
 
@@ -101,12 +30,17 @@ CGI::Maypole - CGI-based front-end to Maypole
 Now to access the beer database, type this URL into your browser:
 http://your.site/cgi-bin/beer.cgi/frontpage
 
+NOTE: this Maypole frontend requires additional modules that won't be installed
+or included with Maypole. Please see below.
+
 =head1 DESCRIPTION
 
 This is a CGI platform driver for Maypole. Your application can inherit from
 CGI::Maypole directly, but it is recommended that you use
 L<Maypole::Application>.
 
+This module requires CGI::Simple which you will have to install yourself via
+CPAN or manually.
 
 =head1 METHODS
 
@@ -118,6 +52,14 @@ Call this from your CGI script to start the Maypole application.
 
 =back
 
+=cut
+
+sub run 
+{
+    my $self = shift;
+    return $self->handler;
+}
+
 =head1 Implementation
 
 This class overrides a set of methods in the base Maypole class to provide it's
@@ -127,16 +69,155 @@ functionality. See L<Maypole> for these:
 
 =item get_request
 
-=item get_template_root
+=cut
 
-=item parse_args
+sub get_request 
+{
+    shift->cgi( CGI::Simple->new );
+}
 
 =item parse_location
 
+=cut
+
+sub parse_location 
+{
+    my $r = shift;
+    my $cgi = $r->cgi;
+
+    # Reconstruct the request headers (as far as this is possible)
+    $r->headers_in(Maypole::Headers->new);
+    for my $http_header ($cgi->http) {
+        (my $field_name = $http_header) =~ s/^HTTPS?_//;
+        $r->headers_in->set($field_name => $cgi->http($http_header));
+    }
+
+    my $path = $cgi->url( -absolute => 1, -path_info => 1 );
+    my $loc = $cgi->url( -absolute => 1 );
+    {
+        no warnings 'uninitialized';
+        $path .= '/' if $path eq $loc;
+        $path =~ s/^($loc)?\///;
+    }
+    $r->path($path);
+    
+    $r->parse_path;
+    $r->parse_args;
+}
+
+=item parse_args
+
+=cut
+
+sub parse_args 
+{
+    my $r = shift;
+    my (%vars) = $r->cgi->Vars;
+    while ( my ( $key, $value ) = each %vars ) {
+        my @values = split "\0", $value;
+        $vars{$key} = @values <= 1 ? $values[0] : \@values;
+    }
+    $r->params( {%vars} );
+    $r->query( $r->params );
+}
+
+=item redirect_request
+
+=cut
+
+# FIXME: use headers_in to gather host and other information?
+sub redirect_request 
+{
+  my $r = shift;
+  my $redirect_url = $_[0];
+  my $status = "302";
+  if ($_[1]) {
+    my %args = @_;
+    if ($args{url}) {
+      $redirect_url = $args{url};
+    } else {
+      my $path = $args{path} || $r->cgi->url(-absolute => 1, -query=>1);
+      my $host = $args{domain};
+      ($host = $r->cgi->url(-base => 1)) =~ s/^https?:\/\///i unless ($host);
+      my $protocol = $args{protocol} || $r->get_protocol;
+      $redirect_url = "${protocol}://${host}/${path}";
+    }
+    $status = $args{status} if ($args{status});
+  }
+
+  $r->headers_out->set('Status' => $status);
+  $r->headers_out->set('Location' => $redirect_url);
+
+  return;
+}
+
+=item get_protocol
+
+=cut
+
+sub get_protocol 
+{
+  my $self = shift;
+  my $protocol = ($self->cgi->https) ? 'https' : 'http';
+  return $protocol;
+}
+
 =item send_output
 
+Generates output (using C<collect_output>) and prints it. 
+
+=cut
+
+sub send_output 
+{
+    my $r = shift;
+    print $r->collect_output;
+}
+
+=item collect_output
+
+Gathers headers and output together into a string and returns it.
+
+Splitting this code out of C<send_output> supports L<Maypole::HTTPD::Frontend>.
+
+=cut
+
+sub collect_output
+{
+    my $r = shift;
+    
+    # Collect HTTP headers
+    my %headers = (
+        -type            => $r->content_type,
+        -charset         => $r->document_encoding,
+        -content_length  => do { use bytes; length $r->output },
+    );
+    foreach ($r->headers_out->field_names) {
+        next if /^Content-(Type|Length)/;
+        $headers{"-$_"} = $r->headers_out->get($_);
+    }
+
+    return $r->cgi->header(%headers) . $r->output;
+}
+
+=item get_template_root
+
+=cut
+
+sub get_template_root {
+    my $r = shift;
+    $r->cgi->document_root . "/" . $r->cgi->url( -relative => 1 );
+}
+
+1;
+
+
 =back
 
+=head1 DEPENDANCIES
+
+CGI::Simple
+
 =head1 AUTHORS
 
 Dave Ranney C<dave@sialia.com>
diff --git a/lib/CGI/Untaint/Maypole.pm b/lib/CGI/Untaint/Maypole.pm
new file mode 100644 (file)
index 0000000..38321ef
--- /dev/null
@@ -0,0 +1,129 @@
+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
+
+=head2 raw_data
+
+Returns the parameters the handler was created with as a hashref
+
+=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 7854caf42f02b4d297f70df6ea8be5f556b4afab..211bd056a007d4505351bee182ccb941ce2cace4 100644 (file)
@@ -3,48 +3,363 @@ use base qw(Class::Accessor::Fast Class::Data::Inheritable);
 use UNIVERSAL::require;
 use strict;
 use warnings;
+use Data::Dumper;
 use Maypole::Config;
 use Maypole::Constants;
 use Maypole::Headers;
+use URI();
+use URI::QueryParam;
+use NEXT;
+use File::MMagic::XS qw(:compat);
 
-our $VERSION = '2.10';
+our $VERSION = '2.111';
+our $mmagic = File::MMagic::XS->new();
+
+# proposed privacy conventions:
+# - no leading underscore     - public to custom application code and plugins
+# - single leading underscore - private to the main Maypole stack - *not*
+#     including plugins
+# - double leading underscore - private to the current package
+
+=head1 NAME
+
+Maypole - MVC web application framework
+
+=head1 SYNOPSIS
+
+The canonical example used in the Maypole documentation is the beer database:
+
+    package BeerDB;
+    use strict;
+    use warnings; 
+    
+    # choose a frontend, initialise the config object, and load a plugin
+    use Maypole::Application qw/Relationship/;
+
+    # set everything up
+    __PACKAGE__->setup("dbi:SQLite:t/beerdb.db");
+    
+    # get the empty config object created by Maypole::Application
+    my $config = __PACKAGE__->config;
+    
+    # basic settings
+    $config->uri_base("http://localhost/beerdb");
+    $config->template_root("/path/to/templates");
+    $config->rows_per_page(10);
+    $config->display_tables([qw/beer brewery pub style/]);
+
+    # table relationships
+    $config->relationships([
+        "a brewery produces beers",
+        "a style defines beers",
+        "a pub has beers on handpumps",
+        ]);
+        
+    # validation
+    BeerDB::Brewery->untaint_columns( printable => [qw/name notes url/] );
+    BeerDB::Pub->untaint_columns( printable => [qw/name notes url/] );
+    BeerDB::Style->untaint_columns( printable => [qw/name notes/] );
+    BeerDB::Beer->untaint_columns(
+        printable => [qw/abv name price notes/],
+        integer => [qw/style brewery score/],
+        date => [ qw/date/],
+    );
+
+    # note : set up model before calling this method
+    BeerDB::Beer->required_columns([qw/name/]); 
+
+    1;    
+
+=head1 DESCRIPTION
+
+This documents the Maypole request object. See the L<Maypole::Manual>, for a
+detailed guide to using Maypole.
+
+Maypole is a Perl web application framework similar to Java's struts. It is 
+essentially completely abstracted, and so doesn't know anything about
+how to talk to the outside world.
+
+To use it, you need to create a driver package which represents your entire
+application. This is the C<BeerDB> package used as an example in the manual.
+
+This needs to first use L<Maypole::Application> which will make your package
+inherit from the appropriate platform driver such as C<Apache::MVC> or
+C<CGI::Maypole>. Then, the driver calls C<setup>. This sets up the model classes
+and configures your application. The default model class for Maypole uses
+L<Class::DBI> to map a database to classes, but this can be changed by altering
+configuration (B<before> calling setup.)
+
+
+=head1 DOCUMENTATION AND SUPPORT
+
+Note that some details in some of these resources may be out of date.
+
+=over 4
+
+=item The Maypole Manual
+
+The primary documentation is the Maypole manual. This lives in the 
+C<Maypole::Manual> pod documents included with the distribution. 
+
+=item Embedded POD
+
+Individual packages within the distribution contain (more or less) detailed
+reference documentation for their API.
+
+=item Mailing lists
+
+There are two mailing lists - maypole-devel and maypole-users - see
+http://maypole.perl.org/?MailingList
+
+=item The Maypole Wiki
+
+The Maypole wiki provides a useful store of extra documentation -
+http://maypole.perl.org
+
+In particular, there's a FAQ (http://maypole.perl.org/?FAQ) and a cookbook
+(http://maypole.perl.org/?Cookbook). Again, certain information on these pages
+may be out of date.
+
+=item Web applications with Maypole
+
+A tutorial written by Simon Cozens for YAPC::EU 2005 -
+http://www.aarontrevena.co.uk/opensource/maypole/maypole-tutorial.pdf [228KB].
+
+=item A Database-Driven Web Application in 18 Lines of Code
+
+By Paul Barry, published in Linux Journal, March 2005.
+
+http://www.linuxjournal.com/article/7937
+
+"From zero to Web-based database application in eight easy steps".
+
+Maypole won a 2005 Linux Journal Editor's Choice Award
+(http://www.linuxjournal.com/article/8293) after featuring in this article. 
+
+=item Build Web apps with Maypole
+
+By Simon Cozens, on IBM's DeveloperWorks website, May 2004.
+
+http://www-128.ibm.com/developerworks/linux/library/l-maypole/
+
+=item Rapid Web Application Deployment with Maypole
+
+By Simon Cozens, on O'Reilly's Perl website, April 2004.
+
+http://www.perl.com/pub/a/2004/04/15/maypole.html
+
+=item Authentication
+
+Some notes written by Simon Cozens. A little bit out of date, but still 
+very useful: http://www.aarontrevena.co.uk/opensource/maypole/authentication.html
+
+=item CheatSheet
+
+There's a refcard for the Maypole (and Class::DBI) APIs on the wiki -
+http://maypole.perl.org/?CheatSheet. Probably a little out of date now - it's a
+wiki, so feel free to fix any errors!
+
+=item Plugins and add-ons
+
+There are a large and growing number of plugins and other add-on modules
+available on CPAN - http://search.cpan.org/search?query=maypole&mode=module
+
+=item del.icio.us
+
+You can find a range of useful Maypole links, particularly to several thoughtful
+blog entries, starting here: http://del.icio.us/search/?all=maypole
+
+=item CPAN ratings
+
+There are a couple of short reviews here:
+http://cpanratings.perl.org/dist/Maypole
+
+=back
+
+=cut
+
+__PACKAGE__->mk_classdata($_) for qw( config init_done view_object model_classes_loaded);
 
-__PACKAGE__->mk_classdata($_) for qw( config init_done view_object );
 __PACKAGE__->mk_accessors(
-    qw( ar params query objects model_class template_args output path
+    qw( params query objects model_class template_args output path
         args action template error document_encoding content_type table
-        headers_in headers_out )
+        headers_in headers_out stash status parent)
 );
+
 __PACKAGE__->config( Maypole::Config->new() );
+
 __PACKAGE__->init_done(0);
 
+__PACKAGE__->model_classes_loaded(0);
+
+=head1 HOOKABLE METHODS
+
+As a framework, Maypole provides a number of B<hooks> - methods that are
+intended to be overridden. Some of these methods come with useful default
+behaviour, others do nothing by default. Hooks include:
+
+    Class methods
+    -------------
+    debug 
+    setup 
+    setup_model 
+    load_model_subclass
+    init
+    
+    Instance methods
+    ----------------
+    start_request_hook
+    is_model_applicable
+    get_session
+    authenticate
+    exception
+    additional_data
+    preprocess_path
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item debug
+
+    sub My::App::debug {1}
+
+Returns the debugging flag. Override this in your application class to
+enable/disable debugging.
+
+You can also set the C<debug> flag via L<Maypole::Application>.
+
+Some packages respond to higher debug levels, try increasing it to 2 or 3.
+
+
+=cut
+
 sub debug { 0 }
 
-sub setup {
-    my $calling_class = shift;
-    $calling_class = ref $calling_class if ref $calling_class;
-    {
-        no strict 'refs';
-        no warnings 'redefine';
+=item config
 
-        # Naughty.
-        *{ $calling_class . "::handler" } =
-          sub { Maypole::handler( $calling_class, @_ ) };
-    }
-    my $config = $calling_class->config;
-    $config->model || $config->model("Maypole::Model::CDBI");
-    $config->model->require;
-    die "Couldn't load the model class $config->{model}: $@" if $@;
-    $config->model->setup_database( $config, $calling_class, @_ );
-    for my $subclass ( @{ $config->classes } ) {
-        no strict 'refs';
-        unshift @{ $subclass . "::ISA" }, $config->model;
-        $config->model->adopt($subclass)
-          if $config->model->can("adopt");
-    }
+Returns the L<Maypole::Config> object
+
+=item setup
+
+   My::App->setup($data_source, $user, $password, \%attr);
+
+Initialise the Maypole application and plugins and model classes.
+Your application should call this B<after> setting up configuration data via
+L<"config">.
+
+It calls the hook  C<setup_model> to setup the model. The %attr hash contains
+options and arguments used to set up the model. See the particular model's
+documentation. However here is the most usage of setup where
+Maypole::Model::CDBI is the base class.
+
+ My::App->setup($data_source, $user, $password,
+       {  options => {  # These are DB connection options
+               AutoCommit => 0,
+               RaiseError => 1,
+               ...
+          },
+          # These are Class::DBI::Loader arguments.
+          relationships  => 1,
+          ...
+       }
+ );
+
+Also, see  L<Maypole::Manual::Plugins>.
+
+=cut
+
+
+sub setup
+{
+    my $class = shift;
+    
+    $class->setup_model(@_);   
 }
 
-sub init {
+=item setup_model
+
+Called by C<setup>. This method builds the Maypole model hierarchy. 
+
+A likely target for over-riding, if you need to build a customised model.
+
+This method also ensures any code in custom model classes is loaded, so you
+don't need to load them in the driver.
+
+=cut
+
+sub setup_model {
+  my $class = shift;
+  $class = ref $class if ref $class;
+  my $config = $class->config;
+  $config->model || $config->model('Maypole::Model::CDBI');
+  $config->model->require or die sprintf
+    "Couldn't load the model class %s: %s", $config->model, $@;
+
+  # among other things, this populates $config->classes
+  $config->model->setup_database($config, $class, @_);
+
+  foreach my $subclass ( @{ $config->classes } ) {
+    next if $subclass->isa("Maypole::Model::Base");
+    no strict 'refs';
+    unshift @{ $subclass . "::ISA" }, $config->model;
+  }
+
+  # Load custom model code, if it exists - nb this must happen after the
+  # unshift, to allow code attributes to work, but before adopt(),
+  # in case adopt() calls overridden methods on $subclass
+  foreach my $subclass ( @{ $config->classes } ) {
+    $class->load_model_subclass($subclass) unless ($class->model_classes_loaded());
+    $config->model->adopt($subclass) if $config->model->can("adopt");
+  }
+
+}
+
+=item load_model_subclass($subclass)
+
+This method is called from C<setup_model()>. It attempts to load the
+C<$subclass> package, if one exists. So if you make a customized C<BeerDB::Beer>
+package, you don't need to explicitly load it. 
+
+If automatic loading causes problems, Override load_model_subclass in your driver.
+
+sub load_model_subclass {};
+
+Or perhaps during development, if you don't want to load up custom classes, you 
+can override this method and load them manually. 
+
+=cut
+
+sub load_model_subclass {
+  my ($class, $subclass) = @_;
+
+  my $config = $class->config;
+
+  # Load any external files for the model base class or subclasses
+  # (e.g. BeerDB/DBI.pm or BeerDB/Beer.pm) based on code borrowed from
+  # Maypole::Plugin::Loader and Class::DBI.
+  if ( $subclass->require ) {
+    warn "Loaded external module for '$subclass'\n" if $class->debug > 1;
+  } else {
+    (my $filename = $subclass) =~ s!::!/!g;
+    die "Loading '$subclass' failed: $@\n"
+      unless $@ =~ /Can\'t locate \Q$filename\E\.pm/;
+    warn "No external module for '$subclass'"
+      if $class->debug > 1;
+  }
+}
+
+=item init
+
+Loads the view class and instantiates the view object.
+
+You should not call this directly, but you may wish to override this to add
+application-specific initialisation - see L<Maypole::Manual::Plugins>.
+
+=cut
+
+sub init 
+{
     my $class  = shift;
     my $config = $class->config;
     $config->view || $config->view("Maypole::View::TT");
@@ -54,439 +369,1084 @@ sub init {
       || $config->display_tables( $class->config->tables );
     $class->view_object( $class->config->view->new );
     $class->init_done(1);
-
 }
 
-sub handler {
+=item new
+
+Constructs a very minimal new Maypole request object.
 
-    # See Maypole::Workflow before trying to understand this.
-    my ( $class, $req ) = @_;
-    $class->init unless $class->init_done;
+=cut
 
-    # Create the request object
-    my $r = bless {
-        template_args => {},
-        config        => $class->config
+sub new
+{
+    my ($class) = @_;
+    my $self = bless {
+        config        => $class->config,
     }, $class;
-    $r->headers_out(Maypole::Headers->new);
-    $r->get_request($req);
-    $r->parse_location();
-    my $status = $r->handler_guts();
-    return $status unless $status == OK;
-    $r->send_output;
-    return $status;
+
+       $self->stash({});
+       $self->params({});
+       $self->query({});
+       $self->template_args({});
+       $self->args([]);
+       $self->objects([]);
+    
+    return $self;
 }
 
-# The root of all evil
-sub handler_guts {
-    my $r = shift;
-    $r->model_class( $r->config->model->class_of( $r, $r->{table} ) );
+=item view_object
+
+Get/set the Maypole::View object
+
+=back
+
+=head1 INSTANCE METHODS
+
+=head2 Workflow
+
+=over 4
 
-    my $applicable = $r->is_applicable;
-    unless ( $applicable == OK ) {
+=item handler
+
+This method sets up the class if it's not done yet, sets some defaults and
+leaves the dirty work to C<handler_guts>.
+
+=cut
+
+# handler() has a method attribute so that mod_perl will invoke
+# BeerDB->handler() as a method rather than a plain function
+# BeerDB::handler() and so this inherited implementation will be
+# found. See e.g. "Practical mod_perl" by Bekman & Cholet for
+# more information <http://modperlbook.org/html/ch25_01.html>
+sub handler : method  {
+  # See Maypole::Workflow before trying to understand this.
+  my ($class, $req) = @_;
+    
+  $class->init unless $class->init_done;
+
+  my $self = $class->new;
+    
+  # initialise the request
+  $self->headers_out(Maypole::Headers->new);
+  $self->get_request($req);
+
+  $self->parse_location;
+
+  # hook useful for declining static requests e.g. images, or perhaps for 
+  # sanitizing request parameters
+  $self->status(Maypole::Constants::OK()); # set the default
+  $self->__call_hook('start_request_hook');
+  return $self->status unless $self->status == Maypole::Constants::OK();
+  die "status undefined after start_request_hook()" unless defined
+    $self->status;
+  $self->get_session;
+  $self->get_user;
+  my $status = $self->handler_guts;
+  return $status unless $status == OK;
+  # TODO: require send_output to return a status code
+  $self->send_output;
+  return $status;
+}
+
+=item component
+
+  Run Maypole sub-requests as a component of the request
+
+  [% request.component("/beer/view_as_component/20") %]
+
+  Allows you to integrate the results of a Maypole request into an existing
+request. You'll need to set up actions and templates
+which return fragments of HTML rather than entire pages, but once you've
+done that, you can use the C<component> method of the Maypole request object
+to call those actions. You may pass a query string in the usual URL style.
+
+You should not fully qualify the Maypole URLs.
+
+Note: any HTTP POST or URL parameters passed to the parent are not passed to the
+component sub-request, only what is included in the url passed as an argyument
+to the method
+
+=cut
+
+sub component {
+    my ( $r, $path ) = @_;
+    my $self = bless { parent => $r, config => $r->{config}, } , ref $r;
+       $self->stash({});
+       $self->params({});
+       $self->query({});
+       $self->template_args({});
+       $self->args([]);
+       $self->objects([]);
+
+    $self->get_user;
+    my $url = URI->new($path);
+    warn "path : $path\n";
+    $self->{path} = $url->path;
+    $self->parse_path;
+    $self->params( $url->query_form_hash );
+    $self->handler_guts;
+    return $self->output;
+}
 
-        # It's just a plain template
-        delete $r->{model_class};
-        $r->{path} =~ s{/$}{};    # De-absolutify
-        $r->template( $r->{path} );
+sub get_template_root {
+    my $self = shift;
+    my $r    = shift;
+    return $r->parent->get_template_root if $r->{parent};
+    return $self->NEXT::DISTINCT::get_template_root( $r, @_ );
+}
+
+sub view_object {
+    my $self = shift;
+    my $r    = shift;
+    return $r->parent->view_object if $r->{parent};
+    return $self->NEXT::DISTINCT::view_object( $r, @_ );
+}
+
+# Instead of making plugin authors use the NEXT::DISTINCT hoopla to ensure other 
+# plugins also get to call the hook, we can cycle through the application's 
+# @ISA and call them all here. Doesn't work for setup() though, because it's 
+# too ingrained in the stack. We could add a run_setup() method, but we'd break 
+# lots of existing code.
+sub __call_hook
+{
+    my ($self, $hook) = @_;
+    
+    my @plugins;
+    {
+        my $class = ref($self);
+        no strict 'refs';
+        @plugins = @{"$class\::ISA"};
     }
+    
+    # this is either a custom method in the driver, or the method in the 1st 
+    # plugin, or the 'null' method in the frontend (i.e. inherited from 
+    # Maypole.pm) - we need to be careful to only call it once
+    my $first_hook = $self->can($hook);
+    $self->$first_hook;  
+    
+    my %seen = ( $first_hook => 1 );
+
+    # @plugins includes the frontend
+    foreach my $plugin (@plugins)
+    {
+        next unless my $plugin_hook = $plugin->can($hook);
+        next if $seen{$plugin_hook}++;
+        $self->$plugin_hook;
+    }
+}
+
+=item handler_guts
+
+This is the main request handling method and calls various methods to handle the
+request/response and defines the workflow within Maypole.
+
+B<Currently undocumented and liable to be refactored without warning>.
+
+=cut
+
+# The root of all evil
+sub handler_guts 
+{
+    my ($self) = @_;
+    
+    $self->__load_request_model;
+
+    my $applicable = $self->is_model_applicable == OK;
 
-    # We authenticate every request, needed for proper session management
     my $status;
-    eval { $status = $r->call_authenticate };
-    if ( my $error = $@ ) {
-        $status = $r->call_exception($error);
-        if ( $status != OK ) {
+
+    # handle authentication
+    eval { $status = $self->call_authenticate };
+    if ( my $error = $@ ) 
+    {
+        $status = $self->call_exception($error, "authentication");
+        if ( $status != OK ) 
+        {
             warn "caught authenticate error: $error";
-            return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
+            return $self->debug ? 
+                    $self->view_object->error($self, $error) : ERROR;
         }
     }
-    if ( $r->debug and $status != OK and $status != DECLINED ) {
-        $r->view_object->error( $r,
+    if ( $self->debug and $status != OK and $status != DECLINED ) 
+    {
+        $self->view_object->error( $self,
             "Got unexpected status $status from calling authentication" );
     }
+
     return $status unless $status == OK;
 
     # We run additional_data for every request
-    $r->additional_data;
-    if ( $applicable == OK ) {
-        eval { $r->model_class->process($r) };
-        if ( my $error = $@ ) {
-            $status = $r->call_exception($error);
-            if ( $status != OK ) {
-                warn "caught model error: $error";
-                return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
+    $self->additional_data;
+
+    if ($applicable) {
+      eval { $self->model_class->process($self) };
+      if ( my $error = $@ ) 
+        {
+         $status = $self->call_exception($error, "model");
+         if ( $status != OK )
+            {
+             warn "caught model error: $error";
+             return $self->debug ? 
+               $self->view_object->error($self, $error) : ERROR;
             }
         }
+    } else {
+      $self->__setup_plain_template;
     }
-    if ( !$r->{output} ) {    # You might want to do it yourself
-        eval { $status = $r->view_object->process($r) };
-        if ( my $error = $@ ) {
-            $status = $r->call_exception($error);
-            if ( $status != OK ) {
-                warn "caught view error: $error" if $r->debug;
-                return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
-            }
-        }
-        return $status;
+
+    # less frequent path - perhaps output has been set to an error message
+    return OK if $self->output;
+
+    # normal path - no output has been generated yet
+    my $processed_view_ok = $self->__call_process_view;
+
+    $self->{content_type}      ||= $self->__get_mime_type();
+    $self->{document_encoding} ||= "utf-8";
+
+
+    return $processed_view_ok;
+}
+
+my %filetypes = (
+                'js' => 'text/javascript',
+                'css' => 'text/css',
+                'htm' => 'text/html',
+                'html' => 'text/html',
+               );
+
+sub __get_mime_type {
+  my $self = shift;
+  my $type = 'text/html';
+  if ($self->path =~ m/.*\.(\w{3,4})$/) {
+    $type = $filetypes{$1};
+  } else {
+    my $output = $self->output;
+    if (defined $output) {
+      $type = $mmagic->checktype_contents($output);
+    }
+  }
+  return $type;
+}
+
+sub __load_request_model
+{
+    my ($self) = @_;
+       # We may get a made up class from class_of
+    my $mclass = $self->config->model->class_of($self, $self->table);
+    if ( eval {$mclass->isa('Maypole::Model::Base')} ) {
+        $self->model_class( $mclass );
+    }
+    elsif ($self->debug) {
+      warn "***Warning:  No $mclass class appropriate for model. @_"; 
+    }
+}
+
+
+# is_applicable() returned false, so set up a plain template. Model processing 
+# will be skipped, but need to remove the model anyway so the template can't 
+# access it. 
+sub __setup_plain_template
+{
+    my ($self) = @_;
+
+    # It's just a plain template
+    $self->model_class(undef);
+    
+    my $path = $self->path;
+    $path =~ s{/$}{};    # De-absolutify
+    $self->path($path);
+    
+    $self->template($self->path);
+}
+
+# The model has been processed or skipped (if is_applicable returned false), 
+# any exceptions have been handled, and there's no content in $self->output
+sub __call_process_view {
+  my ($self) = @_;
+
+  my $status = eval { $self->view_object->process($self) };
+
+  my $error = $@ || $self->{error};
+
+  if ( $error ) {
+    $status = $self->call_exception($error, "view");
+
+    if ( $status != OK ) {
+      warn "caught view error: $error" if $self->debug;
+      return $self->debug ? 
+       $self->view_object->error($self, $error) : ERROR;
     }
-    else { return OK; }
+  }
+
+  return $status;
 }
 
-sub is_applicable {
-    my $self   = shift;
+=item get_request
+
+You should only need to define this method if you are writing a new
+Maypole backend. It should return something that looks like an Apache
+or CGI request object, it defaults to blank.
+
+=cut
+
+sub get_request { }
+
+=item parse_location
+
+Turns the backend request (e.g. Apache::MVC, Maypole, CGI) into a Maypole
+request. It does this by setting the C<path>, and invoking C<parse_path> and
+C<parse_args>.
+
+You should only need to define this method if you are writing a new Maypole
+backend.
+
+=cut
+
+sub parse_location 
+{
+    die "parse_location is a virtual method. Do not use Maypole directly; " . 
+               "use Apache::MVC or similar";
+}
+
+=item start_request_hook
+
+This is called immediately after setting up the basic request. The default
+method does nothing. 
+
+The value of C<< $r->status >> is set to C<OK> before this hook is run. Your 
+implementation can change the status code, or leave it alone. 
+
+After this hook has run, Maypole will check the value of C<status>. For any
+value other than C<OK>, Maypole returns the C<status> immediately. 
+
+This is useful for filtering out requests for static files, e.g. images, which
+should not be processed by Maypole or by the templating engine:
+
+    sub start_request_hook
+    {
+        my ($r) = @_;
+       
+        $r->status(DECLINED) if $r->path =~ /\.jpg$/;
+    }
+    
+Multiple plugins, and the driver, can define this hook - Maypole will call all
+of them. You should check for and probably not change any non-OK C<status>
+value:
+
+    package Maypole::Plugin::MyApp::SkipFavicon;
+    
+    sub start_request_hook
+    {
+        my ($r) = @_;
+        
+        # check if a previous plugin has already DECLINED this request
+        # - probably unnecessary in this example, but you get the idea
+        return unless $r->status == OK;
+        
+        # then do our stuff
+        $r->status(DECLINED) if $r->path =~ /favicon\.ico/;
+    }        
+     
+=cut
+
+sub start_request_hook { }
+
+=item is_applicable
+
+B<This method is deprecated> as of version 2.11. If you have overridden it,
+please override C<is_model_applicable> instead, and change the return type
+from a Maypole:Constant to a true/false value.
+
+Returns a Maypole::Constant to indicate whether the request is valid.
+
+=cut
+
+sub is_applicable { return shift->is_model_applicable(@_); }
+
+=item is_model_applicable
+
+Returns true or false to indicate whether the request is valid.
+
+The default implementation checks that C<< $r->table >> is publicly
+accessible and that the model class is configured to handle the
+C<< $r->action >>.
+
+=cut
+
+sub is_model_applicable {
+    my ($self) = @_;
+
+    # Establish which tables should be processed by the model
     my $config = $self->config;
+    
     $config->ok_tables || $config->ok_tables( $config->display_tables );
+    
     $config->ok_tables( { map { $_ => 1 } @{ $config->ok_tables } } )
-      if ref $config->ok_tables eq "ARRAY";
-    warn "We don't have that table ($self->{table}).\n"
-      . "Available tables are: "
-      . join( ",", @{ $config->{display_tables} } )
-      if $self->debug
-      and not $config->ok_tables->{ $self->{table} }
-      and $self->{action};
-    return DECLINED() unless exists $config->ok_tables->{ $self->{table} };
+        if ref $config->ok_tables eq "ARRAY";
+        
+    my $ok_tables = $config->ok_tables;
+      
+    # Does this request concern a table to be processed by the model?
+    my $table = $self->table;
+    
+    my $ok = 0;
+    
+    if (exists $ok_tables->{$table}) 
+    {
+        $ok = 1;
+    } 
 
-    # Is it public?
-    return DECLINED unless $self->model_class->is_public( $self->{action} );
-    return OK();
+    if (not $ok) 
+    {
+        warn "We don't have that table ($table).\n"
+            . "Available tables are: "
+            . join( ",", keys %$ok_tables )
+                if $self->debug and not $ok_tables->{$table};
+                
+        return DECLINED;
+    }
+    
+    # Is the action public?
+    my $action = $self->action;
+    return OK if $self->model_class->is_public($action);
+    
+    warn "The action '$action' is not applicable to the table '$table'"
+         if $self->debug;
+    
+    return DECLINED;
 }
 
-sub call_authenticate {
-    my $self = shift;
+=item get_session
 
-    # Check if we have a model class
-    if ( $self->{model_class} ) {
-        return $self->model_class->authenticate($self)
-          if $self->model_class->can("authenticate");
-    }
-    return $self->authenticate($self);   # Interface consistency is a Good Thing
+Called immediately after C<start_request_hook()>.
+
+This method should return a session, which will be stored in the request's
+C<session> attribute.
+
+The default method is empty. 
+
+=cut
+
+sub get_session { }
+
+=item get_user
+
+Called immediately after C<get_session>.
+
+This method should return a user, which will be stored in the request's C<user>
+attribute.
+
+The default method is empty.
+
+=cut
+
+sub get_user {}
+
+=item call_authenticate
+
+This method first checks if the relevant model class
+can authenticate the user, or falls back to the default
+authenticate method of your Maypole application.
+
+=cut
+
+sub call_authenticate 
+{
+    my ($self) = @_;
+
+    # Check if we have a model class with an authenticate() to delegate to
+    return $self->model_class->authenticate($self) 
+        if $self->model_class and $self->model_class->can('authenticate');
+    
+    # Interface consistency is a Good Thing - 
+    # the invocant and the argument may one day be different things 
+    # (i.e. controller and request), like they are when authenticate() 
+    # is called on a model class (i.e. model and request)
+    return $self->authenticate($self);   
 }
 
-sub call_exception {
-    my $self = shift;
-    my ($error) = @_;
+=item authenticate
 
-    # Check if we have a model class
-    if (   $self->{model_class}
-        && $self->model_class->can('exception') )
+Returns a Maypole::Constant to indicate whether the user is authenticated for
+the Maypole request.
+
+The default implementation returns C<OK>
+
+=cut
+
+sub authenticate { return OK }
+
+
+=item call_exception
+
+This model is called to catch exceptions, first after authenticate, then after
+processing the model class, and finally to check for exceptions from the view
+class.
+
+This method first checks if the relevant model class
+can handle exceptions the user, or falls back to the default
+exception method of your Maypole application.
+
+=cut
+
+sub call_exception 
+{
+    my ($self, $error, $when) = @_;
+
+    # Check if we have a model class with an exception() to delegate to
+    if ( $self->model_class && $self->model_class->can('exception') )
     {
-        my $status = $self->model_class->exception( $self, $error );
+        my $status = $self->model_class->exception( $self, $error, $when );
         return $status if $status == OK;
     }
-    return $self->exception($error);
+    
+    return $self->exception($error, $when);
+}
+
+
+=item exception
+
+This method is called if any exceptions are raised during the authentication or
+model/view processing. It should accept the exception as a parameter and return
+a Maypole::Constant to indicate whether the request should continue to be
+processed.
+
+=cut
+
+sub exception { 
+    my ($self, $error, $when) = @_;
+    if (ref $self->view_object && $self->view_object->can("report_error") and $self->debug) {
+        $self->view_object->report_error($self, $error, $when);
+        return OK;
+    }
+    return ERROR;
 }
 
+=item additional_data
+
+Called before the model processes the request, this method gives you a chance to
+do some processing for each request, for example, manipulating C<template_args>.
+
+=cut
+
 sub additional_data { }
 
-sub authenticate { return OK }
+=item send_output
+
+Sends the output and additional headers to the user.
 
-sub exception { return ERROR }
+=cut
 
-sub parse_path {
-    my $self = shift;
-    $self->{path} ||= "frontpage";
-    my @pi = $self->{path} =~ m{([^/]+)/?}g;
-    $self->{table}  = shift @pi;
-    $self->{action} = shift @pi;
-    $self->{action} ||= "index";
-    $self->{args}   = \@pi;
+sub send_output {
+    die "send_output is a virtual method. Do not use Maypole directly; use Apache::MVC or similar";
+}
+
+
+=back
+
+=head2 Path processing and manipulation
+
+=over 4
+
+=item path
+
+Returns the request path
+
+=item parse_path
+
+Parses the request path and sets the C<args>, C<action> and C<table>
+properties. Calls C<preprocess_path> before parsing path and setting properties.
+
+=cut
+
+sub parse_path 
+{
+    my ($self) = @_;
+
+    # Previous versions unconditionally set table, action and args to whatever 
+    # was in @pi (or else to defaults, if @pi is empty).
+    # Adding preprocess_path(), and then setting table, action and args 
+    # conditionally, broke lots of tests, hence this:
+    $self->$_(undef) for qw/action table args/;
+    $self->preprocess_path;
+    $self->path || $self->path('frontpage');
+
+    my @pi = grep {length} split '/', $self->path;
+
+
+    $self->table  || $self->table(shift @pi);
+    $self->action || $self->action( shift @pi or 'index' );
+    $self->args   || $self->args(\@pi);
 }
 
-sub param { # like CGI::param(), but read-only
+=item preprocess_path
+
+Sometimes when you don't want to rewrite or over-ride parse_path but
+want to rewrite urls or extract data from them before it is parsed.
+
+This method is called after parse_location has populated the request
+information and before parse_path has populated the model and action
+information, and is passed the request object.
+
+You can set action, args or table in this method and parse_path will
+then leave those values in place or populate them if not present
+
+=cut
+
+sub preprocess_path { };
+
+=item make_path( %args or \%args or @args )
+
+This is the counterpart to C<parse_path>. It generates a path to use
+in links, form actions etc. To implement your own path scheme, just override
+this method and C<parse_path>.
+
+    %args = ( table      => $table,
+              action     => $action,        
+              additional => $additional,    # optional - generally an object ID
+              );
+              
+    \%args = as above, but a ref
+    
+    @args = ( $table, $action, $additional );   # $additional is optional
+
+C<id> can be used as an alternative key to C<additional>.
+
+C<$additional> can be a string, an arrayref, or a hashref. An arrayref is
+expanded into extra path elements, whereas a hashref is translated into a query
+string. 
+
+=cut
+
+sub make_path
+{
     my $r = shift;
-    my ($key) = @_;
-    if (defined $key) {
-        unless (exists $r->{params}{$key}) {
-            return wantarray() ? () : undef;
-        }
-        my $val = $r->{params}{$key};
-        if (wantarray()) {
-            return ref $val ? @$val : $val;
-        } else {
-            return ref $val ? $val->[0] : $val;
-        }
-    } else {
-        return keys %{$r->{params}};
+    
+    my %args;
+    
+    if (@_ == 1 and ref $_[0] and ref $_[0] eq 'HASH')
+    {
+        %args = %{$_[0]};
+    }
+    elsif ( @_ > 1 and @_ < 4 )
+    {
+        $args{table}      = shift;
+        $args{action}     = shift;
+        $args{additional} = shift;
     }
+    else
+    {
+        %args = @_;
+    }
+    
+    do { die "no $_" unless $args{$_} } for qw( table action );    
+
+    my $additional = $args{additional} || $args{id};
+    
+    my @add = ();
+    
+    if ($additional)
+    {
+        # if $additional is a href, make_uri() will transform it into a query
+        @add = (ref $additional eq 'ARRAY') ? @$additional : ($additional);
+    }    
+    
+    my $uri = $r->make_uri($args{table}, $args{action}, @add);
+    
+    return $uri->as_string;
 }
 
-sub get_template_root { "." }
-sub get_request       { }
 
-sub parse_location {
-    die "Do not use Maypole directly; use Apache::MVC or similar";
-}
 
-sub send_output {
-    die "Do not use Maypole directly; use Apache::MVC or similar";
+=item make_uri( @segments )
+
+Make a L<URI> object given table, action etc. Automatically adds
+the C<uri_base>. 
+
+If the final element in C<@segments> is a hash ref, C<make_uri> will render it
+as a query string.
+
+=cut
+
+sub make_uri
+{
+    my ($r, @segments) = @_;
+
+    my $query = (ref $segments[-1] eq 'HASH') ? pop(@segments) : undef;
+    
+    my $base = $r->config->uri_base; 
+    $base =~ s|/$||;
+    
+    my $uri = URI->new($base);
+    $uri->path_segments($uri->path_segments, grep {length} @segments);
+    
+    my $abs_uri = $uri->abs('/');
+    $abs_uri->query_form($query) if $query;
+    return $abs_uri;
 }
 
-# Session and Repeat Submission Handling
+=item parse_args
 
-sub make_random_id {
-    use Maypole::Session;
-    return Maypole::Session::generate_unique_id();
+Turns post data and query string paramaters into a hash of C<params>.
+
+You should only need to define this method if you are writing a new Maypole
+backend.
+
+=cut 
+
+sub parse_args
+{
+    die "parse_args() is a virtual method. Do not use Maypole directly; ".
+            "use Apache::MVC or similar";
 }
 
-=head1 NAME
+=item get_template_root
 
-Maypole - MVC web application framework
+Implementation-specific path to template root.
 
-=head1 SYNOPSIS
+You should only need to define this method if you are writing a new Maypole
+backend. Otherwise, see L<Maypole::Config/"template_root">
 
-See L<Maypole::Application>.
+=cut
 
-=head1 DESCRIPTION
+=back
 
-This documents the Maypole request object. See the L<Maypole::Manual>, for a
-detailed guide to using Maypole.
+=head2 Request properties
 
-Maypole is a Perl web application framework similar to Java's struts. It is 
-essentially completely abstracted, and so doesn't know anything about
-how to talk to the outside world.
+=over 4
 
-To use it, you need to create a package which represents your entire
-application. In our example above, this is the C<BeerDB> package.
+=item model_class
 
-This needs to first use L<Maypole::Application> which will make your package
-inherit from the appropriate platform driver such as C<Apache::MVC> or
-C<CGI::Maypole>, and then call setup.  This sets up the model classes and
-configures your application. The default model class for Maypole uses
-L<Class::DBI> to map a database to classes, but this can be changed by altering
-configuration. (B<Before> calling setup.)
+Returns the perl package name that will serve as the model for the
+request. It corresponds to the request C<table> attribute.
 
-=head2 CLASS METHODS
 
-=head3 config
+=item objects
 
-Returns the L<Maypole::Config> object
+Get/set a list of model objects. The objects will be accessible in the view
+templates.
 
-=head3 setup
+If the first item in C<$self-E<gt>args> can be C<retrieve()>d by the model
+class, it will be removed from C<args> and the retrieved object will be added to
+the C<objects> list. See L<Maypole::Model> for more information.
 
-    My::App->setup($data_source, $user, $password, \%attr);
 
-Initialise the maypole application and model classes. Your application should
-call this after setting configuration via L<"config">
+=item object
 
-=head3 init
+Alias to get/set the first/only model object. The object will be accessible
+in the view templates.
 
-You should not call this directly, but you may wish to override this to
-add
-application-specific initialisation.
+When used to set the object, will overwrite the request objects
+with a single object.
 
-=head3 view_object
+=cut
 
-Get/set the Maypole::View object
+sub object {
+  my ($r,$object) = @_;
+  $r->objects([$object]) if ($object);
+  return undef unless $r->objects();
+  return $r->objects->[0];
+}
 
-=head3 debug
+=item template_args
 
-    sub My::App::debug {1}
+    $self->template_args->{foo} = 'bar';
 
-Returns the debugging flag. Override this in your application class to
-enable/disable debugging.
+Get/set a hash of template variables.
 
-=head2 INSTANCE METHODS
+Maypole reserved words for template variables will over-ride values in template_variables.
 
-=head3 parse_location
+Reserved words are : r, request, object, objects, base, config and errors, as well as the
+current class or object name.
 
-Turns the backend request (e.g. Apache::MVC, Maypole, CGI) into a
-Maypole
-request. It does this by setting the C<path>, and invoking C<parse_path>
-and
-C<parse_args>.
+=item stash
 
-You should only need to define this method if you are writing a new
-Maypole
-backend.
+A place to put custom application data. Not used by Maypole itself.
 
-=head3 path
+=item template
 
-Returns the request path
+Get/set the template to be used by the view. By default, it returns
+C<$self-E<gt>action>
 
-=head3 parse_path
 
-Parses the request path and sets the C<args>, C<action> and C<table> 
-properties
+=item error
+
+Get/set a request error
 
-=head3 table
+=item output
+
+Get/set the response output. This is usually populated by the view class. You
+can skip view processing by setting the C<output>.
+
+=item table
 
 The table part of the Maypole request path
 
-=head3 action
+=item action
 
 The action part of the Maypole request path
 
-=head3 args
+=item args
 
 A list of remaining parts of the request path after table and action
 have been
 removed
 
-=head3 headers_in
+=item headers_in
 
 A L<Maypole::Headers> object containing HTTP headers for the request
 
-=head3 headers_out
+=item headers_out
 
 A L<HTTP::Headers> object that contains HTTP headers for the output
 
-=head3 parse_args
+=item document_encoding
 
-Turns post data and query string paramaters into a hash of C<params>.
-
-You should only need to define this method if you are writing a new
-Maypole
-backend.
-
-=head3 param
-
-An accessor for request parameters. It behaves similarly to CGI::param() for
-accessing CGI parameters.
-
-=head3 params
-
-Returns a hash of request parameters. The source of the parameters may vary
-depending on the Maypole backend, but they are usually populated from request
-query string and POST data.
-
-B<Note:> Where muliple values of a parameter were supplied, the
-C<params> 
-value
-will be an array reference.
+Get/set the output encoding. Default: utf-8.
 
-=head3 get_template_root
+=item content_type
 
-Implementation-specific path to template root.
+Get/set the output content type. Default: text/html
 
-You should only need to define this method if you are writing a new
-Maypole
-backend. Otherwise, see L<Maypole::Config/"template_root">
+=item get_protocol
 
-=head3 get_request
+Returns the protocol the request was made with, i.e. https
 
-You should only need to define this method if you are writing a new
-Maypole backend. It should return something that looks like an Apache
-or CGI request object, it defaults to blank.
+=cut
 
+sub get_protocol {
+  die "get_protocol is a virtual method. Do not use Maypole directly; use Apache::MVC or similar";
+}
 
-=head3 is_applicable
+=back
 
-Returns a Maypole::Constant to indicate whether the request is valid.
+=head2 Request parameters
 
-The default implementation checks that C<$r-E<gt>table> is publicly
-accessible
-and that the model class is configured to handle the C<$r-E<gt>action>
+The source of the parameters may vary depending on the Maypole backend, but they
+are usually populated from request query string and POST data.
 
-=head3 authenticate
+Maypole supplies several approaches for accessing the request parameters. Note
+that the current implementation (via a hashref) of C<query> and C<params> is
+likely to change in a future version of Maypole. So avoid direct access to these
+hashrefs:
 
-Returns a Maypole::Constant to indicate whether the user is
-authenticated for
-the Maypole request.
+    $r->{params}->{foo}      # bad
+    $r->params->{foo}        # better
 
-The default implementation returns C<OK>
+    $r->{query}->{foo}       # bad
+    $r->query->{foo}         # better
 
-=head3 model_class
+    $r->param('foo')         # best
 
-Returns the perl package name that will serve as the model for the
-request. It corresponds to the request C<table> attribute.
+=over 4
 
-=head3 additional_data
+=item param
 
-Called before the model processes the request, this method gives you a
-chance
-to do some processing for each request, for example, manipulating
-C<template_args>.
+An accessor (get or set) for request parameters. It behaves similarly to
+CGI::param() for accessing CGI parameters, i.e.
 
-=head3 objects
+    $r->param                   # returns list of keys
+    $r->param($key)             # returns value for $key
+    $r->param($key => $value)   # returns old value, sets to new value
 
-Get/set a list of model objects. The objects will be accessible in the
-view
-templates.
+=cut
 
-If the first item in C<$r-E<gt>args> can be C<retrieve()>d by the model
-class,
-it will be removed from C<args> and the retrieved object will be added
-to the
-C<objects> list. See L<Maypole::Model> for more information.
+sub param 
+{ 
+    my ($self, $key) = (shift, shift);
+    
+    return keys %{$self->params} unless defined $key;
+    
+    return unless exists $self->params->{$key};
+    
+    my $val = $self->params->{$key};
+    
+    if (@_)
+    {
+        my $new_val = shift;
+       $self->params->{$key} = $new_val;
+    }
+    
+    return ref $val ? @$val : ($val) if wantarray;
+        
+    return ref $val ? $val->[0] : $val;
+}
 
-=head3 template_args
 
-    $r->template_args->{foo} = 'bar';
+=item params
 
-Get/set a hash of template variables.
+Returns a hashref of request parameters. 
 
-=head3 template
+B<Note:> Where muliple values of a parameter were supplied, the C<params> value
+will be an array reference.
 
-Get/set the template to be used by the view. By default, it returns
-C<$r-E<gt>action>
+=item query
 
-=head3 exception
+Alias for C<params>.
 
-This method is called if any exceptions are raised during the
-authentication 
-or
-model/view processing. It should accept the exception as a parameter and 
-return
-a Maypole::Constant to indicate whether the request should continue to
-be
-processed.
+=back
 
-=head3 error
+=head3 Utility methods
 
-Get/set a request error
+=over 4
 
-=head3 output
+=item redirect_request
 
-Get/set the response output. This is usually populated by the view
-class. You
-can skip view processing by setting the C<output>.
+Sets output headers to redirect based on the arguments provided
 
-=head3 document_encoding
+Accepts either a single argument of the full url to redirect to, or a hash of
+named parameters :
 
-Get/set the output encoding. Default: utf-8.
+$r->redirect_request('http://www.example.com/path');
 
-=head3 content_type
+or
 
-Get/set the output content type. Default: text/html
+$r->redirect_request(protocol=>'https', domain=>'www.example.com', path=>'/path/file?arguments', status=>'302', url=>'..');
 
-=head3 send_output
+The named parameters are protocol, domain, path, status and url
 
-Sends the output and additional headers to the user.
+Only 1 named parameter is required but other than url, they can be combined as
+required and current values (from the request) will be used in place of any
+missing arguments. The url argument must be a full url including protocol and
+can only be combined with status.
 
-=head3 call_authenticate
+=cut
 
-This method first checks if the relevant model class
-can authenticate the user, or falls back to the default
-authenticate method of your Maypole application.
+sub redirect_request {
+  die "redirect_request is a virtual method. Do not use Maypole directly; use Apache::MVC or similar";
+}
 
+=item redirect_internal_request 
 
-=head3 call_exception
+=cut
 
-This model is called to catch exceptions, first after authenticate, then after
-processing the model class, and finally to check for exceptions from the view
-class.
+sub redirect_internal_request {
 
-This method first checks if the relevant model class
-can handle exceptions the user, or falls back to the default
-exception method of your Maypole application.
+}
 
-=head3 make_random_id
 
-returns a unique id for this request can be used to prevent or detect repeat submissions.
+=item make_random_id
 
-=head3 handler
+returns a unique id for this request can be used to prevent or detect repeat
+submissions.
 
-This method sets up the class if it's not done yet, sets some
-defaults and leaves the dirty work to handler_guts.
+=cut
 
-=head3 handler_guts
+# Session and Repeat Submission Handling
+sub make_random_id {
+    use Maypole::Session;
+    return Maypole::Session::generate_unique_id();
+}
 
-This is the core of maypole. You don't want to know.
+=back
+
+=head1 SEQUENCE DIAGRAMS
+
+See L<Maypole::Manual::Workflow> for a detailed discussion of the sequence of 
+calls during processing of a request. This is a brief summary:
+
+    INITIALIZATION
+                               Model e.g.
+         BeerDB           Maypole::Model::CDBI
+           |                        |
+   setup   |                        |
+ o-------->||                       |
+           || setup_model           |     setup_database() creates
+           ||------+                |      a subclass of the Model
+           |||<----+                |        for each table
+           |||                      |                |
+           |||   setup_database     |                |
+           |||--------------------->|| 'create'      *
+           |||                      ||----------> $subclass
+           |||                      |                  |
+           ||| load_model_subclass  |                  |
+ foreach   |||------+  ($subclass)  |                  |
+ $subclass ||||<----+               |    require       |
+           ||||--------------------------------------->|
+           |||                      |                  |
+           |||   adopt($subclass)   |                  |
+           |||--------------------->||                 |
+           |                        |                  |
+           |                        |                  |
+           |-----+ init             |                  |
+           ||<---+                  |                  |
+           ||                       |     new          |     view_object: e.g.
+           ||---------------------------------------------> Maypole::View::TT
+           |                        |                  |          |
+           |                        |                  |          |
+           |                        |                  |          |
+           |                        |                  |          |
+           |                        |                  |          |
+           
+
+
+    HANDLING A REQUEST
+
+
+          BeerDB                                Model  $subclass  view_object
+            |                                      |       |         |
+    handler |                                      |       |         |
+  o-------->| new                                  |       |         |
+            |-----> r:BeerDB                       |       |         |
+            |         |                            |       |         |
+            |         |                            |       |         |
+            |         ||                           |       |         |
+            |         ||-----+ parse_location      |       |         |
+            |         |||<---+                     |       |         |
+            |         ||                           |       |         |
+            |         ||-----+ start_request_hook  |       |         |
+            |         |||<---+                     |       |         |
+            |         ||                           |       |         |
+            |         ||-----+ get_session         |       |         |
+            |         |||<---+                     |       |         |
+            |         ||                           |       |         |
+            |         ||-----+ get_user            |       |         |
+            |         |||<---+                     |       |         |
+            |         ||                           |       |         |
+            |         ||-----+ handler_guts        |       |         |
+            |         |||<---+                     |       |         |
+            |         |||     class_of($table)     |       |         |
+            |         |||------------------------->||      |         |
+            |         |||       $subclass          ||      |         |
+            |         |||<-------------------------||      |         |
+            |         |||                          |       |         |
+            |         |||-----+ is_model_applicable|       |         |
+            |         ||||<---+                    |       |         |
+            |         |||                          |       |         |
+            |         |||-----+ call_authenticate  |       |         |
+            |         ||||<---+                    |       |         |
+            |         |||                          |       |         |
+            |         |||-----+ additional_data    |       |         |
+            |         ||||<---+                    |       |         |
+            |         |||             process      |       |         |
+            |         |||--------------------------------->||  fetch_objects
+            |         |||                          |       ||-----+  |
+            |         |||                          |       |||<---+  |
+            |         |||                          |       ||        |
+            |         |||                          |       ||   $action
+            |         |||                          |       ||-----+  |
+            |         |||                          |       |||<---+  |            
+            |         |||         process          |       |         |
+            |         |||------------------------------------------->|| template
+            |         |||                          |       |         ||-----+
+            |         |||                          |       |         |||<---+
+            |         |||                          |       |         |
+            |         ||     send_output           |       |         |
+            |         ||-----+                     |       |         |
+            |         |||<---+                     |       |         |
+   $status  |         ||                           |       |         |
+   <------------------||                           |       |         |
+            |         |                            |       |         |
+            |         X                            |       |         |           
+            |                                      |       |         |
+            |                                      |       |         |
+            |                                      |       |         |
+           
+           
 
 =head1 SEE ALSO
 
-There's more documentation, examples, and information on our mailing lists
+There's more documentation, examples, and information on our mailing lists
 at the Maypole web site:
 
 L<http://maypole.perl.org/>
@@ -495,12 +1455,14 @@ L<Maypole::Application>, L<Apache::MVC>, L<CGI::Maypole>.
 
 =head1 AUTHOR
 
-Maypole is currently maintained by Simon Flack C<simonflk#cpan.org>
+Maypole is currently maintained by Aaron Trevena.
 
 =head1 AUTHOR EMERITUS
 
 Simon Cozens, C<simon#cpan.org>
 
+Simon Flack maintained Maypole from 2.05 to 2.09
+
 Sebastian Riedel, C<sri#oook.de> maintained Maypole from 1.99_01 to 2.04
 
 =head1 THANKS TO
@@ -516,3 +1478,55 @@ You may distribute this code under the same terms as Perl itself.
 =cut
 
 1;
+
+__END__
+
+ =item register_cleanup($coderef)
+
+Analogous to L<Apache>'s C<register_cleanup>. If an Apache request object is
+available, this call simply redispatches there. If not, the cleanup is
+registered in the Maypole request, and executed when the request is
+C<DESTROY>ed.
+
+This method is only useful in persistent environments, where you need to ensure
+that some code runs when the request finishes, no matter how it finishes (e.g.
+after an unexpected error). 
+
+ =cut
+
+{
+    my @_cleanups;
+
+    sub register_cleanup
+    {
+        my ($self, $cleanup) = @_;
+        
+        die "register_cleanup() is an instance method, not a class method" 
+            unless ref $self;
+        die "Cleanup must be a coderef" unless ref($cleanup) eq 'CODE';
+        
+        if ($self->can('ar') && $self->ar)
+        {
+            $self->ar->register_cleanup($cleanup);
+        }
+        else
+        {
+            push @_cleanups, $cleanup;
+        }
+    }
+
+    sub DESTROY
+    {
+        my ($self) = @_;
+        
+        while (my $cleanup = shift @_cleanups)
+        {
+            eval { $cleanup->() };
+            if ($@)
+            {
+                warn "Error during request cleanup: $@";
+            }
+        }        
+    }    
+}
+    
index cd4318f9c48f786f1490225eccea642064d6553a..ae95bb7ee3447df5524c2925adbc7b193d4c0bdc 100644 (file)
@@ -2,19 +2,22 @@ package Maypole::Application;
 
 use strict;
 use warnings;
+
 use UNIVERSAL::require;
 use Maypole;
 use Maypole::Config;
 
-our @ISA;
-our $VERSION = '2.09';
+our $VERSION = '2.11';
 
 sub import {
-    my ( $class, @plugins ) = @_;
+    shift; # not interested in this - we manipulate the caller's @ISA directly
+    my @plugins = @_;
     my $caller = caller(0);
     
     my $frontend = 'Apache::MVC' if $ENV{MOD_PERL};
     
+    $frontend = 'Maypole::HTTPD::Frontend' if $ENV{MAYPOLE_HTTPD};
+    
     my $masonx;
     if ( grep { /^MasonX$/ } @plugins )
     {
@@ -26,39 +29,41 @@ sub import {
     $frontend ||= 'CGI::Maypole';
     
     $frontend->require or die "Loading $frontend frontend failed: $@";
-    push @ISA, $frontend;
 
     my $autosetup=0;
+    my $autoinit=0;
     my @plugin_modules;
+
+    foreach (@plugins) 
     {
-        foreach (@plugins) {
-            if    (/^\-Setup$/) { $autosetup++; }
-            elsif (/^\-Debug(\d*)$/) {
-                my $d = $1 || 1;
-                no strict 'refs';
-                *{"$caller\::debug"} = sub { $d };
-                warn "Debugging (level $d) enabled for $caller";
-            }
-            elsif (/^-.*$/) { warn "Unknown flag: $_" }
-            else {
-                my $plugin = "Maypole::Plugin::$_";
-                if ($plugin->require) {
-                    push @plugin_modules, "Maypole::Plugin::$_";
-                   unshift @ISA, "Maypole::Plugin::$_";
-                    warn "Loaded plugin: $plugin for $caller"
-                        if $caller->can('debug') && $caller->debug;
-                } else {
-                    die qq(Loading plugin "$plugin" for $caller failed: )
-                        . $UNIVERSAL::require::ERROR;
-                }
+        if    (/^\-Setup$/) { $autosetup++; }
+        elsif (/^\-Init$/)  { $autoinit++ }
+        elsif (/^\-Debug(\d*)$/) {
+            my $d = $1 || 1;
+            no strict 'refs';
+            *{"$caller\::debug"} = sub { $d };
+            warn "Debugging (level $d) enabled for $caller";
+        }
+        elsif (/^-.*$/) { warn "Unknown flag: $_" }
+        else {
+            my $plugin = "Maypole::Plugin::$_";
+            if ($plugin->require) {
+                push @plugin_modules, "Maypole::Plugin::$_";
+                warn "Loaded plugin: $plugin for $caller"
+                    if $caller->can('debug') && $caller->debug;
+            } else {
+                die qq(Loading plugin "$plugin" for $caller failed: )
+                    . $UNIVERSAL::require::ERROR;
             }
         }
     }
+    
     no strict 'refs';
-    push @{"${caller}::ISA"}, @plugin_modules, $class;
+    push @{"${caller}::ISA"}, @plugin_modules, $frontend;
     $caller->config(Maypole::Config->new);
     $caller->config->masonx({}) if $masonx;
     $caller->setup() if $autosetup;
+    $caller->init() if $autosetup && $autoinit;
 }
 
 1;
@@ -83,19 +88,42 @@ Maypole::Application - Universal Maypole Frontend
 
 This is a universal frontend for mod_perl1, mod_perl2, HTML::Mason and CGI.
 
-You can omit the Maypole::Plugin:: prefix from plugins.
-So Maypole::Plugin::Config::YAML becomes Config::YAML.
+Automatically determines the appropriate frontend for your environment (unless
+you want to use L<MasonX::Maypole>, in which case include C<MasonX> in the
+arguments).
+
+Loads plugins supplied in the C<use> statement. 
+
+Responds to flags supplied in the C<use> statement. 
+
+Initializes the application's configuration object. 
+
+You can omit the Maypole::Plugin:: prefix from plugins. So
+Maypole::Plugin::Config::YAML becomes Config::YAML.
 
     use Maypole::Application qw(Config::YAML);
 
-You can also set special flags like -Setup and -Debug.
+You can also set special flags like -Setup, -Debug and -Init.
 
     use Maypole::Application qw(-Debug Config::YAML -Setup);
 
-The position of plugins and flags in the chain is important,
-because they are loaded/executed in the same order they appear.
+The position of plugins in the chain is important, because they are
+loaded/executed in the same order they appear.
+
+=head1 FRONTEND
+
+Under mod_perl (1 or 2), selects L<Apache::MVC>. 
+
+Otherwise, selects L<CGI::Maypole>.
 
-=head2 -Setup
+If C<MasonX> is specified, sets L<MasonX::Maypole> as the frontend. This
+currently also requires a mod_perl environment.
+
+=head1 FLAGS
+
+=over
+
+=item -Setup
 
     use Maypole::Application qw(-Setup);
 
@@ -108,7 +136,26 @@ Note that no options are passed to C<setup()>. You must ensure that the
 required model config parameters are set in C<MyApp-E<gt>config>. See
 L<Maypole::Config> for more information.
 
-=head2 -Debug
+=item -Init
+
+    use Maypole::Application qw(-Setup -Init);
+    
+is equivalent to
+
+    use Maypole::Application;
+    MyApp->setup;
+    MyApp->init;
+    
+Note that the C<-Setup> flag is required for the C<-Init> flag to work.
+
+In persistent environments (e.g. C<mod_perl>), it is useful to call C<init> 
+once in the parent server, rather than at the beginning of the first request
+to each child server, in order to share the view code loaded during C<init>. 
+Note that you must supply all the config data to your app before calling 
+C<setup> and C<init>, probably by using one of the C<Maypole::Plugin::Config::*> 
+plugins.
+
+=item -Debug
 
     use Maypole::Application qw(-Debug);
 
@@ -119,6 +166,8 @@ is equivalent to
 
 You can specify a higher debug level by saying C<-Debug2> etc. 
 
+=back
+
 =head1 AUTHOR
 
 Sebastian Riedel, C<sri@oook.de>
index 88dc8eeee0fae3dd9e12a700e83a56090df4008c..040a4c8e8baa0975d9a3bffcebca788a57655e01 100644 (file)
@@ -5,7 +5,7 @@ use attributes ();
 use strict;
 use warnings;
 
-our $VERSION = "1." . sprintf "%04d", q$Rev: 333 $ =~ /: (\d+)/;
+our $VERSION = "1." . sprintf "%04d", q$Rev: 334 $ =~ /: (\d+)/;
 
 # Public accessors.
 __PACKAGE__->mk_accessors(
index 9c018f9325f4e7782723fd3d77abd0bde00edcac..b70a06ceb79f7e10bd8e547ddb357d83451ad902 100644 (file)
@@ -3,9 +3,9 @@ use strict;
 use base 'Exporter';
 use constant OK       => 0;
 use constant DECLINED => -1;
-use constant ERROR    => -1;
+use constant ERROR    => 500;
 our @EXPORT = qw(OK DECLINED ERROR);
-our $VERSION = "1." . sprintf "%04d", q$Rev: 354 $ =~ /: (\d+)/;
+our $VERSION = "1." . sprintf "%04d", q$Rev: 483 $ =~ /: (\d+)/;
 
 1;
 
index dff1ced896ee6b4d5f0c56d4efac5732bd2755fd..28675fce3dd0252b1e75a89dce92f6e6d6007ad3 100644 (file)
@@ -4,7 +4,7 @@ use base 'HTTP::Headers';
 use strict;
 use warnings;
 
-our $VERSION = "1." . sprintf "%04d", q$Rev: 324 $ =~ /: (\d+)/;
+our $VERSION = "1." . sprintf "%04d", q$Rev: 376 $ =~ /: (\d+)/;
 
 sub get {
     shift->header(shift);
@@ -14,6 +14,8 @@ sub set {
     shift->header(@_);
 }
 
+*add = \&push; # useful for Apache::Session::Wrapper support
+
 sub push {
     shift->push_header(@_);
 }
@@ -84,6 +86,10 @@ Add a value to the field named C<$header>. Previous values are maintained.
 
 An alias to C<HTTP::Headers-E<gt>push_header>
 
+=item add
+
+Alias to C<push> - useful for C<Apache::Session::Wrapper> support, in CGI mode.
+
 =item init($header =C<gt> $value)
 
 Set the value for the field named C<$header>, but only if that header is
index dc60222467a234b8cae8cd4ab54ac94e323bfde6..d556ae93d4274245aa04a482e200e80acafa2995 100644 (file)
@@ -71,6 +71,15 @@ This document also introduces the theory behind Maypole's
 actions and templates, showing you how to write your own
 so that you can have a highly customized application.
 
+=item L<Maypole::Manual::Plugins> - writing Maypole plugins
+
+Useful information for plugin authors. 
+
+=item L<Maypole::Manual::Terminology> - pinning down usage
+
+As well as defining common terms used in Maypole discussions, this document 
+briefly discusses the MVC-ness of Maypole. 
+
 =item L<Maypole::Manual::Workflow> - Description of the Request Workflow 
 
 This is a technical document that describes the progress of a
@@ -83,7 +92,7 @@ and not to most of those who are using it.
 This document gives a close look at the Beer database that
 was introduced in L<Maypole::Manual::About>.
 
-=item L<Maypole::Manual::Request> - The Request Cookbook 
+=item L<Maypole::Manual::Cookbook> - The Maypole Cookbook 
 
 This extensive document is Maypole's main "How do I do X?" FAQ.
 It provides a wide variety of cookbook-like techniques that
index d78be34aea8df871866f7706dea05b054598062d..6f48663e45787434fd658b00ebfc863b1fd344f8 100644 (file)
@@ -74,48 +74,6 @@ logic of your application. This is one of the reasons why Maypole lets
 you develop so rapidly: because most of the time, you don't need to do
 any development at all.
 
-=head2 Installing Maypole
-
-The first thing you're going to need to do to get Maypole running is to
-install it. Maypole needs an absolute shedload of Perl modules from CPAN
-to do its job. I am unrepentant about this. Maypole does a lot of work,
-so that you don't have to. This is called code re-use, and if we're
-serious about code re-use, then Maypole should be re-using as much code
-as possible in terms of Perl modules. In another sense, this gives the
-impression that Maypole doesn't actually do all that much itself,
-because all it's doing is gluing together already-existing code. Well,
-welcome to code re-use.
-
-The downside of code re-use is, of course, that you then have to install
-a shedload of Perl modules from CPAN. If you're using OpenBSD or
-FreeBSD, the wonderful ports system will be your friend. There's a
-Maypole port in C<p5-Maypole>. Just type C<make install>.
-
-Debian users, hang in there. There's a package coming.
-
-For other Unices, the L<CPANPLUS> or C<CPAN> modules will help with
-this. If you don't have C<CPANPLUS> installed, my recommendation is to
-use C<perl -MCPAN -e install CPANPLUS> to install it and then throw
-C<CPAN.pm> away. In any case, one of these two should get all that
-Maypole needs:
-
-    % perl -MCPANPLUS -e 'install Maypole'
-    % perl -MCPAN -e 'install Maypole'
-
-I don't know if Maypole works on Windows. I'm not sure I care.
-
-You're also going to need a database server and a web server. For
-databases, I recommend SQLite (if you install the C<DBD::SQLite> module,
-you get the SQLite library for free) for prototyping and mysql for
-production; heavier duty users should use Postgresql or Oracle - Maypole
-should be happy with them all. Maypole is happiest when running under
-Apache C<mod_perl>, with the C<Apache::Request> module installed, but as
-I said, it is a blank slate, and everything is customizable. There is a
-C<CGI::Maypole> frontend available to run as a standalone CGI script.
-
-As well as the documentation embedded in the Perl modules the distribution
-also includes the manual, of which this is a part. You can access it using the
-perldoc command, the man command, or by browsing CPAN.
 
 =head2 The Beer Database example
 
index 542b570caa7fbd04cd8c75b718a4fd85b51d3365..f99e0a2ecd9a9baf5dbd1e946c4f7c3c14b34dfb 100644 (file)
@@ -185,12 +185,15 @@ The equivalent in ordinary C<Class::DBI> would be:
 
        BeerDB::Handpump->has_a(beer => "BeerDB::Beer");
        BeerDB::Handpump->has_a(pub => "BeerDB::Pub");
-       BeerDB::Pub->has_many(beers => [ BeerDB::Handpump => 'beer' ]);
-       BeerDB::Beer->has_many(pubs => [ BeerDB::Handpump => 'pub' ]);
+       BeerDB::Pub->has_many(beers => [ 'BeerDB::Handpump' => 'beer' ]);
+       BeerDB::Beer->has_many(pubs => [ 'BeerDB::Handpump' => 'pub' ]);
 
 Maypole's default templates will use this information to display, for
 instance, a list of a brewery's beers on the brewery view page.
 
+Note the quoting in 'BeerDB::Handpump' => 'beer', if you forget to quote the
+left side when using strict you will get compilation errors.
+
 This is the complete beer database application; Maypole's default templates
 and the actions in the view class do the rest. But what if we want to do a
 little more. How would we begin to extend this application?
diff --git a/lib/Maypole/Manual/Cookbook.pod b/lib/Maypole/Manual/Cookbook.pod
new file mode 100644 (file)
index 0000000..1d2395c
--- /dev/null
@@ -0,0 +1,839 @@
+=head1 NAME\r
+\r
+Maypole::Manual::Cookbook - Maypole Cookbook\r
+\r
+=head1 DESCRIPTION\r
+\r
+Hacks; design patterns; recipes: call it what you like, this chapter is a\r
+developing collection of techniques which can be slotted in to Maypole\r
+applications to solve common problems or make the development process easier.\r
+\r
+As Maypole developers, we don't necessarily know the "best practice" for\r
+developing Maypole applications ourselves, in the same way that Larry Wall\r
+didn't know all about the best Perl programming style as soon as he wrote\r
+Perl. These techniques are what we're using at the moment, but they may\r
+be refined, modularized, or rendered irrelevant over time. But they've\r
+certainly saved us a bunch of hours work.\r
+\r
+=head2 Frontend hacks\r
+\r
+These hacks deal with changing the way Maypole relates to the outside world;\r
+alternate front-ends to the Apache and CGI interfaces, or subclassing chunks\r
+of the front-end modules to alter Maypole's behaviour in particular ways.\r
+\r
+=head3 Separate model class modules\r
+\r
+You want to put all the C<BeerDB::Beer> routines in a separate module,\r
+so you say:\r
+\r
+    package BeerDB::Beer;\r
+    BeerDB::Beer->has_a(brewery => "BeerDB::Brewery");\r
+    sub foo :Exported {}\r
+\r
+And in F<BeerDB.pm>, you put:\r
+\r
+    use BeerDB::Beer;\r
+\r
+It doesn't work.\r
+\r
+B<Solution>: It doesn't work because of the timing of the module loading.\r
+C<use BeerDB::Beer> will try to set up the C<has_a> relationships\r
+at compile time, when the database tables haven't even been set up,\r
+since they're set up by\r
+\r
+    BeerDB->setup("...")\r
+\r
+which does its stuff at runtime. There are two ways around this; you can\r
+either move the C<setup> call to compile time, like so:\r
+\r
+    BEGIN { BeerDB->setup("...") }\r
+\r
+or move the module loading to run-time (my preferred solution):\r
+\r
+    BeerDB->setup("...");\r
+    BeerDB::Beer->require;\r
+\r
+=head3 Redirecting to SSL for sensitive information\r
+\r
+You have a website with forms that people will be entering sensitive information into,\r
+such as credit cards or login details. You want to make sure that they aren't sent\r
+in plain text but over SSL instead.\r
+\r
+B<Solution>\r
+\r
+The solution is a bit tricky for 2 reasons :\r
+\r
+Firstly -- Many browsers and web clients will change a redirected \r
+POST request into a GET request (which displays all that sensitive information in the\r
+browser, or access logs and possibly elsewhere) and/or drops the values on the floor.\r
+\r
+Secondly -- If somebody has sent that sensitive information in plain text already, then\r
+sending it again over SSL won't solve the problem.\r
+\r
+Redirecting a request is actually rather simple :\r
+\r
+$r->redirect_request('https://www.example.com/path'); # perldoc Maypole for API\r
+\r
+.. as is checking the protocol :\r
+\r
+$r->get_protocol(); # returns 'http' or 'https'\r
\r
+You should check that the action that generates the form that people will enter\r
+the sensitive information into is https and redirect if not.\r
+\r
+You should also check that no information is lost when redirecting, possibly by \r
+storing it in a session and retrieving it later - see Maypole::Plugin::Session\r
+\r
+=head3 Debugging with the command line\r
+\r
+You're seeing bizarre problems with Maypole output, and you want to test it in\r
+some place outside of the whole Apache/mod_perl/HTTP/Internet/browser circus.\r
+\r
+B<Solution>: Use the L<Maypole::CLI> module to go directly from a URL to\r
+standard output, bypassing Apache and the network altogether.\r
+\r
+L<Maypole::CLI> is not a standalone front-end, but to allow you to debug your\r
+applications without having to change the front-end they use, it temporarily\r
+"borgs" an application. If you run it from the command line, you're expected\r
+to use it like so:\r
+\r
+    perl -MMaypole::CLI=Application -e1 'http://your.server/path/table/action'\r
+\r
+For example:\r
+\r
+    perl -MMaypole::CLI=BeerDB -e1 'http://localhost/beerdb/beer/view/1?o2=desc'\r
+\r
+You can also use the C<Maypole::CLI> module programatically to create\r
+test suites for your application. See the Maypole tests themselves or\r
+the documentation to C<Maypole::CLI> for examples of this.\r
+\r
+Don't forget also to turn on debugging output in your application:\r
+\r
+    package BeerDB;\r
+    use strict;\r
+    use warnings;\r
+    use Maypole::Application qw(-Debug);\r
+\r
+=head3 Changing how URLs are parsed\r
+\r
+You don't like the way Maypole URLs look, and want something that either\r
+fits in with the rest of your site or hides the internal workings of the\r
+system.\r
+\r
+B<Solution>: So far we've been using the C</table/action/id/args> form\r
+of a URL as though it was "the Maypole way"; well, there is no Maypole\r
+way. Maypole is just a framework and absolutely everything about it is \r
+overridable. \r
+\r
+If we want to provide our own URL handling, the method to override in\r
+the driver class is C<parse_path>. This is responsible for taking\r
+C<$r-E<gt>path> and filling the C<table>, C<action> and C<args> slots\r
+of the request object. Normally it does this just by splitting the path\r
+on 'C</>' characters, but you can do it any way you want, including\r
+getting the information from C<POST> form parameters or session variables. \r
+\r
+For instance, suppose we want our URLs to be of the form\r
+C<ProductDisplay.html?id=123>, we could provide a C<parse_path> method\r
+like so:\r
+\r
+    sub parse_path {\r
+        my $r = shift;\r
+        $r->path("ProductList.html") unless $r->path;\r
+        ($r->path =~ /^(.*?)([A-Z]\w+)\.html/);\r
+        $r->table(lc $1);\r
+        $r->action(lc $2);\r
+        my %query = $r->ar->args;\r
+        $self->args([ $query{id} ]);\r
+    }\r
+\r
+This takes the path, which already has the query parameters stripped off\r
+and parsed, and finds the table and action portions of the filename,\r
+lower-cases them, and then grabs the C<id> from the query. Later methods\r
+will confirm whether or not these tables and actions exist.\r
+\r
+See the L<iBuySpy Portal|Maypole::Manual::BuySpy> for another\r
+example of custom URL processing.\r
+\r
+=head3 Maypole for mobile devices\r
+\r
+You want Maypole to use different templates to display on particular\r
+browsers.\r
+\r
+B<Solution>: There are several ways to do this, but here's the neatest\r
+we've found. Maypole chooses where to get its templates either by\r
+looking at the C<template_root> config parameter or, if this is not\r
+given, calling the C<get_template_root> method to ask the front-end to\r
+try to work it out. We can give the front-end a little bit of help, by\r
+putting this method in our driver class:\r
+\r
+    sub get_template_root {\r
+        my $r = shift;\r
+        my $browser = $r->headers_in->get('User-Agent');\r
+        if ($browser =~ /mobile|palm|nokia/i) {\r
+            "/home/myapp/templates/mobile";\r
+        } else {\r
+            "/home/myapp/templates/desktop";\r
+        }\r
+    }\r
+\r
+(Maybe there's a better way to detect a mobile browser, but you get the\r
+idea.)\r
+\r
+=head2 Content display hacks\r
+\r
+These hacks deal primarily with the presentation of data to the user,\r
+modifying the F<view> template or changing the way that the results of\r
+particular actions are displayed.\r
+\r
+=head3 Null Action\r
+\r
+You need an "action" which doesn't really do anything, but just formats\r
+up a template.\r
+\r
+B<Solution>: There are two ways to do this, depending on what precisely\r
+you need. If you just need to display a template, C<Apache::Template>\r
+style, with no Maypole objects in it, then you don't need to write any\r
+code; just create your template, and it will be available in the usual\r
+way.\r
+\r
+If, on the other hand, you want to display some data, and what you're\r
+essentially doing is a variant of the C<view> action, then you need to\r
+ensure that you have an exported action, as described in the\r
+L<templates and actions|Maypole::Manual::StandardTemplates/"C<view> and C<edit>">\r
+chapter:\r
+\r
+    sub my_view :Exported { }\r
+\r
+=head3 Template Switcheroo\r
+\r
+An action doesn't have any data of its own to display, but needs to display\r
+B<something>.\r
+\r
+B<Solution>: This is an B<extremely> common hack. You've just issued an\r
+action like C<beer/do_edit>, which updates the database. You don't want\r
+to display a page that says "Record updated" or similar. Lesser\r
+application servers would issue a redirect to have the browser request\r
+C</beer/view/I<id>> instead, but we can actually modify the Maypole\r
+request on the fly and, after doing the update, pretend that we were\r
+going to C</beer/view/I<id>> all along. We do this by setting the\r
+objects in the C<objects> slot and changing the C<template> to the\r
+one we wanted to go to.\r
+\r
+In this example from L<Flox|Maypole::Manual::Flox>, we've just\r
+performed an C<accept> method on a C<Flox::Invitation> object and we\r
+want to go back to viewing a user's page.\r
+\r
+    sub accept :Exported {\r
+        my ($self, $r) = @_;\r
+        my $invitation = $r->objects->[0];\r
+        # [... do stuff to $invitation ...]\r
+        $r->objects([$r->user]);\r
+        $r->model_class("Flox::User");\r
+        $r->template("view");\r
+    }\r
+\r
+This hack is so common that it's expected that there'll be a neater\r
+way of doing this in the future.\r
+\r
+=head3 XSLT\r
+\r
+Here's a hack I've used a number of times. You want to store structured\r
+data in a database and to abstract out its display.\r
+\r
+B<Solution>: You have your data as XML, because handling big chunks of\r
+XML is a solved problem. Build your database schema as usual around the\r
+important elements that you want to be able to search and browse on. For\r
+instance, I have an XML format for songs which has a header section of\r
+the key, title and so on, plus another section for the lyrics and\r
+chords:\r
+\r
+    <song>\r
+        <header>\r
+            <title>Layla</title>\r
+            <artist>Derek and the Dominos</artist>\r
+            <key>Dm</key>\r
+        </header>\r
+        <lyrics>\r
+          <verse>...</verse>\r
+          <chorus>\r
+            <line> <sup>A</sup>Lay<sup>Dm</sup>la <sup>Bb</sup> </line> \r
+            <line> <sup>C</sup>Got me on my <sup>Dm</sup>knees </line> \r
+            ...\r
+\r
+I store the title, artist and key in the database, as well as an "xml"\r
+field which contains the whole song as XML.\r
+\r
+To load the songs into the database, I can C<use> the driver class for\r
+my application, since that's a handy way of setting up the database classes\r
+we're going to need to use. Then the handy L<XML::TreeBuilder> will handle\r
+the XML parsing for us:\r
+\r
+    use Songbook;\r
+    use XML::TreeBuilder;\r
+    my $t = XML::TreeBuilder->new;\r
+    $t->parse_file("songs.xml");\r
+\r
+    for my $song ($t->find("song")) {\r
+        my ($key) = $song->find("key"); $key &&= $key->as_text;\r
+        my ($title) = $song->find("title"); $title = $title->as_text;\r
+        my ($artist) = $song->find("artist"); $artist = $artist->as_text;\r
+        my ($first_line) = $song->find("line");\r
+        $first_line = join "", grep { !ref } $first_line->content_list;\r
+        $first_line =~ s/[,\.\?!]\s*$//;\r
+        Songbook::Song->find_or_create({\r
+            title => $title,\r
+            first_line => $first_line,\r
+            song_key => Songbook::SongKey->find_or_create({name => $key}),\r
+            artist => Songbook::Artist->find_or_create({name => $artist}),\r
+            xml => $song->as_XML\r
+        });\r
+    }\r
+\r
+Now we need to set up the custom display for each song; thankfully, with\r
+the L<Template::Plugin::XSLT> module, this is as simple as putting the\r
+following into F<templates/song/view>:\r
+\r
+    [%\r
+        USE transform = XSLT("song.xsl");\r
+        song.xml | $transform\r
+    %]\r
+\r
+We essentially pipe the XML for the selected song through to an XSL\r
+transformation, and this will fill out all the HTML we need. Job done.\r
+\r
+=head3 Displaying pictures\r
+\r
+You want to serve a picture, a Word document, or something else which\r
+doesn't have a content type of C<text/html>, out of your database.\r
+\r
+B<Solution>: Fill the content and content-type yourself.\r
+\r
+Here's a subroutine which displays the C<photo> for either a specified\r
+user or the currently logged in user. We set the C<output> slot of the\r
+Maypole request object: if this is done then the view class is not called\r
+upon to process a template, since we already have some output to display.\r
+We also set the C<content_type> using one from the database.\r
+\r
+    sub view_picture :Exported {\r
+        my ($self, $r) = @_;\r
+        my $user = $r->objects->[0];\r
+        $r->content_type($user->photo_type);\r
+        $r->output($user->photo);\r
+    }\r
+\r
+Of course, the file doesn't necessarily need to be in the database\r
+itself; if your file is stored in the filesystem, but you have a file\r
+name or some other pointer in the database, you can still arrange for\r
+the data to be fetched and inserted into C<$r-E<gt>output>.\r
+\r
+=head3 REST\r
+\r
+You want to provide a programmatic interface to your Maypole site.\r
+\r
+B<Solution>: The best way to do this is with C<REST>, which uses a\r
+descriptive URL to encode the request. For instance, in\r
+L<Flox|Maypole::Manual::Flox> we\r
+describe a social networking system. One neat thing you can do with\r
+social networks is to use them for reputation tracking, and we can use\r
+that information for spam detection. So if a message arrives from\r
+C<person@someco.com>, we want to know if they're in our network of\r
+friends or not and mark the message appropriately. We'll do this by\r
+having a web agent (say, L<WWW::Mechanize> or L<LWP::UserAgent>) request\r
+a URL of the form\r
+C<http://flox.simon-cozens.org/user/relationship_by_email/person%40someco.com>.\r
+Naturally, they'll need to present the appropriate cookie just like a\r
+normal browser, but that's a solved problem. We're just interested in\r
+the REST request.\r
+\r
+The request will return a single integer status code: 0 if they're not\r
+in the system at all, 1 if they're in the system, and 2 if they're our\r
+friend.\r
+\r
+All we need to do to implement this is provide the C<relationship_by_email>\r
+action, and use it to fill in the output in the same way as we did when\r
+displaying a picture. Since C<person%40someco.com> is not the ID of a\r
+row in the user table, it will appear in the C<args> array:\r
+\r
+    use URI::Escape;\r
+    sub relationship_by_email :Exported {\r
+        my ($self, $r) = @_;\r
+        my $email = uri_unescape($r->args->[0]);\r
+        $r->content_type("text/plain");\r
+        my $user;\r
+        unless (($user) = Flox::User->search(email => $email)) {\r
+            $r->content("0\n"); return;\r
+        }\r
+\r
+        if ($r->user->is_friend($user)) { $r->contenti("2\n"); return; };\r
+        $r->content("1\n"); return;\r
+    }\r
+\r
+=head3 Component-based Pages\r
+\r
+You're designing something like a portal site which has a number of\r
+components, all displaying different bits of information about different\r
+objects. You want to include the output of one Maypole request call while\r
+building up another. \r
+\r
+B<Solution>: Use L<Maypole::Plugin::Component>. By inheriting like this:\r
+\r
+    package BeerDB;\r
+    use Maypole::Application qw(Component);\r
+\r
+you can call the C<component> method on the Maypole request object to\r
+make a "sub-request". For instance, if you have a template\r
+\r
+    <DIV class="latestnews">\r
+    [% request.component("/news/latest_comp") %]\r
+    </DIV>\r
+\r
+    <DIV class="links">\r
+    [% request.component("/links/list_comp") %]\r
+    </DIV>\r
+\r
+then the results of calling the C</news/latest_comp> action and template\r
+will be inserted in the C<latestnews> DIV, and the results of calling\r
+C</links/list_comp> will be placed in the C<links> DIV. Naturally, you're\r
+responsible for exporting actions and creating templates which return \r
+fragments of HTML suitable for inserting into the appropriate locations.\r
+\r
+Alternatively, if you've already got all the objects you need, you can\r
+probably just C<[% PROCESS %]> the templates directly.\r
+\r
+=head3 Bailing out with an error\r
+\r
+Maypole's error handling sucks. Something really bad has happened to the\r
+current request, and you want to stop processing now and tell the user about\r
+it.\r
+\r
+B<Solution>: Maypole's error handling sucks because you haven't written it\r
+yet. Maypole doesn't know what you want to do with an error, so it doesn't\r
+guess. One common thing to do is to display a template with an error message\r
+in it somewhere.\r
+\r
+Put this in your driver class:\r
+\r
+    sub error { \r
+        my ($r, $message) = @_;\r
+        $r->template("error");\r
+        $r->template_args->{error} = $message;\r
+        return OK;\r
+    }\r
+\r
+And then have a F<custom/error> template like so:\r
+\r
+    [% PROCESS header %]\r
+    <H2> There was some kind of error... </H2>\r
+    <P>\r
+    I'm sorry, something went so badly wrong, we couldn't recover. This\r
+    may help:\r
+    </P>\r
+    <DIV CLASS="messages"> [% error %] </DIV>\r
+\r
+Now in your actions you can say things like this:\r
+\r
+    if (1 == 0) { return $r->error("Sky fell!") }\r
+\r
+This essentially uses the template switcheroo hack to always display the\r
+error template, while populating the template with an C<error> parameter.\r
+Since you C<return $r-E<gt>error>, this will terminate the processing\r
+of the current action.\r
+\r
+The really, really neat thing about this hack is that since C<error>\r
+returns C<OK>, you can even use it in your C<authenticate> routine:\r
+\r
+    sub authenticate {\r
+        my ($self, $r) = @_;\r
+        $r->get_user;\r
+        return $r->error("You do not exist. Go away.")\r
+            if $r->user and $r->user->status ne "real";\r
+        ...\r
+    }\r
+\r
+This will bail out processing the authentication, the model class, and\r
+everything, and just skip to displaying the error message. \r
+\r
+Non-showstopper errors or other notifications are best handled by tacking a\r
+C<messages> template variable onto the request:\r
+\r
+    if ((localtime)[6] == 1) {\r
+        push @{$r->template_args->{messages}}, "Warning: Today is Monday";\r
+    }\r
+\r
+Now F<custom/messages> can contain:\r
+\r
+    [% IF messages %]\r
+    <DIV class="messages">\r
+    <UL>\r
+        [% FOR message = messages %]\r
+           <LI> [% message %] </LI>\r
+        [% END %]\r
+    </UL>\r
+    </DIV>\r
+    [% END %]\r
+\r
+And you can display messages to your user by adding C<PROCESS messages> at an\r
+appropriate point in your template; you may also want to use a template\r
+switcheroo to ensure that you're displaying a page that has the messages box in\r
+it.\r
+\r
+=head2 Authentication and Authorization hacks\r
+\r
+The next series of hacks deals with providing the concept of a "user" for\r
+a site, and what you do with one when you've got one.\r
+\r
+=head3 Logging In\r
+\r
+You need the concept of a "current user".\r
+\r
+B<Solution>: Use something like\r
+L<Maypole::Plugin::Authentication::UserSessionCookie> to authenticate\r
+a user against a user class and store a current user object in the\r
+request object.\r
+\r
+C<UserSessionCookie> provides the C<get_user> method which tries to get\r
+a user object, either based on the cookie for an already authenticated\r
+session, or by comparing C<user> and C<password> form parameters\r
+against a C<user> table in the database. Its behaviour is highly\r
+customizable and described in its documentation.\r
+\r
+=head3 Pass-through login\r
+\r
+You want to intercept a request from a non-logged-in user and have\r
+them log in before sending them on their way to wherever they were\r
+originally going. Override C<Maypole::authenticate> in your driver\r
+class, something like this:\r
+\r
+B<Solution>:\r
+\r
+    use Maypole::Constants; # Otherwise it will silently fail!\r
+\r
+    sub authenticate {\r
+        my ($self, $r) = @_;\r
+        $r->get_user;\r
+        return OK if $r->user;\r
+        # Force them to the login page.\r
+        $r->template("login");\r
+        return OK;\r
+    }\r
+\r
+This will display the C<login> template, which should look something\r
+like this:\r
+\r
+    [% INCLUDE header %]\r
+\r
+      <h2> You need to log in </h2>\r
+\r
+    <DIV class="login">\r
+    [% IF login_error %]\r
+       <FONT COLOR="#FF0000"> [% login_error %] </FONT>\r
+    [% END %]\r
+      <FORM ACTION="[% base ; '/' ; request.path %]" METHOD="post">\r
+    Username: \r
+        <INPUT TYPE="text" NAME="[% config.auth.user_field || "user" %]"><BR>\r
+    Password: <INPUT TYPE="password" NAME="password"> <BR>\r
+    <INPUT TYPE="submit">\r
+    </FORM>\r
+    </DIV>\r
+    [% INCLUDE footer %]\r
+\r
+Notice that this request gets C<POST>ed back to wherever it came from, using\r
+C<request.path>. This is because if the user submits correct credentials,\r
+C<get_user> will now return a valid user object, and the request will pass\r
+through unhindered to the original URL.\r
+\r
+=head3 Logging Out\r
+\r
+Now your users are logged in, you want a way of having them log out\r
+again and taking the authentication cookie away from them, sending\r
+them back to the front page as an unprivileged user.\r
+\r
+B<Solution>: Just call the C<logout> method of\r
+C<Maypole::Plugin::Authentication::UserSessionCookie>. You may also want\r
+to use the template switcheroo hack to send them back to the frontpage.\r
+\r
+=head3 Multi-level Authorization\r
+\r
+You have both a global site access policy (for instance, requiring a\r
+user to be logged in except for certain pages) and a policy for\r
+particular tables. (Only allowing an admin to delete records in some\r
+tables, say, or not wanting people to get at the default set of methods\r
+provided by the model class.) \r
+\r
+You don't know whether to override the global C<authenticate> method or\r
+provide one for each class.\r
+\r
+B<Solution>: Do both.\r
+Maypole checks whether there is an C<authenticate> method for the model\r
+class (e.g. BeerDB::Beer) and if so calls that. If there's no such\r
+method, it calls the default global C<authenticate> method in C<Maypole>,\r
+which always succeeds. You can override the global method as we saw\r
+above, and you can provide methods in the model classes.\r
+\r
+To use per-table access control you can just add methods to your model\r
+subclasses that specify individual policies, perhaps like this:\r
+\r
+    sub authenticate { # Ensure we can only create, reject or accept\r
+        my ($self, $r) = @_;\r
+        return OK if $r->action =~ /^(issue|accept|reject|do_edit)$/;\r
+        return; # fail if any other action\r
+    }\r
+\r
+If you define a method like this, the global C<authenticate> method will\r
+not be called, so if you want it to be called you need to do so\r
+explicitly:\r
+\r
+    sub authenticate { # Ensure we can only create, reject or accept\r
+        my ($self, $r) = @_;\r
+        return unless $r->authenticate($r) == OK; # fail if not logged in\r
+        # now it's safe to use $r->user\r
+        return OK if $r->action =~ /^(accept|reject)$/\r
+            or ($r->user eq 'fred' and $r->action =~ /^(issue|do_edit)$/);\r
+        return; # fail if any other action\r
+    }\r
+\r
+=head2 Creating and editing hacks\r
+\r
+These hacks particularly deal with issues related to the C<do_edit>\r
+built-in action.\r
+\r
+=head3 Limiting data for display\r
+\r
+You want the user to be able to type in some text that you're later\r
+going to display on the site, but you don't want them to stick images in\r
+it, launch cross-site scripting attacks or otherwise insert messy HTML.\r
+\r
+B<Solution>: Use the L<CGI::Untaint::html> module to sanitize the HTML\r
+on input. C<CGI::Untaint::html> uses L<HTML::Sanitizer> to ensure that\r
+tags are properly closed and can restrict the use of certain tags and\r
+attributes to a pre-defined list.\r
+\r
+Simply replace:\r
+\r
+    App::Table->untaint_columns(\r
+        text      => [qw/name description/]\r
+    );\r
+\r
+with:\r
+\r
+    App::Table->untaint_columns(\r
+        html      => [qw/name description/]\r
+    );\r
+\r
+And incoming HTML will be checked and cleaned before it is written to\r
+the database.\r
+\r
+=head3 Getting data from external sources\r
+\r
+You want to supplement the data received from a form with additional\r
+data from another source.\r
+\r
+B<Solution>: Munge the contents of C< $r-E<gt>params > before jumping\r
+to the original C<do_edit> routine. For instance, in this method,\r
+we use a L<Net::Amazon> object to fill in some fields of a database row\r
+based on an ISBN:\r
+\r
+    use Net::Amazon;\r
+    my $amazon = Net::Amazon->new(token => 'YOUR_AMZN_TOKEN');\r
+\r
+    ...\r
+\r
+    sub create_from_isbn :Exported {\r
+       my ($self, $r) = @_;\r
+       my $book_info = $amazon->search(asin => $r->params->{isbn})->properties;\r
+\r
+       # Rewrite the CGI parameters with the ones from Amazon\r
+       $r->params->{title} = $book_info->title;\r
+       $r->params->{publisher} = $book_info->publisher;\r
+       $r->params->{year} = $book_info->year;\r
+       $r->params->{author} = join('and', $book_info->authors());\r
\r
+       # And jump to the usual edit/create routine\r
+       $self->do_edit($r);\r
+    }\r
+\r
+The request will carry on as though it were a normal C<do_edit> POST, but\r
+with the additional fields we have provided.\r
+You might also want to add a template switcheroo so the user can verify\r
+the details you imported.\r
+\r
+=head3 Catching errors in a form\r
+\r
+A user has submitted erroneous input to an edit/create form. You want to\r
+send him back to the form with errors displayed against the erroneous\r
+fields, but have the other fields maintain the values that the user\r
+submitted.\r
+\r
+B<Solution>: This is basically what the default C<edit> template and\r
+C<do_edit> method conspire to do, but it's worth highlighting again how\r
+they work. \r
+\r
+If there are any errors, these are placed in a hash, with each error\r
+keyed to the erroneous field. The hash is put into the template as\r
+C<errors>, and we process the same F<edit> template again:\r
+\r
+        $r->template_args->{errors} = \%errors;\r
+        $r->template("edit");\r
+\r
+This throws us back to the form, and so the form's template should take\r
+note of the errors, like so:\r
+\r
+     FOR col = classmetadata.columns;\r
+        NEXT IF col == "id";\r
+        "<P>";\r
+        "<B>"; classmetadata.colnames.$col; "</B>";\r
+        ": ";\r
+            item.to_field(col).as_HTML;\r
+        "</P>";\r
+        IF errors.$col;\r
+            "<FONT COLOR=\"#ff0000\">"; errors.$col; "</FONT>";\r
+        END;\r
+    END;\r
+\r
+If we're designing our own templates, instead of using generic ones, we\r
+can make this process a lot simpler. For instance:\r
+\r
+    <TR><TD>\r
+    First name: <INPUT TYPE="text" NAME="forename">\r
+    </TD>\r
+    <TD>\r
+    Last name: <INPUT TYPE="text" NAME="surname">\r
+    </TD></TR>\r
+\r
+    [% IF errors.forename OR errors.surname %]\r
+        <TR>\r
+        <TD><SPAN class="error">[% errors.forename %]</SPAN> </TD>\r
+        <TD><SPAN class="error">[% errors.surname %]</SPAN> </TD>\r
+        </TR>\r
+    [% END %]\r
+\r
+The next thing we want to do is to put the originally-submitted values\r
+back into the form. We can do this relatively easily because Maypole\r
+passes the Maypole request object to the form, and the POST parameters\r
+are going to be stored in a hash as C<request.params>. Hence:\r
+\r
+    <TR><TD>\r
+    First name: <INPUT TYPE="text" NAME="forename"\r
+    VALUE="[%request.params.forename%]">\r
+    </TD>\r
+    <TD>\r
+    Last name: <INPUT TYPE="text" NAME="surname"\r
+    VALUE="[%request.params.surname%]"> \r
+    </TD></TR>\r
+\r
+Finally, we might want to only re-fill a field if it is not erroneous, so\r
+that we don't get the same bad input resubmitted. This is easy enough:\r
+\r
+    <TR><TD>\r
+    First name: <INPUT TYPE="text" NAME="forename"\r
+    VALUE="[%request.params.forename UNLESS errors.forename%]">\r
+    </TD>\r
+    <TD>\r
+    Last name: <INPUT TYPE="text" NAME="surname"\r
+    VALUE="[%request.params.surname UNLESS errors.surname%]"> \r
+    </TD></TR>\r
+\r
+=head3 Uploading files and other data\r
+\r
+You want the user to be able to upload files to store in the database.\r
+\r
+B<Solution>: It's messy.\r
+\r
+First, we set up an upload form, in an ordinary dummy action. Here's\r
+the action:\r
+\r
+    sub upload_picture : Exported {}\r
+\r
+And here's the F<custom/upload_picture> template:\r
+\r
+    <FORM action="/user/do_upload" enctype="multipart/form-data" method="POST">\r
+\r
+    <P> Please provide a picture in JPEG, PNG or GIF format:\r
+    </P>\r
+    <INPUT TYPE="file" NAME="picture">\r
+    <BR>\r
+    <INPUT TYPE="submit">\r
+    </FORM>\r
+\r
+(Although you'll probably want a bit more HTML around it than that.)\r
+\r
+Now we need to write the C<do_upload> action. At this point we have to get a\r
+little friendly with the front-end system. If we're using L<Apache::Request>,\r
+then the C<upload> method of the C<Apache::Request> object (which\r
+L<Apache::MVC> helpfully stores in C<$r-E<gt>{ar}>) will work for us:\r
+\r
+    sub do_upload :Exported {\r
+        my ($class, $r) = @_;\r
+        my $user = $r->user;\r
+        my $upload = $r->ar->upload("picture");\r
+\r
+This returns a L<Apache::Upload> object, which we can query for its\r
+content type and a file handle from which we can read the data. It's\r
+also worth checking the image isn't going to be too massive before we\r
+try reading it and running out of memory, and that the content type is\r
+something we're prepared to deal with. \r
+\r
+    if ($upload) {\r
+        my $ct = $upload->info("Content-type");\r
+        return $r->error("Unknown image file type $ct")\r
+            if $ct !~ m{image/(jpeg|gif|png)};\r
+        return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)\r
+            if $upload->size > MAX_IMAGE_SIZE;\r
+\r
+        my $fh = $upload->fh;\r
+        my $image = do { local $/; <$fh> };\r
+\r
+Don't forget C<binmode()> in there if you're on a platform that needs it.\r
+Now we can store the content type and data into our database, store it\r
+into a file, or whatever:\r
+\r
+        $r->user->photo_type($ct);\r
+        $r->user->photo($image);\r
+    }\r
+\r
+And finally, we use our familiar template switcheroo hack to get back to\r
+a useful page:\r
+\r
+        $r->objects([ $user ]);\r
+        $r->template("view");\r
+    }\r
+\r
+Now, as we've mentioned, this only works because we're getting familiar with\r
+C<Apache::Request> and its C<Apache::Upload> objects. If we're using\r
+L<CGI::Maypole> instead, we can write the action in a similar style:\r
+\r
+    sub do_upload :Exported {\r
+        my ($class, $r) = @_;\r
+        my $user = $r->user;\r
+        my $cgi = $r->cgi;\r
+        if ($cgi->upload == 1) { # if there was one file uploaded\r
+            my $filename = $cgi->param('picture');\r
+            my $ct = $cgi->upload_info($filename, 'mime');\r
+            return $r->error("Unknown image file type $ct")\r
+                if $ct !~ m{image/(jpeg|gif|png)};\r
+            return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)\r
+                if $cgi->upload_info($filename, 'size') > MAX_IMAGE_SIZE;\r
+            my $fh = $cgi->upload($filename);\r
+            my $image = do { local $/; <$fh> };\r
+            $r->user->photo_type($ct);\r
+            $r->user->photo($image);\r
+        }\r
+\r
+        $r->objects([ $user ]);\r
+        $r->template("view");\r
+    }\r
+\r
+It's easy to adapt this to upload multiple files if desired.\r
+You will also need to enable uploads in your driver initialization,\r
+with the slightly confusing statement:\r
+\r
+    $CGI::Simple::DISABLE_UPLOADS = 0; # enable uploads\r
+\r
+Combine with the "Displaying pictures" hack above for a happy time.\r
+\r
+=head2 Links\r
+\r
+L<Contents|Maypole::Manual>,\r
+Next L<Flox|Maypole::Manual::Flox>,\r
+Previous L<The Beer Database, Twice|Maypole::Manual::Beer>\r
+\r
diff --git a/lib/Maypole/Manual/Inheritance.pod b/lib/Maypole/Manual/Inheritance.pod
new file mode 100644 (file)
index 0000000..bff339d
--- /dev/null
@@ -0,0 +1,303 @@
+\r
+=head1 NAME\r
+\r
+Maypole::Manual::Inheritance - structure of a Maypole application\r
+\r
+=head1 DESCRIPTION\r
+\r
+Discusses the inheritance structure of a basic and a more advanced Maypole\r
+application.\r
+\r
+=head1 CONVENTIONS\r
+          \r
+=over 4\r
+\r
+=item inheritance\r
+\r
+        +\r
+        |\r
+     +-   -+\r
+       |\r
+       +\r
+       \r
+=item notes\r
+\r
+    target *-------- note about the target\r
+\r
+=item association\r
+\r
+    source ------> target\r
+\r
+=back\r
+\r
+=head1 Structure of a standard Maypole application\r
+\r
+A minimal Maypole application (such as the Beer database example from the\r
+L<Maypole> synopsis) consists of a custom driver class (BeerDB.pm), a set of\r
+auto-generated model classes, and a view class:\r
+\r
+\r
+           THE DRIVER\r
+                                          +------- init() is a factory method,\r
+                   1      Maypole         |           it sets up the view\r
+   Maypole::Config <----- config();       |              classes\r
+   model();               init(); *-------+                           THE VIEW\r
+    |                     view_object(); -------+\r
+    |    +--------------* setup();              |      Maypole::View::Base\r
+    |    |                   +                  |              +\r
+    |    |                   |                  |     1        |\r
+    |    |    PLUGINS    Apache::MVC *-----+    +-----> Maypole::View::TT\r
+    |    |       +           +             |             (or another view class)\r
+    |    |       |           |             |\r
+    |    |       +-----+-----+             |\r
+    |    |             |                   |\r
+    |    |           BeerDB                +----- or CGI::Maypole\r
+    |    |                                         or MasonX:::Maypole\r
+    |    |\r
+    |   setup() is a factory method,\r
+    |     it sets up the model\r
+    |         classes\r
+    |\r
+    |                                             THE MODEL\r
+    |\r
+    |  Maypole::Model::Base    Class::DBI\r
+    |             +             +      +\r
+    |             |             |      |\r
+    +-------> Maypole::Model::CDBI   Class::DBI::<db_driver>\r
+                      +                     +\r
+                      |                     |\r
+           +------------+--------+-------+---------+\r
+           |            |        |       |         |\r
+       BeerDB::Pub      |   BeerDB::Beer | BeerDB::Brewery\r
+       beers();         |   pubs();      | beers();\r
+                        |   brewery();   |\r
+                        |   style();     |\r
+          BeerDB::Handpump               |\r
+          pub();                      BeerDB::Style\r
+          beer();                     beers();\r
+\r
+=head2 What about Maypole::Application - loading plugins\r
+\r
+The main job of L<Maypole::Application> is to insert the plugins into the\r
+hierarchy. It is also the responsibility of L<Maypole::Application> to decide\r
+which frontend to use. It builds the list of plugins, then pushes them onto the\r
+driver's C<@ISA>, then pushes the frontend onto the end of the driver's C<@ISA>.\r
+So method lookup first searches all the plugins, before searching the frontend\r
+and finally L<Maypole> itself.\r
+\r
+From Maypole 2.11, L<Maypole::Application> makes no appearance in the\r
+inheritance structure of a Maypole application. (In prior versions,\r
+L<Maypole::Application> would make itself inherit the plugins, and then insert\r
+itself in the hierarchy, but this was unnecessary).\r
+\r
+=head2 Who builds the model?\r
+\r
+First, remember we are talking about the standard, unmodified Maypole here. It\r
+is possible, and common, to override some or all of this stage and build a\r
+customised model. See below - An advanced Maypole application - for one\r
+approach. Also, see L<Maypole's|Maypole> C<setup_model()> method. \r
+\r
+The standard model is built in 3 stages. \r
+\r
+First, C<Maypole::setup_model> calls C<setup_database> on the Maypole model\r
+class, in this case L<Maypole::Model::CDBI>. C<setup_database> then uses\r
+L<Class::DBI::Loader> to autogenerate individual L<Class::DBI> classes for each\r
+of the tables in the database (C<BeerDB::Beer>, C<BeerDB::Pub> etc).\r
+L<Class::DBI::Loader> identifies the appropriate L<Class::DBI> subclass and\r
+inserts it into each of these table classes' C<@ISA> ( C<<\r
+Class::DBI::<db_driver> >> in the diagrams)..\r
+\r
+Next, C<Maypole::setup> B<unshifts> L<Maypole::Model::CDBI> onto the C<@ISA> \r
+array of each of these classes. \r
+\r
+Finally, the relationships among these tables are set up. Either do this\r
+manually, using the standard L<Class::DBI> syntax for configuring table\r
+relationships, or try L<Class::DBI::Relationship> (which you can use via\r
+L<Maypole::Plugin::Relationship>). If you use the plugin, you need to set up the\r
+relationships configuration before calling C<setup()>. Be aware that some people\r
+like the convenience of L<Class::DBI::Relationship>, others dislike the\r
+abstraction. YMMV. \r
+\r
+=head1 An advanced Maypole application\r
+\r
+We'll call it C<BeerDB2>.\r
+\r
+Maypole is a framework, and you can replace different bits as you wish. So what \r
+follows is one example of good practice, other people may do things differently. \r
+\r
+We assume this application is being built from the ground up, but it will often\r
+be straightforward to adapt an existing L<Class::DBI> application to this\r
+general model.\r
+\r
+The main idea is that the autogenerated Maypole model is used as a layer on top\r
+of a separate L<Class::DBI> model. I am going to refer to this model as the\r
+'Offline' model, and the Maypole classes as the 'Maypole' model. The idea is\r
+that the Offline model can (potentially or in actuality) be used as part of\r
+another application, perhaps a command line program or a cron script, whatever.\r
+The Offline model does not know about the Maypole model, whereas the Maypole\r
+model does know about the Offline model.\r
+\r
+Let's call the offline model C<OfflineBeer>. As a traditional L<Class::DBI>\r
+application, individual table classes in this model will inherit from a common\r
+base (C<OfflineBeer>), which inherits from L<Class::DBI>).\r
+\r
+One advantage of this approach is that you can still use Maypole's autogenerated\r
+model. Another is that you do not mix online and offline code in the same\r
+packages.\r
+\r
+=head2 Building it\r
+\r
+Build a driver in a similar way as for the basic app, calling C<setup()> after\r
+setting up all the configuration. \r
+\r
+It is a good habit to use a custom Maypole model class for each application, as\r
+it's a likely first target for customisation. Start it like this:\r
+\r
+    package BeerDB2::Maypole::Model;\r
+    use strict;\r
+    use warnings;\r
+    use base 'Maypole::Model::CDBI';\r
+    1;\r
+    \r
+You can add methods which should be shared by all table classes to this package \r
+as and when required.\r
+    \r
+Configure it like this, before the C<setup()> call in the driver class:\r
+\r
+    # in package BeerDB2\r
+    __PACKAGE__->config->model('BeerDB2::Maypole::Model');\r
+    __PACKAGE__->setup;\r
+\r
+The C<setup()> call will ensure your custom model is loaded via C<require>.\r
+\r
+B<Note>: by default, this will create Maypole/CDBI classes for all the tables in\r
+the database. You can control this by passing options for L<Class::DBI::Loader>\r
+in the call to C<setup()>.\r
+\r
+For each class in the model, you need to create a separate file. So for\r
+C<BeerDB2::Beer>, you would write:\r
+\r
+    package BeerDB2::Beer;\r
+    use strict;\r
+    use warnings;\r
+    use base 'OfflineBeer::Beer';\r
+    1;\r
+    \r
+From Maypole 2.11, this package will be loaded automatically during C<setup()>,\r
+and C<BeerDB2::Maypole::Model> is B<unshifted> onto it's C<@ISA>.\r
+\r
+Configure relationships either in the individual C<OfflineBeer::*> classes, or\r
+else all together in C<OfflineBeer> itself i.e. not in the Maypole model. This \r
+way, you only define the relationships in one place.\r
+\r
+The resulting model looks like this:\r
+\r
+                                       Class::DBI\r
+    MAYPOLE 'MODEL'                       |\r
+                                          |\r
+   Maypole::Model::Base                   |\r
+           +                              |\r
+           |       +-----------------+----+-----------------+\r
+           |       |                 |                      |\r
+           |       |                 |                      |\r
+     Maypole::Model::CDBI            |                      |     OFFLINE\r
+             +                       |                      |        MODEL\r
+             |                       |                      |\r
+     BeerDB2::Maypole::Model  Class::DBI::<db_driver>  OfflineBeer\r
+       +                             +                      +\r
+       |                             |                      |\r
+       +-----------------------------+                      |\r
+       |                                                    |\r
+       +--- BeerDB2::Pub --------+ OfflineBeer::Pub --------+\r
+       |                           beers();                 |\r
+       |                                                    |\r
+       |                           OfflineBeer::Handpump ---+\r
+       |                           beer();                  |\r
+       |                           pub();                   |\r
+       |                                                    |\r
+       +--- BeerDB2::Beer -------+ OfflineBeer::Beer -------+\r
+       |                           pubs();                  |\r
+       |                           brewery();               |\r
+       |                           style();                 |\r
+       |                                                    |\r
+       +--- BeerDB2::Style ------+ OfflineBeer::Style ------+\r
+       |                           beers();                 |\r
+       |                                                    |\r
+       +--- BeerDB2::Brewery ----+ OfflineBeer::Brewery ----+\r
+                                   beers();\r
+\r
+\r
+\r
+=head3 Features\r
+\r
+1. Non-Maypole applications using the Offline model are completely isolated from\r
+the Maypole application, and need not know it exists at all.\r
+\r
+2. Methods defined in the Maypole table classes, override methods defined in the\r
+Offline table classes, because C<BeerDB2::Maypole::Model> was unshifted onto the\r
+beginning of each Maypole table class's C<@ISA>. Perl's depth first,\r
+left-to-right method lookup from e.g. C<BeerDB2::Beer> starts in\r
+C<BeerDB2::Beer>, then C<BeerDB2::Maypole::Model>, C<Maypole::Model::CDBI>,\r
+C<Maypole::Model::Base>, and C<Class::DBI>, before moving on to\r
+C<OfflineBeer::Beer> and finally C<OfflineBeer>.\r
+\r
+B<CAVEAT> - if your Offline model overrides L<Class::DBI> methods, these methods\r
+will B<not> be overridden when called from the Maypole application, because the\r
+Maypole model provides an alternative path to L<Class::DBI> which is searched\r
+first. The solution is to place such methods in a separate package, e.g.\r
+C<OfflineBeer::CDBI>. Place this B<first> in the C<@ISA> of both\r
+C<BeerDB2::Maypole::Model> and C<OfflineBeer>. Note that C<OfflineBeer::CDBI>\r
+does not itself need to inherit from L<Class::DBI>.\r
+\r
+3. Methods defined in the Maypole model base class (C<BeerDB2::Maypole::Model>),\r
+override methods in the individual Offline table classes, and in the Offline\r
+model base class (C<Offline>). \r
+\r
+4. Relationships defined in the Offline classes are inherited by the Maypole\r
+model.\r
+\r
+5. The Maypole model has full access to the underlying Offline model. \r
+\r
+=head3 Theory \r
+\r
+This layout illustrates more clearly why the Maypole model may be thought of as\r
+part of the controller, rather than part of the model of MVC. Its function is to \r
+mediate web requests, translating them into method calls on the Offline model, \r
+munging the results, and returning them via the Maypole request object. \r
+\r
+Another way of thinking about it is that Maypole implements a two-layer\r
+controller. The first layer translates a raw request into a single method call\r
+on the Maypole model layer, which then translates that call into one or more\r
+calls on the underlying model.\r
+\r
+Whatever label you prefer to use, this approach provides for clear separation of\r
+concerns between the underlying model and the web/user interface, and that's\r
+what it's all about.\r
+\r
+=head1 Advanced applications - building the model by hand ** TODO\r
+\r
+- using Maypole::Model::CDBI::Plain or Maypole::FormBuilder::Model::Plain\r
+- setup_model() and load_model_subclass()\r
+- cutting out all those separate paths to CDBI - they're confusing \r
+\r
+\r
+=head1 Method inheritance ** TODO\r
+\r
+More description of Perl's left-to-right, depth-first method lookup, and where\r
+it's particularly important in Maypole.\r
+\r
+\r
+          \r
+=head1 AUTHOR\r
+\r
+David Baird, C<< <cpan@riverside-cms.co.uk> >>\r
+\r
+=head1 COPYRIGHT & LICENSE\r
+\r
+Copyright 2005 David Baird, All Rights Reserved.\r
+\r
+This text is free documentation; you can redistribute it and/or modify it\r
+under the same terms as the Perl documentation itself.\r
+\r
+=cut\r
+\r
diff --git a/lib/Maypole/Manual/Install.pod b/lib/Maypole/Manual/Install.pod
new file mode 100644 (file)
index 0000000..1c4067a
--- /dev/null
@@ -0,0 +1,52 @@
+\r
+=head1 NAME\r
+\r
+Maypole::Manual::Install - installing Maypole\r
+\r
+=head1 Installing Maypole\r
+\r
+The first thing you're going to need to do to get Maypole running is to\r
+install it. Maypole needs an absolute shedload of Perl modules from CPAN\r
+to do its job. I am unrepentant about this. Maypole does a lot of work,\r
+so that you don't have to. This is called code re-use, and if we're\r
+serious about code re-use, then Maypole should be re-using as much code\r
+as possible in terms of Perl modules. In another sense, this gives the\r
+impression that Maypole doesn't actually do all that much itself,\r
+because all it's doing is gluing together already-existing code. Well,\r
+welcome to code re-use.\r
+\r
+The downside of code re-use is, of course, that you then have to install\r
+a shedload of Perl modules from CPAN. If you're using OpenBSD or\r
+FreeBSD, the wonderful ports system will be your friend. There's a\r
+Maypole port in C<p5-Maypole>. Just type C<make install>.\r
+\r
+Debian users, hang in there. There's a package coming.\r
+\r
+For other Unices, the L<CPANPLUS> or C<CPAN> modules will help with\r
+this. If you don't have C<CPANPLUS> installed, my recommendation is to\r
+use C<perl -MCPAN -e install CPANPLUS> to install it and then throw\r
+C<CPAN.pm> away. In any case, one of these two should get all that\r
+Maypole needs:\r
+\r
+    % perl -MCPANPLUS -e 'install Maypole'\r
+    % perl -MCPAN -e 'install Maypole'\r
+\r
+    \r
+Information on installing under Windows is available from the wiki - \r
+http://maypole.perl.org/?WindowsInstall\r
+\r
+More information of installing under various Linux flavours is available on the\r
+Wiki - http://maypole.perl.org/?LinuxInstall\r
+\r
+You're also going to need a database server and a web server. For\r
+databases, I recommend SQLite (if you install the C<DBD::SQLite> module,\r
+you get the SQLite library for free) for prototyping and mysql for\r
+production; heavier duty users should use Postgresql or Oracle - Maypole\r
+should be happy with them all. Maypole is happiest when running under\r
+Apache C<mod_perl>, with the C<Apache::Request> module installed, but as\r
+I said, it is a blank slate, and everything is customizable. There is a\r
+C<CGI::Maypole> frontend available to run as a standalone CGI script.\r
+\r
+As well as the documentation embedded in the Perl modules the distribution\r
+also includes the manual, of which this is a part. You can access it using the\r
+perldoc command, the man command, or by browsing CPAN.\r
index 5f310fe7f091a7c290b01aef776b55dbe525ad4b..112effcb44d90e3823319adf4b994f5dfc63cb68 100644 (file)
@@ -41,8 +41,8 @@ make writing Maypole applications a lot easier:
 
     package Maypole::Model::CDBI;
     use base qw(Maypole::Model::Base Class::DBI);
-    use Class::DBI::AsForm;
-    use Class::DBI::FromCGI;
+    use Maypole::Model::CDBI::AsForm;
+    use Class::DBI::FromCGI;  # probabyly broken . 
     use Class::DBI::Loader;
     use Class::DBI::AbstractSearch;
     use Class::DBI::Plugin::RetrieveAll;
@@ -113,10 +113,6 @@ We'll look more at how to put together actions in the
 L<Standard Templates and Actions|Maypole::Manual::StandardTemplates>
 chapter and our case studies.
 
-=head2 What Maypole wants from a model
-
-=head2 Building your own model class
-
 =head2 Links
 
 L<Contents|Maypole::Manual>,
index f0582c63225344c0b16b7d1c42f948be75cba65f..7389b60875798bdb0807142c905258e0c4d49791 100644 (file)
@@ -8,11 +8,12 @@ This version written for Maypole 2.10
 
 =head1 LOADING PLUGINS
 
-Plugins occupy the C<Maypole::Plugin::*> namespace on CPAN. At time of writing, there are 16 plugin modules
-available - see http://search.cpan.org/search?query=Maypole%3A%3APlugin&mode=module
+Plugins occupy the C<Maypole::Plugin::*> namespace on CPAN. At time of writing, 
+there are 16 plugin modules available - see 
+http://search.cpan.org/search?query=Maypole%3A%3APlugin&mode=module
 
-Plugins are loaded into a Maypole application by L<Maypole::Application>. For instance, to add L<HTML::QuickTable>
-support to the BeerDB example application:
+Plugins are loaded into a Maypole application by L<Maypole::Application>. For 
+instance, to add L<HTML::QuickTable> support to the BeerDB example application:
 
    package BeerDB;
    use strict;
@@ -22,11 +23,12 @@ support to the BeerDB example application:
 
 Note that the leading C<Maypole::Plugin::*> is omitted.
 
-For some plugins, that's it. You probably have a bunch of new methods available on your Maypole request objects -
-see the documentation for the plugin.
+For some plugins, that's it. You probably have a bunch of new methods available 
+on your Maypole request objects - see the documentation for the plugin.
 
 For others, you will need to set configuration variables or customise other
-parts of the application. For instance, to add sessions to your application, you can use L<Maypole::Plugin::Session>:
+parts of the application. For instance, to add sessions to your application, you 
+can use L<Maypole::Plugin::Session>:
 
    package BeerDB;
    use strict;
@@ -34,7 +36,9 @@ parts of the application. For instance, to add sessions to your application, you
 
    use Maypole::Application( 'Session' );
 
-That's all, if you're willing to stick with the defaults (L<Apache::Session::File> backend, session and lock files in C</tmp/sessions> and C</tmp/sessionlock>). Otherwise, you need to supply some configuration:
+That's all, if you're willing to stick with the defaults 
+(L<Apache::Session::File> backend, session and lock files in C</tmp/sessions> 
+and C</tmp/sessionlock>). Otherwise, you need to supply some configuration:
 
    __PACKAGE__->config->session( { class => "Apache::Session::Flex",
                                    args  => {
@@ -45,23 +49,28 @@ That's all, if you're willing to stick with the defaults (L<Apache::Session::Fil
                                        }
                                    } );
 
-The plugin module is responsible for adding slots to L<Maypole::Config>, in this case, the C<session> accessor.
+The plugin module is responsible for adding slots to L<Maypole::Config>, in this 
+case, the C<session> accessor.
 
 =head1 WRITING PLUGINS
 
 =head2 Modifying the Maypole request object
 
-Plugins are inserted into the C<@ISA> of the Maypole request object. So method calls on the request object will
-first search the plugin classes, before looking in L<Maypole>. Methods defined in the plugin are
-therefore directly available on the request. That also goes for methods inherited by the plugin. I'm not aware
-of any plugins that currently inherit from another package, but there's no reason not to.
+Plugins are inserted into the C<@ISA> of the Maypole request object. So method 
+calls on the request object will first search the plugin classes, before looking 
+in L<Maypole>. Methods defined in the plugin are therefore directly available on 
+the request. That also goes for methods inherited by the plugin. I'm not aware
+of any plugins that currently inherit from another package, but there's no 
+reason not to.
 
-Note that if you need simple accessor methods on the request, you can add them by saying
+Note that if you need simple accessor methods on the request, you can add them 
+by saying
 
    Maypole->mk_accessors( qw/ fee fi fo / );
 
-at the start of your plugin. Under mod_perl, you've just added these accessors to B<all> Maypole applications
-on the server, even ones that do not use this plugin. You could instead make the call inside the C<setup> method:
+at the start of your plugin. Under mod_perl, you've just added these accessors 
+to B<all> Maypole applications on the server, even ones that do not use this 
+plugin. You could instead make the call inside the C<setup> method:
 
    $r->mk_accessors( qw/ fee fi fo / );
 
@@ -69,20 +78,24 @@ Now the accessors are only added to applications that use this plugin.
 
 =head2 Initialisation with C<setup>
 
-After loading plugins via L<Maypole::Application>, setting configuration variables in calls to
-C<< __PACKAGE__->config->foo( 'bar' ) >>, and optionally defining custom request methods, your
-application should call its C<setup> method, generally including arguments for the database connection:
+After loading plugins via L<Maypole::Application>, setting configuration 
+variables in calls to C<< __PACKAGE__->config->foo( 'bar' ) >>, and optionally 
+defining custom request methods, your application should call its C<setup> 
+method, generally including arguments for the database connection:
 
    __PACKAGE__->setup( $dsn, $user, $pass, @more_args );
 
-All of these arguments will be passed to the C<setup_database> method of the model class.
+All of these arguments will be passed to the C<setup_database> method of the 
+model class.
 
-C<Maypole::setup()> is responsible for loading the model class, calling the C<setup_database> method
-on the model class, and making each table class in the application inherit from the model. It is therefore
-recommended that you call C<setup> B<after> setting up all your configuration options.
+C<Maypole::setup()> is responsible for loading the model class, calling the 
+C<setup_database> method on the model class, and making each table class in the 
+application inherit from the model. It is therefore recommended that you call 
+C<setup> B<after> setting up all your configuration options.
 
-Plugins can intercept the call to C<setup> to carry out their own initialisation, as long as they propagate
-the call up through the hierarchy. A common idiom for this is:
+Plugins can intercept the call to C<setup> to carry out their own 
+initialisation, as long as they propagate the call up through the hierarchy. A 
+common idiom for this is:
 
    Maypole::Plugin::Foo;
    use strict;
@@ -102,20 +115,23 @@ the call up through the hierarchy. A common idiom for this is:
        # do something with $option
    }
 
-L<NEXT> is a replacement for the built-in C<SUPER> syntax. C<SUPER> dispatches a call to the superclass
-of the current package - B<but> it determines the superclass at compile time. At that time, the superclass
-is something like C<main::>. L<NEXT> does the superclass lookup at runtime, after L<Maypole::Application> has
-inserted the plugin into the request class's inheritance chain.
+L<NEXT> is a replacement for the built-in C<SUPER> syntax. C<SUPER> dispatches a 
+call to the superclass of the current package - B<but> it determines the 
+superclass at compile time. At that time, the superclass is something like 
+C<main::>. L<NEXT> does the superclass lookup at runtime, after 
+L<Maypole::Application> has inserted the plugin into the request class's 
+inheritance chain.
 
-The C<DISTINCT> modifier ensures each plugin's C<setup> method is only called once, and protects against
-diamond inheritance. This may or may not be an issue in your app - and if you always use the C<DISTINCT>
-syntax, it won't be.
+The C<DISTINCT> modifier ensures each plugin's C<setup> method is only called 
+once, and protects against diamond inheritance. This may or may not be an issue 
+in your app - and if you always use the C<DISTINCT> syntax, it won't be.
 
-Notice that the C<setup> call is re-dispatched before running the plugin's own initialisation code. This
-allows C<Maypole::setup()> to set up the database, model, and table classes, before your plugin starts tweaking
-things.
+Notice that the C<setup> call is re-dispatched before running the plugin's own 
+initialisation code. This allows C<Maypole::setup()> to set up the database, 
+model, and table classes, before your plugin starts tweaking things.
 
-You can use the C<setup> method to load modules into the request class namespace. L<Maypole::Plugin::I18N> has:
+You can use the C<setup> method to load modules into the request class 
+namespace. L<Maypole::Plugin::I18N> has:
 
    sub setup {
        my $r = shift;
@@ -127,35 +143,39 @@ You can use the C<setup> method to load modules into the request class namespace
          Path   => $r->config->lexicon;
 }
 
-Now the application namespace has a C<_loc> function (exported by L<Locale::Maketext::Simple>), (plus C<lang> and
-C<maketext> methods inherited from L<Maypole::Plugin::I18N>).
+Now the application namespace has a C<_loc> function (exported by 
+L<Locale::Maketext::Simple>), (plus C<lang> and C<maketext> methods inherited 
+from L<Maypole::Plugin::I18N>).
 
 =head3 More initialisation with C<init>
 
 L<Maypole> also defines an C<init> method. It
-pulls the name of the view class from the config, loads it, instantiates an object in the view class, and
-sets this in the C<view_object> config slot.
+pulls the name of the view class from the config, loads it, instantiates an 
+object in the view class, and sets this in the C<view_object> config slot.
 
 In CGI applications, C<init> is called at the start of every request.
 
-Under mod_perl, this method will only ever be called once per server child, at the start of the first request after
-server startup. If instead, you call this method in your application module (after the call to C<setup>),
-then the code loaded by this call will be shared by all child servers.
+Under mod_perl, this method will only ever be called once per server child, at 
+the start of the first request after server startup. If instead, you call this 
+method in your application module (after the call to C<setup>), then the code 
+loaded by this call will be shared by all child servers.
 
 See B<Hacking the view> for a plugin that uses C<init>.
 
 =head2 Adding configuration
 
-The configuration object can be retrieved from the Maypole request object (C<< $r->config >>) or as a class method
-on the application (e.g. C<< BeerDB->config >>).
+The configuration object can be retrieved from the Maypole request object 
+(C<< $r->config >>) or as a class method on the application (e.g. 
+C<< BeerDB->config >>).
 
-If your plugin needs some custom configuration settings, you can add methods to the config object by
-saying
+If your plugin needs some custom configuration settings, you can add methods to 
+the config object by saying
 
    Maypole::Config->mk_accessors( qw/ foo bar baz / );
 
-at the start of your plugin. In the application, after the C<Maypole::Application> call, these methods will
-be available on the config object.
+at the start of your plugin. In the application, after the 
+C<Maypole::Application> call, these methods will be available on the config 
+object.
 
 =head2 Modifying the Maypole model
 
@@ -163,28 +183,33 @@ be available on the config object.
 
 =item Replacing the model
 
-To load a different model, set C<< __PACKAGE__->config->model( 'Custom::Model' ) >> in the application
-before calling C<setup>. You could instead set C<< $r->config->model >> before re-dispatching the C<setup> call,
-but this is going to confuse and annoy your users.
+To load a different model, set 
+C<< __PACKAGE__->config->model( 'Custom::Model' ) >> in the application
+before calling C<setup>. You could instead set C<< $r->config->model >> before 
+re-dispatching the C<setup> call, but this is going to confuse and annoy your 
+users.
 
 =item Hacking the model
 
 B<CAVEAT>: the way I do this just seems dirty, so there must be a Better Way.
 
-L<Maypole::Plugin::FormBuilder> (part of the L<Maypole::FormBuilder> distribution), in its C<setup> method,
-loads a custom pager class into the model by saying
+L<Maypole::Plugin::FormBuilder> (part of the L<Maypole::FormBuilder> 
+distribution), in its C<setup> method, loads a custom pager class into the model 
+by saying
 
    eval "package $model; use $pager";
 
-Yuk. Note that under mod_perl, you have just forced B<every> application using C<$model> to also use C<$pager>.
+Yuk. Note that under mod_perl, you have just forced B<every> application using 
+C<$model> to also use C<$pager>.
 
-C<Maypole::Plugin::AutoUntaint::setup()> loads an extra method into the model by saying
+C<Maypole::Plugin::AutoUntaint::setup()> loads an extra method into the model by 
+saying
 
    no strict 'refs';
    *{"$model\::auto_untaint"} = \&Class::DBI::Plugin::AutoUntaint::auto_untaint;
 
-Yuk again. And again, under mod_perl, now every application using C<$model> has an C<auto_untaint> method
-added to its model.
+Yuk again. And again, under mod_perl, now every application using C<$model> has 
+an C<auto_untaint> method added to its model.
 
 Same plugin, next line has
 
@@ -193,8 +218,6 @@ Same plugin, next line has
 Same yuk, same mod_perl caveat.
 
 
-
-
 =back
 
 
@@ -208,11 +231,13 @@ Again, just specify a different view in the application configuration.
 
 =item Hacking the view
 
-L<Maypole::Plugin::FormBuilder> intercepts the C<init> call to override the C<vars> method in the view class.
-First it re-dispatches the C<init> call, which will set up either a default view class and object, or those
-configured in the application. Then it builds a new view class on-the-fly, and makes this new class inherit from
-L<Maypole::FormBuilder::View> and from the original view class. Finally it replaces the C<view> and C<view_object>
-in the application's config object.
+L<Maypole::Plugin::FormBuilder> intercepts the C<init> call to override the 
+C<vars> method in the view class. First it re-dispatches the C<init> call, which 
+will set up either a default view class and object, or those configured in the 
+application. Then it builds a new view class on-the-fly, and makes this new 
+class inherit from L<Maypole::FormBuilder::View> and from the original view 
+class. Finally it replaces the C<view> and C<view_object> in the application's 
+config object.
 
    sub init
    {
diff --git a/lib/Maypole/Manual/Request.pod b/lib/Maypole/Manual/Request.pod
deleted file mode 100644 (file)
index f9788bd..0000000
+++ /dev/null
@@ -1,804 +0,0 @@
-=head1 NAME
-
-Maypole::Manual::Request - Maypole Request Hacking Cookbook
-
-=head1 DESCRIPTION
-
-Hacks; design patterns; recipes: call it what you like, this chapter is a
-developing collection of techniques which can be slotted in to Maypole
-applications to solve common problems or make the development process easier.
-
-As Maypole developers, we don't necessarily know the "best practice" for
-developing Maypole applications ourselves, in the same way that Larry Wall
-didn't know all about the best Perl programming style as soon as he wrote
-Perl. These techniques are what we're using at the moment, but they may
-be refined, modularized, or rendered irrelevant over time. But they've
-certainly saved us a bunch of hours work.
-
-=head2 Frontend hacks
-
-These hacks deal with changing the way Maypole relates to the outside world;
-alternate front-ends to the Apache and CGI interfaces, or subclassing chunks
-of the front-end modules to alter Maypole's behaviour in particular ways.
-
-=head3 Separate model class modules
-
-You want to put all the C<BeerDB::Beer> routines in a separate module,
-so you say:
-
-    package BeerDB::Beer;
-    BeerDB::Beer->has_a(brewery => "BeerDB::Brewery");
-    sub foo :Exported {}
-
-And in F<BeerDB.pm>, you put:
-
-    use BeerDB::Beer;
-
-It doesn't work.
-
-B<Solution>: It doesn't work because of the timing of the module loading.
-C<use BeerDB::Beer> will try to set up the C<has_a> relationships
-at compile time, when the database tables haven't even been set up,
-since they're set up by
-
-    BeerDB->setup("...")
-
-which does its stuff at runtime. There are two ways around this; you can
-either move the C<setup> call to compile time, like so:
-
-    BEGIN { BeerDB->setup("...") }
-
-or move the module loading to run-time (my preferred solution):
-
-    BeerDB->setup("...");
-    BeerDB::Beer->require;
-
-=head3 Debugging with the command line
-
-You're seeing bizarre problems with Maypole output, and you want to test it in
-some place outside of the whole Apache/mod_perl/HTTP/Internet/browser circus.
-
-B<Solution>: Use the L<Maypole::CLI> module to go directly from a URL to
-standard output, bypassing Apache and the network altogether.
-
-L<Maypole::CLI> is not a standalone front-end, but to allow you to debug your
-applications without having to change the front-end they use, it temporarily
-"borgs" an application. If you run it from the command line, you're expected
-to use it like so:
-
-    perl -MMaypole::CLI=Application -e1 'http://your.server/path/table/action'
-
-For example:
-
-    perl -MMaypole::CLI=BeerDB -e1 'http://localhost/beerdb/beer/view/1?o2=desc'
-
-You can also use the C<Maypole::CLI> module programatically to create
-test suites for your application. See the Maypole tests themselves or
-the documentation to C<Maypole::CLI> for examples of this.
-
-Don't forget also to turn on debugging output in your application:
-
-    package BeerDB;
-    use strict;
-    use warnings;
-    use Maypole::Application qw(-Debug);
-
-=head3 Changing how URLs are parsed
-
-You don't like the way Maypole URLs look, and want something that either
-fits in with the rest of your site or hides the internal workings of the
-system.
-
-B<Solution>: So far we've been using the C</table/action/id/args> form
-of a URL as though it was "the Maypole way"; well, there is no Maypole
-way. Maypole is just a framework and absolutely everything about it is 
-overridable. 
-
-If we want to provide our own URL handling, the method to override in
-the driver class is C<parse_path>. This is responsible for taking
-C<$r-E<gt>path> and filling the C<table>, C<action> and C<args> slots
-of the request object. Normally it does this just by splitting the path
-on 'C</>' characters, but you can do it any way you want, including
-getting the information from C<POST> form parameters or session variables. 
-
-For instance, suppose we want our URLs to be of the form
-C<ProductDisplay.html?id=123>, we could provide a C<parse_path> method
-like so:
-
-    sub parse_path {
-        my $r = shift;
-        $r->path("ProductList.html") unless $r->path;
-        ($r->path =~ /^(.*?)([A-Z]\w+)\.html/);
-        $r->table(lc $1);
-        $r->action(lc $2);
-        my %query = $r->ar->args;
-        $self->args([ $query{id} ]);
-    }
-
-This takes the path, which already has the query parameters stripped off
-and parsed, and finds the table and action portions of the filename,
-lower-cases them, and then grabs the C<id> from the query. Later methods
-will confirm whether or not these tables and actions exist.
-
-See the L<iBuySpy Portal|Maypole::Manual::BuySpy> for another
-example of custom URL processing.
-
-=head3 Maypole for mobile devices
-
-You want Maypole to use different templates to display on particular
-browsers.
-
-B<Solution>: There are several ways to do this, but here's the neatest
-we've found. Maypole chooses where to get its templates either by
-looking at the C<template_root> config parameter or, if this is not
-given, calling the C<get_template_root> method to ask the front-end to
-try to work it out. We can give the front-end a little bit of help, by
-putting this method in our driver class:
-
-    sub get_template_root {
-        my $r = shift;
-        my $browser = $r->headers_in->get('User-Agent');
-        if ($browser =~ /mobile|palm|nokia/i) {
-            "/home/myapp/templates/mobile";
-        } else {
-            "/home/myapp/templates/desktop";
-        }
-    }
-
-(Maybe there's a better way to detect a mobile browser, but you get the
-idea.)
-
-=head2 Content display hacks
-
-These hacks deal primarily with the presentation of data to the user,
-modifying the F<view> template or changing the way that the results of
-particular actions are displayed.
-
-=head3 Null Action
-
-You need an "action" which doesn't really do anything, but just formats
-up a template.
-
-B<Solution>: There are two ways to do this, depending on what precisely
-you need. If you just need to display a template, C<Apache::Template>
-style, with no Maypole objects in it, then you don't need to write any
-code; just create your template, and it will be available in the usual
-way.
-
-If, on the other hand, you want to display some data, and what you're
-essentially doing is a variant of the C<view> action, then you need to
-ensure that you have an exported action, as described in the
-L<templates and actions|Maypole::Manual::StandardTemplates/"C<view> and C<edit>">
-chapter:
-
-    sub my_view :Exported { }
-
-=head3 Template Switcheroo
-
-An action doesn't have any data of its own to display, but needs to display
-B<something>.
-
-B<Solution>: This is an B<extremely> common hack. You've just issued an
-action like C<beer/do_edit>, which updates the database. You don't want
-to display a page that says "Record updated" or similar. Lesser
-application servers would issue a redirect to have the browser request
-C</beer/view/I<id>> instead, but we can actually modify the Maypole
-request on the fly and, after doing the update, pretend that we were
-going to C</beer/view/I<id>> all along. We do this by setting the
-objects in the C<objects> slot and changing the C<template> to the
-one we wanted to go to.
-
-In this example from L<Flox|Maypole::Manual::Flox>, we've just
-performed an C<accept> method on a C<Flox::Invitation> object and we
-want to go back to viewing a user's page.
-
-    sub accept :Exported {
-        my ($self, $r) = @_;
-        my $invitation = $r->objects->[0];
-        # [... do stuff to $invitation ...]
-        $r->objects([$r->user]);
-        $r->model_class("Flox::User");
-        $r->template("view");
-    }
-
-This hack is so common that it's expected that there'll be a neater
-way of doing this in the future.
-
-=head3 XSLT
-
-Here's a hack I've used a number of times. You want to store structured
-data in a database and to abstract out its display.
-
-B<Solution>: You have your data as XML, because handling big chunks of
-XML is a solved problem. Build your database schema as usual around the
-important elements that you want to be able to search and browse on. For
-instance, I have an XML format for songs which has a header section of
-the key, title and so on, plus another section for the lyrics and
-chords:
-
-    <song>
-        <header>
-            <title>Layla</title>
-            <artist>Derek and the Dominos</artist>
-            <key>Dm</key>
-        </header>
-        <lyrics>
-          <verse>...</verse>
-          <chorus>
-            <line> <sup>A</sup>Lay<sup>Dm</sup>la <sup>Bb</sup> </line> 
-            <line> <sup>C</sup>Got me on my <sup>Dm</sup>knees </line> 
-            ...
-
-I store the title, artist and key in the database, as well as an "xml"
-field which contains the whole song as XML.
-
-To load the songs into the database, I can C<use> the driver class for
-my application, since that's a handy way of setting up the database classes
-we're going to need to use. Then the handy L<XML::TreeBuilder> will handle
-the XML parsing for us:
-
-    use Songbook;
-    use XML::TreeBuilder;
-    my $t = XML::TreeBuilder->new;
-    $t->parse_file("songs.xml");
-
-    for my $song ($t->find("song")) {
-        my ($key) = $song->find("key"); $key &&= $key->as_text;
-        my ($title) = $song->find("title"); $title = $title->as_text;
-        my ($artist) = $song->find("artist"); $artist = $artist->as_text;
-        my ($first_line) = $song->find("line");
-        $first_line = join "", grep { !ref } $first_line->content_list;
-        $first_line =~ s/[,\.\?!]\s*$//;
-        Songbook::Song->find_or_create({
-            title => $title,
-            first_line => $first_line,
-            song_key => Songbook::SongKey->find_or_create({name => $key}),
-            artist => Songbook::Artist->find_or_create({name => $artist}),
-            xml => $song->as_XML
-        });
-    }
-
-Now we need to set up the custom display for each song; thankfully, with
-the L<Template::Plugin::XSLT> module, this is as simple as putting the
-following into F<templates/song/view>:
-
-    [%
-        USE transform = XSLT("song.xsl");
-        song.xml | $transform
-    %]
-
-We essentially pipe the XML for the selected song through to an XSL
-transformation, and this will fill out all the HTML we need. Job done.
-
-=head3 Displaying pictures
-
-You want to serve a picture, a Word document, or something else which
-doesn't have a content type of C<text/html>, out of your database.
-
-B<Solution>: Fill the content and content-type yourself.
-
-Here's a subroutine which displays the C<photo> for either a specified
-user or the currently logged in user. We set the C<output> slot of the
-Maypole request object: if this is done then the view class is not called
-upon to process a template, since we already have some output to display.
-We also set the C<content_type> using one from the database.
-
-    sub view_picture :Exported {
-        my ($self, $r) = @_;
-        my $user = $r->objects->[0];
-        $r->content_type($user->photo_type);
-        $r->output($user->photo);
-    }
-
-Of course, the file doesn't necessarily need to be in the database
-itself; if your file is stored in the filesystem, but you have a file
-name or some other pointer in the database, you can still arrange for
-the data to be fetched and inserted into C<$r-E<gt>output>.
-
-=head3 REST
-
-You want to provide a programmatic interface to your Maypole site.
-
-B<Solution>: The best way to do this is with C<REST>, which uses a
-descriptive URL to encode the request. For instance, in
-L<Flox|Maypole::Manual::Flox> we
-describe a social networking system. One neat thing you can do with
-social networks is to use them for reputation tracking, and we can use
-that information for spam detection. So if a message arrives from
-C<person@someco.com>, we want to know if they're in our network of
-friends or not and mark the message appropriately. We'll do this by
-having a web agent (say, L<WWW::Mechanize> or L<LWP::UserAgent>) request
-a URL of the form
-C<http://flox.simon-cozens.org/user/relationship_by_email/person%40someco.com>.
-Naturally, they'll need to present the appropriate cookie just like a
-normal browser, but that's a solved problem. We're just interested in
-the REST request.
-
-The request will return a single integer status code: 0 if they're not
-in the system at all, 1 if they're in the system, and 2 if they're our
-friend.
-
-All we need to do to implement this is provide the C<relationship_by_email>
-action, and use it to fill in the output in the same way as we did when
-displaying a picture. Since C<person%40someco.com> is not the ID of a
-row in the user table, it will appear in the C<args> array:
-
-    use URI::Escape;
-    sub relationship_by_email :Exported {
-        my ($self, $r) = @_;
-        my $email = uri_unescape($r->args->[0]);
-        $r->content_type("text/plain");
-        my $user;
-        unless (($user) = Flox::User->search(email => $email)) {
-            $r->content("0\n"); return;
-        }
-
-        if ($r->user->is_friend($user)) { $r->contenti("2\n"); return; };
-        $r->content("1\n"); return;
-    }
-
-=head3 Component-based Pages
-
-You're designing something like a portal site which has a number of
-components, all displaying different bits of information about different
-objects. You want to include the output of one Maypole request call while
-building up another. 
-
-B<Solution>: Use L<Maypole::Plugin::Component>. By inheriting like this:
-
-    package BeerDB;
-    use Maypole::Application qw(Component);
-
-you can call the C<component> method on the Maypole request object to
-make a "sub-request". For instance, if you have a template
-
-    <DIV class="latestnews">
-    [% request.component("/news/latest_comp") %]
-    </DIV>
-
-    <DIV class="links">
-    [% request.component("/links/list_comp") %]
-    </DIV>
-
-then the results of calling the C</news/latest_comp> action and template
-will be inserted in the C<latestnews> DIV, and the results of calling
-C</links/list_comp> will be placed in the C<links> DIV. Naturally, you're
-responsible for exporting actions and creating templates which return 
-fragments of HTML suitable for inserting into the appropriate locations.
-
-Alternatively, if you've already got all the objects you need, you can
-probably just C<[% PROCESS %]> the templates directly.
-
-=head3 Bailing out with an error
-
-Maypole's error handling sucks. Something really bad has happened to the
-current request, and you want to stop processing now and tell the user about
-it.
-
-B<Solution>: Maypole's error handling sucks because you haven't written it
-yet. Maypole doesn't know what you want to do with an error, so it doesn't
-guess. One common thing to do is to display a template with an error message
-in it somewhere.
-
-Put this in your driver class:
-
-    sub error { 
-        my ($r, $message) = @_;
-        $r->template("error");
-        $r->template_args->{error} = $message;
-        return OK;
-    }
-
-And then have a F<custom/error> template like so:
-
-    [% PROCESS header %]
-    <H2> There was some kind of error... </H2>
-    <P>
-    I'm sorry, something went so badly wrong, we couldn't recover. This
-    may help:
-    </P>
-    <DIV CLASS="messages"> [% error %] </DIV>
-
-Now in your actions you can say things like this:
-
-    if (1 == 0) { return $r->error("Sky fell!") }
-
-This essentially uses the template switcheroo hack to always display the
-error template, while populating the template with an C<error> parameter.
-Since you C<return $r-E<gt>error>, this will terminate the processing
-of the current action.
-
-The really, really neat thing about this hack is that since C<error>
-returns C<OK>, you can even use it in your C<authenticate> routine:
-
-    sub authenticate {
-        my ($self, $r) = @_;
-        $r->get_user;
-        return $r->error("You do not exist. Go away.")
-            if $r->user and $r->user->status ne "real";
-        ...
-    }
-
-This will bail out processing the authentication, the model class, and
-everything, and just skip to displaying the error message. 
-
-Non-showstopper errors or other notifications are best handled by tacking a
-C<messages> template variable onto the request:
-
-    if ((localtime)[6] == 1) {
-        push @{$r->template_args->{messages}}, "Warning: Today is Monday";
-    }
-
-Now F<custom/messages> can contain:
-
-    [% IF messages %]
-    <DIV class="messages">
-    <UL>
-        [% FOR message = messages %]
-           <LI> [% message %] </LI>
-        [% END %]
-    </UL>
-    </DIV>
-    [% END %]
-
-And you can display messages to your user by adding C<PROCESS messages> at an
-appropriate point in your template; you may also want to use a template
-switcheroo to ensure that you're displaying a page that has the messages box in
-it.
-
-=head2 Authentication and Authorization hacks
-
-The next series of hacks deals with providing the concept of a "user" for
-a site, and what you do with one when you've got one.
-
-=head3 Logging In
-
-You need the concept of a "current user".
-
-B<Solution>: Use something like
-L<Maypole::Plugin::Authentication::UserSessionCookie> to authenticate
-a user against a user class and store a current user object in the
-request object.
-
-C<UserSessionCookie> provides the C<get_user> method which tries to get
-a user object, either based on the cookie for an already authenticated
-session, or by comparing C<user> and C<password> form parameters
-against a C<user> table in the database. Its behaviour is highly
-customizable and described in its documentation.
-
-=head3 Pass-through login
-
-You want to intercept a request from a non-logged-in user and have
-them log in before sending them on their way to wherever they were
-originally going. Override C<Maypole::authenticate> in your driver
-class, something like this:
-
-B<Solution>:
-
-    use Maypole::Constants; # Otherwise it will silently fail!
-
-    sub authenticate {
-        my ($self, $r) = @_;
-        $r->get_user;
-        return OK if $r->user;
-        # Force them to the login page.
-        $r->template("login");
-        return OK;
-    }
-
-This will display the C<login> template, which should look something
-like this:
-
-    [% INCLUDE header %]
-
-      <h2> You need to log in </h2>
-
-    <DIV class="login">
-    [% IF login_error %]
-       <FONT COLOR="#FF0000"> [% login_error %] </FONT>
-    [% END %]
-      <FORM ACTION="[% base ; '/' ; request.path %]" METHOD="post">
-    Username: 
-        <INPUT TYPE="text" NAME="[% config.auth.user_field || "user" %]"><BR>
-    Password: <INPUT TYPE="password" NAME="password"> <BR>
-    <INPUT TYPE="submit">
-    </FORM>
-    </DIV>
-    [% INCLUDE footer %]
-
-Notice that this request gets C<POST>ed back to wherever it came from, using
-C<request.path>. This is because if the user submits correct credentials,
-C<get_user> will now return a valid user object, and the request will pass
-through unhindered to the original URL.
-
-=head3 Logging Out
-
-Now your users are logged in, you want a way of having them log out
-again and taking the authentication cookie away from them, sending
-them back to the front page as an unprivileged user.
-
-B<Solution>: Just call the C<logout> method of
-C<Maypole::Plugin::Authentication::UserSessionCookie>. You may also want
-to use the template switcheroo hack to send them back to the frontpage.
-
-=head3 Multi-level Authorization
-
-You have both a global site access policy (for instance, requiring a
-user to be logged in except for certain pages) and a policy for
-particular tables. (Only allowing an admin to delete records in some
-tables, say, or not wanting people to get at the default set of methods
-provided by the model class.) 
-
-You don't know whether to override the global C<authenticate> method or
-provide one for each class.
-
-B<Solution>: Do both.
-Maypole checks whether there is an C<authenticate> method for the model
-class (e.g. BeerDB::Beer) and if so calls that. If there's no such
-method, it calls the default global C<authenticate> method in C<Maypole>,
-which always succeeds. You can override the global method as we saw
-above, and you can provide methods in the model classes.
-
-To use per-table access control you can just add methods to your model
-subclasses that specify individual policies, perhaps like this:
-
-    sub authenticate { # Ensure we can only create, reject or accept
-        my ($self, $r) = @_;
-        return OK if $r->action =~ /^(issue|accept|reject|do_edit)$/;
-        return; # fail if any other action
-    }
-
-If you define a method like this, the global C<authenticate> method will
-not be called, so if you want it to be called you need to do so
-explicitly:
-
-    sub authenticate { # Ensure we can only create, reject or accept
-        my ($self, $r) = @_;
-        return unless $r->authenticate($r) == OK; # fail if not logged in
-        # now it's safe to use $r->user
-        return OK if $r->action =~ /^(accept|reject)$/
-            or ($r->user eq 'fred' and $r->action =~ /^(issue|do_edit)$/);
-        return; # fail if any other action
-    }
-
-=head2 Creating and editing hacks
-
-These hacks particularly deal with issues related to the C<do_edit>
-built-in action.
-
-=head3 Limiting data for display
-
-You want the user to be able to type in some text that you're later
-going to display on the site, but you don't want them to stick images in
-it, launch cross-site scripting attacks or otherwise insert messy HTML.
-
-B<Solution>: Use the L<CGI::Untaint::html> module to sanitize the HTML
-on input. C<CGI::Untaint::html> uses L<HTML::Sanitizer> to ensure that
-tags are properly closed and can restrict the use of certain tags and
-attributes to a pre-defined list.
-
-Simply replace:
-
-    App::Table->untaint_columns(
-        text      => [qw/name description/]
-    );
-
-with:
-
-    App::Table->untaint_columns(
-        html      => [qw/name description/]
-    );
-
-And incoming HTML will be checked and cleaned before it is written to
-the database.
-
-=head3 Getting data from external sources
-
-You want to supplement the data received from a form with additional
-data from another source.
-
-B<Solution>: Munge the contents of C< $r-E<gt>params > before jumping
-to the original C<do_edit> routine. For instance, in this method,
-we use a L<Net::Amazon> object to fill in some fields of a database row
-based on an ISBN:
-
-    sub create_from_isbn :Exported {
-       my ($self, $r) = @_;
-       my $response = $ua->search(asin => $r->params->{isbn});
-       my ($prop) = $response->properties;
-       # Rewrite the CGI parameters with the ones from Amazon
-       @{$r->params->{qw(title publisher author year)} =            
-           ($prop->title,
-           $prop->publisher,
-           (join "/", $prop->authors()),
-           $prop->year());
-       # And jump to the usual edit/create routine
-       $self->do_edit($r);
-    }
-
-The request will carry on as though it were a normal C<do_edit> POST, but
-with the additional fields we have provided.
-You might also want to add a template switcheroo so the user can verify
-the details you imported.
-
-=head3 Catching errors in a form
-
-A user has submitted erroneous input to an edit/create form. You want to
-send him back to the form with errors displayed against the erroneous
-fields, but have the other fields maintain the values that the user
-submitted.
-
-B<Solution>: This is basically what the default C<edit> template and
-C<do_edit> method conspire to do, but it's worth highlighting again how
-they work. 
-
-If there are any errors, these are placed in a hash, with each error
-keyed to the erroneous field. The hash is put into the template as
-C<errors>, and we process the same F<edit> template again:
-
-        $r->template_args->{errors} = \%errors;
-        $r->template("edit");
-
-This throws us back to the form, and so the form's template should take
-note of the errors, like so:
-
-     FOR col = classmetadata.columns;
-        NEXT IF col == "id";
-        "<P>";
-        "<B>"; classmetadata.colnames.$col; "</B>";
-        ": ";
-            item.to_field(col).as_HTML;
-        "</P>";
-        IF errors.$col;
-            "<FONT COLOR=\"#ff0000\">"; errors.$col; "</FONT>";
-        END;
-    END;
-
-If we're designing our own templates, instead of using generic ones, we
-can make this process a lot simpler. For instance:
-
-    <TR><TD>
-    First name: <INPUT TYPE="text" NAME="forename">
-    </TD>
-    <TD>
-    Last name: <INPUT TYPE="text" NAME="surname">
-    </TD></TR>
-
-    [% IF errors.forename OR errors.surname %]
-        <TR>
-        <TD><SPAN class="error">[% errors.forename %]</SPAN> </TD>
-        <TD><SPAN class="error">[% errors.surname %]</SPAN> </TD>
-        </TR>
-    [% END %]
-
-The next thing we want to do is to put the originally-submitted values
-back into the form. We can do this relatively easily because Maypole
-passes the Maypole request object to the form, and the POST parameters
-are going to be stored in a hash as C<request.params>. Hence:
-
-    <TR><TD>
-    First name: <INPUT TYPE="text" NAME="forename"
-    VALUE="[%request.params.forename%]">
-    </TD>
-    <TD>
-    Last name: <INPUT TYPE="text" NAME="surname"
-    VALUE="[%request.params.surname%]"> 
-    </TD></TR>
-
-Finally, we might want to only re-fill a field if it is not erroneous, so
-that we don't get the same bad input resubmitted. This is easy enough:
-
-    <TR><TD>
-    First name: <INPUT TYPE="text" NAME="forename"
-    VALUE="[%request.params.forename UNLESS errors.forename%]">
-    </TD>
-    <TD>
-    Last name: <INPUT TYPE="text" NAME="surname"
-    VALUE="[%request.params.surname UNLESS errors.surname%]"> 
-    </TD></TR>
-
-=head3 Uploading files and other data
-
-You want the user to be able to upload files to store in the database.
-
-B<Solution>: It's messy.
-
-First, we set up an upload form, in an ordinary dummy action. Here's
-the action:
-
-    sub upload_picture : Exported {}
-
-And here's the F<custom/upload_picture> template:
-
-    <FORM action="/user/do_upload" enctype="multipart/form-data" method="POST">
-
-    <P> Please provide a picture in JPEG, PNG or GIF format:
-    </P>
-    <INPUT TYPE="file" NAME="picture">
-    <BR>
-    <INPUT TYPE="submit">
-    </FORM>
-
-(Although you'll probably want a bit more HTML around it than that.)
-
-Now we need to write the C<do_upload> action. At this point we have to get a
-little friendly with the front-end system. If we're using L<Apache::Request>,
-then the C<upload> method of the C<Apache::Request> object (which
-L<Apache::MVC> helpfully stores in C<$r-E<gt>{ar}>) will work for us:
-
-    sub do_upload :Exported {
-        my ($class, $r) = @_;
-        my $user = $r->user;
-        my $upload = $r->ar->upload("picture");
-
-This returns a L<Apache::Upload> object, which we can query for its
-content type and a file handle from which we can read the data. It's
-also worth checking the image isn't going to be too massive before we
-try reading it and running out of memory, and that the content type is
-something we're prepared to deal with. 
-
-    if ($upload) {
-        my $ct = $upload->info("Content-type");
-        return $r->error("Unknown image file type $ct")
-            if $ct !~ m{image/(jpeg|gif|png)};
-        return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)
-            if $upload->size > MAX_IMAGE_SIZE;
-
-        my $fh = $upload->fh;
-        my $image = do { local $/; <$fh> };
-
-Don't forget C<binmode()> in there if you're on a platform that needs it.
-Now we can store the content type and data into our database, store it
-into a file, or whatever:
-
-        $r->user->photo_type($ct);
-        $r->user->photo($image);
-    }
-
-And finally, we use our familiar template switcheroo hack to get back to
-a useful page:
-
-        $r->objects([ $user ]);
-        $r->template("view");
-    }
-
-Now, as we've mentioned, this only works because we're getting familiar with
-C<Apache::Request> and its C<Apache::Upload> objects. If we're using
-L<CGI::Maypole> instead, we can write the action in a similar style:
-
-    sub do_upload :Exported {
-        my ($class, $r) = @_;
-        my $user = $r->user;
-        my $cgi = $r->cgi;
-        if ($cgi->upload == 1) { # if there was one file uploaded
-            my $filename = $cgi->param('picture');
-            my $ct = $cgi->upload_info($filename, 'mime');
-            return $r->error("Unknown image file type $ct")
-                if $ct !~ m{image/(jpeg|gif|png)};
-            return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)
-                if $cgi->upload_info($filename, 'size') > MAX_IMAGE_SIZE;
-            my $fh = $cgi->upload($filename);
-            my $image = do { local $/; <$fh> };
-            $r->user->photo_type($ct);
-            $r->user->photo($image);
-        }
-
-        $r->objects([ $user ]);
-        $r->template("view");
-    }
-
-It's easy to adapt this to upload multiple files if desired.
-You will also need to enable uploads in your driver initialization,
-with the slightly confusing statement:
-
-    $CGI::Simple::DISABLE_UPLOADS = 0; # enable uploads
-
-Combine with the "Displaying pictures" hack above for a happy time.
-
-=head2 Links
-
-L<Contents|Maypole::Manual>,
-Next L<Flox|Maypole::Manual::Flox>,
-Previous L<The Beer Database, Twice|Maypole::Manual::Beer>
-
-
index dbf127998c9963c88cf6363942c3339b01ecf6b5..e3866c877ceebfea28df74096d4b97d3b12cc77e 100644 (file)
@@ -271,7 +271,7 @@ template view
 =head3 F<edit>
 
 The F<edit> template is pretty much the same as F<view>, but it uses 
-L<Class::DBI::AsForm>'s
+L<Maypole::Model::CDBI::AsForm>'s
 C<to_field> method on each column of an object to return a C<HTML::Element>
 object representing a form element to edit that property. These elements
 are then rendered to HTML with C<as_HTML> or to XHTML with C<as_XML>.
index 38ba8e71c50162d149529cc011f412db5761010a..fe46edd60c9db6e9ed9161f57d502593bc8d1f7f 100644 (file)
@@ -408,6 +408,23 @@ C<template_args> hash in the request object, and supply its value:
 You can also override the value of any of the standard variables by
 giving their name as the key.
 
+=head2 Accessing other classes
+
+When building a frontpage, login or other template that isn't directly
+linked to a particular table, (and therefore it's class,) that you wish to
+use, you can access the classes directly.
+
+When using C<Maypole::View::TT> you are reccomended to use Richard Clamp's
+incredibly useful Template::Plugin::Class -- see the and Template::Plugin::Class
+and C<Maypole::View::TT> documentation for details.
+
+Mason and MasonX views also allow you to pull in arbitary classes, see
+the relevent Mason and Plugin/View documentation for details.
+
+If you are using HTML::Template you are out of luck on this front due
+to philosophy and architecture this templating system cannot call code,
+and only reads the data provided when the template is processed.
+
 =head2 Other view classes
 
 Please note that these template variables, C<config>, C<classmetadata>,
@@ -551,3 +568,5 @@ Patches are always welcome!
 L<Contents|Maypole::Manual>,
 Next L<Standard Templates and Actions|Maypole::Manual::StandardTemplates>,
 Previous L<Maypole Model Classes|Maypole::Manual::Model>,
+
+=cut
index d0e9ba81670c357160685195b205736513712b0a..d5d325c6dfaaf374414ac728ec9f894e9b2de1a3 100644 (file)
@@ -4,11 +4,22 @@ use strict;
 use Maypole::Constants;
 use attributes ();
 
+# don't know why this is a global - drb
 our %remember;
 
-sub MODIFY_CODE_ATTRIBUTES { $remember{ $_[1] } = $_[2]; () }
+sub MODIFY_CODE_ATTRIBUTES 
+{ 
+    shift; # class name not used
+    my ($coderef, @attrs) = @_;
+    
+    $remember{$coderef} = \@attrs; 
+    
+    # previous version took care to return an empty array, not sure why, 
+    # but shall cargo cult it until know better
+    return; 
+}
 
-sub FETCH_CODE_ATTRIBUTES { $remember{ $_[1] } || () }
+sub FETCH_CODE_ATTRIBUTES { @{ $remember{$_[1]} || [] } }
 
 sub process {
     my ( $class, $r ) = @_;
@@ -18,6 +29,7 @@ sub process {
     $r->{template} = $method;
     my $obj = $class->fetch_objects($r);
     $r->objects([$obj]) if $obj;
+    
     $class->$method( $r, $obj, @{ $r->{args} } );
 }
 
@@ -121,6 +133,9 @@ Empty Action.
 
 Empty Action.
 
+=item index
+
+Empty Action, calls list if provided with a table.
 
 =back
 
@@ -136,6 +151,14 @@ sub view : Exported {
 sub edit : Exported {
 }
 
+sub index : Exported {
+    my ( $self, $r ) = @_;
+    if ($r->table) {
+       $r->template("list");
+       return $self->list($r);
+    } 
+}
+
 =pod
 
 Also, see the exported commands in C<Maypole::Model::CDBI>.
@@ -178,17 +201,40 @@ Defaults to checking if the sub has the C<:Exported> attribute.
 =cut
 
 sub is_public {
-    my ( $self, $action ) = @_;
+    my ( $self, $action, $attrs ) = @_;
     my $cv = $self->can($action);
-    return 0 unless $cv;
-    my $attrs = join " ", (attributes::get($cv) || ());
+    warn "is_public failed . action is $action. self is $self" and return 0 unless $cv;
+
+    my %attrs = (ref $attrs) ?  %$attrs : map {$_ => 1} $self->method_attrs($action,$cv) ;
+
     do {
-        warn "$action not exported" if Maypole->debug;
-        return 0;
-    } unless $attrs =~ /\bExported\b/i;
+       warn "is_public failed. $action not exported. attributes are : ", %attrs;
+       return 0;
+    } unless $attrs{Exported};
     return 1;
 }
 
+
+
+=head2 method_attrs
+
+Returns the list of attributes defined for a method. Maypole itself only
+defines the C<Exported> attribute. 
+
+=cut
+
+sub method_attrs {
+    my ($class, $method, $cv) = @_;
+    
+    $cv ||= $class->can($method);
+    
+    return unless $cv;
+    
+    my @attrs = attributes::get($cv);
+
+    return @attrs;
+}
+
 =head2 related
 
 This can go either in the master model class or in the individual
index d99eb500d35eafb5beaefe281fc546ad468e4932..e15745b08f613eca147ae6aa8c63d4d7f738892d 100644 (file)
@@ -1,16 +1,8 @@
 package Maypole::Model::CDBI;
-use base qw(Maypole::Model::Base Class::DBI);
-use Class::DBI::AsForm;
-use Class::DBI::FromCGI;
-use Class::DBI::Loader;
-use Class::DBI::AbstractSearch;
-use Class::DBI::Plugin::RetrieveAll;
-use Class::DBI::Pager;
-
-use Lingua::EN::Inflect::Number qw(to_PL);
-use CGI::Untaint;
 use strict;
 
+use Data::Dumper;
+
 =head1 NAME
 
 Maypole::Model::CDBI - Model class based on Class::DBI
@@ -23,153 +15,208 @@ model to copy if you're replacing it with other database abstraction
 modules.
 
 It implements a base set of methods required for a Maypole Data Model.
-See L<Maypole::Model::Base> for these:
 
-=over 4
+It inherits accessor and helper methods from L<Maypole::Model::Base>.
 
-=item adopt
+When specified as the application model, it will use Class::DBI::Loader
+to generate the model classes from the provided database. If you do not
+wish to use this functionality, use L<Maypole::Model::CDBI::Plain> which
+will instead use Class::DBI classes provided.
 
-=item class_of
+=cut
 
-=item do_edit
+use base qw(Maypole::Model::Base Class::DBI);
+#use Class::DBI::Plugin::Type;
+use Class::DBI::Loader;
+use Class::DBI::AbstractSearch;
+use Class::DBI::Plugin::RetrieveAll;
+use Class::DBI::Pager;
+use Lingua::EN::Inflect::Number qw(to_PL);
+use attributes ();
 
-=item list
+use Maypole::Model::CDBI::AsForm;
+use Maypole::Model::CDBI::FromCGI; 
+use CGI::Untaint::Maypole;
 
-=item related
+=head2 Untainter
 
-=item setup_database
+Set the class you use to untaint and validate form data
+Note it must be of type CGI::Untaint::Maypole (takes $r arg) or CGI::Untaint
 
-=item fetch_objects
+=cut
+sub Untainter { 'CGI::Untaint::Maypole' };
 
-=back 
+# or if you like bugs 
 
-=head1 Additional Actions
+#use Class::DBI::FromCGI;
+#use CGI::Untaint;
+#sub Untainter { 'CGI::Untaint' };
 
-=over 
 
-=item delete
+__PACKAGE__->mk_classdata($_) for (qw/COLUMN_INFO/);
 
-Unsuprisingly, this command causes a database record to be forever lost.
+=head1 Action Methods
 
-=item search
+Action methods are methods that are accessed through web (or other public) interface.
 
-The search action 
+=head2 do_edit
 
-=back
+If there is an object in C<$r-E<gt>objects>, then it should be edited
+with the parameters in C<$r-E<gt>params>; otherwise, a new object should
+be created with those parameters, and put back into C<$r-E<gt>objects>.
+The template should be changed to C<view>, or C<edit> if there were any
+errors. A hash of errors will be passed to the template.
 
-=head1 Helper Methods
+=cut
 
-=over 
+sub do_edit : Exported {
+  my ($self, $r, $obj) = @_;
 
-=item order
+  my $config   = $r->config;
+  my $table    = $r->table;
 
-=item stringify_column
+  # handle cancel button hit
+  if ( $r->{params}->{cancel} ) {
+    $r->template("list");
+    $r->objects( [$self->retrieve_all] );
+    return;
+  }
 
-=item do_pager
+  my $required_cols = $config->{$table}{required_cols} || $self->required_columns;
+  my $ignored_cols  = $config->{$table}{ignore_cols} || [];
 
-=item related_class
+  ($obj, my $fatal, my $creating) = $self->_do_update_or_create($r, $obj, $required_cols, $ignored_cols);
 
-Given an accessor name as a method, this function returns the class this accessor returns.
+  # handle errors, if none, proceed to view the newly created/updated object
+  my %errors = $fatal ? (FATAL => $fatal) : $obj->cgi_update_errors;
 
-=back
+  if (%errors) {
+    # Set it up as it was:
+    $r->template_args->{cgi_params} = $r->params;
 
-=cut
+    # replace user unfriendly error messages with something nicer
 
-sub related {
-    my ( $self, $r ) = @_;
-    return keys %{ $self->meta_info('has_many') || {} };
-}
+    foreach (@{$config->{$table}->{required_cols}}) {
+      next unless ($errors{$_});
+      my $key = $_;
+      s/_/ /g;
+      $r->template_args->{errors}{ucfirst($_)} = 'This field is required, please provide a valid value';
+      $r->template_args->{errors}{$key} = 'This field is required, please provide a valid value';
+      delete $errors{$key};
+    }
 
-sub related_class {
-    my ( $self, $r, $accessor ) = @_;
+    foreach (keys %errors) {
+      my $key = $_;
+      s/_/ /g;
+      $r->template_args->{errors}{ucfirst($_)} = 'Please provide a valid value for this field';
+      $r->template_args->{errors}{$key} = 'Please provide a valid value for this field';
+    }
 
-    my $related = $self->meta_info( has_many => $accessor ) ||
-                  $self->meta_info( has_a    => $accessor ) ||
-                  return;
+    undef $obj if $creating;
 
-    my $mapping = $related->{args}->{mapping};
-    if ( @$mapping ) {
-        return $related->{foreign_class}->meta_info('has_a')->{ $$mapping[0] }
-          ->{foreign_class};
-    }
-    else {
-        return $related->{foreign_class};
-    }
-}
+    die "do_update failed with error : $fatal" if ($fatal);
+    $r->template("edit");
+  } else {
+    $r->template("view");
+  }
 
-sub do_edit : Exported {
-    my ( $self, $r ) = @_;
-    my $h        = CGI::Untaint->new( %{ $r->{params} } );
-    my $creating = 0;
-    my ($obj) = @{ $r->objects || [] };
-    my $fatal;
-    if ($obj) {
-        # We have something to edit
-        eval {
-            $obj->update_from_cgi( $h =>
-                { required => $r->{config}{ $r->{table} }{required_cols} || [], }
-            );
-        };
-        $fatal = $@;
-    }
-    else {
-        eval {
-            $obj =
-                $self->create_from_cgi( $h =>
-                    { required => $r->{config}{ $r->{table} }{required_cols} || [], }
-            );
-        };
-        if ($fatal = $@) {
-            warn "$fatal" if $r->debug;
-        }
-        $creating++;
-    }
-    if ( my %errors = $fatal ? (FATAL => $fatal) : $obj->cgi_update_errors ) {
 
-        # Set it up as it was:
-        $r->{template_args}{cgi_params} = $r->{params};
-        $r->{template_args}{errors}     = \%errors;
 
-        undef $obj if $creating;
-        $r->template("edit");
-    }
-    else {
-        $r->{template} = "view";
-    }
-    $r->objects( $obj ? [$obj] : []);
+  $r->objects( $obj ? [$obj] : []);
 }
 
-sub delete : Exported {
-    return shift->SUPER::delete(@_) if caller ne "Maypole::Model::Base";
-    my ( $self, $r ) = @_;
-    $_->SUPER::delete for @{ $r->objects || [] };
-    $r->objects( [ $self->retrieve_all ] );
-    $r->{template} = "list";
-    $self->list($r);
+# split out from do_edit to be reported by Mp::P::Trace
+sub _do_update_or_create {
+  my ($self, $r, $obj, $required_cols, $ignored_cols) = @_;
+
+  my $fatal;
+  my $creating = 0;
+
+  my $h = $self->Untainter->new( %{$r->params} );
+
+  # update or create
+  if ($obj) {
+    # We have something to edit
+    eval { $obj->update_from_cgi( $h => {
+                                        required => $required_cols,
+                                        ignore => $ignored_cols,
+                                       }); 
+          $obj->update(); # pos fix for bug 17132 'autoupdate required by do_edit'
+        };
+    $fatal = $@;
+  } else {
+       eval {
+       $obj = $self->create_from_cgi( $h => {
+                                           required => $required_cols,
+                                           ignore => $ignored_cols,
+                                          } );
+       };
+       $fatal = $@;
+       $creating++;
+  }
+  return $obj, $fatal, $creating;
 }
 
-sub stringify_column {
-    my $class = shift;
-    return (
-        $class->columns("Stringify"),
-        ( grep { /^(name|title)$/i } $class->columns ),
-        ( grep { /(name|title)/i } $class->columns ),
-        ( grep { !/id$/i } $class->primary_columns ),
-    )[0];
+=head2 delete
+
+Deprecated method that calls do_delete or a given classes delete method, please
+use do_delete instead
+
+=head2 do_delete
+
+Unsuprisingly, this command causes a database record to be forever lost.
+
+This method replaces the, now deprecated, delete method provided in prior versions
+
+=cut
+
+sub delete : Exported {
+  my $self = shift;
+  my ($sub) = (caller(1))[3];
+  # So subclasses can still send delete down ...
+  $sub =~ /^(.+)::([^:]+)$/;
+  if ($1 ne "Maypole::Model::Base" && $2 ne "delete") {
+    $self->SUPER::delete(@_);
+  } else {
+    warn "Maypole::Model::CDBI delete method is deprecated\n";
+    $self->do_delete(@_);
+  }
 }
 
-sub adopt {
-    my ( $self, $child ) = @_;
-    $child->autoupdate(1);
-    if ( my $col = $child->stringify_column ) {
-        $child->columns( Stringify => $col );
-    }
+sub do_delete {
+  my ( $self, $r ) = @_;
+  # FIXME: handle fatal error with exception
+  $_->SUPER::delete for @{ $r->objects || [] };
+#  $self->dbi_commit;
+  $r->objects( [ $self->retrieve_all ] );
+  $r->{template} = "list";
+  $self->list($r);
 }
 
+=head2 search
+
+Deprecated searching method - use do_search instead.
+
+=head2 do_search
+
+This action method searches for database records, it replaces
+the, now deprecated, search method previously provided.
+
+=cut
+
 sub search : Exported {
-    return shift->SUPER::search(@_) if caller ne "Maypole::Model::Base";
+  my $self = shift;
+  my ($sub) = (caller(1))[3];
+  # So subclasses can still send search down ...
+  if ($sub =~ /^(.+)::([^:]+)$/) {
+    return ($1 ne "Maypole::Model::Base" && $2 ne "search") ?
+      $self->SUPER::search(@_) : $self->do_search(@_);
+  } else {
+    $self->SUPER::search(@_);
+  }
+}
 
-    # A real CDBI search.
+sub do_search : Exported {
     my ( $self, $r ) = @_;
     my %fields = map { $_ => 1 } $self->columns;
     my $oper   = "like";                                # For now
@@ -192,6 +239,133 @@ sub search : Exported {
     $r->{template_args}{search} = 1;
 }
 
+=head2 list
+
+The C<list> method fills C<$r-E<gt>objects> with all of the
+objects in the class. The results are paged using a pager.
+
+=cut
+
+sub list : Exported {
+    my ( $self, $r ) = @_;
+    my $order = $self->order($r);
+    $self = $self->do_pager($r);
+    if ($order) {
+        $r->objects( [ $self->retrieve_all_sorted_by($order) ] );
+    }
+    else {
+        $r->objects( [ $self->retrieve_all ] );
+    }
+}
+
+###############################################################################
+# Helper methods
+
+=head1 Helper Methods
+
+
+=head2 adopt
+
+This class method is passed the name of a model class that represensts a table
+and allows the master model class to do any set-up required.
+
+=cut
+
+sub adopt {
+    my ( $self, $child ) = @_;
+    $child->autoupdate(1);
+    if ( my $col = $child->stringify_column ) {
+        $child->columns( Stringify => $col );
+    }
+}
+
+
+=head2 related
+
+This method returns a list of has-many accessors. A brewery has many
+beers, so C<BeerDB::Brewery> needs to return C<beers>.
+
+=cut
+
+sub related {
+    my ( $self, $r ) = @_;
+    return keys %{ $self->meta_info('has_many') || {} };
+}
+
+
+=head2 related_class
+
+Given an accessor name as a method, this function returns the class this accessor returns.
+
+=cut
+
+sub related_class {
+     my ( $self, $r, $accessor ) = @_;
+     my $meta = $self->meta_info;
+     my @rels = keys %$meta;
+     my $related;
+     foreach (@rels) {
+         $related = $meta->{$_}{$accessor};
+         last if $related;
+     }
+     return unless $related;
+
+     my $mapping = $related->{args}->{mapping};
+     if ( $mapping and @$mapping ) {
+       return $related->{foreign_class}->meta_info('has_a')->{$$mapping[0]}->{foreign_class};
+     }
+     else {
+         return $related->{foreign_class};
+     }
+ }
+
+=head2 related_meta
+
+  $class->related_meta($col);
+
+Returns the hash ref of relationship meta info for a given column.
+
+=cut
+
+sub related_meta {
+    my ($self,$r, $accssr) = @_;
+    $self->_croak("You forgot to put the place holder for 'r' or forgot the accssr parameter") unless $accssr;
+    my $class_meta = $self->meta_info;
+    if (my ($rel_type) = grep { defined $class_meta->{$_}->{$accssr} }
+        keys %$class_meta)
+    { return  $class_meta->{$rel_type}->{$accssr} };
+}
+
+
+
+=head2 stringify_column
+
+   Returns the name of the column to use when stringifying
+   and object.
+
+=cut
+
+sub stringify_column {
+    my $class = shift;
+    return (
+        $class->columns("Stringify"),
+        ( grep { /^(name|title)$/i } $class->columns ),
+        ( grep { /(name|title)/i } $class->columns ),
+        ( grep { !/id$/i } $class->primary_columns ),
+    )[0];
+}
+
+=head2 do_pager
+
+   Sets the pager template argument ($r->{template_args}{pager})
+   to a Class::DBI::Pager object based on the rows_per_page
+   value set in the configuration of the application.
+
+   This pager is used via the pager macro in TT Templates, and
+   is also accessible via Mason.
+
+=cut
+
 sub do_pager {
     my ( $self, $r ) = @_;
     if ( my $rows = $r->config->rows_per_page ) {
@@ -201,6 +375,21 @@ sub do_pager {
     else { return $self }
 }
 
+
+=head2 order
+
+    Returns the SQL order syntax based on the order parameter passed
+    to the request, and the valid columns.. i.e. 'title ASC' or 'date_created DESC'.
+
+    $sql .= $self->order($r);
+
+    If the order column is not a column of this table,
+    or an order argument is not passed, then the return value is undefined.
+
+    Note: the returned value does not start with a space.
+
+=cut
+
 sub order {
     my ( $self, $r ) = @_;
     my %ok_columns = map { $_ => 1 } $self->columns;
@@ -211,17 +400,28 @@ sub order {
     return $order;
 }
 
-sub list : Exported {
-    my ( $self, $r ) = @_;
-    my $order = $self->order($r);
-    $self = $self->do_pager($r);
-    if ($order) {
-        $r->objects( [ $self->retrieve_all_sorted_by($order) ] );
-    }
-    else {
-        $r->objects( [ $self->retrieve_all ] );
-    }
-}
+=head2 setup
+
+  This method is inherited from Maypole::Model::Base and calls setup_database,
+  which uses Class::DBI::Loader to create and load Class::DBI classes from
+  the given database schema.
+
+=cut
+
+=head2 setup_database
+
+The $opts argument is a hashref of options.  The "options" key is a hashref of
+Database connection options . Other keys may be various Loader arguments or
+flags.  It has this form:
+ {
+   # DB connection options
+   options { AutoCommit => 1 , ... },
+   # Loader args
+   relationships => 1,
+   ...
+ }
+
+=cut
 
 sub setup_database {
     my ( $class, $config, $namespace, $dsn, $u, $p, $opts ) = @_;
@@ -242,15 +442,29 @@ sub setup_database {
     );
     $config->{classes} = [ $config->{loader}->classes ];
     $config->{tables}  = [ $config->{loader}->tables ];
-    warn( 'Loaded tables: ' . join ',', @{ $config->{tables} } )
+
+    my @table_class = map { $_ . " => " . $config->{loader}->_table2class($_) } @{ $config->{tables} };
+    warn( 'Loaded tables to classes: ' . join ', ', @table_class )
       if $namespace->debug;
 }
 
+=head2 class_of
+
+  returns class for given table
+
+=cut
+
 sub class_of {
     my ( $self, $r, $table ) = @_;
-    return $r->config->loader->_table2class($table);
+    return $r->config->loader->_table2class($table); # why not find_class ?
 }
 
+=head2 fetch_objects
+
+Returns 1 or more objects of the given class when provided with the request
+
+=cut
+
 sub fetch_objects {
     my ($class, $r)=@_;
     my @pcs = $class->primary_columns;
@@ -262,4 +476,303 @@ sub fetch_objects {
     return $class->retrieve( $r->{args}->[0] );
 }
 
+
+
+
+
+=head2 _isa_class
+
+Private method to return the class a column 
+belongs to that was inherited by an is_a relationship.
+This should probably be public but need to think of API
+
+=cut
+
+sub _isa_class {
+    my ($class, $col) = @_;
+    $class->_croak( "Need a column for _isa_class." ) unless $col;
+    my $isaclass;
+    my $isa = $class->meta_info("is_a") || {};
+    foreach ( keys %$isa ) {
+        $isaclass = $isa->{$_}->foreign_class;
+        return $isaclass if ($isaclass->find_column($col));
+    }
+    return; # col not in a is_a class
+}
+
+
+# Thanks to dave baird --  form builder for these private functions
+# sub _column_info {
+sub _column_info {
+  my $self = shift;
+  my $dbh = $self->db_Main;
+
+  my $meta;                    # The info we are after
+  my ($catalog, $schema) = (undef, undef); 
+  # Dave is suspicious this (above undefs) could 
+  # break things if driver useses this info
+
+  my $original_metadata;
+  # '%' is a search pattern for columns - matches all columns
+  if ( my $sth = $dbh->column_info( $catalog, $schema, $self->table, '%' ) ) {
+    $dbh->errstr && die "Error getting column info sth: " . $dbh->errstr;
+    $self->COLUMN_INFO ($self->_hash_type_meta( $sth ));
+  } else {
+    $self->COLUMN_INFO ($self->_hash_typeless_meta( ));
+  }
+
+  return $self->COLUMN_INFO;
+}
+
+sub _hash_type_meta {
+  my ($self, $sth) = @_;
+  my $meta;
+  while ( my $row = $sth->fetchrow_hashref ) {
+    my $colname = $row->{COLUMN_NAME} || $row->{column_name};
+
+    # required / nullable
+    $meta->{$colname}{nullable} = $row->{NULLABLE};
+    $meta->{$colname}{required} = ( $meta->{$colname}{nullable} == 0 ) ? 1 : 0;
+
+    # default
+    if (defined $row->{COLUMN_DEF}) {
+      my $default = $row->{COLUMN_DEF};
+      $default =~ s/['"]?(.*?)['"]?::.*$/$1/;
+      $meta->{$colname}{default} = $default;
+    }else {
+      $meta->{$colname}{default} = '';
+    }
+
+    # type
+    my $type = $row->{mysql_type_name} || $row->{type};
+    unless ($type) {
+      $type =  $row->{TYPE_NAME};
+      if ($row->{COLUMN_SIZE}) {
+       $type .= "($row->{COLUMN_SIZE})";
+      }
+    }
+    $type =~ s/['"]?(.*)['"]?::.*$/$1/;
+    # Bool if tinyint
+    if ($type and $type =~ /^tinyint/i and $row->{COLUMN_SIZE} == 1) { 
+      $type = 'BOOL';
+    }
+    $meta->{$colname}{type} = $type;
+
+    # order
+    $meta->{$colname}{position} = $row->{ORDINAL_POSITION}
+  }
+  return $meta;
+}
+
+# typeless db e.g. sqlite
+sub _hash_typeless_meta {
+  my ( $self ) = @_;
+
+  $self->set_sql( fb_meta_dummy => 'SELECT * FROM __TABLE__ WHERE 1=0' )
+    unless $self->can( 'sql_fb_meta_dummy' );
+
+  my $sth = $self->sql_fb_meta_dummy;
+
+  $sth->execute or die "Error executing column info: "  . $sth->errstr;;
+
+  # see 'Statement Handle Attributes' in the DBI docs for a list of available attributes
+  my $cols  = $sth->{NAME};
+  my $types = $sth->{TYPE};
+  # my $sizes = $sth->{PRECISION};    # empty
+  # my $nulls = $sth->{NULLABLE};     # empty
+
+  # we haven't actually fetched anything from the sth, so need to tell DBI we're not going to
+  $sth->finish;
+
+  my $order = 0;
+  my $meta;
+  foreach my $col ( @$cols ) {
+    my $col_meta;
+    $col_meta->{nullable}    = 1;
+    $col_meta->{required}    = 0;
+    $col_meta->{default}     = '';
+    $col_meta->{position} = $order++;
+    # type_name is taken literally from the schema, but is not actually used by sqlite,
+    # so it can be anything, e.g. varchar or varchar(xxx) or VARCHAR etc.
+    my $type = shift( @$types );
+    $col_meta->{type} = ($type =~ /(\w+)\((\w+)\)/) ? $1 :$type ;
+    $meta->{$col} = $col_meta;
+  }
+  return $meta;
+}
+
+=head2 column_type
+
+    my $type = $class->column_type('column_name');
+
+This returns the 'type' of this column (VARCHAR(20), BIGINT, etc.)
+For now, it returns "BOOL" for tinyints. 
+
+TODO :: TEST with enums
+
+=cut
+
+sub column_type {
+  my $class = shift;
+  my $colname = shift or die "Need a column for column_type";
+  $class->_column_info() unless (ref $class->COLUMN_INFO);
+
+  if ($class->_isa_class($colname)) {
+    return $class->_isa_class($colname)->column_type($colname);
+  }
+  unless ( $class->find_column($colname) ) {
+    warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
+    return undef;
+  }
+  return $class->COLUMN_INFO->{$colname}{type};
+}
+
+=head2 required_columns
+
+  Accessor to get/set required columns for forms, validation, etc.
+
+  Returns list of required columns. Accepts an array ref of column names.
+
+  $class->required_columns([qw/foo bar baz/]);
+
+  Allows you to specify the required columns for a class, over-riding any
+  assumptions and guesses made by Maypole.
+
+  Use this instead of $config->{$table}{required_cols}
+
+  Note : you need to setup the model class before calling this method.
+
+=cut
+
+sub required_columns {
+  my ($class, $columns) = @_;
+  $class->_column_info() unless ref $class->COLUMN_INFO;
+  my $column_info = $class->COLUMN_INFO;
+
+  if ($columns) {
+    foreach my $colname ( @$columns ) {
+      if ($class->_isa_class($colname)) {
+       $class->_isa_class($colname)->COLUMN_INFO->{$colname}{required} = 1
+         unless ($class->_isa_class($colname)->column_required);
+       next;
+      }
+      unless ( $class->find_column($colname) ) {
+       warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
+       next;
+      }
+      $column_info->{$colname}{required} = 1;
+    }
+    $class->COLUMN_INFO($column_info);
+  }
+
+  return [ grep ($column_info->{$_}{required}, keys %$column_info) ] ;
+}
+
+=head2 column_required
+
+  Returns true if a column is required
+
+  my $required = $class->column_required($column_name);
+
+  Columns can be required by the application but not the database, but not the other way around,
+  hence there is also a column_nullable method which will tell you if the column is nullable
+  within the database itself.
+
+=cut
+
+sub column_required {
+  my ($class, $colname) = @_;
+  $colname or $class->_croak( "Need a column for column_nullable" );
+  $class->_column_info() unless ref $class->COLUMN_INFO;
+  if ($class->_isa_class($colname)) {
+    return $class->_isa_class($colname)->column_required($colname);
+  }
+  unless ( $class->find_column($colname) ) {
+    # handle  non-existant columns
+    warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
+    return undef;
+  }
+  return $class->COLUMN_INFO->{$colname}{required} || 0;
+}
+
+=head2 column_nullable
+
+  Returns true if a column can be NULL within the underlying database and false if not.
+
+  my $nullable = $class->column_nullable($column_name);
+
+  Any columns that are not nullable will automatically be specified as required, you can
+  also specify nullable columns as required within your application.
+
+  It is recomended you use column_required rather than column_nullable within your
+  application, this method is more useful if extending the model or handling your own
+  validation.
+
+=cut
+
+sub column_nullable {
+    my $class = shift;
+    my $colname = shift or $class->_croak( "Need a column for column_nullable" );
+
+  $class->_column_info() unless ref $class->COLUMN_INFO;
+  if ($class->_isa_class($colname)) {
+    return $class->_isa_class($colname)->column_nullable($colname);
+  }
+  unless ( $class->find_column($colname) ) {
+    # handle  non-existant columns
+    warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
+    return undef;
+  }
+  return $class->COLUMN_INFO->{$colname}{nullable} || 0;
+}
+
+=head2 column_default
+
+Returns default value for column or the empty string. 
+Columns with NULL, CURRENT_TIMESTAMP, or Zeros( 0000-00...) for dates and times
+have '' returned.
+
+=cut
+
+sub column_default {
+  my $class = shift;
+  my $colname = shift or $class->_croak( "Need a column for column_default");
+  $class->_column_info() unless (ref $class->COLUMN_INFO);
+  if ($class->_isa_class($colname)) {
+    return $class->_isa_class($colname)->column_default($colname);
+  }
+  unless ( $class->find_column($colname) ) {
+    warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
+    return undef;
+  }
+
+  return $class->COLUMN_INFO->{$colname}{default};
+}
+
+=head2 get_classmetadata
+
+Gets class meta data *excluding cgi input* for the passed in class or the
+calling class. *NOTE* excludes cgi inputs. This method is handy to call from 
+templates when you need some metadata for a related class.
+
+=cut
+
+sub get_classmetadata {
+    my ($self, $class) = @_; # class is class we want data for
+    $class ||= $self;
+    $class = ref $class || $class;
+
+    my %res;
+    $res{name}          = $class;
+    $res{colnames}      = {$class->column_names};
+    $res{columns}       = [$class->display_columns];
+    $res{list_columns}  = [$class->list_columns];
+    $res{moniker}       = $class->moniker;
+    $res{plural}        = $class->plural_moniker;
+    $res{table}         = $class->table;
+    $res{column_metadata} = (ref $class->COLUMN_INFO) ? $class->COLUMN_INFO : $class->_column_info() ;
+    return \%res;
+}
+
+
 1;
diff --git a/lib/Maypole/Model/CDBI/AsForm.pm b/lib/Maypole/Model/CDBI/AsForm.pm
new file mode 100644 (file)
index 0000000..1765482
--- /dev/null
@@ -0,0 +1,1517 @@
+package Maypole::Model::CDBI::AsForm;
+
+#TODO -- 
+
+# TESTED and Works --
+#  has_many select -- $obj->to_field($has_many_col);   # select one form many
+#                  -- $class->to_field($has_many_col); # foreign inputs  
+#  $class->search_inputs; /
+
+
+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  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 = '.95'; 
+
+=head1 NAME
+
+Maypole::Model:CDBI::AsForm - Produce HTML form elements for database columns
+
+=head1 SYNOPSIS
+
+    package Music::CD;
+    use Maypole::Model::CDBI::AsForm;
+    use base 'Class::DBI';
+    use CGI;
+    ...
+
+    sub create_or_edit {
+        my $self = shift;
+        my %cgi_field = $self->to_cgi;
+        return start_form,
+               (map { "<b>$_</b>: ". $cgi_field{$_}->as_HTML." <br>" } 
+                    $class->Columns),
+               end_form;
+    }
+
+
+   . . .
+
+    # 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
+
+    <form ..>
+
+    ...
+
+    <label>
+
+     <span class="field"> [% classmetadata.colnames.$col %] : </span>
+
+     [% object.to_field(col).as_XML %]
+
+    </label>
+
+    . . .
+
+    <label>
+
+     <span class="field"> Brewery : </span>
+
+     [% object.to_field('brewery', { selected => 23} ).as_XML %]
+
+    </label>
+
+    . . .
+
+    </form>
+
+
+    #####################################################
+    # 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
+
+This module helps to generate HTML forms for creating new database rows
+or editing existing rows. It maps column names in a database table to
+HTML form elements which fit the schema. Large text fields are turned
+into textareas, and fields with a has-a relationship to other
+C<Class::DBI> tables are turned into select drop-downs populated with
+objects from the joined 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) = @_; # pjs -- added columns arg
+               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<how> 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<how> 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<HOW Methods>. 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
+
+  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'] } );
+  }
+
+  # 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, $args) = @_;
+  $class = ref $class || $class;
+  #my $accssr_class = { $class->accessor_classes };
+  my %cgi;
+  
+  $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_columns;
+       die ("$class search_fields error: Must specify at least one column to search in the foreign object named '$accssr'");
+      }
+      my $fcgi  = $class->to_field($accssr, 'foreign_inputs', $base_args);
+
+      # unset the default values for a select box
+      foreach (keys %$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 
+
+           #(defined $first->attr('value') or $first->attr('value') ne ''))  
+           # push an empty option on stactk
+           $el->unshift_content(HTML::Element->new('option'));
+         }
+       }
+
+      }
+      $cgi{$accssr} = $fcgi;
+      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 unselect_element
+
+  unselect any selected elements in a HTML::Element select list widget
+
+=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');
+                               }
+               }
+}
+
+=head2 _field_from_how($field, $how,$args)
+
+Returns an input element based the "how" parameter or nothing at all.
+Override at will. 
+
+=cut
+
+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); 
+               return;
+}
+
+=head2 _field_from_relationship($field, $args)
+
+Returns an input based on the relationship associated with the field or nothing.
+Override at will.
+
+For has_a it will give select box
+
+=cut
+
+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 $meta = $self->meta_info;
+               #grep{ defined $meta->{$_}{$field} } keys %$meta;
+               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)
+
+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
+
+  # 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;
+}
+
+
+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 _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"); 
+                                                               }
+                                               }
+
+                               }
+                               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;
+}
+
+
+# Old version
+#sub _to_select {
+#      my ($self, $col, $hint) = @_;
+#      my $fclass = $hint || $self->__hasa_rels->{$col}->[0];
+#      my @objs        = $fclass->retrieve_all;
+#      my $a           = HTML::Element->new("select", name => $col);
+#      for (@objs) {
+#              my $sel = HTML::Element->new("option", value => $_->id);
+#              $sel->attr("selected" => "selected")
+#                      if ref $self
+#                      and eval { $_->id eq $self->$col->id };
+#              $sel->push_content($_->stringify_self);
+#              $a->push_content($sel);
+#      }
+#      $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
+
+
+
+
+# select box requirements
+# 1. a select box for objecs of a has_a related class -- DONE 
+=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'");
+
+  # 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"}); 
+
+
+=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  -- 
+
+  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',);
+
+
+=cut
+
+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;
+       }
+       
+       # Proceed with work
+
+       my $rel_meta;  
+    if (not $col) { 
+               unless ($args->{class}) {
+               $args->{class} = ref $self || $self;
+                       # object selected if called with one
+            $args->{selected} = { $self->id => 1} 
+                               if not $args->{selected} and ref $self;
+               }
+        $col = $args->{class}->primary_column;
+               $args->{name} ||= $col;
+    }
+    # Related Class maybe ? 
+    elsif ($rel_meta =  $self->related_meta('r:)', $col) ) {
+        $args->{class} = $rel_meta->{foreign_class};
+        # related objects pre selected if object
+                               
+               # "Has many" -- Issues:
+               # 1) want to select one  or many from list if self is an object
+               # Thats about all we can do really, 
+               # 2) except for mapping which is TODO and  would 
+               # do something like add to and take away from list of permissions for
+               # example.
+
+               # Hasmany select one from list if ref self
+               if ($rel_meta->{name} =~ /has_many/i and ref $self) {
+                   my @itms =  $self->$col; # need list not iterator
+                       $args->{items} = \@itms;
+                       my $a = $self->_select_guts($col,  $args);
+                   $OLD_STYLE && return $a->as_HTML;
+                   return $a;
+               }
+               else {
+                       $args->{selected} ||= [ $self->$col ] if  ref $self; 
+                       #warn "selected is " . Dumper($args->{selected});
+                       my $c = $rel_meta->{args}{constraint} || {};
+                       my $j = $rel_meta->{args}{join} || {};
+                       my @join ; 
+                       if (ref $self) {
+                               @join   =  map { $_ ." = ". $self->_attr($_) } keys %$j; 
+                       }
+                       my @constr= map { "$_ = '$c->{$_}'"} keys %$c; 
+                       $args->{where}    ||= join (' AND ', (@join, @constr));
+                       $args->{order_by} ||= $rel_meta->{args}{order_by};
+                       $args->{limit}    ||= $rel_meta->{args}{limit};
+               }
+                       
+    }
+    # We could say :Col is name and we are selecting  out of class arg.
+       # DIE for now
+       #else {
+       #       die "Usage _to_select. $col not related to any class to select from. ";
+               
+       #}
+               
+    # Set arguments 
+       unless ( defined  $args->{column_nullable} ) {
+           $args->{column_nullable} = $self->can('column_nullable') ?
+                        $self->column_nullable($col) : 1;
+       }
+
+       # Get items to select from
+    my $items = _select_items($args); # array of hashrefs 
+
+       # Turn items into objects if related 
+       if ($rel_meta and not $args->{no_construct}) { 
+               my @objs = ();
+               push @objs, $rel_meta->{foreign_class}->construct($_) foreach @$items;
+               $args->{items} = \@objs; 
+       }
+       else { $args->{items} = $items; } 
+       
+       #use Data::Dumper;
+       #warn "Just got items. They are  " . Dumper($args->{items});
+
+       # Make select HTML element
+       $a = $self->_select_guts($col, $args);
+
+       if ($args->{multiple}) {$a->attr('multiple', 'multiple');}
+
+       # Return 
+    $OLD_STYLE && return $a->as_HTML;
+    $a;
+
+}
+
+
+##############
+# Function # 
+# #############
+# returns the intersection of list refs a and b
+sub _list_intersect {
+       my ($a, $b) = @_;
+       my %isect; my %union;
+    foreach my $e (@$a, @$b) { $union{$e}++ && $isect{$e}++ }
+       return  %isect;
+}
+############
+# FUNCTION #
+############
+# Get Items  returns array of hashrefs
+sub _select_items { 
+       my $args = shift;
+       my $fclass = $args->{class};
+    my @disp_cols = @{$args->{columns} || []};
+    @disp_cols = $fclass->columns('SelectBox') unless @disp_cols;
+    @disp_cols = $fclass->columns('Stringify')unless @disp_cols;
+    @disp_cols = $fclass->_essential unless @disp_cols;
+       unshift @disp_cols,  $fclass->columns('Primary');
+       #my %isect = _list_intersect(\@pks, \@disp_cols);
+       #foreach (@pks) { push @sel_cols, $_ unless $isect{$_}; } 
+    #push @sel_cols, @disp_cols;               
+
+       #warn "in select items. args are : " . Dumper($args);
+       my $distinct = '';
+       if ($args->{'distinct'}) {
+       $distinct = 'DISTINCT ';
+       }
+
+    my $sql = "SELECT $distinct" . join( ', ', @disp_cols) . 
+                 " FROM " . $fclass->table;
+
+       $sql .= " WHERE " . $args->{where}   if $args->{where};
+       $sql .= " ORDER BY " . $args->{order_by} if $args->{order_by};
+       $sql .= " LIMIT " . $args->{limit} if $args->{limit};
+       #warn "_select_items sql is : $sql";
+
+       my $sth = $fclass->db_Main->prepare($sql);
+       $sth->execute;
+       my @data;
+       while ( my $d = $sth->fetchrow_hashref ) {push @data, $d};  
+       return \@data;
+
+}
+
+
+# Makes a readonly input box out of column's value
+# No args makes object to readonly
+sub _to_readonly {
+    my ($self, $col, $args) = @_;
+    my $val = $args->{value};
+    if (not defined $val ) { # object to readonly
+       $self->_croak("AsForm: To readonly field called as class method without a value") unless ref $self; 
+        $val = $self->id;
+        $col = $self->primary_column;
+    }
+    my $a = HTML::Element->new('input', 'type' => 'text', readonly => '1',
+        'name' => $col, 'value'=>$val);
+       $OLD_STYLE && return $a->as_HTML;
+    $a;
+}
+
+
+=head2 _to_enum_select
+
+Returns a select box for the an enum column type. 
+
+=cut
+
+sub _to_enum_select {
+    my ($self, $col, $args) = @_;
+       my $type = $args->{column_type};
+    $type =~ /ENUM\((.*?)\)/i;
+    (my $enum = $1) =~ s/'//g;
+    my @enum_vals = split /\s*,\s*/, $enum;
+
+    # determine which is pre selected --
+    my $selected = eval { $self->$col  };
+    $selected = $args->{default} unless defined $selected;
+    $selected = $enum_vals[0] unless defined $selected;
+
+    my $a = HTML::Element->new("select", name => $col);
+    for ( @enum_vals ) {
+        my $sel = HTML::Element->new("option", value => $_);
+        $sel->attr("selected" => "selected") if $_ eq $selected ;
+        $sel->push_content($_);
+        $a->push_content($sel);
+    }
+    $OLD_STYLE && return $a->as_HTML;
+    $a;
+}
+
+
+=head2 _to_bool_select
+
+Returns a "No/Yes"  select box for a boolean column type. 
+
+=cut
+# TCODO fix this mess with args
+sub _to_bool_select {
+    my ($self, $col, $args) = @_;
+       my $type = $args->{column_type};
+       my @bool_text = ('No', 'Yes');  
+       if ($type =~ /BOOL\((.+?)\)/i) {
+               (my $bool = $1) =~ s/'//g;
+               @bool_text = split /,/, $bool;
+       }
+
+       # get selected 
+       
+       my $selected = $args->{value} if defined $args->{value};
+       $selected = $args->{selected} unless defined $selected;
+       $selected =  ref $self ? eval {$self->$col;} : $args->{default}
+               unless (defined $selected);
+
+    my $a = HTML::Element->new("select", name => $col);
+    if ($args->{column_nullable} || $args->{value} eq '') {
+               my $null =  HTML::Element->new("option");
+               $null->attr('selected', 'selected') if  $args->{value} eq '';
+           $a->push_content( $null ); 
+       }
+          
+    my ($opt0, $opt1) = ( HTML::Element->new("option", value => 0),
+                                                 HTML::Element->new("option", value => 1) ); 
+    $opt0->push_content($bool_text[0]); 
+    $opt1->push_content($bool_text[1]); 
+       unless ($selected eq '') { 
+       $opt0->attr("selected" => "selected") if not $selected; 
+       $opt1->attr("selected" => "selected") if $selected; 
+       }
+    $a->push_content($opt0, $opt1);
+    $OLD_STYLE && return $a->as_HTML;
+    $a;
+}
+
+
+=head2 _to_hidden($field, $args)
+
+This makes a hidden html element input. It uses the "name" and "value" 
+arguments. If one or both are not there, it will look for an object in 
+"items->[0]" or the caller. Then it will use $field or the primary key for
+name  and the value of the column by the derived name.
+
+=cut
+
+sub _to_hidden {
+    my ($self, $field, $args) = @_;
+    $args ||= {};
+       my ($name, $value) = ($args->{'name'}, $args->{value});
+       $name = $field unless defined $name;
+       if (! defined $name and !defined $value) { # check for objects
+       my $obj = $args->{items}->[0] || $self;
+               unless (ref $obj) { die "_to_hidden cannot determine a value. It was passed a value argument or items object or called with an object."; }
+               $name = $obj->primary_column->name unless $name;
+               $value = $obj->$name unless $value;
+       }
+
+    return HTML::Element->new('input', 'type' => 'hidden',
+                              'name' => $name, 'value'=>$value);
+    
+}
+
+=head2 _to_link_hidden($col, $args) 
+
+Makes a link with a hidden input with the id of $obj as the value and name.
+Name defaults to the objects primary key. The object defaults to self.
+
+=cut
+
+sub _to_link_hidden {
+    my ($self, $accessor, $args) = @_;
+    my $r =  eval {$self->controller} || $args->{r} || '';
+    my $uri = $args->{uri} || '';
+   use Data::Dumper;
+    $self->_croak("_to_link_hidden cant get uri. No  Maypole Request class (\$r) or uri arg. Need one or other.")
+        unless $r;
+    my ($obj, $name);
+    if (ref $self) { # hidding linking self
+         $obj  = $self;
+         $name = $args->{name} || $obj->primary_column->name;
+    }
+    elsif ($obj = $args->{items}->[0]) {
+        $name = $args->{name} || $accessor || $obj->primary_column->name; 
+               # TODO use meta data above maybe
+    }
+    else {           # hiding linking related object with id in args
+        $obj  = $self->related_class($r, $accessor)->retrieve($args->{id});
+        $name = $args->{name} || $accessor ; #$obj->primary_column->name;
+               # TODO use meta data above maybe
+    }
+    $self->_croak("_to_link_hidden has no object") unless ref $obj;
+    my $href =  $uri || $r->config->{uri_base} . "/". $obj->table."/view/".$obj->id;
+    my $a = HTML::Element->new('a', 'href' => $href);
+    $a->push_content("$obj");
+    $a->push_content($self->to_field('blahfooey', 'hidden', {name => $name, value =>  $obj->id} ));
+
+       $OLD_STYLE && return $a->as_HTML;
+    $a;
+}
+
+=head2 _to_foreign_inputs
+
+Creates inputs for a foreign class, usually related to the calling class or 
+object. In names them so they do not clash with other names and so they 
+can be processed generically.  See _rename_foreign_inputs below  and 
+Maypole::Model::CDBI::FromCGI::classify_foreign_inputs.
+
+Arguments this recognizes are :
+
+       related_meta -- if you have this, great, othervise it will determine or die
+       columns  -- list of columns to make inputs for 
+       request (r) -- TODO the Maypole request so we can see what action  
+
+=cut
+
+sub _to_foreign_inputs {
+       my ($self, $accssr, $args) = @_;
+       my $rel_meta = $args->{related_meta} || $self->related_meta('r',$accssr); 
+       my $fields              = $args->{columns};
+       if (!$rel_meta) {
+               $self->_croak( "No relationship for accessor $accssr");
+       }
+
+       my $rel_type = $rel_meta->{name};
+       my $classORobj = ref $self && ref $self->$accssr ? $self->$accssr : $rel_meta->{foreign_class};
+       
+       unless ($fields) {      
+               $fields = $classORobj->can('display_columns') ? 
+                       [$classORobj->display_columns] : [$classORobj->columns];
+       }
+       
+       # Ignore our fkey in them to  prevent infinite recursion 
+       my $me          = eval {$rel_meta->{args}{foreign_key}} || 
+                                         eval {$rel_meta->{args}{foreign_column}}
+                         || ''; # what uses foreign_column has_many or might_have  
+       my $constrained = $rel_meta->{args}{constraint}; 
+       my %inputs;
+       foreach ( @$fields ) {
+               next if $constrained->{$_} || ($_ eq $me); # don't display constrained
+               $inputs{$_} =  $classORobj->to_field($_);
+       }
+
+       # Make hidden inputs for constrained columns unless we are editing object
+       # TODO -- is this right thing to do?
+       unless (ref $classORobj || $args->{no_hidden_constraints}) {
+               $inputs{$_} = $classORobj->to_field('blahfooey', 'hidden', 
+                                     {name => $_, value => $constrained->{$_}} ) 
+                       foreach ( keys %$constrained );  
+       }
+       $self->_rename_foreign_input($accssr, \%inputs);
+       return \%inputs;
+}
+
+
+=head2 _hash_selected
+
+*Function* to make sense out of the "selected" argument which has values of the 
+options that should be selected by default when making a select box.  It
+can be in a number formats.  This method returns a map of which options to 
+select with the values being the keys in the map ( {val1 => 1, val2 = 1} ).
+
+Currently this method  handles the following formats for the "selected" argument
+and in the following ways
+
+  Object                               -- uses the id method  to get the value
+  Scalar                               -- assumes it *is* the value
+  Array ref of objects         -- same as Object
+  Arrays of data               -- uses the 0th element in each
+  Hashes of data               -- uses key named 'id'
+    
+=cut 
+############
+# FUNCTION #
+############
+sub _hash_selected {
+       my ($args) = shift;
+       my $selected = $args->{value} || $args->{selected};
+       #warn "**** SELECTED is $selected ****";
+       my $type = ref $selected;
+    return $selected unless $selected and $type ne 'HASH'; 
+       #warn "Selected dump : " . Dumper($selected);
+       # Single Object 
+    if ($type and $type ne 'ARRAY') {
+          my $id = $selected->id;
+          $id =~ s/^0*//;
+       return  {$id => 1};
+    }
+    # Single Scalar id 
+       elsif (not $type) {
+               return { $selected => 1}; 
+       }
+       
+
+       # Array of objs, arrays, hashes, or just scalalrs. 
+       elsif ($type eq 'ARRAY') {
+               my %hashed;
+               my $ltype = ref $selected->[0];
+               # Objects
+               if ($ltype and $ltype ne 'ARRAY')  {
+                       %hashed = map { $_->id  => 1 } @$selected;
+               }
+               # Arrays of data with id first 
+           elsif ($ltype and $ltype eq 'ARRAY') {
+                       %hashed = map { $_->[0]  => 1 } @$selected; 
+               }
+               # Hashes using pk or id key
+               elsif ($ltype and $ltype eq 'HASH') {
+                       my $pk = $args->{class}->primary_column || 'id';
+                       %hashed = map { $_->{$pk}  => 1 } @$selected; 
+               }
+               # Just Scalars
+        else { 
+                       %hashed = map { $_  => 1 } @$selected; 
+               }
+               return \%hashed;
+       }
+       else { warn "AsForm Could not hash the selected argument: $selected"; }
+} 
+               
+
+
+
+=head2 _select_guts 
+
+Internal api  method to make the actual select box form elements. 
+the data.
+
+Items to make options out of can be 
+  Hash, Array, 
+  Array of CDBI objects.
+  Array of scalars , 
+  Array or  Array refs with cols from class,
+  Array of hashes 
+
+=cut
+
+
+
+sub _select_guts {
+    my ($self, $col, $args) = @_; #$nullable, $selected_id, $values) = @_;
+
+    #$args->{stringify} ||=  'stringify_selectbox';
+
+    $args->{selected} = _hash_selected($args) if defined $args->{selected};
+       my $name = $args->{name} || $col;
+    my $a = HTML::Element->new('select', name => $name);
+       $a->attr( %{$args->{attr}} ) if $args->{attr};
+    
+    if ($args->{column_nullable}) {
+               my $null_element = HTML::Element->new('option', value => '');
+        $null_element->attr(selected => 'selected')
+               if ($args->{selected}{'null'});
+        $a->push_content($null_element);
+    }
+
+       my $items = $args->{items};
+    my $type = ref $items;
+       my $proto = eval { ref $items->[0]; } || "";
+       my $optgroups = $args->{optgroups} || '';
+       
+       # Array of hashes, one for each optgroup
+       if ($optgroups) {
+               my $i = 0;
+               foreach (@$optgroups) {
+                       my $ogrp=  HTML::Element->new('optgroup', label => $_);
+                       $ogrp->push_content($self->_options_from_hash($items->[$i], $args));
+                       $a->push_content($ogrp);
+                       $i++;
+               }
+       }               
+    # Single Hash
+    elsif ($type eq 'HASH') {
+        $a->push_content($self->_options_from_hash($items, $args));
+    }
+    # Single Array
+    elsif ( $type eq 'ARRAY' and not ref $items->[0] ) {
+        $a->push_content($self->_options_from_array($items, $args));
+    }
+    # Array of Objects
+    elsif( $type eq 'ARRAY' and $proto !~ /ARRAY|HASH/i ) {
+        # make select  of objects
+        $a->push_content($self->_options_from_objects($items, $args));
+    }
+    # Array of Arrays
+    elsif ( $type eq 'ARRAY' and $proto eq 'ARRAY' ) {
+        $a->push_content($self->_options_from_arrays($items, $args));
+    }
+    # Array of Hashes
+    elsif ( $type eq 'ARRAY' and $proto eq 'HASH' ) {
+        $a->push_content($self->_options_from_hashes($items, $args));
+    }
+    else {
+        die "You passed a weird type of data structure to me. Here it is: " .
+       Dumper($items );
+    }
+
+    return $a;
+
+
+}
+
+=head2 _options_from_objects ( $objects, $args);
+
+Private method to makes a options out of  objects. It attempts to call each
+objects stringify method specified in $args->{stringify} as the content. Otherwise the default stringification prevails.
+
+*Note only  single primary keys supported
+
+=cut
+sub _options_from_objects {
+    my ($self, $items, $args) = @_;
+       my $selected = $args->{selected} || {};
+       my $stringify = $args->{stringify} || '';
+    my @res;
+       for (@$items) {
+               my $id = $_->id;
+               my $opt = HTML::Element->new("option", value => $id);
+               $id =~ s/^0*//; # leading zeros no good in hash key
+               $opt->attr(selected => "selected") if $selected->{$id}; 
+               my $content = $stringify ? $_->stringify :  "$_";
+               $opt->push_content($content);
+               push @res, $opt; 
+       }
+    return @res;
+}
+
+sub _options_from_arrays {
+    my ($self, $items, $args) = @_;
+       my $selected = $args->{selected} || {};
+    my @res;
+       my $class = $args->{class} || '';
+       my $stringify = $args->{stringify} || '';
+       for my $item (@$items) {
+           my @pks; # for future multiple key support
+           push @pks, shift @$item foreach $class->columns('Primary');
+               my $id = $pks[0];
+               $id =~ s/^0+//;  # In case zerofill is on .
+               my $val = defined $id ? $id : '';
+               my $opt = HTML::Element->new("option", value =>$val);
+               $opt->attr(selected => "selected") if $selected->{$id};
+               
+               my $content = ($class and $stringify and $class->can($stringify)) ? 
+                             $class->$stringify($_) : 
+                                 join( '/', map { $_ if $_; }@{$item} );
+               $opt->push_content( $content );
+        push @res, $opt; 
+    }
+    return @res;
+}
+
+
+sub _options_from_array {
+    my ($self, $items, $args) = @_;
+    my $selected = $args->{selected} || {};
+    my @res;
+    for (@$items) {
+               my $val = defined $_ ? $_ : '';
+        my $opt = HTML::Element->new("option", value => $val);
+        #$opt->attr(selected => "selected") if $selected =~/^$id$/;
+        $opt->attr(selected => "selected") if $selected->{$_};
+        $opt->push_content( $_ );
+        push @res, $opt;
+    }
+    return @res;
+}
+
+sub _options_from_hash {
+    my ($self, $items, $args) = @_;
+    my $selected = $args->{selected} || {};
+    my @res;
+
+    my @values = values %$items;
+    # hash Key is the option content  and the hash value is option value
+    for (sort keys %$items) {
+               my $val = defined $items->{$_} ? $items->{$_} : '';
+        my $opt = HTML::Element->new("option", value => $val);
+        #$opt->attr(selected => "selected") if $selected =~/^$id$/;
+        $opt->attr(selected => "selected") if $selected->{$items->{$_}};
+        $opt->push_content( $_ );
+        push @res, $opt;
+    }
+    return @res;
+}
+
+
+sub _options_from_hashes {
+    my ($self, $items, $args) = @_;
+       my $selected = $args->{selected} || {};
+       my $pk = eval {$args->{class}->primary_column} || 'id';
+       my $fclass = $args->{class} || '';
+       my $stringify = $args->{stringify} || '';
+       my @res;
+       for my $item (@$items) {
+               my $val = defined $item->{$pk} ? $item->{$pk} : '';
+               my $opt = HTML::Element->new("option", value => $val);
+               $opt->attr(selected => "selected") if $selected->{$val};
+               my $content = ($fclass and $stringify and $fclass->can($stringify)) ? 
+                             $fclass->$stringify($_) : 
+                                 join(' ', map {$item->{$_} } keys %$item);
+               $opt->push_content( $content );
+        push @res, $opt; 
+    }
+       return @res;
+}
+
+# TODO -- Maybe
+#sub _to_select_or_create {
+#      my ($self, $col, $args) = @_;
+#      $args->{name} ||= $col;
+#      my $select = $self->to_field($col, 'select', $args);
+#      $args->{name} = "create_" . $args->{name};
+#      my $create = $self->to_field($col, 'foreign_inputs', $args);
+#      $create->{'__select_or_create__'} = 
+#              $self->to_field('__select_or_create__',{ name => '__select_or_create__' , value => 1 } );
+#      return ($select, $create);
+#}
+       
+
+=head2 _to_checkbox 
+
+Makes a checkbox element -- TODO
+
+=cut
+# 
+# checkboxes: if no data in hand (ie called as class method), replace
+# with a radio button, in order to allow this field to be left
+# unspecified in search / add forms.
+# 
+# Not tested
+# TODO  --  make this general checkboxse
+# 
+#
+sub _to_checkbox {
+    my ($self, $col, $args) = @_;
+    my $nullable = eval {self->column_nullable($col)} || 0; 
+    return $self->_to_radio($col) if !ref($self) || $nullable;
+    my $value = $self->$col;
+    my $a = HTML::Element->new("input", type=> "checkbox", name => $col);
+    $a->attr("checked" => 'true') if $value eq 'Y';
+    return $a;
+}
+
+=head2 _to_radio
+
+Makes a radio button element -- TODO
+
+=cut
+# TODO  -- make this general radio butons
+#
+sub _to_radio {
+    my ($self, $col) = @_;
+    my $value = ref $self && $self->$col || '';
+    my $nullable = eval {self->column_nullable($col)} || 0; 
+    my $a = HTML::Element->new("span");
+    my $ry = HTML::Element->new("input", type=> "radio", name=>$col, value=>'Y' );
+    my $rn = HTML::Element->new("input", type=> "radio", name=>$col, value=>'N' );
+    my $ru = HTML::Element->new("input", type=> "radio", name=>$col, value=>'' ) if $nullable;
+    $ry->push_content('Yes'); $rn->push_content('No');
+    $ru->push_content('n/a') if $nullable;
+    if ($value eq 'Y') { $ry->attr("checked" => 'true') }
+    elsif ($value eq 'N') { $rn->attr("checked" => 'true') }
+    elsif ($nullable) { $ru->attr("checked" => 'true') }
+    $a->push_content($ry, $rn);
+    $a->push_content($ru) if $nullable;
+    return $a;
+}
+
+
+
+############################ HELPER METHODS ######################
+##################################################################
+
+=head2 _rename_foreign_input
+
+_rename_foreign_input($html_el_or_hash_of_them); # changes made by reference
+
+Recursively renames the foreign inputs made by _to_foreign_inputs so they 
+can be processed generically.  It uses foreign_input_delimiter. 
+
+So if an Employee is a Person who has_many  Addresses and you call and the
+method 'foreign_input_delimiter' returns '__AF__' then 
+
+  Employee->to_field("person");  
+  
+will get inputs for the Person as well as their Address (by default,
+override _field_from_relationship to change logic) named like this: 
+
+  person__AF__address__AF__street
+  person__AF__address__AF__city
+  person__AF__address__AF__state  
+  person__AF__address__AF__zip  
+
+And the processor would know to create this address, put the address id in
+person->{address} data slot, insert the person and put the person id in the employee->{person} data slot and then insert the employee with that data.
+
+=cut
+
+sub _rename_foreign_input {
+       my ($self, $accssr, $element) = @_;
+       my $del = $self->foreign_input_delimiter;
+       
+       if ( ref $element ne 'HASH' ) {
+       #       my $new_name = $accssr . "__AF__" . $input->attr('name');
+               $element->attr( name => $accssr . $del . $element->attr('name'));
+       }
+       else {
+               $self->_rename_foreign_input($accssr, $element->{$_}) 
+                       foreach (keys %$element);
+       }
+}
+
+=head2 foreign_input_delimiter
+
+This tells AsForm what to use to delmit forieign input names. This is important
+to avoid name clashes as well as automating processing of forms. 
+
+=cut
+
+sub foreign_input_delimiter { '__AF__' };
+
+=head2 _box($value) 
+
+This functions computes the dimensions of a textarea based on the value 
+or the defaults.
+
+=cut
+
+sub _box
+{
+       
+       my ($min_rows, $max_rows, $min_cols, $max_cols) = (2 => 50, 20 => 100);
+    my $text = shift;
+    if ($text) {
+       my @rows = split /^/, $text;
+       my $cols = $min_cols;
+       my $chars = 0;
+       for (@rows) {
+           my $len = length $_;
+           $chars += $len;
+           $cols = $len if $len > $cols;
+           $cols = $max_cols if $cols > $max_cols;
+       }
+       my $rows = @rows;
+       $rows = int($chars/$cols) + 1 if $chars/$cols > $rows;
+       $rows = $min_rows if $rows < $min_rows;
+       $rows = $max_rows if $rows > $max_rows;
+       ($rows, $cols)
+    }
+    else { ($min_rows, $min_cols) }
+}
+
+
+1;
+
+
+=head1 CHANGES
+
+1.0 
+15-07-2004 -- Initial version
+=head1 MAINTAINER 
+
+Maypole Developers
+
+=head1 AUTHORS
+
+Peter Speltz, Aaron Trevena 
+
+=head1 AUTHORS EMERITUS
+
+Simon Cozens, Tony Bowden
+
+=head1 TODO
+
+  Documenting 
+  Testing - lots
+  chekbox generalization
+  radio generalization
+  select work
+  Make link_hidden use standard make_url stuff when it gets in Maypole
+  How do you tell AF --" I want a has_many select box for this every time so,
+     when you call "to_field($this_hasmany)" you get a select box
+
+=head1 BUGS and QUERIES
+
+Please direct all correspondence regarding this module to:
+ Maypole list. 
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2003-2004 by Simon Cozens / Tony Bowden
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<Class::DBI>, L<Class::DBI::FromCGI>, L<HTML::Element>.
+
+=cut
+
diff --git a/lib/Maypole/Model/CDBI/FromCGI.pm b/lib/Maypole/Model/CDBI/FromCGI.pm
new file mode 100644 (file)
index 0000000..6cb95a0
--- /dev/null
@@ -0,0 +1,666 @@
+package Maypole::Model::CDBI::FromCGI;
+use strict;
+use warnings;
+
+=head1 NAME
+
+Maypole::Model:CDBI::FromCGI - Validate form input and populate Model objects
+
+=head1 SYNOPSIS
+
+  $obj = $class->create_from_cgi($r);
+  $obj = $class->create_from_cgi($r, { params => {data1=>...}, required => [..],
+                ignore => [...], all => [...]);
+  $obj = $class->create_from_cgi($h, $options); # CDBI::FromCGI style, see docs
+
+  $obj->update_from_cgi($r);
+  $obj->update_from_cgi($h, $options);
+
+  $obj = $obj->add_to_from_cgi($r);
+  $obj = $obj->add_to_from_cgi($r, { params => {...} } );
+
+  # This does not work like in CDBI::FromCGI and probably never will :
+  # $class->update_from_cgi($h, @columns);
+
+
+=head1 DESCRIPTION
+
+Provides a way to validate form input and populate Model Objects, based
+on Class::DBI::FromCGI.
+
+=cut
+
+
+# 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 add_to_from_cgi
+    cgi_update_errors untaint_type validate_inputs validate_all _do_update_all 
+    _do_create_all _create_related classify_form_inputs/;
+
+
+
+use Data::Dumper; # for debugging
+
+=head1 METHODS
+
+=head2 untaint_columns
+
+Replicates Class::DBI::FromCGI method of same name :
+
+  __PACKAGE__->untaint_columns(
+    printable => [qw/Title Director/],
+    integer   => [qw/DomesticGross NumExplodingSheep],
+    date      => [qw/OpeningDate/],
+  );
+
+=cut
+
+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);
+}
+
+=head2 untaint_type
+
+  gets the  untaint type for a column as set in "untaint_types"
+
+=cut
+
+# 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;
+}
+
+=head2 cgi_update_errors
+
+Returns errors that ocurred during an operation.
+
+=cut
+
+sub cgi_update_errors { %{ shift->{_cgi_update_error} || {} } }
+
+
+
+=head2 create_from_cgi
+
+Based on the same method in Class::DBI::FromCGI.
+
+Creates  multiple objects  from a  cgi form. 
+Errors are returned in cgi_update_errors
+
+It can be called Maypole style passing the Maypole request object as the
+first arg, or Class::DBI::FromCGI style passing the Untaint Handler ($h)
+as the first arg. 
+
+A hashref of options can be passed as the second argument. Unlike 
+in the CDBI equivalent, you can *not* pass a list as the second argument.
+Options can be :
+ params -- hashref of cgi data to use instead of $r->params,
+ required -- list of fields that are required
+ ignore   -- list of fields to ignore
+ all      -- list of all fields (defaults to $class->columns)
+
+=cut
+
+sub create_from_cgi {
+  my ($self, $r, $opts) = @_;
+  $self->_croak( "create_from_cgi can only be called as a class method")
+    if ref $self;
+  my ($errors, $validated);
+  
+  
+  if ($r->isa('CGI::Untaint')) { # FromCGI interface compatibility
+    ($validated, $errors) = $self->validate_inputs($r,$opts); 
+  } else {
+    my $params = $opts->{params} || $r->params;
+    $opts->{params} = $self->classify_form_inputs($params);
+    ($validated, $errors) = $self->validate_all($r, $opts);
+  }
+
+  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 }, $self;
+  }
+  return $obj;
+}
+
+
+=head2 update_from_cgi
+
+Replicates the Class::DBI::FromCGI method of same name. It updates an object and
+returns 1 upon success. It can take the same arguments as create_form_cgi. 
+If errors, it sets the cgi_update_errors.
+
+=cut
+
+sub update_from_cgi {
+  my ($self, $r, $opts) = @_;
+  $self->_croak( "update_from_cgi can only be called as an object method") unless ref $self;
+  my ($errors, $validated);
+  $self->{_cgi_update_error} = {};
+  $opts->{updating} = 1;
+
+  # FromCGI interface compatibility 
+  if ($r->isa('CGI::Untaint')) {
+    # REHASH the $opts for updating:
+    # 1: we ignore any fields we dont have parmeter for. (safe ?)
+    # 2: we dont want to update fields unless they change
+
+    my @ignore = @{$opts->{ignore} || []};
+    push @ignore, $self->primary_column->name;
+    my $raw = $r->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"; 
+       }
+    }
+    $opts->{ignore} = \@ignore;
+    ($validated, $errors) = $self->validate_inputs($r,$opts); 
+  } else {
+    my $params = $opts->{params} || $r->params;
+    $opts->{params} = $self->classify_form_inputs($params);
+    ($validated, $errors) = $self->validate_all($r, $opts);
+    #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 add_to_from_cgi
+
+$obj->add_to_from_cgi($r[, $opts]); 
+
+Like add_to_* for has_many relationships but will add nay objects it can 
+figure out from the data.  It returns a list of objects it creates or nothing
+on error. Call cgi_update_errors with the calling object to get errors.
+Fatal errors are in the respective "FATAL" key.
+
+=cut
+
+sub add_to_from_cgi {
+  my ($self, $r, $opts) = @_;
+  $self->_croak( "add_to_from_cgi can only be called as an object method")
+    unless ref $self;
+  my ($errors, $validated, @created);
+   
+  my $params = $opts->{params} || $r->params;
+  $opts->{params} = $self->classify_form_inputs($params);
+  ($validated, $errors) = $self->validate_all($r, $opts);
+
+  
+  if (keys %$errors) {
+    $self->{_cgi_update_error} = $errors;
+       return;
+  }
+
+  # Insert all the data
+  foreach my $hm (keys %$validated) { 
+       my ($obj, $errs) = $self->_create_related($hm, $validated->{$hm}); 
+       if (not $errs) {
+               push @created, $obj;
+       }else {
+               $errors->{$hm} = $errs;
+       }
+  }
+  
+  if (keys %$errors) {
+    $self->{_cgi_update_error} = $errors;
+       return;
+  }
+
+  return @created;
+}
+
+
+
+=head2 validate_all
+
+Validates (untaints) a hash of possibly mixed table data. 
+Returns validated and errors ($validated, $errors).
+If no errors then undef in that spot.
+
+=cut
+
+sub validate_all {
+  my ($self, $r, $opts) = @_;
+  my $class = ref $self || $self;
+  my $classified = $opts->{params};
+  my $updating   = $opts->{updating};
+
+  # Base case - validate this classes data
+  $opts->{all}   ||= eval{ $r->config->{$self->table}{all_cols} } || [$self->columns('All')];
+  $opts->{required} ||= eval { $r->config->{$self->table}{required_cols} || $self->required_columns } || [];
+  my $ignore = $opts->{ignore} || eval{ $r->config->{$self->table}{ignore_cols} } || [];
+  push @$ignore, $self->primary_column->name if $updating;
+  
+  # Ignore hashes of foreign inputs. This takes care of required has_a's 
+  # for main object that we have foreign inputs for. 
+  foreach (keys %$classified) {
+    push @$ignore, $_ if  ref $classified->{$_} eq 'HASH'; 
+  }
+  $opts->{ignore} = $ignore;
+  my $h = $Untainter->new($classified);
+  my ($validated, $errs) = $self->validate_inputs($h, $opts);
+
+  # Validate all foreign input
+       
+  #warn "Classified data is " . Dumper($classified); 
+  foreach my $field (keys %$classified) {
+    if (ref $classified->{$field} eq "HASH") {
+      my $data = $classified->{$field};
+         my $ignore = [];
+      my @usr_entered_vals = ();
+      foreach ( values %$data ) {
+               push @usr_entered_vals, $_  if $_  ne '';
+      }
+
+      # filled in values
+      # IF we have some inputs for the related
+      if ( @usr_entered_vals ) {
+               # We need to ignore us if we are a required has_a in this foreign class
+               my $rel_meta = $self->related_meta($r, $field);
+           my $fclass   = $rel_meta->{foreign_class};
+               my $fmeta    = $fclass->meta_info('has_a');
+               for (keys %$fmeta) {
+                       if ($fmeta->{$_}{foreign_class} eq $class) {
+                               push @$ignore, $_;
+                       }
+               }
+               my ($valid, $ferrs) = $fclass->validate_all($r,
+               {params => $data, updating => $updating, ignore => $ignore } );         
+
+               $errs->{$field} = $ferrs if $ferrs;
+               $validated->{$field} = $valid;
+
+      } else { 
+               # Check this foreign object is not requeired
+               my %req = map { $_ => 1 } $opts->{required};
+               if ($req{$field}) {
+                       $errs->{$field}{FATAL} = "This is required. Please enter the required fields in this section." 
+                       }
+               }
+       }
+  }
+  #warn "Validated inputs are " . Dumper($validated);
+  undef $errs unless keys %$errs;
+  return ($validated, $errs);  
+}
+
+
+
+=head2 validate_inputs
+
+$self->validate_inputs($h, $opts);
+
+This is the main validation method to validate inputs for a single class.
+Most of the time you use validate_all.
+
+Returns validated and errors.
+
+If no errors then undef in that slot.
+
+Note: This method is currently experimental (in 2.11) and may be subject to change
+without notice.
+
+=cut
+
+sub validate_inputs {
+  my ($self, $h, $opts) = @_;
+  my $updating = $opts->{updating};
+  my %required = map { $_ => 1 } @{$opts->{required}};
+  my %seen;
+  $seen{$_}++ foreach @{$opts->{ignore}};
+  my $errors   = {}; 
+  my $fields   = {};
+  $opts->{all} = [ $self->columns ] unless @{$opts->{all} || [] } ;
+  foreach my $field (@{$opts->{required}}, @{$opts->{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/) {
+      $errors->{$field} = "You must supply '$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 ($fields, $errors);
+}
+
+
+##################
+# _do_create_all #
+##################
+
+# Untaints and Creates objects from hashed params.
+# Returns parent object and errors ($obj, $errors).  
+# If no errors, then undef in that slot.
+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
+  #warn "\n*** validated data is " . Dumper($validated). "***\n";
+  my $me_obj  = eval { $self->create($validated) };
+  if ($@) { 
+       warn "Just failed making a " . $self. " FATAL Error is $@"
+               if (eval{$self->model_debug});  
+    $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;
+       
+  }
+  #warn "Errors are " . Dumper($errors);
+
+  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 
+# Returns related object and errors ($obj, $errors).  
+# If no errors, then undef in that slot.
+
+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};
+       #warn " Dumper of meta is " . Dumper($rel_meta);
+       
+
+       my ($rel, $errs); 
+
+       # Set up params for might_have, has_many, etc
+       if ($rel_type ne 'has_own' and $rel_type ne 'has_a') {
+
+               # Foreign Key meta data not very standardized in CDBI
+               my $fkey= $rel_meta->{args}{foreign_key} || $rel_meta->{foreign_column};
+               unless ($fkey) { die " Could not determine foreign key for $fclass"; }
+               my %data = (%$params, $fkey => $self->id);
+               %data = ( %data, %{$rel_meta->{args}->{constraint} || {}} );
+               #warn "Data is " . Dumper(\%data);
+           ($rel, $errs) =  $fclass->_do_create_all(\%data, $created);
+       }
+       else { 
+           ($rel, $errs) =  $fclass->_do_create_all($params, $created);
+               unless ($errs) {
+                       $self->$accssr($rel->id);
+                       $self->update;
+               }
+       }
+       return ($rel, $errs);
+}
+
+
+
+               
+=head2  classify_form_inputs
+
+$self->classify_form_inputs($params[, $delimiter]);
+
+Foreign inputs are inputs that have data for a related table.
+They come named so we can tell which related class they belong to.
+This assumes the form : $accessor . $delimeter . $column recursively 
+classifies them into hashes. It returns a hashref.
+
+=cut
+
+sub classify_form_inputs {
+       my ($self, $params, $delimiter) = @_;
+       my %hashed = ();
+       my $bottom_level;
+       $delimiter ||= $self->foreign_input_delimiter;
+       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',
+        integer   => 'integer',
+        bigint    => 'integer',
+        year      => 'integer',
+        date      => 'date',
+    );
+    return $map{$type} || "";
+}
+
+=head1 MAINTAINER 
+
+Maypole Developers
+
+=head1 AUTHORS
+
+Peter Speltz, Aaron Trevena 
+
+=head1 AUTHORS EMERITUS
+
+Tony Bowden
+
+=head1 TODO
+
+* Tests
+* add_to_from_cgi, search_from_cgi
+* complete documentation
+* ensure full backward compatibility with Class::DBI::FromCGI
+
+=head1 BUGS and QUERIES
+
+Please direct all correspondence regarding this module to:
+ Maypole list.
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2003-2004 by Peter Speltz 
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<Class::DBI>, L<Class::DBI::FromCGI>
+
+=cut
+
+1;
+
+
index 9cce6886c03f50678d7701da074edab646a2aaa0..3c3296a2fbe3a46c59b4d961d3d00b6a6d02ab1a 100644 (file)
@@ -5,20 +5,6 @@ use strict;
 
 Maypole::Config->mk_accessors(qw(table_to_class));
 
-sub setup_database {
-    my ( $self, $config, $namespace, $classes ) = @_;
-    $config->{classes}        = $classes;
-    $config->{table_to_class} = { map { $_->table => $_ } @$classes };
-    $config->{tables}         = [ keys %{ $config->{table_to_class} } ];
-}
-
-sub class_of {
-    my ( $self, $r, $table ) = @_;
-    return $r->config->{table_to_class}->{$table};
-}
-
-1;
-
 =head1 NAME
 
 Maypole::Model::CDBI::Plain - Class::DBI model without ::Loader
@@ -26,13 +12,23 @@ Maypole::Model::CDBI::Plain - Class::DBI model without ::Loader
 =head1 SYNOPSIS
 
     package Foo;
-    use base 'Maypole::Application';
-    use Foo::SomeTable;
-    use Foo::Other::Table;
+    use 'Maypole::Application';
 
     Foo->config->model("Maypole::Model::CDBI::Plain");
     Foo->setup([qw/ Foo::SomeTable Foo::Other::Table /]);
 
+    # untaint columns and provide custom actions for each class
+
+    Foo::SomeTable->untaint_columns(email => ['email'], printable => [qw/name description/]);
+
+    Foo::Other::Table->untaint_columns ( ... );
+
+    sub Foo::SomeTable::SomeAction : Exported {
+
+        . . .
+
+    }
+
 =head1 DESCRIPTION
 
 This module allows you to use Maypole with previously set-up
@@ -42,15 +38,63 @@ tables and set up the inheritance relationships as normal.
 
 =head1 METHODS
 
-=over 4
+=head2 setup
+
+  This method is inherited from Maypole::Model::Base and calls setup_database,
+  which uses Class::DBI::Loader to create and load Class::DBI classes from
+  the given database schema.
+
+=head2 setup_database
+
+  This method loads the model classes for the application
+
+=cut
+
 
-=item setup_database
 
-=item  class_of
+sub setup_database {
+    my ( $self, $config, $namespace, $classes ) = @_;
+    $config->{classes}        = $classes;
+    foreach my $class (@$classes) { $namespace->load_model_subclass($class); }
+    $namespace->model_classes_loaded(1);
+    $config->{table_to_class} = { map { $_->table => $_ } @$classes };
+    $config->{tables}         = [ keys %{ $config->{table_to_class} } ];
+}
+
+=head2 class_of
+
+  returns class for given table
+
+=cut
+
+sub class_of {
+    my ( $self, $r, $table ) = @_;
+    return $r->config->{table_to_class}->{$table};
+}
 
-=back
+=head2 adopt
 
-See L<Maypole::Model::Base>
+This class method is passed the name of a model class that represensts a table
+and allows the master model class to do any set-up required.
 
 =cut
 
+sub adopt {
+    my ( $self, $child ) = @_;
+    if ( my $col = $child->stringify_column ) {
+        $child->columns( Stringify => $col );
+    }
+}
+
+=head1 SEE ALSO
+
+L<Maypole::Model::Base>
+
+L<Maypole::Model::CDBI>
+
+=cut
+
+
+1;
+
+
index 309ff38e601e8fd9bac1959ea399fa63f5dfde82..774553b5e5eefcb89bd8b192ac9251bc5c805d50 100644 (file)
@@ -12,11 +12,16 @@ my $uid = Maypole::Session::generate_unique_id()
 
 =head1 DESCRIPTION
 
-This class provides session related methods for Maypole such as unique id's for requests.
+This class provides session related methods for Maypole such as unique id's for
+requests.
 
-Currently it provides only the generate_unique_id() function, by checking the id's generated by this function and included in submitted forms, it is possible to see if a form has been submitted before.. implementing these checks is left to the developer of that application.
+Currently it provides only the generate_unique_id() function, by checking the
+id's generated by this function and included in submitted forms, it is possible
+to see if a form has been submitted before.. implementing these checks is left
+to the developer of that application.
 
-Further functionality is to be added here in later versions to provide easy access to sessions, either through plugins or builtin methods.
+Further functionality is to be added here in later versions to provide easy
+access to sessions, either through plugins or builtin methods.
 
 =head1 FUNCTIONS
 
@@ -24,7 +29,8 @@ Further functionality is to be added here in later versions to provide easy acce
 
 my $uid = Maypole::Session::generate_unique_id()
 
-generates a unique id and returns it, requires no arguments but accepts size, default is 32.
+generates a unique id and returns it, requires no arguments but accepts size,
+default is 32.
 
 =cut
 
@@ -35,16 +41,18 @@ our $VERSION = 0.01;
 sub generate_unique_id {
     my $length = shift || 32;
     my $id = substr(Digest::MD5::md5_hex(Digest::MD5::md5_hex(time(). {}. rand(). $$)), 0, $length);
-    return;
+    return $id;
 }
 
 
-###################################################################################################
-###################################################################################################
+################################################################################
+################################################################################
 
 =head1 TODO
 
-Currently implementing uniqueness tests of form submissions is left to the Maypole user, we plan to provide an optional default behaviour to automate this if required.
+Currently implementing uniqueness tests of form submissions is left to the
+Maypole user, we plan to provide an optional default behaviour to automate this
+if required.
 
 =head1 SEE ALSO
 
index 9615950d0bf141babecac61003218050f6e9bed7..5a99580e514f01a0c914f677a2b96a8028cfc47a 100644 (file)
@@ -19,12 +19,13 @@ sub paths {
        push(@output,
             (
               $r->model_class
-             && File::Spec->catdir( $path, $r->model_class->moniker )
+             && File::Spec->catdir( $path, $r->model_class->table )
              )
             );
        push(@output, File::Spec->catdir( $path, "custom" ));
        push(@output, File::Spec->catdir( $path, "factory" ));
     }
+
     return @output;
 }
 
@@ -40,10 +41,11 @@ sub vars {
         request => $r,
         objects => $r->objects,
         base    => $base,
-        config  => $r->config
-
-          # ...
+        config  => $r->config,
     );
+
+    $args{object} = $r->object if ($r->can('object'));
+
     if ($class) {
         my $classmeta = $r->template_args->{classmetadata} ||= {};
         $classmeta->{name}              ||= $class;
@@ -73,8 +75,6 @@ sub vars {
 
 sub process {
     my ( $self, $r ) = @_;
-    $r->{content_type}      ||= "text/html";
-    $r->{document_encoding} ||= "utf-8";
     my $status = $self->template($r);
     return $self->error($r) if $status != OK;
     return OK;
@@ -83,38 +83,41 @@ sub process {
 sub error {
     my ( $self, $r, $desc ) = @_;
     $desc = $desc ? "$desc: " : "";
-    carp $desc . $r->{error};
     if ( $r->{error} =~ /not found$/ ) {
-
+       warn "template not found error : ", $r->{error};
         # This is a rough test to see whether or not we're a template or
         # a static page
         return -1 unless @{ $r->{objects} || [] };
 
+       my $template_error = $r->{error};
         $r->{error} = <<EOF;
+<h1> Template not found </h1>
 
-<H1> Template not found </H1>
-
-This template was not found while processing the following request:
+A template was not found while processing the following request:
 
-<B>@{[$r->{action}]}</B> on table <B>@{[ $r->{table} ]}</B> with objects:
+<strong>@{[$r->{action}]}</strong> on table
+<strong>@{[ $r->{table} ]}</strong> with objects:
 
-<PRE>
+<pre>
 @{[join "\n", @{$r->{objects}}]}
-</PRE>
+</pre>
+
 
-Looking for template <B>@{[$r->{template}]}</B> in paths:
+The main template is <strong>@{[$r->{template}]}</strong>.
+The template subsystem's error message was
+<pre>
+$template_error
+</pre>
+We looked in paths:
 
-<PRE>
+<pre>
 @{[ join "\n", $self->paths($r) ]}
-</PRE>
+</pre>
 EOF
         $r->{content_type} = "text/html";
         $r->{output}       = $r->{error};
         return OK;
     }
-    $r->{content_type} = "text/plain";
-    $r->{output}       = $r->{error};
-    $r->send_output;
     return ERROR;
 }
 
index 1a16a84ac7491734c1af9a0f886fe4a49174ddcf..2d1d60fcfe228376df8b2f592674e619c7f3c1f2 100644 (file)
@@ -3,40 +3,88 @@ use base 'Maypole::View::Base';
 use Maypole::Constants;
 use Template;
 use File::Spec::Functions qw(catdir tmpdir);
+use Template::Constants qw( :all );
+
+our $error_template;
+{ local $/; $error_template = <DATA>; }
+
+our $VERSION = '2.111';
+
+my $debug_flags = DEBUG_ON;
 
 use strict;
-our $VERSION = "1." . sprintf "%04d", q$Rev: 333 $ =~ /: (\d+)/;
 
 sub template {
-    my ( $self, $r ) = @_;
-
-    unless ($self->{tt}) {
-        my $view_options = $r->config->view_options || {};
-        $self->{provider} = Template::Provider->new($view_options);
-        $self->{tt}       = Template->new({
-            %$view_options,
-            LOAD_TEMPLATES => [ $self->{provider} ],
-        });
+  my ( $self, $r ) = @_;
+  unless ($self->{tt}) {
+    my $view_options = $r->config->view_options || {};
+    if ($r->debug) {
+      $view_options->{DEBUG} = $debug_flags;
     }
+    $self->{provider} = Template::Provider->new($view_options);
+    $self->{tt}       = Template->new({
+                                      %$view_options,
+                                      LOAD_TEMPLATES => [ $self->{provider} ],
+                                     });
+  }
+
+  $self->{provider}->include_path([ $self->paths($r) ]);
 
-    $self->{provider}->include_path([ $self->paths($r) ]);
+  my $template_file = $r->template;
+
+  my $ext = $r->config->template_extension;
+  $template_file .= $ext if defined $ext;
+
+  my $output;
+  my $processed_ok = eval{$self->{tt}->process($template_file, { $self->vars($r) }, \$output );};
+  if ($processed_ok) {
+    $r->{output} = $output;
+    return OK;
+  } else {
+    if ($@) {
+      my $error = "fatal error in template '$template_file' : $@\nTT paths : " . join(', ',$self->paths($r)) . "\n";
+      $r->warn($error);
+      $r->{error} = $error;
+    } else {
+      my $error = "TT error for template '$template_file'\n" . $self->{tt}->error . "\nTT paths : " . join(', ',$self->paths($r)) . "\n";
+      $r->warn($error);
+      $r->{error} = $error;
+    }
+    return ERROR;
+  }
+}
 
-    my $template_file = $r->template;
-    my $ext = $r->config->template_extension;
-    $template_file .= $ext if defined $ext;
 
+sub report_error {
+    my ($self, $r, $error, $type) = @_;
     my $output;
-    if ($self->{tt}->process($template_file, { $self->vars($r) }, \$output )) {
+
+    # Need to be very careful here.
+    my $tt = Template->new;
+    unless (ref $r->{config}) {
+      $r->warn("no config for this request");
+      $error .= '<br> There was a problem finding configuration for this request';
+      $r->{config} ||= {};
+    }
+
+    $r->warn("report_error - reporting error to user : $error\n");
+
+    if ($tt->process(\$error_template,
+                    { err_type => $type, error => $error,
+                      config => $r->{config},
+                      request => $r,
+                      paths => [ $self->paths($r) ],
+                      eval{$self->vars($r)} }, \$output )) {
         $r->{output} = $output;
+        if ($tt->error) { $r->{output} = "<html><body>Even the error template
+        errored - ".$tt->error."</body></html>"; }
+        $r->{content_type}      ||= "text/html";
+        $r->{document_encoding} ||= "utf-8";
         return OK;
     }
-    else {
-        $r->{error} = $self->{tt}->error;
-        return ERROR;
-    }
+    return ERROR;
 }
 
-1;
 
 =head1 NAME
 
@@ -52,11 +100,21 @@ Maypole::View::TT - A Template Toolkit view class for Maypole
         COMPILE_DIR => '/var/tmp/mysite/templates',
     } );
 
+    .....
+
+    [% PROCESS macros %]
+
+    [% pager %]
+
+    [% link %]
+
+    [% maybe_link_view %]
+
 =head1 DESCRIPTION
 
-This is the default view class for Maypole; it uses the Template Toolkit to
-fill in templates with the objects produced by Maypole's model classes.  Please
-see the L<Maypole manual|Maypole::Manual>, and in particular, the
+This is the default view class for Maypole; it uses the Template Toolkit to fill
+in templates with the objects produced by Maypole's model classes. Please see
+the L<Maypole manual|Maypole::Manual>, and in particular, the
 L<view|Maypole::Manual::View> chapter for the template variables available and
 for a refresher on how template components are resolved.
 
@@ -70,12 +128,329 @@ options.
 
 Processes the template and sets the output. See L<Maypole::View::Base>
 
+=item report_error
+
+Reports the details of an error, current state and parameters
+
+=back
+
+=head1 TEMPLATE TOOLKIT INTRODUCTION
+
+The Template Toolkit uses it's own mini language described in
+L<Template::Manual::Directives>.
+
+A simple example would be :
+
+=over 4
+
+re:[% subject %]
+
+Dear [% title %] [% surname %],
+Thank you for your letter dated [% your.date %]. This is to
+confirm that we have received it and will respond with a more
+detailed response as soon as possible. In the mean time, we
+enclose more details of ...
+
+=back
+
+TT uses '[%' and '%]' (by default) to delimit directives within a template, and
+the simple directives above just display the value of variable named within
+those delimiters -- [% title %] will be replaced inline with the value of the
+'title' variable passed in the 'stash' to the template when it is processed.
+
+You can access nested data through the dot ('.') operator, which will
+dereference array or hash elements, but can also be used to call methods on
+objects, i.e. '[% name.salutation("Dear %s,") %]'. The other main operator is
+underscore ('_'), which will concatonate strings or variables.
+
+The value returned by a directive replaces the directive inline when the
+template is processes, you can also SET a value which will not return anything,
+or CALL a method or operation which will also not return anything.
+
+You can specify expressions using the logical (and, or, not, ?:) and mathematic
+operators (+ - * / % mod div).
+
+Results of TT commands are interpolated in the place of the template tags, unless
+using SET or CALL, i.e. [% SET foo = 1 %], [% GET foo.bar('quz'); %]
+
+=over 4
+
+[% template.title or default.title %]
+
+[% score * 100 %]
+
+[% order.nitems ? checkout(order.total) : 'no items' %]
+
+=back
+
+TT allows you to include or re-use templates through it's INCLUDE, PROCESS and
+INSERT directives, which are fairly self explainatory. You can also re-use parts
+of template with the BLOCK or MACRO directives.
+
+Conditional and Looping constructs are simple and powerful, and TT provides an
+inbuilt iterator and helper functions and classes that make life sweet.
+
+Conditional directives are IF, UNLESS, ELSIF, ELSE and behave as they would in
+perl :
+
+=over 4
+
+[% IF age < 10 %]
+  Hello [% name %], does your mother know you're  using her AOL account?
+[% ELSIF age < 18 %]
+  Sorry, you're not old enough to enter (and too dumb to lie about your age)
+[% ELSE %]
+  Welcome [% name %].
+[% END %]
+
+[% UNLESS text_mode %] [% INCLUDE biglogo %] [% END %]
+
+=back
+
+Looping directives are FOREACH, LAST and BREAK.
+
+FOREACH loops through a HASH or ARRAY processing the enclosed block for each
+element.
+
+Looping through an array
+
+ [% FOREACH i = items %]
+ [% i %]
+ [% END %]
+
+Looping through a hash
+
+ [% FOREACH u IN users %]
+ * [% u.key %] : [% u.value %]
+ [% END %]
+
+Looping through an array of hashes
+
+ [% FOREACH user IN userlist %]
+ * [% user.id %] [% user.name %]
+ [% END %]
+
+The LAST and BREAK directive can be used to exit the loop.
+
+The FOREACH directive is implemented using the Template::Iterator module. A
+reference to the iterator object for a FOREACH directive is implicitly available
+in the 'loop' variable. The loop iterator object provides a selection of methods
+including size(), max(), first(), last(), count(), etc
+
+=over 4
+
+  [% FOREACH item IN [ 'foo', 'bar', 'baz' ] -%]
+    [%- "<ul>\n" IF loop.first %]
+      <li>[% loop.count %]/[% loop.size %]: [% item %]
+    [%- "</ul>\n" IF loop.last %]
+  [% END %]
+
+=back
+
+See Template::Iterator for further details on looping and the Iterator.
+
+You might notice the minus ('-') operator in the example above, it is used to
+remove a newline before or after a directive so that you can layout the Template
+logic as above but the resulting output will look exactly how you require it.
+
+You will also frequently see comments and multi-line directives, # at the start
+of a directive marks it as a comment, i.e. '[%# this is a comment %]'. A
+multiline directive looks like :
+
+ [% do.this;
+    do.that;
+    do.the_other %]
+
+You can see that lines are terminated with a semi-colon (';') unless the
+delimter ('%]') closes the directive.
+
+For full details of the Template Toolkit see Template::Manual and
+Template::Manual::Directives, you can also check the website, mailing list or
+the Template Toolkit book published by O Reilly.
+
+=head1 TEMPLATE PLUGINS, FILTERS AND MACROS
+
+The Template Toolkit has a popular and powerful selection of Plugins and
+Filters.
+
+TT Plugins provide additional functionality within Templates, from accessing CGI
+and databases directly, handling paging or simple integration with Class::DBI
+(for those rare occasions where you don't actually need Maypole). See
+L<Template::Manual::Plugins>.
+
+One plugin that is indispensible when using Maypole and the Template View is
+C<Template::Plugin::Class> -- This allows you to import and use any class
+installed within a template. For example :
+
+=over 4
+
+[% USE foo = Class('Foo') %]
+[% foo.bar %]
+
+=back
+
+Would do the equivilent of 'use Foo; Foo->bar;' in perl. See
+L<Template::Plugin::Class> for details.
+
+TT Filters process strings or blocks within a template, allowing you to
+truncate, format, escape or encode trivially. A useful selection is included
+with Template Toolkit and they can also be found on CPAN or can be written
+easily. See L<Template::Manual::Filters>.
+
+TT provides stderr and stdout filters, which allow you to write handy macros
+like this one to output debug information to your web server log, etc :
+
+=over 4
+
+[% MACRO debug_msg(text)
+    FILTER stderr; "[TT debug_msg] $text\n"; END;
+%]
+
 =back
 
 
+TT Macros allow you to reuse small blocks of content, directives, etc. The MACRO
+directive allows you to define a directive or directive block which is then
+evaluated each time the macro is called. Macros can be passed named parameters
+when called.
+
+Once a MACRO is defined within a template or 'include'd template it can be used
+as if it were a native TT directive. Maypole provides a selection of powerful
+and useful macros in the templates/ directory of the package and these are used
+in the beerdb and default templates. See the MACRO section of the
+L<Template::Manual::Directives> documentation.
+
+=head1 ACCESSING MAYPOLE VALUES
+
+=head2 request
+
+You can access the request in your templates in order to see the action, table, etc as well
+as parameters passed through forms :
+
+for example
+
+Hello [% request.params.forename %] [% request.params.surname %] !
+
+or 
+
+Are you want to [% request.action %] in the [% request.table %] ?
+
+=head2 config
+
+You can access your maypole application configuration through the config variable :
+
+<link base="[% config.uri_base %]"/>
+
+=head2 object and objects
+
+Objects are passed to the request using r->objects($arrayref) and are accessed in the templates
+as an array called objects.
+
+[% FOR objects %] <a href="[% config.uri_base %]/[% request.table %]/view/[% object.id %]"> [% object %] </a> [% END %]
+
+=head1 MAYPOLE MACROS AND FILTERS
+
+Maypole provides a collection of useful and powerful macros in the templates/factory/macros
+ and other templates. These can be used in any template with [% PROCESS templatename %].
+
+=head2 link
+
+This creates an <A HREF="..."> to a command in the Apache::MVC system by
+catenating the base URL, table, command, and any arguments.
+
+=head2 maybe_link_view
+
+C<maybe_link_view> takes something returned from the database - either
+some ordinary data, or an object in a related class expanded by a
+has-a relationship. If it is an object, it constructs a link to the view
+command for that object. Otherwise, it just displays the data.
+
+=head2 pager
+
+This is an include template rather than a macro, and it controls the pager
+display at the bottom (by default) of the factory list and search views/template.
+It expects a C<pager> template argument which responds to the L<Data::Page> interface.
+
+This macro is in the pager template and used as :
+
+[% PROCESS pager %]
+
+Maypole provides a pager for list and search actions, otherwise you can
+provide a pager in the template using Template::Plugin::Pagination.
+
+[% USE pager = Pagination(objects, page.current, page.rows) %]
+...
+[% PROCESS pager %]
+
+The pager will use a the request action  as the action in the url unless the
+pager_action variable is set, which it will use instead if available.
+
+=head2 other macros
+
 =head1 AUTHOR
 
 Simon Cozens
 
 =cut
 
+1;
+
+__DATA__
+<html><head><title>Maypole error page</title>
+<style type="text/css">
+body { background-color:#7d95b5; font-family: sans-serif}
+p { background-color: #fff; padding: 5px; }
+pre { background-color: #fff; padding: 5px; border: 1px dotted black }
+h1 { color: #fff }
+h2 { color: #fff }
+.lhs {background-color: #ffd; }
+.rhs {background-color: #dff; }
+</style>
+</head> <body>
+<h1> Maypole application error </h1>
+
+<p> This application living at <code>[%request.config.uri_base%]</code>, 
+[%request.config.application_name || "which is unnamed" %], has
+produced an error. The adminstrator should be able to understand
+this error message and fix the problem.</p>
+
+<h2> Some basic facts </h2>
+
+<p> The error was found in the [% err_type %] stage of processing
+the path "[% request.path %]". The error text returned was:
+</p>
+<pre>
+    [% error %]
+</pre>
+
+<h2> Request details </h2>
+
+<table width="85%" cellspacing="2" cellpadding="1">
+    [% FOR attribute = ["model_class", "table", "template", "path",
+    "content_type", "document_encoding", "action", "args", "objects"] %]
+    <tr> <td class="lhs" width="35%"> <b>[% attribute %]</b> </td> <td class="rhs" width="65%"> [%
+    request.$attribute.list.join(" , ") %] </td></tr>
+    [% END %]
+    <tr><td colspan="2"></tr>
+    <tr><td class="lhs" colspan="2"><b>CGI Parameters</b> </td></tr>
+    [% FOREACH param IN request.params %]
+    <tr> <td class="lhs" width="35%">[% param.key %]</td> <td class="rhs" width="65%"> [% param.value %] </td></tr>
+    [% END %]
+</table>
+
+<h2> Website / Template Paths </h2>
+<table width="85%" cellspacing="2" cellpadding="1">
+<tr><td class="lhs" width="35%"> <b>Base URI</b> </td><td class="rhs" width="65%">[% request.config.uri_base %]</td></tr>
+<tr><td class="lhs" width="35%"> <b>Paths</b> </td><td class="rhs" width="65%"> [% paths %] </td></tr>
+</table>
+
+<h2> Application configuration </h2>
+<table width="85%" cellspacing="2" cellpadding="1">
+    <tr><td class="lhs"  width="35%"> <b>Model </b> </td><td class="rhs" width="65%"> [% request.config.model %] </td></tr>
+    <tr><td class="lhs"  width="35%"> <b>View </b> </td><td class="rhs" width="65%"> [% request.config.view %] </td></tr>
+    <tr><td class="lhs" width="35%"> <b>Classes</b> </td><td class="rhs" width="65%"> [% request.config.classes.list.join(" , ") %] </td></tr>
+    <tr><td class="lhs" width="35%"> <b>Tables</b> </td><td class="rhs" width="65%"> [% request.config.display_tables.list.join(" , ") %] </td></tr>
+</table>
+
+</body>
+</html>
diff --git a/lib/Maypole/templates/beer/addnew b/lib/Maypole/templates/beer/addnew
new file mode 100644 (file)
index 0000000..ad51f01
--- /dev/null
@@ -0,0 +1,21 @@
+[% USE element_maker = Class("HTML::Element") %]
+<div id="addnew">
+<form method="post" action="[% base %]/[% classmetadata.moniker %]/do_edit/">
+<fieldset>
+<legend>Add a new [%classmetadata.moniker%]</legend>
+    <input type="hidden" name="action" value="create"/>
+    <input type="hidden" name="class" value="[% classmetadata.name %]"/>
+        [% FOR col = classmetadata.columns;
+            NEXT IF col == "id";
+            SET element = classmetadata.cgi.$col;
+            %]
+       <label>
+                <span class="field">[% classmetadata.colnames.$col; %]</span>
+                [% element.as_XML; %]</label>
+               
+        [% END; %]
+       
+    <input type="submit" name="create" value="create"/>
+    </fieldset>
+</form>
+</div>
diff --git a/lib/Maypole/templates/factory/addnew b/lib/Maypole/templates/factory/addnew
new file mode 100644 (file)
index 0000000..2334496
--- /dev/null
@@ -0,0 +1,41 @@
+[%#
+
+=head1 addnew
+
+This is the interface to adding a new instance of an object. (or a new
+row in the database, if you want to look at it that way) It displays a
+form containing a list of HTML components for each of the columns in the
+table.
+
+=cut
+
+#%]
+
+<div id="addnew">
+<form method="post" action="[% base %]/[% classmetadata.table %]/do_edit/">
+    <fieldset>
+<legend>Add a new [% classmetadata.moniker %]</legend>
+    [% FOR col = classmetadata.columns %]
+        [% NEXT IF col == "id" %]
+            <label><span class="field">[% classmetadata.colnames.$col %]</span>
+            [% 
+            SET elem = classmetadata.cgi.$col.clone;
+            IF request.action == 'do_edit';
+                IF elem.tag == "textarea";
+                    elem = elem.push_content(request.param(col));
+                ELSE;
+                    elem.attr("value", request.param(col));
+                END;
+            END;
+            elem.as_XML; %]
+           </label>
+        [% IF errors.$col %]
+           <span class="error">[% errors.$col | html  %]</span>
+        [% END %]
+
+    [% END; %]
+    <input type="submit" name="create" value="create" />
+    <input type="hidden" name="__form_id" value="[% request.make_random_id %]" />
+</fieldset>
+</form>
+</div>
diff --git a/lib/Maypole/templates/factory/edit b/lib/Maypole/templates/factory/edit
new file mode 100644 (file)
index 0000000..3b0aca6
--- /dev/null
@@ -0,0 +1,70 @@
+[%#
+
+=head1 edit
+
+This is the edit page. It edits the passed-in object, by displaying a
+form similar to L<addnew> but with the current values filled in.
+
+=cut
+
+#%]
+[% PROCESS macros %]
+[% INCLUDE header %]
+[% INCLUDE title %]
+
+[% IF request.action == 'edit' %]
+[% INCLUDE navbar %]
+[% END %]
+
+[% IF object %]
+<div id="title">Edit a [% classmetadata.moniker %]</div>
+<form action="[% base %]/[% object.table %]/do_edit/[% object.id %]" method="post">
+<fieldset>
+<legend>Edit [% object.name %]</legend>
+   [% FOR col = classmetadata.columns;
+    NEXT IF col == "id" OR col == classmetadata.table _ "_id";
+    '<label><span class="field">';
+    classmetadata.colnames.$col; ":</span>";
+    object.to_field(col).as_XML;
+    "</label>";
+    IF errors.$col; 
+       '<span class="error">'; errors.$col;'</span>';
+    END;
+    END %]
+    <input type="submit" name="edit" value="edit"/>
+    <input type="hidden" name="__form_id" value="[% request.make_random_id %]">
+    </fieldset></form>
+    
+[% ELSE %]
+
+<div id="addnew">
+<form method="post" action="[% base %]/[% classmetadata.table %]/do_edit/">
+<fieldset>
+<legend>Add a new [% classmetadata.moniker %]</legend>
+    [% FOR col = classmetadata.columns %]
+        [% NEXT IF col == "id" %]
+            <label><span class="field">[% classmetadata.colnames.$col %]</span>
+            [% 
+            SET elem = classmetadata.cgi.$col.clone;
+            IF request.action == 'do_edit';
+                IF elem.tag == "textarea";
+                    elem = elem.push_content(request.param(col));
+                ELSE;
+                    elem.attr("value", request.param(col));
+                END;
+            END;
+            elem.as_XML; %]
+           </label>
+        [% IF errors.$col %]
+           <span class="error">[% errors.$col | html  %]</span>
+        [% END %]
+
+    [% END; %]
+    <input type="submit" name="create" value="create" />
+    <input type="hidden" name="__form_id" value="[% request.make_random_id %]" />
+</fieldset>
+</form>
+</div>
+
+[% END %]
+[% INCLUDE footer %]
diff --git a/lib/Maypole/templates/factory/footer b/lib/Maypole/templates/factory/footer
new file mode 100644 (file)
index 0000000..1b8ae55
--- /dev/null
@@ -0,0 +1,3 @@
+       </div>
+    </body>
+</html>
diff --git a/lib/Maypole/templates/factory/frontpage b/lib/Maypole/templates/factory/frontpage
new file mode 100644 (file)
index 0000000..ac47269
--- /dev/null
@@ -0,0 +1,27 @@
+[%#
+
+=head1 frontpage
+
+This is the frontpage for your Maypole application.
+It shows a list of all tables it is allowed to display.
+
+=cut
+
+#%]
+[% INCLUDE header %]
+<div id="title">
+    [% config.application_name || "A poorly configured Maypole application" %]
+</div>
+<div id="frontpage_list">
+<ul>
+[% FOR table = config.display_tables %]
+    <li>
+        <a href="[% base %]/[%table%]/list">List by [%table %]</a>
+    </li>      
+[% END %]
+</ul>
+</div>
+
+[% INCLUDE maypole %]
+
+[% INCLUDE footer %]
diff --git a/lib/Maypole/templates/factory/header b/lib/Maypole/templates/factory/header
new file mode 100644 (file)
index 0000000..ba0b190
--- /dev/null
@@ -0,0 +1,16 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <title>
+            [%
+              title || config.application_name ||
+                "A poorly configured Maypole application"
+            %]
+        </title>
+        <meta http-equiv="Content-Type" content="text/html; charset=[% request.document_encoding %]" />
+       <base href="[% config.uri_base%]"/>
+        <link title="Maypole" href="[% config.uri_base %]/maypole.css" type="text/css" rel="stylesheet" />
+   </head>
+    <body>
+        <div class="content">
diff --git a/lib/Maypole/templates/factory/list b/lib/Maypole/templates/factory/list
new file mode 100644 (file)
index 0000000..9abbc01
--- /dev/null
@@ -0,0 +1,63 @@
+[% PROCESS macros %]
+[% INCLUDE header %]
+[% INCLUDE title %]
+[% IF search %]
+    <div id="title">Search results</div>
+[% ELSE %]
+    <div id="title">Listing of all [% classmetadata.plural %]</div>
+[% END %]
+[% INCLUDE navbar %]
+<div class="list">
+    <table id="matrix">
+        <tr>
+            [% FOR col = classmetadata.list_columns.list;
+                NEXT IF col == "id" OR col == classmetadata.table _ "_id";
+                "<th>"; 
+                SET additional = "?order=" _ col;
+                SET additional = additional _ "&page=" _ pager.current_page
+                    IF pager;
+                SET additional = additional _ "&o2=desc" 
+                IF col == request.params.order and request.params.o2 != "desc";
+                SET action = "list";
+                FOR name = classmetadata.columns.list;
+                  IF request.query.$name;
+                    SET additional =
+                        additional  _ "&" _ name _ "=" _
+                        request.params.$name;
+                    SET action = "search";
+                  END;
+                END;
+               USE model_obj = Class request.model_class;
+               IF model_obj.find_column(col);
+                  link(classmetadata.table, action, additional,
+                    classmetadata.colnames.$col);
+                  IF col == request.params.order;
+                    IF request.params.o2 != "desc";
+                        "&darr;";
+                    ELSE;
+                        "&uarr;";
+                    END;
+                  END;
+               ELSE;
+                 classmetadata.colnames.$col || col FILTER ucfirst;
+               END;
+                "</th>";
+            END %]
+           <th id="actionth">Actions</th>
+        </tr>
+        [%  SET count = 0;
+        FOR item = objects;
+            SET count = count + 1;
+            "<tr";
+            ' class="alternate"' IF count % 2;
+            ">";
+            display_line(item);
+            "</tr>";
+        END %]
+    </table>
+
+[% INCLUDE pager %]
+[% INCLUDE addnew %]
+[% INCLUDE search_form %]
+</div>
+[% INCLUDE footer %]
diff --git a/lib/Maypole/templates/factory/login b/lib/Maypole/templates/factory/login
new file mode 100644 (file)
index 0000000..af08e5b
--- /dev/null
@@ -0,0 +1,27 @@
+[% PROCESS macros %]
+[% INCLUDE header %]
+[% INCLUDE title %]
+[% user_field = config.auth.user_field || "user" %]
+
+    <div id="title">You need to log in</div>
+
+    <div id="login">
+    [% IF login_error %]
+        <div class="error"> [% login_error | html %] </div>
+    [% END %]
+    <form method="post" action="[% base %]/[% request.path %]">
+    <fieldset>
+    <legend>Login</legend>
+        <label>
+            <span class="field">Username:</span>
+           <input name="[% user_field %]" type="text" value="[% cgi_params.$user_field | html %]" />
+        </label>
+       <label>
+           <span class="field">Password:</span>
+                   <input name="password" type="password" value="[% cgi_params.passwrd | html %]"/>
+       </label>        
+        <input type="submit" name="login" value="Submit"/>
+    </fieldset>
+    </form>
+    </div>
+
diff --git a/lib/Maypole/templates/factory/macros b/lib/Maypole/templates/factory/macros
new file mode 100644 (file)
index 0000000..8267d92
--- /dev/null
@@ -0,0 +1,186 @@
+[%#
+
+=head1 MACROS
+
+These are some default macros which are used by various templates in the
+system.
+
+=head2 link
+
+This creates an <A HREF="..."> to a command in the Apache::MVC system by
+catenating the base URL, table, command, and any arguments.
+
+#%]
+[%
+MACRO link(table, command, additional, label) BLOCK;
+    SET lnk = base _ "/" _ table _ "/" _ command _ "/" _ additional;
+    lnk = lnk | uri ;
+    '<a href="' _ lnk _ '">';
+    label | html;
+    "</a>";
+END;
+%]
+
+[%#
+
+=head2 maybe_link_view
+
+C<maybe_link_view> takes something returned from the database - either
+some ordinary data, or an object in a related class expanded by a
+has-a relationship. If it is an object, it constructs a link to the view
+command for that object. Otherwise, it just displays the data.
+
+#%]
+
+[%
+MACRO maybe_link_view(object) BLOCK;
+    IF object.isa('Maypole::Model::Base');
+        link(object.table, "view", object.id.join('/'), object);
+    ELSE;
+        object | html ;
+    END;
+END;
+%]
+
+[%#
+
+=head2 display_line
+
+C<display_line> is used in the list template to display a row from the
+database, by iterating over the columns and displaying the data for each
+column. It misses out the C<id> column by default, and magically
+URLifies columns called C<url>. This may be considered too much magic
+for some.
+
+#%]
+[% MACRO display_line(item) BLOCK;
+    FOR col = classmetadata.list_columns;
+       NEXT IF col == "id" OR col == classmetadata.table _ "_id";
+       col_obj = item.find_column(col);
+        "<td>";
+        IF col == "url" AND item.url;
+            '<a href="'; item.url | html ; '"> '; item.url; '</a>';
+        ELSIF col == classmetadata.stringify_column;
+            maybe_link_view(item);
+               ELSIF col_obj; # its a real column
+            accessor = item.accessor_name_for(col_obj) ||
+                                  item.accessor_name(col_obj); # deprecated in cdbi
+            maybe_link_view(item.$accessor);
+        ELSE; 
+            item.$col;
+        END;
+
+        "</td>";
+    END;
+    '<td class="actions">';
+    button(item, "edit");
+    button(item, "delete");
+    "</td>";
+END %]
+[%#
+
+=head2 button
+
+This is a generic button, which performs an action on an object.
+
+=cut
+
+#%]
+[% MACRO button(obj, action) BLOCK; %]
+[% IF obj.is_public(action) %]
+<form class="actionform" action="[% base %]/[% obj.table %]/[% action %]/[% obj.id.join('/') %]" method="post">
+<div class="field"><input class="actionbutton" type="submit" value="[% action %]" /></div></form>
+[% END %]
+[% END %]
+[%#
+
+=head2 view_related
+
+This takes an object, and looks up the C<related_accessors>; this should
+give a list of accessors that can be called to get a list of related
+objects. It then displays a title for that accessor, (i.e. "Beers" for a
+brewery) calls the accesor, and displays a list of the results. 
+
+=cut
+
+#%]
+[% 
+MACRO view_related(object) BLOCK;
+    FOR accessor = classmetadata.related_accessors.list;
+        "<div id=\"subtitle\">"; accessor | ucfirst; "</div>\n";
+        "<ul id=\"vlist\">";
+        FOR thing = object.$accessor;
+            "<li>"; maybe_link_view(thing); "</li>\n";
+        END;
+        "</ul>";
+    END; 
+END;
+
+MACRO test_xxxx(myblock) BLOCK;
+    FOR col = classmetadata.columns;
+        NEXT IF col == "id";
+        myblock;
+    END;
+END;
+%]
+[%#
+
+=head2 view_item
+
+This takes an object and and displays its properties in a table. 
+
+=cut
+
+#%]
+[% MACRO view_item(item) BLOCK; %]
+    [% SET string = classmetadata.stringify_column %]
+    <div id="title"> [% item.$string | html %]</div>
+    [% INCLUDE navbar %]
+    <table class="view">
+        <tr>
+            <td class="field">[% classmetadata.colnames.$string  %]</td>
+            <td>[% item.$string | html %]</td>
+        </tr>
+        [% FOR col = classmetadata.columns.list;
+            NEXT IF col == "id" OR col == string OR col == classmetadata.table _ "_id";;
+            NEXT UNLESS item.$col;
+        %]
+[%# 
+
+=for doc
+
+It gets the displayable form of a column's name from the hash returned
+from the C<column_names> method:
+
+#%]
+            <tr>
+                <td class="field">[% classmetadata.colnames.$col || 
+                     col | ucfirst | replace('_',' '); %]</td>
+                <td>
+                    [% IF col == "url" && item.url;  # Possibly too much magic.
+                        '<a href="'; item.url | html ; '"> '; item.url; '</a>';
+                                       ELSIF item.$col.size > 1; # has_many column
+                                               FOR thing IN item.$col; 
+                                                       maybe_link_view(thing);",  ";
+                                                END;
+
+                    ELSE;
+                                       
+                        maybe_link_view(item.$col); 
+                    END; %]
+[%#
+
+This tests whether or not the returned value is an object, and if so,
+creates a link to a page viewing that object; if not, it just displays
+the text as normal. The object is linked using its stringified name;
+by default this calls the C<name> method, or returns the object's ID
+if there is no C<name> method or other stringification method defined.
+
+=cut
+
+#%] 
+                </td>
+            </tr>
+        [% END; %]
+    </table>
+[% END %]
diff --git a/lib/Maypole/templates/factory/maypole b/lib/Maypole/templates/factory/maypole
new file mode 100644 (file)
index 0000000..7ab2744
--- /dev/null
@@ -0,0 +1,7 @@
+<!-- boxes -->
+<div style='position:absolute;top:220px;left:130px;border-bottom-width:260px;border-right-width:370px;' class='deco1'>&nbsp;</div>
+<div style='position:absolute;top:260px;left:190px;border-bottom-width:170px;border-right-width:530px;' class='deco2'>&nbsp;</div>
+<div style='position:absolute;top:240px;left:220px;border-bottom-width:340px;border-right-width:440px;' class='deco4'>&nbsp;</div>
+<div style='position:absolute;top:160px;left:330px;border-bottom-width:160px;border-right-width:280px;' class='deco1'>&nbsp;</div>
+<div style='position:absolute;top:190px;left:290px;border-bottom-width:430px;border-right-width:130px;' class='deco2'>&nbsp;</div>
+<!-- end of boxes -->
diff --git a/lib/Maypole/templates/factory/maypole.css b/lib/Maypole/templates/factory/maypole.css
new file mode 100644 (file)
index 0000000..51d99a4
--- /dev/null
@@ -0,0 +1,376 @@
+html {
+    padding-right: 0px;
+    padding-left: 0px; 
+    padding-bottom: 0px; 
+    margin: 0px; 
+    padding-top: 0px
+}
+body {
+    font-family: sans-serif;
+    padding-right: 0px; 
+    padding-left: 0px; 
+    padding-bottom: 0px;
+    margin: 0px; padding-top: 0px;
+    background-color: #fff;
+}
+#frontpage_list {
+    position: absolute;
+    z-index: 5;
+    padding: 0px 100px 0px 0px;
+    margin:0 0.5%;     
+    margin-bottom:1em; 
+    margin-top: 1em;
+    background-color: #fff;
+}
+
+#frontpage_list a:hover {
+    background-color: #d0d8e4;
+}
+
+#frontpage_list ul {
+    list-style-type: square;
+}
+
+.content {
+    padding: 12px;
+    margin-top: 1px;  
+    margin-bottom:0px;
+    margin-left: 15px; 
+    margin-right: 15px;
+    border-color: #000000;
+    border-top: 0px;
+    border-bottom: 0px;
+    border-left: 1px;
+    border-right: 1px;
+}
+
+A { 
+    text-decoration: none; 
+    color:#225 
+}
+A:hover { 
+    text-decoration: underline; 
+    color:#222 
+}
+
+#title {
+    z-index: 6;
+    width: 100%;
+    height: 18px;
+    margin-top: 10px;
+    font-size: 90%;
+    border-bottom: 1px solid #ddf;
+    text-align: left;
+}
+
+#subtitle {
+    postion: absolute;
+    z-index: 6;
+    padding: 10px;
+    margin-top: 2em;
+    height: 18px;
+    text-align: left;
+    background-color: #fff;
+}
+
+input[type=text] {
+    height: 16px;
+    width: 136px;
+    font-family: sans-serif;
+    font-size: 11px;
+    color: #2E415A;
+    padding: 0px;
+    margin-bottom: 5px;
+}
+
+input[type=submit] {
+    height: 18px;
+    width: 60px;
+    font-family: sans-serif;
+    font-size: 11px;
+    border: 1px outset;
+    background-color: #fff;
+    padding: 0px 0px 2px 0px;
+    margin-bottom: 5px;
+}
+
+input:hover[type=submit] {
+    color: #fff;
+    background-color: #7d95b5;
+}
+
+textarea {
+    width: 136px;
+    font-family: sans-serif;
+    font-size: 11px;
+    color: #2E415A;
+    padding: 0px;
+    margin-bottom: 5px;
+}
+
+select {
+    height: 16px;
+    width: 140px;
+    font-family: sans-serif;
+    font-size: 12px;
+    color: #202020;
+    padding: 0px;
+    margin-bottom: 5px;
+}
+
+.deco1 {
+    font-size: 0px;
+    z-index:1;
+    border:0px;
+    border-style:solid;
+    border-color:#4d6d99;
+    background-color:#4d6d99;
+}
+
+.deco2 {
+    z-index:2;
+    border:0px;
+    border-style:solid;
+    border-color:#627ea5;
+    background-color:#627ea5;
+}
+
+
+.deco3 {
+    z-index:3;
+    border:0px;
+    border-style:solid;
+    border-color:#7d95b5;
+    background-color:#7d95b5;
+}
+                   
+.deco4 {
+    z-index:4;
+    border:0px;
+    border-style:solid;
+    border-color:#d0d8e4;
+    background-color:#d0d8e4;
+}
+                   
+
+table { 
+    border: 0px solid; 
+    background-color: #ffffff;
+}
+
+#matrix { width: 100%; }
+
+#matrix th {
+    background-color: #b5cadc;
+    border: 1px solid #778;
+    font: bold 12px Verdana, sans-serif;
+}
+
+#matrix #actionth {
+    width: 1px; 
+    padding: 0em 1em 0em 1em;
+}
+
+#matrix tr.alternate { background-color:#e3eaf0; }
+#matrix tr:hover { background-color: #b5cadc; }
+#matrix td { font: 12px Verdana, sans-serif; }
+
+#navlist {
+    padding: 3px 0;
+    margin-left: 0;
+    margin-top:3em;
+    border-bottom: 1px solid #778;
+    font: bold 12px Verdana, sans-serif;
+}
+
+#navlist li {
+    list-style: none;
+    margin: 0;
+    display: inline;
+}
+
+#navlist li a {
+    padding: 3px 0.5em;
+    margin-left: 3px;
+    border: 1px solid #778;
+    border-bottom: none;
+    background: #b5cadc;
+    text-decoration: none;
+}
+
+#navlist li a:link { color: #448; }
+#navlist li a:visited { color: #667; }
+
+#navlist li a:hover {
+    color: #000;
+    background: #eef;
+    border-top: 4px solid #7d95b5;
+    border-color: #227;
+}
+
+#navlist #active a {
+    background: white;
+    border-bottom: 1px solid white;
+    border-top: 4px solid;
+}
+
+td { font: 12px Verdana, sans-serif; }
+
+
+fieldset {
+    margin-top: 1em;
+    padding: 1em;
+    background-color: #f3f6f8;
+    font:80%/1 sans-serif;
+    border:1px solid #ddd;
+}
+
+legend {
+    padding: 0.2em 0.5em;
+    background-color: #fff;
+    border:1px solid #aaa;
+    font-size:90%;
+    text-align:right;
+}
+
+label {
+    display:block;
+}
+
+label .field {
+    float:left;
+    width:25%;
+    margin-right:0.5em;
+    padding-top:0.2em;
+    text-align:right;
+    font-weight:bold;
+}
+
+#vlist {
+    padding: 0 1px 1px;
+    margin-left: 0;
+    font: bold 12px Verdana, sans-serif;
+    background: gray;
+    width: 13em;
+}
+
+#vlist li {
+    list-style: none;
+    margin: 0;
+    border-top: 1px solid gray;
+    text-align: left;
+}
+
+#vlist li a {
+    display: block;
+    padding: 0.25em 0.5em 0.25em 0.75em;
+    border-left: 1em solid #7d95b5;
+    background: #d0d8e4;
+    text-decoration: none;
+}
+
+#vlist li a:hover { 
+    border-color: #227;
+}
+
+.view .field {
+    background-color: #f3f6f8;
+    border-left: 1px solid #7695b5;
+    border-top: 1px solid #7695b5;
+    padding: 1px 10px 0px 2px;
+}
+
+#addnew {
+    width: 50%;
+    float: left;
+}
+
+#search {
+    width: 50%;
+    float:right;
+}
+
+.error { color: #d00; }
+
+.action {
+    border: 1px outset #7d95b5;
+    style:block;
+}
+
+.action:hover {
+    color: #fff;
+    text-decoration: none;
+    background-color: #7d95b5;
+}
+
+.actionform {
+    display: inline;
+}
+
+.actionbutton {
+    height: 16px;
+    width: 40px;
+    font-family: sans-serif;
+    font-size: 10px;
+    border: 1px outset;
+    background-color: #fff;
+    margin-bottom: 0px;
+}
+
+.actionbutton:hover {
+    color: #fff;
+    background-color: #7d95b5;
+}
+
+.actions {
+    white-space: nowrap;
+}
+
+.field {
+    display:inline;
+}
+
+#login { width: 400px; }
+
+#login input[type=text] { width: 150px; }
+#login input[type=password] { width: 150px; }
+
+.pager {
+    font: 11px Arial, Helvetica, sans-serif;
+    text-align: center;
+    border: solid 1px #e2e2e2;
+    border-left: 0;
+    border-right: 0;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    margin: 0px;
+    background-color: #f3f6f8;
+}
+
+.pager a {
+    padding: 2px 6px;
+    border: solid 1px #ddd;
+    background: #fff;
+    text-decoration: none;
+}
+
+.pager a:visited {
+    padding: 2px 6px;
+    border: solid 1px #ddd;
+    background: #fff;
+    text-decoration: none;
+}
+
+.pager .current-page {
+    padding: 2px 6px;
+    font-weight: bold;
+    vertical-align: top;
+}
+
+.pager a:hover {
+    color: #fff;
+    background: #7d95b5;
+    border-color: #036;
+    text-decoration: none;
+}
+
diff --git a/lib/Maypole/templates/factory/navbar b/lib/Maypole/templates/factory/navbar
new file mode 100644 (file)
index 0000000..0c8b168
--- /dev/null
@@ -0,0 +1,22 @@
+[%#
+
+=head1 navbar
+
+This is a navigation bar to go across the page. (Or down the side, or
+whatetver you want to do with it.) It displays all the tables which are
+accessible, with a link to the list page for each one.
+
+#%]
+[% PROCESS macros %]
+<div id="navcontainer">
+<ul id="navlist">
+[%
+    FOR table = config.display_tables;
+        '<li '; 'id="active"' IF table == classmetadata.table; '>';
+        # Hack
+        link(table, "list", "", table);
+        '</li>';
+    END;
+%]
+</ul>
+</div> 
diff --git a/lib/Maypole/templates/factory/pager b/lib/Maypole/templates/factory/pager
new file mode 100644 (file)
index 0000000..78c89fd
--- /dev/null
@@ -0,0 +1,48 @@
+[%#
+
+=head1 pager
+
+This controls the pager display at the bottom (by default) of the list
+and search views. It expects a C<pager> template argument which responds
+to the L<Data::Page> interface.
+
+#%]
+[%
+IF pager AND pager.first_page != pager.last_page;
+%]
+<p class="pager">Pages: 
+[%
+    UNLESS pager_action;
+       SET pager_action = request.action;
+    END;
+   
+    SET begin_page = pager.current_page - 10;
+    IF begin_page < 1;
+        SET begin_page = pager.first_page;
+    END;
+    SET end_page = pager.current_page + 10;
+    IF pager.last_page < end_page;
+        SET end_page = pager.last_page;
+    END;
+     FOREACH num = [begin_page .. end_page];
+          IF num == pager.current_page;
+            "<span class='current-page'>"; num; "</span>";
+          ELSE;
+            SET label = num;
+            SET args = "?page=" _ num;
+           SET args = args _ "&order=" _ request.params.order
+             IF request.params.order;
+           SET args = args _ "&amp;o2=desc"
+             IF request.params.o2 == "desc";
+            FOR col = classmetadata.columns.list;
+              IF request.params.$col;
+                SET args = args _ "&" _ col _ "=" _ request.params.$col;
+                SET action = "search";
+              END;
+            END;
+            link(classmetadata.table, pager_action, args, label);
+          END;
+     END;
+%]
+</p>
+[% END %]
diff --git a/lib/Maypole/templates/factory/search_form b/lib/Maypole/templates/factory/search_form
new file mode 100644 (file)
index 0000000..d10101e
--- /dev/null
@@ -0,0 +1,22 @@
+<div id="search">
+<form method="get" action="[% base %]/[% classmetadata.moniker %]/search/">
+<fieldset>
+<legend>Search</legend>
+        [% FOR col = classmetadata.columns;
+            NEXT IF col == "id" OR col == classmetadata.table _ "_id";
+         %]
+           <label>
+                <span class="field">[% classmetadata.colnames.$col; %]</span>
+                    [% SET element = classmetadata.cgi.$col;
+                    IF element.tag == "select";
+                        USE element_maker = Class("HTML::Element");
+                        SET element = element.unshift_content(
+                            element_maker.new("option", value," "));
+                    END;
+                   element.as_XML; %]
+                  </label>
+        [% END; %]
+    <input type="submit" name="search" value="search"/>
+    </fieldset>
+</form>
+</div>
diff --git a/lib/Maypole/templates/factory/title b/lib/Maypole/templates/factory/title
new file mode 100644 (file)
index 0000000..401f0a3
--- /dev/null
@@ -0,0 +1 @@
+    <a href="[% base %]/frontpage">[% config.application_name %]</a>
diff --git a/lib/Maypole/templates/factory/view b/lib/Maypole/templates/factory/view
new file mode 100644 (file)
index 0000000..9f06086
--- /dev/null
@@ -0,0 +1,32 @@
+[%#
+
+=for doc
+
+The C<view> template takes some objects (usually just one) from
+C<objects> and displays the object's properties in a table. 
+
+=cut
+
+#%]
+[% PROCESS macros %]
+[% INCLUDE header %]
+[% view_item(object); %]
+[%#
+
+=for doc
+
+The C<view> template also displays a list of other objects related to the first
+one via C<has_many> style relationships; this is done by calling the
+C<related_accessors> method - see L<Model/related_accessors> - to return
+a list of has-many accessors. Next it calls each of those accessors, and
+displays the results in a table.
+
+#%]
+    <br /><a href="[%base%]/[%object.table%]/list">Back to listing</a>
+[% view_related(object); %]
+    
+[%
+    button(object, "edit");
+    button(object, "delete");
+%]
+[% INCLUDE footer %]
diff --git a/t/01.httpd-basic.t b/t/01.httpd-basic.t
new file mode 100644 (file)
index 0000000..cbf62d5
--- /dev/null
@@ -0,0 +1,10 @@
+use Test::More tests=>2;
+SKIP: {
+  no warnings 'all';
+  my $have_httpd = eval ' use HTTP::Server::Simple::Static; $HTTP::Server::Simple::Static::VERSION; ';
+  warn "have_httpd : $have_httpd\n";
+  skip ('Maypole::HTTPD tests', 2) unless ( $have_httpd );
+  use_ok("Maypole::HTTPD");
+  use_ok("Maypole::HTTPD::Frontend");
+};
+
index 3a77f02cff97cff1d252118f91fcd9203f71c46d..ba7b83434e7feba3735a7f459e14f4c2931d51e4 100644 (file)
@@ -1,13 +1,16 @@
-# vim:ft=perl
+#!/usr/bin/perl -w
 use Test::More;
 use lib 'ex'; # Where BeerDB should live
 BEGIN {
+    $ENV{BEERDB_DEBUG} = 0;
+
     eval { require BeerDB };
     Test::More->import( skip_all =>
         "SQLite not working or BeerDB module could not be loaded: $@"
     ) if $@;
 
-    plan tests => 15;
+    plan tests => 18;
+    
 }
 use Maypole::CLI qw(BeerDB);
 use Maypole::Constants;
@@ -36,9 +39,13 @@ like ($classdata{cgi},qr/^HTML::Element/,'classdata.cgi');
 is ($classdata{table},'beer','classdata.table');
 is ($classdata{name},'BeerDB::Beer','classdata.name');
 is ($classdata{colnames},'Abv','classdata.colnames');
-is($classdata{columns}, 'abv brewery id name notes price score style url',
+is($classdata{columns}, 'abv brewery id name notes price score style tasted url',
    'classdata.columns');
 is($classdata{list_columns}, 'score name price style brewery url',
    'classdata.list_columns');
 is ($classdata{related_accessors},'pubs','classdata.related_accessors');
 
+# test Maypole::load_custom_class()
+can_ok(BeerDB::Beer => 'fooey');     # defined in BeerDB::Beer
+can_ok(BeerDB::Beer => 'floob');     # defined in BeerDB::Base
+is_deeply( [@BeerDB::Beer::ISA], [qw/Maypole::Model::CDBI Class::DBI::SQLite BeerDB::Base/] );
index d8d403de77c31bb16452f439866ccb42563db401..fa45ce2fbe681c2175cb95106ae8b3af866be0e7 100644 (file)
@@ -4,6 +4,6 @@ use strict;
 use Test::More;
 
 eval "use Test::Pod::Coverage 1.04";
-plan skip_all => "Test::Pod::Coverage 1.04 required for testing POD coverage" if $@;
+plan skip_all => "Test::Pod::Coverage 1.04 required for testing POD coverage ($@)" if $@;
 all_pod_coverage_ok({  also_private => [ qr/^[A-Z_]+$/ ], });
 
index 07bfae931f95da2405b407a4c18ad3c8de911ed2..e6a110047b69b79d1b519c3960d406955c9c0544 100644 (file)
@@ -3,7 +3,7 @@ use strict;
 use Test::More;
 BEGIN {
     if (eval { require Apache::Request }) {
-        plan tests => 2;
+        plan tests => 3;
     } else {
         Test::More->import(skip_all =>"Apache::Request is not installed: $@");
     }
@@ -11,6 +11,7 @@ BEGIN {
 
 require_ok('Apache::MVC');
 ok($Apache::MVC::VERSION, 'defines $VERSION');
+ok(Apache::MVC->can('ar'), 'defines an "ar" accessor');
 # defines $VERSION
 # uses mod_perl
 # @ISA = 'Maypole'
index e1798667303f12b20eabf887b729664b13317547..22a50b37046f09d9744caa8bc56c93a613175bbd 100644 (file)
@@ -108,11 +108,13 @@ SKIP: {
     };
     diag $@ if $@;
 
-    $compare = join "\cM\cJ", 'Content-length: 12',
-        'X-bender: kiss my shiny metal ass',
+    my $CL = 'Content-length: 12';
+    my $XB = 'X-bender: kiss my shiny metal ass';
+    my $nl = "\cM\cJ";
+    my $re = join $nl, "($CL$nl$XB)|($XB$nl$CL)",
         'Content-Type: text/plain; charset=iso8859-1',
         '', 'Hello World!';
-    is($stdout, $compare, '... prints output, including custom headers');
+    like($stdout, qr/$re/, '... prints output, including custom headers');
 }
 
 # get_template_root()
index 89759c73e49f18a9f3507183c3b086c10aadf65c..1e70cd4bcf1d3a04b752ac0ca8312022fc476c29 100755 (executable)
@@ -8,6 +8,6 @@ ok($Maypole::Constants::VERSION, 'defines $VERSION');
 is(\&OK, \&Maypole::Constants::OK, 'exports OK');
 is(OK(), 0, 'OK correctly defined');
 is(\&ERROR, \&Maypole::Constants::ERROR, 'exports ERROR');
-is(ERROR(), -1, 'ERROR correctly defined');
+is(ERROR(), 500, 'ERROR correctly defined');
 is(\&DECLINED, \&Maypole::Constants::DECLINED, 'exports DECLINED');
 is(DECLINED(), -1, 'DECLINED correctly defined');
diff --git a/t/db_colinfo.t b/t/db_colinfo.t
new file mode 100755 (executable)
index 0000000..41c95a6
--- /dev/null
@@ -0,0 +1,251 @@
+#!/usr/bin/perl -w
+use Test::More;
+use Data::Dumper;
+use DBI;
+use lib 'examples'; # Where BeerDB should live
+BEGIN {
+       my $drh = eval {
+         DBI->install_driver("mysql");
+         my @databases = DBI->data_sources("mysql");
+         die "couldn't connect to mysql" unless (@databases);
+       };
+       warn "error : $@ \n" if ($@);
+        my $testcount = ($@) ? 45 : 65 ;
+        plan tests => $testcount;
+}
+
+$db            = 'test';
+$dbuser        = 'test';
+$dbpasswd   = '';
+$table = "beer_test";
+$sql = "
+create table $table (
+    id integer auto_increment primary key,
+    name char(30) NOT NULL default 'noname',
+    url varchar(120),
+    score smallint(2),
+    price decimal(3,2),
+    abv varchar(10),
+    image blob,
+    notes text,
+    tasted date NOT NULL,
+    created timestamp default CURRENT_TIMESTAMP,
+    modified datetime  default NULL,
+    style mediumint(8) NOT NULL default 1,
+    brewery integer default NULL
+);";
+
+# correct column types and the ones we test
+%correct_types = (
+                 id            =>      'int', # mysql 4.1 stores this for 'integer' 
+                 brewery       =>      'int',
+                 style         =>      'int',
+                 name          =>      'char',
+                 url           =>  'varchar',
+                 tasted        =>      'date',
+                 created       =>      '(time|time)',
+                 modified      =>      '(date|time)',
+                 score         =>      'smallint',
+                 price         =>      'decimal',
+                 abv           =>      'varchar',
+                 notes         =>  '(text|blob)',
+                 image         =>      'blob',
+);
+
+# correct defaults 
+%correct_defaults = (
+                 created       =>      'CURRENT_TIMESTAMP', 
+                 modified      =>      undef, 
+                 style         => 1,   
+                 name      => 'noname',
+);
+
+# correct nullables 
+%correct_nullables = (
+                 brewery   => 1, 
+                 modified      => 1,
+                 style         => 0,   
+                 name      => 0, 
+                 tasted    => 0,
+);
+
+
+# Runs tests on column_* method of $class using %correct data hash  
+# usage: run_method_tests ($class, $method, %correct);
+sub run_method_tests { 
+  ($class, $method,  %correct)  = @_;
+  for $col (sort keys %correct) {
+
+    $val = $class->$method($col);
+
+    # Hacks for various val types
+    $val = lc $val if $method eq 'column_type';
+
+    my $correct = $correct{$col};
+    like($val, qr/$correct/,"$method $col is $val");
+  }
+
+}
+
+
+# mysql test
+
+# Make test class 
+package BeerDB::BeerTestmysql;
+use base qw(Maypole::Model::CDBI Class::DBI);
+package main;
+
+$DB_Class = 'BeerDB::BeerTestmysql';
+
+my $drh = eval { DBI->install_driver("mysql"); };
+$err = $@;
+if ($err) {
+  $skip_msg = "no driver for MySQL";
+} else {
+  my %databases = map { $_ => 1 } $drh->func('localhost', 3306, '_ListDBs');
+
+  unless ($databases{test}) {
+    my $rc = $drh->func("createdb", 'test', 'admin');
+  }
+
+  %databases = map { $_ => 1 } $drh->func('localhost', 3306, '_ListDBs');
+
+  if ($databases{test}) {
+    eval {$DB_Class->connection("dbi:mysql:$db", "$dbuser", "$dbpasswd"); };
+    $err = $@;
+    $skip_msg = "Could not connect to MySQL using database 'test', username 'test', and password ''. Check privileges and try again.";
+  } else {
+    $err = 'no test db';
+    $skip_msg = "Could not connect to MySQL using database 'test' as it doesn't exist, sorry";
+  }
+}
+$skip_howmany = 22;
+
+SKIP: {
+       skip $skip_msg, $skip_howmany  if $err;
+       $DB_Class->db_Main->do("drop table if exists $table;");
+       $DB_Class->db_Main->do($sql);
+       $DB_Class->table($table);
+       $DB_Class->columns(All => keys %correct_types);
+       $DB_Class->columns(Primary => 'id');
+       run_method_tests($DB_Class,'column_type', %correct_types);
+       run_method_tests($DB_Class,'column_default', %correct_defaults);
+       run_method_tests($DB_Class,'column_nullable', %correct_nullables);
+
+
+       foreach my $colname ( @{$DB_Class->required_columns()} ) {
+           ok($correct_nullables{$colname} == 0,"nullable column $colname is required (via required_columns)");
+       }
+
+       foreach my $colname (keys %correct_nullables) {
+         ok( $DB_Class->column_required($colname) == !$correct_nullables{$colname}, "nullable column $colname is required (via column_required)" )
+       }
+
+       ok($DB_Class->required_columns([qw/style name tasted score/]), 'set required column(s)');
+       
+       foreach my $colname ( @{$DB_Class->required_columns()} ) {
+           ok($correct_nullables{$colname} == 0 || $colname eq 'score',"nullable or required column $colname is required (via required_columns)" );
+       }
+       
+       foreach my $colname (keys %correct_nullables) {
+           if ($colname eq 'score') {
+               ok( $DB_Class->column_required($colname) == 0, "nullable column $colname is required (via column_required)");
+           } else {
+               ok( $DB_Class->column_required($colname) == !$correct_nullables{$colname}, "nullable column $colname is required (via column_required)");
+           }
+       }       
+};
+
+# SQLite  test
+
+package BeerDB::BeerTestsqlite;
+use base qw(Maypole::Model::CDBI Class::DBI);
+package main;
+use Cwd;
+
+$DB_Class = 'BeerDB::BeerTestsqlite';
+
+$err = undef;
+if ( !-e "t/test.db" ) {
+       eval {make_sqlite_db($sql)};
+       $err = $@;
+       if ($err) { print "Skipping sql tests because couldnt make sqlite test db
+               because of error: $err";};
+}
+unless ($err) {
+       my $driver = sqlite_driver();
+       warn "using driver : $driver";
+       my $cwd = cwd;
+       eval { $DB_Class->connection("dbi:$driver:dbname=$cwd/t/test.db");};
+       $err = $@;
+}
+
+$skip_msg = "Could not connect to SQLite database 't/test.db'";
+$skip_howmany = 13;
+
+SKIP: {
+       skip $skip_msg, $skip_howmany  if $err; 
+       $DB_Class->table($table); 
+       $DB_Class->columns(All => keys %correct_types);
+       $DB_Class->columns(Primary => 'id');
+#use Data::Dumper; 
+       run_method_tests($DB_Class,'column_type', %correct_types);
+       # No support default
+       #run_method_tests($DB_Class,'column_default', %correct_defaults);
+       # I think sqlite driver allows everything to be nullable.
+       #run_method_tests($DB_Class,'column_nullable', %correct_nullables);
+
+       ok($DB_Class->required_columns([qw/score style name tasted/]), 'set required column(s)');
+       
+
+       foreach my $colname ( @{$DB_Class->required_columns()} ) {
+           ok($correct_nullables{$colname} == 0 || $colname eq 'score',"nullable or required column $colname is required (via required_columns)" );
+       }
+       
+       foreach my $colname (keys %correct_nullables) {
+           if ($colname eq 'score') {
+               ok( $DB_Class->column_required($colname) == 0, "nullable column $colname is required (via column_required)");
+           } else {
+               ok( $DB_Class->column_required($colname) == !$correct_nullables{$colname}, "nullable column $colname is required (via column_required)");
+           }
+       }
+
+};
+
+
+# Helper methods, TODO -- put these somewhere where tests can use them.
+
+# returns "best" available sqlite driver or dies
+sub sqlite_driver { 
+    my $driver = 'SQLite';
+    eval { require DBD::SQLite } or do {
+        print "Error loading DBD::SQLite, trying DBD::SQLite2\n";
+        eval {require DBD::SQLite2} ? $driver = 'SQLite2'
+            : die "DBD::SQLite2 is not installed";
+   };
+       return $driver;
+}
+
+
+# make_sqlite_db -- makes an sqlite database from params
+# usage -- make_sqlite_db($sql [, $dbname ]);   
+sub make_sqlite_db {
+       my ($sql, $dbname) = @_;
+       die "Must provide SQL string" unless length $sql;
+       $dbname ||= 't/test.db';
+       print "Making SQLite DB $dbname\n";
+    my $driver = sqlite_driver; 
+    require DBI;
+    my $dbh = DBI->connect("dbi:$driver:dbname=$dbname");
+
+    for my $statement ( split /;/, $sql ) {
+        $statement =~ s/\#.*$//mg;           # strip # comments
+        $statement =~ s/auto_increment//g;
+        next unless $statement =~ /\S/;
+        eval { $dbh->do($statement) };
+        die "$@: $statement" if $@;
+    }
+       $dbh->disconnect;
+       print "Successfully made  SQLite DB $dbname\n";
+       return 1;
+}
index 4c07d21b2834f0a044de9b6983beabadc0baa72f..75924536f8cbe616e6e873170b5efc8625c5341a 100755 (executable)
@@ -1,57 +1,71 @@
 #!/usr/bin/perl
 use strict;
 use warnings;
-use Test::More tests => 108;
+use Test::More tests => 84;
 use Test::MockModule;
+use Data::Dumper;
 
 # module compilation
+# Test 1
 require_ok('Maypole');
+
+# loaded modules 
+# Tests 2 - 8
+{
+    ok($Maypole::VERSION, 'defines $VERSION');
+    ok($INC{'Maypole/Config.pm'}, 'loads Maypole::Config');
+    ok($INC{'UNIVERSAL/require.pm'}, 'loads UNIVERSAL::require');
+    ok($INC{'Maypole/Constants.pm'}, 'loads Maypole::Constants');
+    ok($INC{'Maypole/Headers.pm'}, 'loads Maypole::Headers');
+    ok($INC{'Class/Accessor/Fast.pm'}, 'loads Class::Accessor::Fast');
+    ok($INC{'Class/Data/Inheritable.pm'}, 'loads Class::Data::Inheritable');
+}
+
 my $OK       = Maypole::Constants::OK();
 my $DECLINED = Maypole::Constants::DECLINED();
 my $ERROR    = Maypole::Constants::ERROR();
 
-ok($Maypole::VERSION, 'defines $VERSION');
-ok($INC{'Maypole/Config.pm'}, 'loads Maypole::Config');
-ok($INC{'UNIVERSAL/require.pm'}, 'loads UNIVERSAL::require');
-ok($INC{'Maypole/Constants.pm'}, 'loads Maypole::Constants');
-ok($INC{'Maypole/Headers.pm'}, 'loads Maypole::Headers');
-ok($INC{'Class/Accessor/Fast.pm'}, 'loads Class::Accessor::Fast');
-ok($INC{'Class/Data/Inheritable.pm'}, 'loads Class::Data::Inheritable');
-ok(Maypole->can('config'), 'defines a config attribute');
+# Maypole API
+my @API = qw/ config init_done view_object params query param objects model_class
+              template_args output path args action template error document_encoding
+              content_type table headers_in headers_out 
+              is_model_applicable setup setup_model init handler handler_guts
+              call_authenticate call_exception additional_data
+              authenticate exception parse_path make_path
+              make_uri get_template_root get_request
+              parse_location send_output
+             start_request_hook
+             get_session
+          get_user
+              /;
+
+# Tests 9 to 13                
+can_ok(Maypole => @API);
+ok( UNIVERSAL::can(Maypole => 'is_applicable'), 'is_applicable() method' ); # added is_applicable back in
 ok(Maypole->config->isa('Maypole::Config'), 'config is a Maypole::Config object');
-ok(Maypole->can('init_done'), 'defines an init_done attribute');
 ok(! Maypole->init_done, '... which is false by default');
-ok(Maypole->can('view_object'), 'defines a view_object attribute');
 is(Maypole->view_object, undef, '... which is undefined');
-ok(Maypole->can('ar'), 'defines an "ar" accessor');
-ok(Maypole->can('params'), 'defines a "params" accessor');
-ok(Maypole->can('query'), 'defines a "query" accessor');
-ok(Maypole->can('objects'), 'defines an "objects" accessor');
-ok(Maypole->can('model_class'), 'defines a "model_class" accessor');
-ok(Maypole->can('template_args'), 'defines a "template_args" accessor');
-ok(Maypole->can('output'), 'defines an "output" accessor');
-ok(Maypole->can('path'), 'defines a "path" accessor');
-ok(Maypole->can('args'), 'defines an "args" accessor');
-ok(Maypole->can('action'), 'defines an "action" accessor');
-ok(Maypole->can('template'), 'defines a "template" accessor');
-ok(Maypole->can('error'), 'defines an "error" accessor');
-ok(Maypole->can('document_encoding'), 'defines a "document_encoding" accessor');
-ok(Maypole->can('content_type'), 'defines a "content_type" accessor');
-ok(Maypole->can('table'), 'defines a "table" accessor');
-ok(Maypole->can('headers_in'), 'defines a "headers_in" accessor');
-ok(Maypole->can('headers_out'), 'defines a "headers_out" accessor');
 
 # simple test class that inherits from Maypole
-package MyDriver;
-@MyDriver::ISA = 'Maypole';
-@MyDriver::VERSION = 1;
-package main;
+{
+    package MyDriver;
+    @MyDriver::ISA = 'Maypole';
+    @MyDriver::VERSION = 1;
+    MyDriver->config->template_root('t/templates');
+}
+
+# back to package main;
 my $driver_class = 'MyDriver';
 
+# Test 14
+# subclass inherits API
+can_ok($driver_class => @API);
+
 # Mock the model class
 my (%required, @db_args, @adopted);
 my $model_class = 'Maypole::Model::CDBI';
 my $table_class = $driver_class . '::One';
+
 my $mock_model = Test::MockModule->new($model_class);
 $mock_model->mock(
     require        => sub {$required{+shift} = 1},
@@ -63,28 +77,44 @@ $mock_model->mock(
     adopt          => sub {push @adopted, \@_},
 );
 
-# setup()
-can_ok($driver_class => 'setup');
-my $handler = $driver_class->can('handler');
-is($handler, Maypole->can('handler'), 'calling package inherits handler()');
-$driver_class->setup('dbi:foo'); # call setup()
-isnt($handler, $driver_class->can('handler'), 'setup() installs new handler()');
-ok($required{$model_class}, '... requires model class');
-is($driver_class->config->model(),
-   'Maypole::Model::CDBI', '... default model is CDBI');
-is(@db_args, 1, '... calls model->setup_database');
-like(join (' ', @{$db_args[0]}),
-     qr/$model_class Maypole::Config=\S* $driver_class dbi:foo/,
-     '... setup_database passed setup() args');
-is(@adopted, 2, '... calls model->adopt foreach class in the model');
-ok($adopted[0][0]->isa($model_class),
-   '... sets up model subclasses to inherit from model');
-$driver_class->config->model('NonExistant::Model');
-eval {$driver_class->setup};
-like($@, qr/Couldn't load the model class/,
-     '... dies if unable to load model class');
-$@ = undef; $driver_class->config->model($model_class);
 
+# Tests 15 - 21
+warn "Tests 15 to 21\n\n";
+# setup
+{
+    # 2.11 - removed tests to check the installed handler was a different ref after setup().
+    # The handler tests were testing Maypole's old (pre 2.11) method of importing handler() 
+    # into the subclass - it works via standard inheritance now, by setting the 'method' 
+    # attribute on Maypole::handler(). The reason the handlers were different 
+    # was because setup() would create a new anonymous ref to Maypole::handler(), and install 
+    # that - i.e. it installed the same code, but in a different ref, so they tested unequal
+    # although they referred to the same code
+
+    $driver_class->setup('dbi:foo'); 
+    
+    ok($required{$model_class}, '... requires model class');
+    is($driver_class->config->model(),
+        'Maypole::Model::CDBI', '... default model is CDBI');
+    is(@db_args, 1, '... calls model->setup_database');
+    like(join (' ', @{$db_args[0]}),
+        qr/$model_class Maypole::Config=\S* $driver_class dbi:foo/,
+        '... setup_database passed setup() args');
+    is(@adopted, 2, '... calls model->adopt foreach class in the model');
+    ok($adopted[0][0]->isa($model_class),
+    '... sets up model subclasses to inherit from model');
+    $driver_class->config->model('NonExistant::Model');
+    eval {$driver_class->setup};
+    like($@, qr/Couldn't load the model class/,
+        '... dies if unable to load model class');
+    
+    # cleanup
+    $@ = undef; 
+    $driver_class->config->model($model_class);
+}
+
+
+# Tests 22 - 27
+warn "Tests 22 to 27\n\n";
 # Mock the view class
 my $view_class = 'Maypole::View::TT';
 my $mock_view = Test::MockModule->new($view_class);
@@ -94,40 +124,48 @@ $mock_view->mock(
 );
 
 # init()
-can_ok($driver_class => 'init');
-$driver_class->init();
-ok($required{$view_class}, '... requires the view class');
-is($driver_class->config->view, $view_class, '... the default view class is TT');
-is(join(' ', @{$driver_class->config->display_tables}), 'one two',
-   '... config->display_tables defaults to all tables');
-ok($driver_class->view_object->isa($view_class),
-   '... creates an instance of the view object');
-ok($driver_class->init_done, '... sets init_done');
-$driver_class->config->view('NonExistant::View');
-eval {$driver_class->init};
-like($@, qr/Couldn't load the view class/,
-     '... dies if unable to load view class');
-$@ = undef; $driver_class->config->view($view_class);
-
+{
+    $driver_class->init();
+    ok($required{$view_class}, '... requires the view class');
+    is($driver_class->config->view, $view_class, '... the default view class is TT');
+    is(join(' ', @{$driver_class->config->display_tables}), 'one two',
+        '... config->display_tables defaults to all tables');
+    ok($driver_class->view_object->isa($view_class),
+        '... creates an instance of the view object');
+    ok($driver_class->init_done, '... sets init_done');
+    $driver_class->config->view('NonExistant::View');
+    eval {$driver_class->init};
+    like($@, qr/Couldn't load the view class/,
+        '... dies if unable to load view class');
+        
+    # cleanup
+    $@ = undef; 
+    $driver_class->config->view($view_class);
+}
 
 my ($r, $req); # request objects
+
+# Tests 28 - 38
+warn "tests 28 to 38\n\n";
+# handler()
 {
-    no strict 'refs';
     my $init = 0;
     my $status = 0;
     my %called;
+    
     my $mock_driver = Test::MockModule->new($driver_class, no_auto => 1);
     $mock_driver->mock(
         init           => sub {$init++; shift->init_done(1)},
         get_request    => sub {($r, $req) = @_; $called{get_request}++},
         parse_location => sub {$called{parse_location}++},
-        handler_guts   => sub {$called{handler_guts}++; $status},
+        handler_guts   => sub { 
+                               $called{handler_guts}++; $status
+                             },
         send_output    => sub {$called{send_output}++},
     );
 
-    # handler()
-    can_ok($driver_class => 'handler');
     my $rv = $driver_class->handler();
+    
     ok($r && $r->isa($driver_class), '... created $r');
     ok($called{get_request}, '... calls get_request()');
     ok($called{parse_location}, '... calls parse_location');
@@ -135,8 +173,10 @@ my ($r, $req); # request objects
     ok($called{send_output}, '... call send_output');
     is($rv, 0, '... return status (should be ok?)');
     ok(!$init, "... doesn't call init() if init_done()");
+    
     ok($r->headers_out && $r->headers_out->isa('Maypole::Headers'),
        '... populates headers_out() with a Maypole::Headers object');
+       
     # call again, testing other branches
     $driver_class->init_done(0);
     $status = -1;
@@ -144,10 +184,15 @@ my ($r, $req); # request objects
     ok($called{handler_guts} == 2 && $called{send_output} == 1,
        '... returns early if handler_guts failed');
     is($rv, -1, '... returning the error code from handler_guts');
+    
     $driver_class->handler();
     ok($init && $driver_class->init_done, "... init() called if !init_done()");
 }
 
+
+# Tests 39 - 48
+warn "Tests 39 - 48\n\n";
+# Testing handler_guts
 {
     # handler_guts()
     {
@@ -155,58 +200,99 @@ my ($r, $req); # request objects
         @{$table_class . "::ISA"} = $model_class;
     }
 
-    my ($applicable, %called, $status);
+    my ($applicable, %called);
+    
     my $mock_driver = new Test::MockModule($driver_class, no_auto => 1);
     my $mock_table  = new Test::MockModule($table_class, no_auto => 1);
+    
     $mock_driver->mock(
         is_applicable   => sub {push @{$called{applicable}},\@_; $applicable},
+        is_model_applicable   => 
+            sub {push @{$called{applicable}},\@_; $applicable},
         get_request     => sub {($r, $req) = @_},
         additional_data => sub {$called{additional_data}++},
     );
+    
     $mock_table->mock(
         table_process   => sub {push @{$called{process}},\@_},
     );
+    
     $mock_model->mock(
         class_of        => sub {push @{$called{class_of}},\@_; $table_class},
         process         => sub {push @{$called{model_process}}, \@_},
     );
+    
     $mock_view->mock(
         process         => sub {push @{$called{view_process}}, \@_; $OK}
     );
-    can_ok(Maypole => 'handler_guts');
-
-    $applicable = $OK;
-    $r->{path} = '/table/action';    $r->parse_path;
-    $status = $r->handler_guts();
+    
+    # allow request
+    $applicable = 1;
+    
+    $r->{path} = '/one/list';
+    $r->parse_path;
+  
+    my $status = $r->handler_guts();
+    # set model_class (would be done in handler_guts, but hard to mock earlier)
+    $r->model_class( $r->config->model->class_of($r, $r->table) );
+     
+    warn "status : $status\n";
 
     is($r->model_class, $table_class, '... sets model_class from table()');
     ok($called{additional_data}, '... call additional_data()');
     is($status, $OK, '... return status = OK');
-    ok($called{model_process},
-       '... if_applicable, call model_class->process');
 
+    TODO: {
+        local $TODO = "test needs fixing";
+        ok($called{model_process},
+        '... if_applicable, call model_class->process');
+    }
+
+    # decline request
     %called = ();
-    $applicable = $DECLINED;
-    $r->{path} = '/table/action';
+    
+    $applicable = 0;
+    
+    $r->{path} = '/one/list';
     $r->parse_path;
+    
     $status = $r->handler_guts();
+    # set model_class (would be done in handler_guts, but hard to mock earlier)
+    $r->model_class( $r->config->model->class_of($r, $r->table) );
+    
     is($r->template, $r->path,
        '... if ! is_applicable set template() to path()');
+    
+    TODO: {
+        local $TODO = "test needs fixing";
     ok(!$called{model_process},
        '... !if_applicable, call model_class->process');
+    }
+
     is_deeply($called{view_process}[0][1], $r,
               ' ... view_object->process called');
     is($status, $OK, '... return status = OK');
 
+    # pre-load some output
     %called = ();
+    
     $r->parse_path;
     $r->{output} = 'test';
+    
     $status = $r->handler_guts();
+    # set model_class (would be done in handler_guts, but hard to mock earlier)
+    $r->model_class( $r->config->model->class_of($r, $r->table) );
+    
     ok(!$called{view_process},
        '... unless output, call view_object->process to get output');
 
+    # fail authentication
     $mock_driver->mock(call_authenticate => sub {$DECLINED});
     $status = $r->handler_guts();
+    # set model_class (would be done in handler_guts, but hard to mock earlier)
+    $r->model_class( $r->config->model->class_of($r, $r->table) );
+
     is($status, $DECLINED,
        '... return DECLINED unless call_authenticate == OK');
 
@@ -215,127 +301,193 @@ my ($r, $req); # request objects
     # ... TODO view processing error handling
 }
 
-# is_applicable()
-can_ok(Maypole => 'is_applicable');
-$r->config->display_tables([qw(one two)]);
-$r->config->ok_tables(undef);
-$r->model_class($table_class);
-$r->table('one');
-$r->action('unittest');
-my $is_public;
-$mock_model->mock('is_public', sub {0});
-my $status = $r->is_applicable;
-is($status, $DECLINED,
-   '... return DECLINED unless model_class->is_public(action)');
-$mock_model->mock('is_public', sub {$is_public = \@_; 1});
-$status = $r->is_applicable;
-is($status, $OK, '... returns OK if table is in ok_tables');
-is_deeply($is_public, [$r->model_class, 'unittest'],
-          '... calls model_class->is_public with request action');
-is_deeply($r->config->ok_tables, {one => 1, two => 1},
-          '... config->ok_tables defaults to config->display_tables');
-delete $r->config->ok_tables->{one};
-$status = $r->is_applicable;
-is($status, $DECLINED, '... return DECLINED unless $r->table is in ok_tables');
+# Tests 49 - 53
+warn "Tests 49 to 53\n\n";
+# is_model_applicable()
+{
+TODO: {
+    local $TODO = "test needs fixing";
+    $r->config->ok_tables([qw(one two)]);
+    $r->config->display_tables([qw(one two)]);
+    $r->model_class($table_class);
+    $r->table('one');
+    $r->action('unittest');
+    my $is_public;
+    $mock_model->mock('is_public', sub {0});
+    my $true_false = $r->is_model_applicable;
+    is($true_false, 0,
+       '... returns 0 unless model_class->is_public(action)');
+    $mock_model->mock('is_public', sub {$is_public = \@_; 1});
+    $true_false = $r->is_model_applicable;
+    is($true_false, 1, '... returns 1 if table is in ok_tables');
+    is_deeply($is_public, [$r->model_class, 'unittest'],
+             '... calls model_class->is_public with request action');
+    is_deeply($r->config->ok_tables, {one => 1, two => 1},
+             '... config->ok_tables defaults to config->display_tables');
+    delete $r->config->ok_tables->{one};
+    $true_false = $r->is_model_applicable;
+    is($true_false, 0, '... returns 0 unless $r->table is in ok_tables');
+  }
+}
 
-# call_authenticate()
-can_ok(Maypole => 'call_authenticate');
+# Tests 54 - 58
+warn "Tests 54 to 58\n\n";
 my $mock_driver = new Test::MockModule($driver_class, no_auto => 1);
 my $mock_table  = new Test::MockModule($table_class, no_auto => 1);
-my %auth_calls;
-$mock_table->mock(
-    authenticate => sub {$auth_calls{model_auth} = \@_; $OK}
-);
-$status = $r->call_authenticate;
-is_deeply($auth_calls{model_auth}, [$table_class, $r],
-          '... calls model_class->authenticate if it exists');
-is($status, $OK, '... and returns its status (OK)');
-$mock_table->mock(authenticate => sub {$DECLINED});
-$status = $r->call_authenticate;
-is($status, $DECLINED, '... or DECLINED, as appropriate');
-
-$mock_table->unmock('authenticate');
-$mock_driver->mock(authenticate => sub {return $DECLINED});
-$status = $r->call_authenticate;
-is($status, $DECLINED, '... otherwise it calls authenticte()');
-$mock_driver->unmock('authenticate');
-$status = $r->call_authenticate;
-is($status, $OK, '... the default authenticate is OK');
+# call_authenticate()
+{
+    my %auth_calls;
+    $mock_table->mock(
+        authenticate => sub {$auth_calls{model_auth} = \@_; $OK}
+    );
+    my $status = $r->call_authenticate;
+    is_deeply($auth_calls{model_auth}, [$table_class, $r],
+            '... calls model_class->authenticate if it exists'); # 54
+    is($status, $OK, '... and returns its status (OK)'); # 55
+    $mock_table->mock(authenticate => sub {$DECLINED});
+    $status = $r->call_authenticate;
+    is($status, $DECLINED, '... or DECLINED, as appropriate'); # 56
+    
+    $mock_table->unmock('authenticate');
+    $mock_driver->mock(authenticate => sub {return $DECLINED});
+    $status = $r->call_authenticate;
+    is($status, $DECLINED, '... otherwise it calls authenticte()'); # 57
+    $mock_driver->unmock('authenticate');
+    $status = $r->call_authenticate;
+    is($status, $OK, '... the default authenticate is OK'); # 58
+}
 
+# Tests 59 - 63
+warn "Tests 59 to 63\n\n";
 # call_exception()
-can_ok(Maypole => 'call_exception');
-my %ex_calls;
-$mock_table->mock(
-    exception => sub {$ex_calls{model_exception} = \@_; $OK}
-);
-$mock_driver->mock(
-    exception => sub {$ex_calls{driver_exception} = \@_; 'X'}
-);
-$status = $r->call_exception('ERR');
-is_deeply($ex_calls{model_exception}, [$table_class, $r, 'ERR'],
-          '... calls model_class->exception if it exists');
-is($status, $OK, '... and returns its status (OK)');
-$mock_table->mock(exception => sub {$DECLINED});
-$status = $r->call_exception('ERR');
-is_deeply($ex_calls{driver_exception}, [$r, 'ERR'],
-          '... or calls driver->exception if model returns !OK');
-is($status, 'X', '... and returns the drivers status');
-
-$mock_table->unmock('exception');
-$mock_driver->unmock('exception');
-$status = $r->call_exception('ERR');
-is($status, $ERROR, '... the default exception is ERROR');
-
-# additional_data()
-can_ok(Maypole => 'additional_data');
+{
+TODO: {
+       local $TODO = "test needs fixing";
 
+    my %ex_calls;
+    $mock_table->mock(
+        exception => sub {$ex_calls{model_exception} = \@_; $OK}
+    );
+    $mock_driver->mock(
+        exception => sub {$ex_calls{driver_exception} = \@_; 'X'}
+    );
+    my $status = $r->call_exception('ERR');
+    is_deeply($ex_calls{model_exception}, [$table_class, $r, 'ERR'],
+            '... calls model_class->exception if it exists');
+    is($status, $OK, '... and returns its status (OK)');
+    $mock_table->mock(exception => sub {$DECLINED});
+    $status = $r->call_exception('ERR');
+    is_deeply($ex_calls{driver_exception}, [$r, 'ERR'],
+            '... or calls driver->exception if model returns !OK');
+    is($status, 'X', '... and returns the drivers status');
+    
+    $mock_table->unmock('exception');
+    $mock_driver->unmock('exception');
+    $status = $r->call_exception('ERR');
+    is($status, $ERROR, '... the default exception is ERROR');
+    }
+}
+
+# Test 64
 # authenticate()
-can_ok(Maypole => 'authenticate');
-is(Maypole->authenticate(), $OK, '... returns OK');
+{
+    is(Maypole->authenticate(), $OK, '... returns OK');
+}
 
+# Test 65
 # exception()
-can_ok(Maypole => 'exception');
-is(Maypole->exception(), $ERROR, '... returns ERROR');
+{
+    is(Maypole->exception(), $ERROR, '... returns ERROR');
+}
 
+# Tests 66 to 71
+warn "Tests 66 to 71\n\n";
 # parse_path()
-can_ok(Maypole => 'parse_path');
-$r->path(undef);
-$r->parse_path;
-is($r->path, 'frontpage', '... path() defaults to "frontpage"');
-
-$r->path('/table');
-$r->parse_path;
-is($r->table, 'table', '... parses "table" from the first part of path');
-ok(@{$r->args} == 0, '... "args" default to empty list');
-
-$r->path('/table/action');
-$r->parse_path;
-ok($r->table eq 'table' && $r->action eq 'action',
-   '... action is parsed from second part of path');
-
-$r->path('/table/action/arg1/arg2');
-$r->parse_path;
-is_deeply($r->args, [qw(arg1 arg2)],
-   '... "args" are populated from remaning components');
-
-# ... action defaults to index
-$r->path('/table');
-$r->parse_path;
-is($r->action, 'index', '... action defaults to index');
+{
+    $r->path(undef);
+    
+    $r->parse_path;
+    is($r->path, 'frontpage', '... path() defaults to "frontpage"');
+    
+    $r->path('/table');
+    $r->parse_path;
+    is($r->table, 'table', '... parses "table" from the first part of path');
+    ok(@{$r->args} == 0, '... "args" default to empty list');
+    
+    $r->path('/table/action');
+    $r->parse_path;
+    ok($r->table eq 'table' && $r->action eq 'action',
+    '... action is parsed from second part of path');
+    
+    $r->path('/table/action/arg1/arg2');
+    $r->parse_path;
+    is_deeply($r->args, [qw(arg1 arg2)],
+    '... "args" are populated from remaning components');
+    
+    # ... action defaults to index
+    $r->path('/table');
+    $r->parse_path;
+    is($r->action, 'index', '... action defaults to index');
+}
 
-# get_template_root()
-can_ok(Maypole => 'get_template_root');
-is(Maypole->get_template_root(), '.', '... returns "."');
+# make_uri() and make_path() - see pathtools.t
 
-# get_request()
-can_ok(Maypole => 'get_request');
+# Test 72
+# get_template_root()
+{
+TODO: {
+       local $TODO = "test needs fixing";
+       is(Maypole->get_template_root(), '.', '... returns "."');
+       }
+}
 
+# Test 73
 # parse_location()
-can_ok(Maypole => 'parse_location');
-eval {Maypole->parse_location()};
-like($@, qr/Do not use Maypole directly/, '... croaks - must be overriden');
+{
+    eval {Maypole->parse_location()};
+    like($@, qr/Do not use Maypole directly/, '... croaks - must be overriden');
+}
 
+# Test 74
 # send_output()
-can_ok(Maypole=> 'send_output');
-eval {Maypole->send_output};
-like($@, qr/Do not use Maypole directly/, '... croaks - must be overriden');
+{
+    eval {Maypole->send_output};
+    like($@, qr/Do not use Maypole directly/, '... croaks - must be overriden');
+}
+
+# Tests 75 - 84
+warn "Tests 75 to 84\n\n";
+# param()
+{
+       my $p = { foo => 'bar', 
+                 quux => [ qw/one two three/ ],
+                 buz => undef,
+                 num => 3,
+                 zero => 0,
+                 };
+                 
+       $r->{params} = $p;
+       
+       is_deeply( [keys %$p], [$r->param] ); # 75
+       
+       cmp_ok( $r->param('foo'), eq => 'bar' ); # 76
+       cmp_ok( $r->param('num'), '==' => 3 ); # 77
+       cmp_ok( $r->param('zero'), '==' => 0 ); # 78
+       
+       ok( ! defined $r->param('buz') ); # 79
+       
+       # scalar context returns the 1st value, not a ref
+       cmp_ok( scalar $r->param('quux'), eq => 'one' ); # 80
+       is_deeply( [$r->param('quux')], [ qw/one two three/ ] ); # 81
+       
+       $r->param(foo => 'booze');
+       cmp_ok( $r->param('foo'), 'eq', 'booze' ); # 82
+       
+       $r->param(foo => undef);
+       ok( ! defined $r->param('foo') ); # 83
+       
+       # cannot introduce new keys
+       $r->param(new => 'sox');
+       ok( ! defined $r->param('new') ); # 84
+}
+
diff --git a/t/pathtools.t b/t/pathtools.t
new file mode 100644 (file)
index 0000000..a5404b7
--- /dev/null
@@ -0,0 +1,160 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Test::More tests => 172;
+use Test::MockModule;
+
+use CGI;
+use URI;
+
+use Maypole;
+
+# simple test class that inherits from Maypole
+{
+  package MyDriver;
+  @MyDriver::ISA = 'Maypole';
+  @MyDriver::VERSION = 1;
+}
+
+# back to package main;
+my $driver_class = 'MyDriver';
+my $r = $driver_class->new;
+
+my $query = { list   => [ qw/ fee fi fo / ], string => 'baz', number => 4 };
+
+my $query_string = '?number=4&string=baz&list=fee&list=fi&list=fo';
+
+my @bases = ( 'http://www.example.com',
+             'http://www.example.com/', 'http://www.example.com/foo',
+             'http://www.example.com/foo/', );
+
+# make_uri
+{
+  my @uris = (
+             { expect   =>'',
+               send     => [ '' ],
+             },
+             { expect   => '',
+               send     => [ () ],
+             },
+             { expect   => '/table',
+               send     => [ qw( table ) ],
+             },
+             { expect   => '/table/action',
+               send     => [ qw( table action ) ],
+             },
+             { expect   => '/table/action/id',
+               send     => [ qw( table action id ) ],
+             },
+             { expect   =>'',
+               send     => [ '', $query ],
+             },
+             { expect   => '',
+               send     => [ $query ],
+             },
+             { expect   => '/table',
+               send     => [ qw( table ), $query ],
+             },
+             { expect   => '/table/action',
+               send     => [ qw( table action ), $query ],
+             },
+             { expect   => '/table/action/id',
+               send     => [ qw( table action id ), $query ],
+             },
+            );
+
+  foreach my $base (@bases) {
+    $driver_class->config->uri_base($base);
+    (my $base_no_slash = $base) =~ s|/$||;
+    my $base_or_slash = $base_no_slash || '/';
+    my $i = 1;
+
+    foreach my $test (@uris) {
+      #diag "BASE: $base - URI #$i"; $i++;
+      my @s      = @{ $test->{send} };
+      my $expect = $test->{expect};
+      my $uri = $r->make_uri(@s);
+
+      my $expected = $base_or_slash.$test->{expect};
+
+      my ($uri_basepath,$uri_query) = split(/\?/,$uri);
+
+      my $q_got = new CGI($uri_query);
+
+      if ($uri_query) {
+       # check query params
+       # list   => [ qw/ fee fi fo / ], string => 'baz', number => 4
+       is($q_got->param('string'),'baz','string param correct');
+       is($q_got->param('number'),4,'number param correct');
+       is_deeply([$q_got->param('list')],[ qw/ fee fi fo / ],'list param correct');
+      }
+      ok(URI::eq($expected,$uri_basepath),'host and path match');
+
+    }
+  }
+} ;
+
+
+# make_path
+{
+  # expect       # send
+  my @uris = ( 
+             { expect   => '/table/action',
+               send     => [ qw( table action ) ],
+             },
+             { expect   => '/table/action/id',
+               send     => [ qw( table action id ) ],
+             },
+             { expect   => '/table/action',
+               send     => [ qw( table action ), $query ],
+             },
+            );
+
+  foreach my $base (@bases) {
+    $driver_class->config->uri_base($base);
+
+    (my $base_no_slash = $base) =~ s|/$||;
+    my $base_or_slash = $base_no_slash || '/';
+
+    my $i = 1;
+    foreach my $test (@uris) {
+      #diag "BASE: $base - URI #$i"; $i++;
+
+      my @args = @{ $test->{send} };
+
+      my %args = ( table  => $args[0],
+                  action => $args[1],
+                  additional => $args[2],
+                );
+
+      my %arg_sets = ( array => \@args, 
+                      hash  => \%args, 
+                      hashref => \%args,
+                    );
+
+      my $expect = $test->{expect};
+
+      foreach my $set (keys %arg_sets) {
+
+       my $path;
+       $path = $r->make_path(@{ $arg_sets{$set} }) if $set eq 'array';
+       $path = $r->make_path(%{ $arg_sets{$set} }) if $set eq 'hash';
+       $path = $r->make_path($arg_sets{$set})   if $set eq 'hashref';
+
+       my ($uri_path,$uri_query) = split(/\?/,$path);
+       my $q_got = new CGI($uri_query);
+
+       my $expected = $expect =~ m|^/| ? "$base_no_slash$expect" : "$base_or_slash$expect";
+       if ($uri_query) {
+         # check query params
+         # list   => [ qw/ fee fi fo / ], string => 'baz', number => 4
+         is($q_got->param('string'),'baz','string param correct');
+         is($q_got->param('number'),4,'number param correct');
+         is_deeply([$q_got->param('list')],[ qw/ fee fi fo / ],'list param correct');
+       }
+       ok(URI::eq($expected,$uri_path),'host and path match');
+
+      }
+    }
+  }
+};
index c5f9229538ec798aed1a2ab2c085f541ccd9f3db..ab110c62c8a31586d0dc771e73242ee45dec282d 100644 (file)
@@ -1,3 +1,4 @@
+# view 
 # Begin object list
 [% FOR obj = objects %]
 - [% obj.name %]
diff --git a/templates/beer/addnew b/templates/beer/addnew
deleted file mode 100644 (file)
index ad51f01..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-[% USE element_maker = Class("HTML::Element") %]
-<div id="addnew">
-<form method="post" action="[% base %]/[% classmetadata.moniker %]/do_edit/">
-<fieldset>
-<legend>Add a new [%classmetadata.moniker%]</legend>
-    <input type="hidden" name="action" value="create"/>
-    <input type="hidden" name="class" value="[% classmetadata.name %]"/>
-        [% FOR col = classmetadata.columns;
-            NEXT IF col == "id";
-            SET element = classmetadata.cgi.$col;
-            %]
-       <label>
-                <span class="field">[% classmetadata.colnames.$col; %]</span>
-                [% element.as_XML; %]</label>
-               
-        [% END; %]
-       
-    <input type="submit" name="create" value="create"/>
-    </fieldset>
-</form>
-</div>
diff --git a/templates/factory/addnew b/templates/factory/addnew
deleted file mode 100644 (file)
index aec9fca..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[%#
-
-=head1 addnew
-
-This is the interface to adding a new instance of an object. (or a new
-row in the database, if you want to look at it that way) It displays a
-form containing a list of HTML components for each of the columns in the
-table.
-
-=cut
-
-#%]
-
-<div id="addnew">
-<form method="post" action="[% base %]/[% classmetadata.table %]/do_edit/">
-    <fieldset>
-<legend>Add a new [% classmetadata.moniker %]</legend>
-    [% FOR col = classmetadata.columns %]
-        [% NEXT IF col == "id" %]
-            <label><span class="field">[% classmetadata.colnames.$col %]</span>
-            [% 
-            SET elem = classmetadata.cgi.$col.clone;
-            IF request.action == 'do_edit';
-                IF elem.tag == "textarea";
-                    elem = elem.push_content(request.param(col));
-                ELSE;
-                    elem.attr("value", request.param(col));
-                END;
-            END;
-            elem.as_XML; %]
-           </label>
-        [% IF errors.$col %]
-           <span class="error">[% errors.$col %]</span>
-        [% END %]
-
-    [% END; %]
-    <input type="submit" name="create" value="create"/>
-    <input type="hidden" name="__form_id" value="[% request.make_random_id %]">
-</fieldset>
-</form>
-</div>
diff --git a/templates/factory/edit b/templates/factory/edit
deleted file mode 100644 (file)
index 6bdad79..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-[%#
-
-=head1 edit
-
-This is the edit page. It edits the passed-in object, by displaying a
-form similar to L<addnew> but with the current values filled in.
-
-=cut
-
-#%]
-[% PROCESS macros %]
-[% INCLUDE header %]
-[% INCLUDE title %]
-[% IF objects.size %]
-<div id="title">Edit a [% classmetadata.moniker %]</div>
-[% FOR item = objects; %]
-<form action="[% base %]/[% item.table %]/do_edit/[% item.id %]" method="post">
-<fieldset>
-<legend>Edit [% item.name %]</legend>
-[% FOR col = classmetadata.columns;
-    NEXT IF col == "id";
-    '<label><span class="field">';
-    classmetadata.colnames.$col; ":</span>";
-    item.to_field(col).as_XML;
-    "</label>";
-    IF errors.$col; 
-       '<span class="error">'; errors.$col;'</span>';
-    END;
-    END %]
-    <input type="submit" name="edit" value="edit"/>
-    <input type="hidden" name="__form_id" value="[% request.make_random_id %]">
-    </fieldset></form>
-    
-    [% END %]
-[% ELSE %]
-[% INCLUDE addnew %]
-[% END %]
-[% INCLUDE footer %]
diff --git a/templates/factory/footer b/templates/factory/footer
deleted file mode 100644 (file)
index 1b8ae55..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-       </div>
-    </body>
-</html>
diff --git a/templates/factory/frontpage b/templates/factory/frontpage
deleted file mode 100644 (file)
index ac47269..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-[%#
-
-=head1 frontpage
-
-This is the frontpage for your Maypole application.
-It shows a list of all tables it is allowed to display.
-
-=cut
-
-#%]
-[% INCLUDE header %]
-<div id="title">
-    [% config.application_name || "A poorly configured Maypole application" %]
-</div>
-<div id="frontpage_list">
-<ul>
-[% FOR table = config.display_tables %]
-    <li>
-        <a href="[% base %]/[%table%]/list">List by [%table %]</a>
-    </li>      
-[% END %]
-</ul>
-</div>
-
-[% INCLUDE maypole %]
-
-[% INCLUDE footer %]
diff --git a/templates/factory/header b/templates/factory/header
deleted file mode 100644 (file)
index 6b706ef..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-    <head>
-        <title>
-            [%
-              title || config.application_name ||
-                "A poorly configured Maypole application"
-            %]
-        </title>
-        <meta http-equiv="Content-Type" content="text/html; charset=[% 
-           request.document_encoding
-       %]"/>
-       <base href="[% config.uri_base%]"/>
-        <link title="Maypole" href="/maypole.css" type="text/css"
-            rel="stylesheet"/>
-   </head>
-    <body>
-        <div class="content">
diff --git a/templates/factory/list b/templates/factory/list
deleted file mode 100644 (file)
index 5a4388e..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-[% PROCESS macros %]
-[% INCLUDE header %]
-[% INCLUDE title %]
-[% IF search %]
-    <div id="title">Search results</div>
-[% ELSE %]
-    <div id="title">Listing of all [% classmetadata.plural %]</div>
-[% END %]
-[% INCLUDE navbar %]
-<div class="list">
-    <table id="matrix">
-        <tr>
-            [% FOR col = classmetadata.list_columns.list;
-                NEXT IF col == "id";
-                "<th>"; 
-                SET additional = "?order=" _ col;
-                SET additional = additional _ "&page=" _ pager.current_page
-                    IF pager;
-                SET additional = additional _ "&o2=desc" 
-                IF col == request.params.order and request.params.o2 != "desc";
-                SET action = "list";
-                FOR name = classmetadata.columns.list;
-                  IF request.query.$name;
-                    SET additional =
-                        additional  _ "&" _ name _ "=" _
-                        request.params.$name;
-                    SET action = "search";
-                  END;
-                END;
-                link(classmetadata.table, action, additional,
-                    classmetadata.colnames.$col);
-                IF col == request.params.order;
-                    IF request.params.o2 != "desc";
-                        "&darr;";
-                    ELSE;
-                        "&uarr;";
-                    END;
-                END;
-                "</th>";
-            END %]
-           <th id="actionth">Actions</th>
-        </tr>
-        [%  SET count = 0;
-        FOR item = objects;
-            SET count = count + 1;
-            "<tr";
-            ' class="alternate"' IF count % 2;
-            ">";
-            display_line(item);
-            "</tr>";
-        END %]
-    </table>
-
-[% INCLUDE pager %]
-[% INCLUDE addnew %]
-[% INCLUDE search_form %]
-</div>
-[% INCLUDE footer %]
diff --git a/templates/factory/login b/templates/factory/login
deleted file mode 100644 (file)
index 266921a..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-[% PROCESS macros %]
-[% INCLUDE header %]
-[% INCLUDE title %]
-    <div id="title">You need to log in</div>
-
-    <div id="login">
-    [% IF login_error %]
-        <div class="error"> [% login_error %] </div>
-    [% END %]
-    <form method="post" action="[% base %]/[% request.path %]">
-    <fieldset>
-    <legend>Login</legend>
-        <label>
-            <span class="field">Username:</span>
-           <input name="[% config.auth.user_field || "user" %]" type="text" />
-        </label>
-       <label>
-           <span class="field">Password:</span>
-                   <input name="password" type="password" />
-       </label>        
-        <input type="submit" name="login" value="Submit"/>
-    </fieldset>
-    </form>
-    </div>
-
diff --git a/templates/factory/macros b/templates/factory/macros
deleted file mode 100644 (file)
index 59d6c92..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-[%#
-
-=head1 MACROS
-
-These are some default macros which are used by various templates in the
-system.
-
-=head2 link
-
-This creates an <A HREF="..."> to a command in the Apache::MVC system by
-catenating the base URL, table, command, and any arguments.
-
-#%]
-[%
-MACRO link(table, command, additional, label) BLOCK;
-    SET lnk = base _ "/" _ table _ "/" _ command _ "/" _ additional;
-    lnk = lnk | uri | html;
-    '<a href="' _ lnk _ '">';
-    label;
-    "</a>";
-END;
-%]
-
-[%#
-
-=head2 maybe_link_view
-
-C<maybe_link_view> takes something returned from the database - either
-some ordinary data, or an object in a related class expanded by a
-has-a relationship. If it is an object, it constructs a link to the view
-command for that object. Otherwise, it just displays the data.
-
-#%]
-
-[%
-MACRO maybe_link_view(object) BLOCK;
-    IF object.isa('Maypole::Model::Base');
-        link(object.table, "view", object.id.join('/'), object);
-    ELSE;
-        object;
-    END;
-END;
-%]
-
-[%#
-
-=head2 display_line
-
-C<display_line> is used in the list template to display a row from the
-database, by iterating over the columns and displaying the data for each
-column. It misses out the C<id> column by default, and magically
-URLifies columns called C<url>. This may be considered too much magic
-for some.
-
-#%]
-[% MACRO display_line(item) BLOCK;
-    FOR col = classmetadata.list_columns;
-        NEXT IF col == "id";
-        "<td>";
-        IF col == "url" AND item.url;
-            '<a href="'; item.url; '"> '; item.url; '</a>';
-        ELSIF col == classmetadata.stringify_column;
-            maybe_link_view(item);
-        ELSE;
-            accessor = item.accessor_name(col);
-            maybe_link_view(item.$accessor);
-        END;
-        "</td>";
-    END;
-    '<td class="actions">';
-    button(item, "edit");
-    button(item, "delete");
-    "</td>";
-END %]
-[%#
-
-=head2 button
-
-This is a generic button, which performs an action on an object.
-
-=cut
-
-#%]
-[% MACRO button(obj, action) BLOCK; %]
-[% IF obj.is_public(action) %]
-<form class="actionform" action="[% base %]/[% obj.table %]/[% action %]/[% obj.id.join('/') %]" method="post">
-<div class="field"><input class="actionbutton" type="submit" value="[% action %]" /></div></form>
-[% END %]
-[% END %]
-[%#
-
-=head2 view_related
-
-This takes an object, and looks up the C<related_accessors>; this should
-give a list of accessors that can be called to get a list of related
-objects. It then displays a title for that accessor, (i.e. "Beers" for a
-brewery) calls the accesor, and displays a list of the results. 
-
-=cut
-
-#%]
-[% 
-MACRO view_related(object) BLOCK;
-    FOR accessor = classmetadata.related_accessors.list;
-        "<div id=\"subtitle\">"; accessor | ucfirst; "</div>\n";
-        "<ul id=\"vlist\">";
-        FOR thing = object.$accessor;
-            "<li>"; maybe_link_view(thing); "</li>\n";
-        END;
-        "</ul>";
-    END; 
-END;
-
-MACRO test_xxxx(myblock) BLOCK;
-    FOR col = classmetadata.columns;
-        NEXT IF col == "id";
-        myblock;
-    END;
-END;
-%]
-[%#
-
-=head2 view_item
-
-This takes an object and and displays its properties in a table. 
-
-=cut
-
-#%]
-[% MACRO view_item(item) BLOCK; %]
-    [% SET string = classmetadata.stringifycolumn %]
-    <div id="title"> [% item.$string %]</div>
-    [% INCLUDE navbar %]
-    <table class="view">
-        <tr>
-            <td class="field">[% classmetadata.colnames.$string %]</td>
-            <td>[% item.$string %]</td>
-        </tr>
-        [% FOR col = classmetadata.columns.list;
-            NEXT IF col == "id" OR col == string;
-            NEXT UNLESS item.$col;
-        %]
-[%# 
-
-=for doc
-
-It gets the displayable form of a column's name from the hash returned
-from the C<column_names> method:
-
-#%]
-            <tr>
-                <td class="field">[% classmetadata.colnames.$col; %]</td>
-                <td>
-                    [% IF col == "url" && item.url;  # Possibly too much magic.
-                        '<a href="'; item.url; '"> '; item.url; '</a>';
-                    ELSE;
-                        maybe_link_view(item.$col); 
-                    END; %]
-[%#
-
-This tests whether or not the returned value is an object, and if so,
-creates a link to a page viewing that object; if not, it just displays
-the text as normal. The object is linked using its stringified name;
-by default this calls the C<name> method, or returns the object's ID
-if there is no C<name> method or other stringification method defined.
-
-=cut
-
-#%] 
-                </td>
-            </tr>
-        [% END; %]
-    </table>
-[% END %]
diff --git a/templates/factory/maypole b/templates/factory/maypole
deleted file mode 100644 (file)
index 7ab2744..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<!-- boxes -->
-<div style='position:absolute;top:220px;left:130px;border-bottom-width:260px;border-right-width:370px;' class='deco1'>&nbsp;</div>
-<div style='position:absolute;top:260px;left:190px;border-bottom-width:170px;border-right-width:530px;' class='deco2'>&nbsp;</div>
-<div style='position:absolute;top:240px;left:220px;border-bottom-width:340px;border-right-width:440px;' class='deco4'>&nbsp;</div>
-<div style='position:absolute;top:160px;left:330px;border-bottom-width:160px;border-right-width:280px;' class='deco1'>&nbsp;</div>
-<div style='position:absolute;top:190px;left:290px;border-bottom-width:430px;border-right-width:130px;' class='deco2'>&nbsp;</div>
-<!-- end of boxes -->
diff --git a/templates/factory/navbar b/templates/factory/navbar
deleted file mode 100644 (file)
index 0c8b168..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-[%#
-
-=head1 navbar
-
-This is a navigation bar to go across the page. (Or down the side, or
-whatetver you want to do with it.) It displays all the tables which are
-accessible, with a link to the list page for each one.
-
-#%]
-[% PROCESS macros %]
-<div id="navcontainer">
-<ul id="navlist">
-[%
-    FOR table = config.display_tables;
-        '<li '; 'id="active"' IF table == classmetadata.table; '>';
-        # Hack
-        link(table, "list", "", table);
-        '</li>';
-    END;
-%]
-</ul>
-</div> 
diff --git a/templates/factory/pager b/templates/factory/pager
deleted file mode 100644 (file)
index 6aed9c2..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-[%#
-
-=head1 pager
-
-This controls the pager display at the bottom (by default) of the list
-and search views. It expects a C<pager> template argument which responds
-to the L<Data::Page> interface.
-
-#%]
-[%
-IF pager AND pager.first_page != pager.last_page;
-%]
-<p class="pager">Pages: 
-[%
-    SET begin_page = pager.current_page - 10;
-    IF begin_page < 1;
-        SET begin_page = pager.first_page;
-    END;
-    SET end_page = pager.current_page + 10;
-    IF pager.last_page < end_page;
-        SET end_page = pager.last_page;
-    END;
-     FOREACH num = [begin_page .. end_page];
-          IF num == pager.current_page;
-            "<span class='current-page'>"; num; "</span>";
-          ELSE;
-            SET label = num;
-            SET args = "?page=" _ num;
-           SET args = args _ "&order=" _ request.params.order
-             IF request.params.order;
-           SET args = args _ "&amp;o2=desc"
-             IF request.params.o2 == "desc";
-            SET action = "list";
-            FOR col = classmetadata.columns.list;
-              IF request.params.$col;
-                SET args = args _ "&" _ col _ "=" _ request.params.$col;
-                SET action = "search";
-              END;
-            END;
-            link(classmetadata.table, action, args, label);
-          END;
-     END;
-%]
-</p>
-[% END %]
diff --git a/templates/factory/search_form b/templates/factory/search_form
deleted file mode 100644 (file)
index 1f07194..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<div id="search">
-<form method="get" action="[% base %]/[% classmetadata.moniker %]/search/">
-<fieldset>
-<legend>Search</legend>
-        [% FOR col = classmetadata.columns;
-            NEXT IF col == "id";
-         %]
-           <label>
-                <span class="field">[% classmetadata.colnames.$col; %]</span>
-                    [% SET element = classmetadata.cgi.$col;
-                    IF element.tag == "select";
-                        USE element_maker = Class("HTML::Element");
-                        SET element = element.unshift_content(
-                            element_maker.new("option", value," "));
-                    END;
-                   element.as_XML; %]
-                  </label>
-        [% END; %]
-    <input type="submit" name="search" value="search"/>
-    </fieldset>
-</form>
-</div>
diff --git a/templates/factory/title b/templates/factory/title
deleted file mode 100644 (file)
index 401f0a3..0000000
+++ /dev/null
@@ -1 +0,0 @@
-    <a href="[% base %]/frontpage">[% config.application_name %]</a>
diff --git a/templates/factory/view b/templates/factory/view
deleted file mode 100644 (file)
index 328678c..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-[%#
-
-=for doc
-
-The C<view> template takes some objects (usually just one) from
-C<objects> and displays the object's properties in a table. 
-
-=cut
-
-#%]
-[% PROCESS macros %]
-[% INCLUDE header %]
-[% FOR item = objects %]
-[% view_item(item); %]
-[%#
-
-=for doc
-
-The C<view> template also displays a list of other objects related to the first
-one via C<has_many> style relationships; this is done by calling the
-C<related_accessors> method - see L<Model/related_accessors> - to return
-a list of has-many accessors. Next it calls each of those accessors, and
-displays the results in a table.
-
-#%]
-    <br /><a href="[%base%]/[%item.table%]/list">Back to listing</a>
-[% view_related(item); %]
-    
-[%
-    button(item, "edit");
-    button(item, "delete");
-%]
-[% END; %]
-[% INCLUDE footer %]
diff --git a/templates/maypole.css b/templates/maypole.css
deleted file mode 100644 (file)
index 51d99a4..0000000
+++ /dev/null
@@ -1,376 +0,0 @@
-html {
-    padding-right: 0px;
-    padding-left: 0px; 
-    padding-bottom: 0px; 
-    margin: 0px; 
-    padding-top: 0px
-}
-body {
-    font-family: sans-serif;
-    padding-right: 0px; 
-    padding-left: 0px; 
-    padding-bottom: 0px;
-    margin: 0px; padding-top: 0px;
-    background-color: #fff;
-}
-#frontpage_list {
-    position: absolute;
-    z-index: 5;
-    padding: 0px 100px 0px 0px;
-    margin:0 0.5%;     
-    margin-bottom:1em; 
-    margin-top: 1em;
-    background-color: #fff;
-}
-
-#frontpage_list a:hover {
-    background-color: #d0d8e4;
-}
-
-#frontpage_list ul {
-    list-style-type: square;
-}
-
-.content {
-    padding: 12px;
-    margin-top: 1px;  
-    margin-bottom:0px;
-    margin-left: 15px; 
-    margin-right: 15px;
-    border-color: #000000;
-    border-top: 0px;
-    border-bottom: 0px;
-    border-left: 1px;
-    border-right: 1px;
-}
-
-A { 
-    text-decoration: none; 
-    color:#225 
-}
-A:hover { 
-    text-decoration: underline; 
-    color:#222 
-}
-
-#title {
-    z-index: 6;
-    width: 100%;
-    height: 18px;
-    margin-top: 10px;
-    font-size: 90%;
-    border-bottom: 1px solid #ddf;
-    text-align: left;
-}
-
-#subtitle {
-    postion: absolute;
-    z-index: 6;
-    padding: 10px;
-    margin-top: 2em;
-    height: 18px;
-    text-align: left;
-    background-color: #fff;
-}
-
-input[type=text] {
-    height: 16px;
-    width: 136px;
-    font-family: sans-serif;
-    font-size: 11px;
-    color: #2E415A;
-    padding: 0px;
-    margin-bottom: 5px;
-}
-
-input[type=submit] {
-    height: 18px;
-    width: 60px;
-    font-family: sans-serif;
-    font-size: 11px;
-    border: 1px outset;
-    background-color: #fff;
-    padding: 0px 0px 2px 0px;
-    margin-bottom: 5px;
-}
-
-input:hover[type=submit] {
-    color: #fff;
-    background-color: #7d95b5;
-}
-
-textarea {
-    width: 136px;
-    font-family: sans-serif;
-    font-size: 11px;
-    color: #2E415A;
-    padding: 0px;
-    margin-bottom: 5px;
-}
-
-select {
-    height: 16px;
-    width: 140px;
-    font-family: sans-serif;
-    font-size: 12px;
-    color: #202020;
-    padding: 0px;
-    margin-bottom: 5px;
-}
-
-.deco1 {
-    font-size: 0px;
-    z-index:1;
-    border:0px;
-    border-style:solid;
-    border-color:#4d6d99;
-    background-color:#4d6d99;
-}
-
-.deco2 {
-    z-index:2;
-    border:0px;
-    border-style:solid;
-    border-color:#627ea5;
-    background-color:#627ea5;
-}
-
-
-.deco3 {
-    z-index:3;
-    border:0px;
-    border-style:solid;
-    border-color:#7d95b5;
-    background-color:#7d95b5;
-}
-                   
-.deco4 {
-    z-index:4;
-    border:0px;
-    border-style:solid;
-    border-color:#d0d8e4;
-    background-color:#d0d8e4;
-}
-                   
-
-table { 
-    border: 0px solid; 
-    background-color: #ffffff;
-}
-
-#matrix { width: 100%; }
-
-#matrix th {
-    background-color: #b5cadc;
-    border: 1px solid #778;
-    font: bold 12px Verdana, sans-serif;
-}
-
-#matrix #actionth {
-    width: 1px; 
-    padding: 0em 1em 0em 1em;
-}
-
-#matrix tr.alternate { background-color:#e3eaf0; }
-#matrix tr:hover { background-color: #b5cadc; }
-#matrix td { font: 12px Verdana, sans-serif; }
-
-#navlist {
-    padding: 3px 0;
-    margin-left: 0;
-    margin-top:3em;
-    border-bottom: 1px solid #778;
-    font: bold 12px Verdana, sans-serif;
-}
-
-#navlist li {
-    list-style: none;
-    margin: 0;
-    display: inline;
-}
-
-#navlist li a {
-    padding: 3px 0.5em;
-    margin-left: 3px;
-    border: 1px solid #778;
-    border-bottom: none;
-    background: #b5cadc;
-    text-decoration: none;
-}
-
-#navlist li a:link { color: #448; }
-#navlist li a:visited { color: #667; }
-
-#navlist li a:hover {
-    color: #000;
-    background: #eef;
-    border-top: 4px solid #7d95b5;
-    border-color: #227;
-}
-
-#navlist #active a {
-    background: white;
-    border-bottom: 1px solid white;
-    border-top: 4px solid;
-}
-
-td { font: 12px Verdana, sans-serif; }
-
-
-fieldset {
-    margin-top: 1em;
-    padding: 1em;
-    background-color: #f3f6f8;
-    font:80%/1 sans-serif;
-    border:1px solid #ddd;
-}
-
-legend {
-    padding: 0.2em 0.5em;
-    background-color: #fff;
-    border:1px solid #aaa;
-    font-size:90%;
-    text-align:right;
-}
-
-label {
-    display:block;
-}
-
-label .field {
-    float:left;
-    width:25%;
-    margin-right:0.5em;
-    padding-top:0.2em;
-    text-align:right;
-    font-weight:bold;
-}
-
-#vlist {
-    padding: 0 1px 1px;
-    margin-left: 0;
-    font: bold 12px Verdana, sans-serif;
-    background: gray;
-    width: 13em;
-}
-
-#vlist li {
-    list-style: none;
-    margin: 0;
-    border-top: 1px solid gray;
-    text-align: left;
-}
-
-#vlist li a {
-    display: block;
-    padding: 0.25em 0.5em 0.25em 0.75em;
-    border-left: 1em solid #7d95b5;
-    background: #d0d8e4;
-    text-decoration: none;
-}
-
-#vlist li a:hover { 
-    border-color: #227;
-}
-
-.view .field {
-    background-color: #f3f6f8;
-    border-left: 1px solid #7695b5;
-    border-top: 1px solid #7695b5;
-    padding: 1px 10px 0px 2px;
-}
-
-#addnew {
-    width: 50%;
-    float: left;
-}
-
-#search {
-    width: 50%;
-    float:right;
-}
-
-.error { color: #d00; }
-
-.action {
-    border: 1px outset #7d95b5;
-    style:block;
-}
-
-.action:hover {
-    color: #fff;
-    text-decoration: none;
-    background-color: #7d95b5;
-}
-
-.actionform {
-    display: inline;
-}
-
-.actionbutton {
-    height: 16px;
-    width: 40px;
-    font-family: sans-serif;
-    font-size: 10px;
-    border: 1px outset;
-    background-color: #fff;
-    margin-bottom: 0px;
-}
-
-.actionbutton:hover {
-    color: #fff;
-    background-color: #7d95b5;
-}
-
-.actions {
-    white-space: nowrap;
-}
-
-.field {
-    display:inline;
-}
-
-#login { width: 400px; }
-
-#login input[type=text] { width: 150px; }
-#login input[type=password] { width: 150px; }
-
-.pager {
-    font: 11px Arial, Helvetica, sans-serif;
-    text-align: center;
-    border: solid 1px #e2e2e2;
-    border-left: 0;
-    border-right: 0;
-    padding-top: 10px;
-    padding-bottom: 10px;
-    margin: 0px;
-    background-color: #f3f6f8;
-}
-
-.pager a {
-    padding: 2px 6px;
-    border: solid 1px #ddd;
-    background: #fff;
-    text-decoration: none;
-}
-
-.pager a:visited {
-    padding: 2px 6px;
-    border: solid 1px #ddd;
-    background: #fff;
-    text-decoration: none;
-}
-
-.pager .current-page {
-    padding: 2px 6px;
-    font-weight: bold;
-    vertical-align: top;
-}
-
-.pager a:hover {
-    color: #fff;
-    background: #7d95b5;
-    border-color: #036;
-    text-decoration: none;
-}
-