]> git.decadent.org.uk Git - maypole.git/commitdiff
Merge branch 'upstream'
authorBen Hutchings <ben@decadent.org.uk>
Sun, 9 Nov 2008 14:24:57 +0000 (14:24 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Sun, 9 Nov 2008 15:12:10 +0000 (15:12 +0000)
26 files changed:
META.yml
examples/fancy_example/templates/factory/addnew [new file with mode: 0644]
examples/fancy_example/templates/factory/edit [new file with mode: 0644]
examples/fancy_example/templates/factory/footer [new file with mode: 0644]
examples/fancy_example/templates/factory/frontpage [new file with mode: 0644]
examples/fancy_example/templates/factory/header [new file with mode: 0644]
examples/fancy_example/templates/factory/list [new file with mode: 0644]
examples/fancy_example/templates/factory/login [new file with mode: 0644]
examples/fancy_example/templates/factory/macros [new file with mode: 0644]
examples/fancy_example/templates/factory/maypole [new file with mode: 0644]
examples/fancy_example/templates/factory/maypole.css [new file with mode: 0644]
examples/fancy_example/templates/factory/navbar [new file with mode: 0644]
examples/fancy_example/templates/factory/pager [new file with mode: 0644]
examples/fancy_example/templates/factory/search_form [new file with mode: 0644]
examples/fancy_example/templates/factory/search_form_recursive [new file with mode: 0644]
examples/fancy_example/templates/factory/title [new file with mode: 0644]
examples/fancy_example/templates/factory/view [new file with mode: 0644]
lib/Apache/MVC.pm
lib/Maypole/Constants.pm
lib/Maypole/Headers.pm
lib/Maypole/Manual/Terminology.pod [new file with mode: 0644]
lib/Maypole/Model/CDBI/AsForm.pm
lib/Maypole/View/TT.pm
t/crud.t [new file with mode: 0755]
t/templates/custom/edit [new file with mode: 0644]
wishlist.txt [new file with mode: 0644]

index 17c5cfb12fb2c4ac947c421d3dd4558c8b3defc7..2a9b00d46a27122def33f0c3a0c7c9062dfe0e98 100644 (file)
--- a/META.yml
+++ b/META.yml
@@ -1,7 +1,7 @@
 # http://module-build.sourceforge.net/META-spec.html
 #XXXXXXX This is a prototype!!!  It will change in the future!!! XXXXX#
 name:         Maypole
-version:      2.13
+version:      2.11_pre5
 version_from: lib/Maypole.pm
 installdirs:  site
 requires:
@@ -16,16 +16,19 @@ requires:
     Class::DBI::Pager:             0
     Class::DBI::Plugin::RetrieveAll: 0
     Class::DBI::Plugin::Type:      0
+    Class::DBI::SQLite:            0.08
     Digest::MD5:                   0
     File::MMagic::XS:              0.08
-    HTML::Tree:                    0
+    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.30
+generated_by: ExtUtils::MakeMaker version 6.17
diff --git a/examples/fancy_example/templates/factory/addnew b/examples/fancy_example/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/examples/fancy_example/templates/factory/edit b/examples/fancy_example/templates/factory/edit
new file mode 100644 (file)
index 0000000..cf88311
--- /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 %]/[% item.table %]/do_edit/[% item.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 || col | ucfirst | replace('_',' '); ":</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/examples/fancy_example/templates/factory/footer b/examples/fancy_example/templates/factory/footer
new file mode 100644 (file)
index 0000000..1b8ae55
--- /dev/null
@@ -0,0 +1,3 @@
+       </div>
+    </body>
+</html>
diff --git a/examples/fancy_example/templates/factory/frontpage b/examples/fancy_example/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/examples/fancy_example/templates/factory/header b/examples/fancy_example/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/examples/fancy_example/templates/factory/list b/examples/fancy_example/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/examples/fancy_example/templates/factory/login b/examples/fancy_example/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/examples/fancy_example/templates/factory/macros b/examples/fancy_example/templates/factory/macros
new file mode 100644 (file)
index 0000000..fc75d09
--- /dev/null
@@ -0,0 +1,185 @@
+[%#
+
+=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 | 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/examples/fancy_example/templates/factory/maypole b/examples/fancy_example/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/examples/fancy_example/templates/factory/maypole.css b/examples/fancy_example/templates/factory/maypole.css
new file mode 100644 (file)
index 0000000..d63be55
--- /dev/null
@@ -0,0 +1,381 @@
+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 {
+       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/examples/fancy_example/templates/factory/navbar b/examples/fancy_example/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/examples/fancy_example/templates/factory/pager b/examples/fancy_example/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/examples/fancy_example/templates/factory/search_form b/examples/fancy_example/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/examples/fancy_example/templates/factory/search_form_recursive b/examples/fancy_example/templates/factory/search_form_recursive
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>
diff --git a/examples/fancy_example/templates/factory/title b/examples/fancy_example/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/examples/fancy_example/templates/factory/view b/examples/fancy_example/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 %]
index 0dba642b9cdb2237fd623c5da5f8ce6f3b324246..db2bbed641fec4f064b405764f518b6ba4f7cd3f 100644 (file)
@@ -27,6 +27,7 @@ BEGIN {
      }
      require Apache2::RequestIO;
      require Apache2::RequestRec;
+     use Apache2::Log;
      require Apache2::RequestUtil;
      eval 'use Apache2::Const -compile => qw/REDIRECT/;'; # -compile 4 no import
      require APR::URI;
@@ -116,9 +117,15 @@ sub warn {
   my ($package, $line) = (caller)[0,2];
   my $ar = $self->parent ? $self->parent->{ar} : $self->{ar};
   if ( $args[0] and ref $self ) {
+    my @lines = split /\n/, (join '', @args);
+    $ar->warn("[$package line $line] ".shift(@lines));
+    foreach(@lines) {
+      next unless $_;
+      $ar->warn(" $_");
+    }
     $ar->warn("[$package line $line] ", @args) ;
   } else {
-    print "warn called by ", caller, " with ", @_, "\n";
+    print STDERR "warn called by ", caller, " with ", @_, "\n";
   }
   return;
 }
index b70a06ceb79f7e10bd8e547ddb357d83451ad902..cee418f6ba583a3f2eab545514ec97af6b40dc58 100644 (file)
@@ -5,7 +5,7 @@ use constant OK       => 0;
 use constant DECLINED => -1;
 use constant ERROR    => 500;
 our @EXPORT = qw(OK DECLINED ERROR);
-our $VERSION = "1." . sprintf "%04d", q$Rev: 483 $ =~ /: (\d+)/;
+our $VERSION = "1." . sprintf "%04d", q$Rev$ =~ /: (\d+)/;
 
 1;
 
index 28675fce3dd0252b1e75a89dce92f6e6d6007ad3..320cb0541841aaa4fe1b0f96127f919cb84b6998 100644 (file)
@@ -4,7 +4,7 @@ use base 'HTTP::Headers';
 use strict;
 use warnings;
 
-our $VERSION = "1." . sprintf "%04d", q$Rev: 376 $ =~ /: (\d+)/;
+our $VERSION = "1." . sprintf "%04d", q$Rev$ =~ /: (\d+)/;
 
 sub get {
     shift->header(shift);
diff --git a/lib/Maypole/Manual/Terminology.pod b/lib/Maypole/Manual/Terminology.pod
new file mode 100644 (file)
index 0000000..adc5dc8
--- /dev/null
@@ -0,0 +1,212 @@
+=head1 NAME\r
+\r
+Maypole::Manual::Terminology - common terms\r
+\r
+=head1 VERSION\r
+\r
+This version written for Maypole 2.11\r
+\r
+=head1 TERMINOLOGY\r
+\r
+For the avoidance of confusion, the following terms are defined. We'll try and \r
+ensure the Maypole docs stick to these usages.\r
+\r
+=over 4\r
+\r
+=item driver\r
+\r
+The custom package written to set up a Maypole application. This is the package \r
+that has the C<use Maypole::Application> statement. If you're not using \r
+L<Maypole::Application> to set up your app (not recommended for newbies, but \r
+common enough), the driver class will directly inherit from one of Maypole's \r
+frontend classes. \r
+\r
+=item controller\r
+\r
+Occasionally this term is used in place of C<driver>.\r
+\r
+See the entry below (MVC) for the main usage of the term C<controller> within \r
+Maypole. \r
+\r
+=item application\r
+\r
+Sometimes this is used to refer to the driver, or the driver plus configuration\r
+data, but this term is best reserved to refer to the complete application, i.e.\r
+driver, plugins, templates, model, config, the whole shebang.\r
+\r
+=item frontend\r
+\r
+An adapter class that allows Maypole to work in a number of different server \r
+environments. The currently available frontends are:\r
+\r
+    Frontend        Distribution    Environment\r
+    ==============================================\r
+    CGI::Maypole    Maypole         CGI\r
+    Apache::MVC            Maypole         Apache/mod_perl or Apache2/mod_perl2\r
+    MasonX::Maypole    MasonX::Maypole Apache/mod_perl with Mason\r
+       \r
+The driver class inherits from the appropriate frontend, which inherits from \r
+L<Maypole>.\r
+       \r
+=item backend\r
+\r
+Confusingly, sometimes you'll see the frontend referred to as the backend. It \r
+depends on your point of view.\r
+\r
+Also confusingly, the Maypole model (e.g. L<Maypole::Model::CDBI>) is sometimes\r
+referred to as the backend.\r
+\r
+You'll just need to pay attention to context. In general, it's probably best to \r
+avoid using this term altogether. \r
+\r
+=item request\r
+\r
+The Maypole request object. This contains all data sent in the request\r
+(including headers, cookies, CGI parameters), and accumulates data to be sent in\r
+the response (headers and content). It also provides access to the configuration\r
+object, and stores the information parsed out of the URL (action, table, args\r
+etc.). Plugins often add methods and further data members to the request object. \r
+\r
+=item workflow\r
+\r
+The sequence of events when a browser sends a request to a Maypole \r
+application. \r
+\r
+You will also often see this referred to as the C<request> (distinct from the \r
+request object).\r
+\r
+=item Exported method\r
+\r
+A method in a Maypole model class that is labelled with the C<Exported> \r
+attribute. These methods are mapped to part of the request URI. So requesting \r
+a path will result in a particular method being called on a particular model \r
+class.\r
+\r
+=item action\r
+\r
+An Exported method.\r
+\r
+Note: this is not the action attribute of a form, although the form's action URI\r
+will generally include a Maypole action component. For instance, a form might\r
+submit to the following URL: C<[% $base %]/beer/drink/5>. The form action is the\r
+URL, whereas the Maypole action is the C<drink> method on the C<BeerDB::Beer>\r
+object with an ID of 5.\r
+\r
+=item command\r
+\r
+In some of the standard factory templates, an C<action> is referred to as a \r
+C<command>.\r
+\r
+=item template\r
+\r
+A file used to generate HTML for part or all of a web page. Maypole currently \r
+supports Template Toolkit and Mason as templating languages, but others could \r
+be added easily. Of course, a template doesn't have to generate only HTML.\r
+\r
+=back\r
+\r
+=head2 MVC and Maypole\r
+\r
+=head3 MVC - Model-View-Controller\r
+\r
+A pattern describing separation of concerns in a complex application. The \r
+C<model> represents the domain or business logic. The C<view> represents the \r
+user interface. The C<controller> mediates the interaction between the two. \r
+\r
+Opinions vary between how closely Maypole adheres to this pattern. \r
+\r
+Here's one opinion:\r
+\r
+=over 4 \r
+\r
+=item view\r
+\r
+This is represented in Maypole by the view class (L<Maypole::View::TT>, \r
+L<Maypole::View::Mason>, or L<MasonX::Maypole::View>), and by the templates. \r
+\r
+=item controller\r
+\r
+An abstract concept in Maypole, i.e. there is no specific controller class. \r
+\r
+The main sequence of events that occur during the processing of a request is\r
+controlled by methods in C<Maypole.pm>. Thus, the controller logic is in the\r
+same class as the request object. This may seem a bit strange, but in practice\r
+it works well.\r
+\r
+More detailed events within the processing of a request are actually handled by \r
+methods in the Maypole 'model'. For instance, switching from one template to \r
+another - the "Template Switcheroo" referred to in L<Maypole::Manual::Cookbook>. \r
+\r
+Be aware that occasionally authors refer to the C<controller> when they are\r
+describing the C<driver>.\r
+\r
+=item model\r
+\r
+In Maypole, the 'model' is the set of classes representing individual tables in\r
+the database. Tables are related to each other in a more or less complex way.\r
+Each table class inherits from a Maypole model class, such as\r
+L<Maypole::Model::CDBI> or L<Maypole::Model::CDBI::Plain>.\r
+\r
+The functionality provided by the Maypole model class is more accurately\r
+described as a Presentation Model (see below). In complex Maypole applications,\r
+it is good practise to separate the domain model (the 'heart' of the\r
+application) into a separate class hierarchy (see\r
+L<Maypole::Manual::Inheritance>).\r
+\r
+The distinction is relatively unimportant when using Maypole in 'default' mode - \r
+i.e. using L<Maypole::Model::CDBI>, and allowing Maypole to autogenerate the \r
+'model' classes straight out of the database. \r
+\r
+However, in many applications, a more complex domain model is required, or may\r
+already exist. In this case, the Maypole model is more clearly seen as a layer\r
+that sits on top of the domain model, mediating access to it from the web UI, \r
+via the controller.\r
+\r
+This conceptualisation helps developers maintain a separation between the\r
+Maypole model classes (presentation model), and the domain model. Without this\r
+distinction, developers may add domain-specific code to the Maypole model\r
+classes. To a certain extent, in simple applications, this is fine. But if you\r
+find yourself adding lots of non-Exported methods to your Maypole model classes,\r
+and these methods are not there to directly support Exported methods, consider\r
+whether you could separate out the domain model into a separate hierarchy of\r
+classes - see L<Maypole::Manual::Inheritance>.\r
+\r
+Otherwise, the 'model' classes may develop into two quite uncoupled code bases,\r
+but which co-exist in the same files. They will interact through a relatively\r
+small number of methods. These methods should in fact become the public API of\r
+the domain model, which should be moved to a separate class hierarchy. At some\r
+point, the convenience of dropping new methods into the 'shared' classes will be\r
+outweighed by the heuristic advantage of separating different layers into\r
+separate class hierarchies.\r
+\r
+=back\r
+\r
+=head3 Presentation Model\r
+\r
+This pattern more accurately describes the role of the Maypole model.\r
+Martin Fowler describes I<Presentation Model> in L<Separting presentation logic\r
+from the View|http://www.martinfowler.com/eaaDev/OrganizingPresentations.html>\r
+and L<Presentation\r
+Model|http://www.martinfowler.com/eaaDev/PresentationModel.html>.\r
+\r
+The user sends an event (e.g. an HTTP request) to the Controller. The Controller\r
+translates the request into a method call on the Presentation Model. The\r
+Presentation Model interacts with the underlying Domain Model, and stores the\r
+results in a bunch of variables, which I<represent the new state of the View>\r
+(that's why it's a Presentation Model, not a Domain Model). The View then\r
+queries the Presentation Model to retrieve these new values. In Maypole, this is\r
+the role of the C<vars()> method on L<Maypole::View::Base>, which transmits the\r
+new values to the templates.\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
index d76ecb42da5bcca50de5497b3231e169d54f2fad..8a7f06c95e3f8237aad4d82009b0eba928353060 100644 (file)
@@ -880,9 +880,9 @@ sub _to_bool_select {
     unless (defined $selected);
 
   my $a = HTML::Element->new("select", name => $col);
-  if ($args->{column_nullable} || $args->{value} eq '') {
+  if ($args->{column_nullable} || !defined $args->{value} ) {
     my $null =  HTML::Element->new("option");
-    $null->attr('selected', 'selected') if  $args->{value} eq '';
+    $null->attr('selected', 'selected') if  (!defined $args->{value});
     $a->push_content( $null ); 
   }
 
index c966a7df262bbefe9e96a3114b50e0514f26a4e9..433309e98a911f1d2fd2d2f58ec705ff09d005e9 100644 (file)
@@ -8,7 +8,7 @@ use Template::Constants qw( :all );
 our $error_template;
 { local $/; $error_template = <DATA>; }
 
-our $VERSION = '2.12';
+our $VERSION = '2.13';
 
 my $debug_flags = DEBUG_ON;
 
@@ -17,7 +17,7 @@ use strict;
 sub template {
   my ( $self, $r ) = @_;
   unless ($self->{tt}) {
-    my $view_options = $r->config->view_options || {};
+    my $view_options = $r->config->view_options || { POST_CHOMP=>1, PRE_CHOMP=>1, TRIM=>1 };
     if ($r->debug) {
       $view_options->{DEBUG} = $debug_flags;
     }
@@ -400,16 +400,14 @@ Simon Cozens
 __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; }
+body { background-color:#fff; font-family: sans-serif}
+p { background-color: #e3eaf0; padding: 5px; }
+pre { background-color: #e3eaf0; padding: 5px; border: 1px dotted red }
+.lhs {background-color: #b5cadc; }
+.rhs {background-color: #e3eaf0; }
 </style>
 </head> <body>
-<h1> Maypole application error </h1>
+<h1> Maypole Application Error </h1>
 
 <p> This application living at <code>[%request.config.uri_base%]</code>, 
 [%request.config.application_name || "which is unnamed" %], has
@@ -427,31 +425,31 @@ the path "[% request.path %]". The error text returned was:
 
 <h2> Request details </h2>
 
-<table width="85%" cellspacing="2" cellpadding="1">
+<table width="100%" 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%"> [%
+    <tr> <td class="lhs" width="20%"> <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>
+    <tr> <td class="lhs" width="20%">[% 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>
+<tr><td class="lhs" width="20%"> <b>Base URI</b> </td><td class="rhs" width="65%">[% request.config.uri_base %]</td></tr>
+<tr><td class="lhs" width="20%"> <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>
+    <tr><td class="lhs"  width="20%"> <b>Model </b> </td><td class="rhs" width="65%"> [% request.config.model %] </td></tr>
+    <tr><td class="lhs"  width="20%"> <b>View </b> </td><td class="rhs" width="65%"> [% request.config.view %] </td></tr>
+    <tr><td class="lhs" width="20%"> <b>Classes</b> </td><td class="rhs" width="65%"> [% request.config.classes.list.join(" , ") %] </td></tr>
+    <tr><td class="lhs" width="20%"> <b>Tables</b> </td><td class="rhs" width="65%"> [% request.config.display_tables.list.join(" , ") %] </td></tr>
 </table>
 
 </body>
diff --git a/t/crud.t b/t/crud.t
new file mode 100755 (executable)
index 0000000..8afe888
--- /dev/null
+++ b/t/crud.t
@@ -0,0 +1,112 @@
+#!/usr/bin/perl -w
+use Test::More;
+use lib 'examples'; # Where BeerDB should live
+BEGIN {
+    $ENV{BEERDB_DEBUG} = 2;
+
+    eval { require BeerDB };
+    Test::More->import( skip_all =>
+        "SQLite not working or BeerDB module could not be loaded: $@"
+    ) if $@;
+
+    plan tests =>21;
+    
+}
+use Maypole::CLI qw(BeerDB);
+use Maypole::Constants;
+$ENV{MAYPOLE_TEMPLATES} = "t/templates";
+
+isa_ok( (bless {},"BeerDB") , "Maypole");
+
+
+
+# Test create missing required 
+like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit?name=&url=www.sammysmiths.com&notes=Healthy Brew"), qr/name' => 'This field is required/, "Required fields necessary to create ");
+
+# Test create with all  required
+like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit?name=Samuel Smiths&url=www.sammysmiths.com&notes=Healthy Brew"), qr/^# view/, "Created a brewery");
+     
+($brewery,@other) = BeerDB::Brewery->search(name=>'Samuel Smiths'); 
+
+
+SKIP: {
+    skip "Could not create and retrieve Brewery", 8 unless $brewery;
+       like(eval {$brewery->name}, qr/Samuel Smiths/, "Retrieved Brewery, $brewery, we just created");
+
+       #-------- Test updating printable fields ------------------   
+
+    # TEST clearing out  required printable column 
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id."?name="), qr/name' => 'This field is required/, "Required printable field can not be cleared on update");
+
+       # Test cgi update errors hanging around from last request 
+       unlike(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id), qr/name' => 'This field is required/, "cgi_update_errors did not persist"); 
+
+       # Test update no columns 
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id), qr/^# view/, "Updated no columns"); 
+       
+       # Test only updating one non required column
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id."?notes="), qr/^# view/, "Updated a single non required column"); 
+
+       # TEST empty input for non required  printable 
+       like(BeerDB->call_url("http://localhost/beerdb/brewery/do_edit/".$brewery->id."?notes=&name=Sammy Smiths"), qr/^# view/, "Updated brewery" );
+
+       # TEST update actually cleared out a printable field
+       $val  = $brewery->notes ;
+    if ($val eq '') { $val = undef }; 
+       is($val, undef, "Verified non required printable field was cleared");
+
+       # TEST update did not change a field not in parameter list
+       is($brewery->url, 'www.sammysmiths.com', "A field not in parameter list is not updated.");
+};
+
+#-----------------  Test other types of  fields --------------
+
+$style = BeerDB::Style->insert({name => 'Stout', notes => 'Rich, dark, creamy, mmmmmm.'});
+
+# TEST create with integer, date, printable fields
+like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&score=5&notes=Healthy Brew&price=5.00&tasted=2000-12-01"),  qr/^# view/, "Created a beer with date, integer and printable fields");
+
+($beer, @other) = BeerDB::Beer->search(name=>'Oatmeal Stout');
+
+SKIP: {
+       skip "Could not create and retrieve Beer", 7 unless $beer;
+
+       # TEST wiping out an integer field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&score=&notes=Healthy Brew&price=5.00"),  qr/^# view/, "Updated a beer");
+
+       # TEST update actually cleared out a the integer field
+       $val  = $beer->score ;
+    if ($val eq '') { $val = undef }; 
+       is($val, undef, "Verified non required integer field was cleared");
+
+       
+       # TEST invalid integer field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=Stout&price=5.00"),  qr/style' => 'Please provide a valid value/, "Integer field invalid");
+
+       # TEST update with empty  date field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&tasted=&notes=Healthy Brew&price=5.00"),  qr/^# view/, "Updated a beer");
+
+       # TEST update actually cleared out a  date field
+       $tasted = $beer->tasted ;
+    if ($tasted eq '') { $tasted = undef }; 
+       is($tasted, undef, "Verified non required date field was cleared.");
+
+       # TEST invalid date 
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&style=".$style->id."&tasted=baddate&notes=Healthy Brew&price=5.00"),  qr/tasted' => 'Please provide a valid value/, "Date field invalid");
+
+       # TEST  negative value allowed for required field
+       like(BeerDB->call_url("http://localhost/beerdb/beer/do_edit/".$beer->id."?name=Oatmeal Stout&brewery=".$brewery->id."&price=-5.00"),  qr/^# view/, "Negative values allowed for required field");
+       
+       # TEST negative value actually got stored
+       like($beer->price, qr/-5(\.00)?/, "Negative value for required field stored in database") 
+};
+$beer_id = $beer->id;
+$beer->delete;
+
+# TEST delete
+$beer = BeerDB::Beer->retrieve($beer_id);
+is($beer, undef, "Deleted Beer");
+
+$brewery->delete;
+$style->delete;
diff --git a/t/templates/custom/edit b/t/templates/custom/edit
new file mode 100644 (file)
index 0000000..ceee5d2
--- /dev/null
@@ -0,0 +1,11 @@
+# edit
+[%     
+
+       USE  dumper;
+"# errors dump"; 
+       dumper.dump(errors);
+"# parameters dump"; 
+       dumper.dump(request.params);
+%]
+
+# End errors dump 
diff --git a/wishlist.txt b/wishlist.txt
new file mode 100644 (file)
index 0000000..385eb99
--- /dev/null
@@ -0,0 +1,56 @@
+2.12/3.0 wishlist\r
+=================\r
+\r
+Not for inclusion in the MANIFEST. \r
+\r
+2.11\r
+====\r
+Fix factory roots, document and explain behaviour\r
+send_output() should return a status code\r
+Move template test out of process() and into handler_guts() - maybe\r
+Fix bug 14570 - returning error codes breaks CGI::Maypole\r
+Write Maypole::Manual::Exceptions\r
+Test and refactor external_redirect()\r
+\r
+Fix Mp::P::USC. \r
+\r
+2.12\r
+====\r
+Maypole::instance()\r
+Better plugin architecture, for models and bits of models. \r
+Investigate problems reported with adopt() - rt 15598\r
+Re-implement Maypole::Cache as Maypole::Plugin::Cache, probably using \r
+    start_request_hook, and not overriding handler_guts()\r
+Handle repeat form submissions.\r
+Implement internal_redirect().\r
+Build a more sophisticated app for testing. \r
+Move class_of() to the controller - need to do this to support multiple models \r
+    - maybe\r
+Multiple model support - URLs like /$base/$model/$table/$action/$id.\r
+Refactor M-P-USC and M-P-Session into M-P-User, M-P-Session, and M-P-Cookie\r
+\r
+\r
+3.0\r
+====\r
+Encapsulate all request data in HTTP::Request object, and all response data \r
+in HTTP::Response object\r
+\r
+Look at HTTP::Body\r
+\r
+Easier file uploads - look at incorporating Mp::P::Upload\r
+\r
+Add email handling - like Rails - via model plugins. \r
+\r
+An e-commerce model plugin would be nice - or proof of concept - maybe look \r
+at Handel.  \r
+\r
+Add validation layer(s), or just an API\r
+\r
+killer apps: SVN model; mitiki; Pet Shop; adventure builder \r
+\r
+Multiple views - HTML, text-only, PDF, SOAP, XML  - use request data to switch\r
+to an alternate view_object - switch via a factory method.\r
+\r
+Maybe rename the model to PModel (Presentation Model)?\r
+\r
+Pseudo-continuations...\r