From: Ben Hutchings Date: Sun, 9 Nov 2008 14:24:57 +0000 (+0000) Subject: Merge branch 'upstream' X-Git-Url: https://git.decadent.org.uk/gitweb/?a=commitdiff_plain;h=ccffaddb7564c652448befe4d67d0ae5276d8975;hp=ac095b83019b7eff094549fa33f66147170ea35f;p=maypole.git Merge branch 'upstream' --- diff --git a/META.yml b/META.yml index 17c5cfb..2a9b00d 100644 --- 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 index 0000000..2334496 --- /dev/null +++ b/examples/fancy_example/templates/factory/addnew @@ -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 + +#%] + +
+
+
+Add a new [% classmetadata.moniker %] + [% FOR col = classmetadata.columns %] + [% NEXT IF col == "id" %] + + [% IF errors.$col %] + [% errors.$col | html %] + [% END %] + + [% END; %] + + +
+
+
diff --git a/examples/fancy_example/templates/factory/edit b/examples/fancy_example/templates/factory/edit new file mode 100644 index 0000000..cf88311 --- /dev/null +++ b/examples/fancy_example/templates/factory/edit @@ -0,0 +1,70 @@ +[%# + +=head1 edit + +This is the edit page. It edits the passed-in object, by displaying a +form similar to L but with the current values filled in. + +=cut + +#%] +[% PROCESS macros %] +[% INCLUDE header %] +[% INCLUDE title %] + +[% IF request.action == 'edit' %] +[% INCLUDE navbar %] +[% END %] + +[% IF object %] +
Edit a [% classmetadata.moniker %]
+
+
+Edit [% object.name %] + [% FOR col = classmetadata.columns; + NEXT IF col == "id" OR col == classmetadata.table _ "_id"; + '"; + IF errors.$col; + ''; errors.$col;''; + END; + END %] + + +
+ +[% ELSE %] + +
+
+
+Add a new [% classmetadata.moniker %] + [% FOR col = classmetadata.columns %] + [% NEXT IF col == "id" %] + + [% IF errors.$col %] + [% errors.$col | html %] + [% END %] + + [% END; %] + + +
+
+
+ +[% END %] +[% INCLUDE footer %] diff --git a/examples/fancy_example/templates/factory/footer b/examples/fancy_example/templates/factory/footer new file mode 100644 index 0000000..1b8ae55 --- /dev/null +++ b/examples/fancy_example/templates/factory/footer @@ -0,0 +1,3 @@ + + + diff --git a/examples/fancy_example/templates/factory/frontpage b/examples/fancy_example/templates/factory/frontpage new file mode 100644 index 0000000..ac47269 --- /dev/null +++ b/examples/fancy_example/templates/factory/frontpage @@ -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 %] +
+ [% config.application_name || "A poorly configured Maypole application" %] +
+
+ +
+ +[% INCLUDE maypole %] + +[% INCLUDE footer %] diff --git a/examples/fancy_example/templates/factory/header b/examples/fancy_example/templates/factory/header new file mode 100644 index 0000000..ba0b190 --- /dev/null +++ b/examples/fancy_example/templates/factory/header @@ -0,0 +1,16 @@ + + + + + [% + title || config.application_name || + "A poorly configured Maypole application" + %] + + + + + + +
diff --git a/examples/fancy_example/templates/factory/list b/examples/fancy_example/templates/factory/list new file mode 100644 index 0000000..9abbc01 --- /dev/null +++ b/examples/fancy_example/templates/factory/list @@ -0,0 +1,63 @@ +[% PROCESS macros %] +[% INCLUDE header %] +[% INCLUDE title %] +[% IF search %] +
Search results
+[% ELSE %] +
Listing of all [% classmetadata.plural %]
+[% END %] +[% INCLUDE navbar %] +
+ + + [% FOR col = classmetadata.list_columns.list; + NEXT IF col == "id" OR col == classmetadata.table _ "_id"; + ""; + END %] + + + [% SET count = 0; + FOR item = objects; + SET count = count + 1; + ""; + display_line(item); + ""; + END %] +
"; + 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"; + "↓"; + ELSE; + "↑"; + END; + END; + ELSE; + classmetadata.colnames.$col || col FILTER ucfirst; + END; + "Actions
+ +[% INCLUDE pager %] +[% INCLUDE addnew %] +[% INCLUDE search_form %] +
+[% INCLUDE footer %] diff --git a/examples/fancy_example/templates/factory/login b/examples/fancy_example/templates/factory/login new file mode 100644 index 0000000..af08e5b --- /dev/null +++ b/examples/fancy_example/templates/factory/login @@ -0,0 +1,27 @@ +[% PROCESS macros %] +[% INCLUDE header %] +[% INCLUDE title %] +[% user_field = config.auth.user_field || "user" %] + +
You need to log in
+ +
+ [% IF login_error %] +
[% login_error | html %]
+ [% END %] +
+
+ Login + + + +
+
+
+ diff --git a/examples/fancy_example/templates/factory/macros b/examples/fancy_example/templates/factory/macros new file mode 100644 index 0000000..fc75d09 --- /dev/null +++ b/examples/fancy_example/templates/factory/macros @@ -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 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; + ''; + label | html; + ""; +END; +%] + +[%# + +=head2 maybe_link_view + +C 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 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 column by default, and magically +URLifies columns called C. 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); + ""; + IF col == "url" AND item.url; + ' '; item.url; ''; + 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; + + ""; + END; + ''; + button(item, "edit"); + button(item, "delete"); + ""; +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) %] +
+
+[% END %] +[% END %] +[%# + +=head2 view_related + +This takes an object, and looks up the C; 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; + "
"; accessor | ucfirst; "
\n"; + "
    "; + FOR thing = object.$accessor; + "
  • "; maybe_link_view(thing); "
  • \n"; + END; + "
"; + 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 %] +
[% item.$string | html %]
+ [% INCLUDE navbar %] + + + + + + [% 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 method: + +#%] + + + + + [% END; %] +
[% classmetadata.colnames.$string %][% item.$string | html %]
[% classmetadata.colnames.$col || + col | ucfirst | replace('_',' '); %] + [% IF col == "url" && item.url; # Possibly too much magic. + ' '; item.url; ''; + 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 method, or returns the object's ID +if there is no C method or other stringification method defined. + +=cut + +#%] +
+[% END %] diff --git a/examples/fancy_example/templates/factory/maypole b/examples/fancy_example/templates/factory/maypole new file mode 100644 index 0000000..7ab2744 --- /dev/null +++ b/examples/fancy_example/templates/factory/maypole @@ -0,0 +1,7 @@ + +
 
+
 
+
 
+
 
+
 
+ diff --git a/examples/fancy_example/templates/factory/maypole.css b/examples/fancy_example/templates/factory/maypole.css new file mode 100644 index 0000000..d63be55 --- /dev/null +++ b/examples/fancy_example/templates/factory/maypole.css @@ -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 index 0000000..0c8b168 --- /dev/null +++ b/examples/fancy_example/templates/factory/navbar @@ -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 %] + diff --git a/examples/fancy_example/templates/factory/pager b/examples/fancy_example/templates/factory/pager new file mode 100644 index 0000000..78c89fd --- /dev/null +++ b/examples/fancy_example/templates/factory/pager @@ -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 template argument which responds +to the L interface. + +#%] +[% +IF pager AND pager.first_page != pager.last_page; +%] +

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; + ""; num; ""; + ELSE; + SET label = num; + SET args = "?page=" _ num; + SET args = args _ "&order=" _ request.params.order + IF request.params.order; + SET args = args _ "&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; +%] +

+[% END %] diff --git a/examples/fancy_example/templates/factory/search_form b/examples/fancy_example/templates/factory/search_form new file mode 100644 index 0000000..d10101e --- /dev/null +++ b/examples/fancy_example/templates/factory/search_form @@ -0,0 +1,22 @@ + diff --git a/examples/fancy_example/templates/factory/search_form_recursive b/examples/fancy_example/templates/factory/search_form_recursive new file mode 100644 index 0000000..5d540fb --- /dev/null +++ b/examples/fancy_example/templates/factory/search_form_recursive @@ -0,0 +1,9 @@ + diff --git a/examples/fancy_example/templates/factory/title b/examples/fancy_example/templates/factory/title new file mode 100644 index 0000000..401f0a3 --- /dev/null +++ b/examples/fancy_example/templates/factory/title @@ -0,0 +1 @@ + [% config.application_name %] diff --git a/examples/fancy_example/templates/factory/view b/examples/fancy_example/templates/factory/view new file mode 100644 index 0000000..9f06086 --- /dev/null +++ b/examples/fancy_example/templates/factory/view @@ -0,0 +1,32 @@ +[%# + +=for doc + +The C template takes some objects (usually just one) from +C and displays the object's properties in a table. + +=cut + +#%] +[% PROCESS macros %] +[% INCLUDE header %] +[% view_item(object); %] +[%# + +=for doc + +The C template also displays a list of other objects related to the first +one via C style relationships; this is done by calling the +C method - see L - to return +a list of has-many accessors. Next it calls each of those accessors, and +displays the results in a table. + +#%] +
Back to listing +[% view_related(object); %] + +[% + button(object, "edit"); + button(object, "delete"); +%] +[% INCLUDE footer %] diff --git a/lib/Apache/MVC.pm b/lib/Apache/MVC.pm index 0dba642..db2bbed 100644 --- a/lib/Apache/MVC.pm +++ b/lib/Apache/MVC.pm @@ -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; } diff --git a/lib/Maypole/Constants.pm b/lib/Maypole/Constants.pm index b70a06c..cee418f 100644 --- a/lib/Maypole/Constants.pm +++ b/lib/Maypole/Constants.pm @@ -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; diff --git a/lib/Maypole/Headers.pm b/lib/Maypole/Headers.pm index 28675fc..320cb05 100644 --- a/lib/Maypole/Headers.pm +++ b/lib/Maypole/Headers.pm @@ -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 index 0000000..adc5dc8 --- /dev/null +++ b/lib/Maypole/Manual/Terminology.pod @@ -0,0 +1,212 @@ +=head1 NAME + +Maypole::Manual::Terminology - common terms + +=head1 VERSION + +This version written for Maypole 2.11 + +=head1 TERMINOLOGY + +For the avoidance of confusion, the following terms are defined. We'll try and +ensure the Maypole docs stick to these usages. + +=over 4 + +=item driver + +The custom package written to set up a Maypole application. This is the package +that has the C statement. If you're not using +L to set up your app (not recommended for newbies, but +common enough), the driver class will directly inherit from one of Maypole's +frontend classes. + +=item controller + +Occasionally this term is used in place of C. + +See the entry below (MVC) for the main usage of the term C within +Maypole. + +=item application + +Sometimes this is used to refer to the driver, or the driver plus configuration +data, but this term is best reserved to refer to the complete application, i.e. +driver, plugins, templates, model, config, the whole shebang. + +=item frontend + +An adapter class that allows Maypole to work in a number of different server +environments. The currently available frontends are: + + Frontend Distribution Environment + ============================================== + CGI::Maypole Maypole CGI + Apache::MVC Maypole Apache/mod_perl or Apache2/mod_perl2 + MasonX::Maypole MasonX::Maypole Apache/mod_perl with Mason + +The driver class inherits from the appropriate frontend, which inherits from +L. + +=item backend + +Confusingly, sometimes you'll see the frontend referred to as the backend. It +depends on your point of view. + +Also confusingly, the Maypole model (e.g. L) is sometimes +referred to as the backend. + +You'll just need to pay attention to context. In general, it's probably best to +avoid using this term altogether. + +=item request + +The Maypole request object. This contains all data sent in the request +(including headers, cookies, CGI parameters), and accumulates data to be sent in +the response (headers and content). It also provides access to the configuration +object, and stores the information parsed out of the URL (action, table, args +etc.). Plugins often add methods and further data members to the request object. + +=item workflow + +The sequence of events when a browser sends a request to a Maypole +application. + +You will also often see this referred to as the C (distinct from the +request object). + +=item Exported method + +A method in a Maypole model class that is labelled with the C +attribute. These methods are mapped to part of the request URI. So requesting +a path will result in a particular method being called on a particular model +class. + +=item action + +An Exported method. + +Note: this is not the action attribute of a form, although the form's action URI +will generally include a Maypole action component. For instance, a form might +submit to the following URL: C<[% $base %]/beer/drink/5>. The form action is the +URL, whereas the Maypole action is the C method on the C +object with an ID of 5. + +=item command + +In some of the standard factory templates, an C is referred to as a +C. + +=item template + +A file used to generate HTML for part or all of a web page. Maypole currently +supports Template Toolkit and Mason as templating languages, but others could +be added easily. Of course, a template doesn't have to generate only HTML. + +=back + +=head2 MVC and Maypole + +=head3 MVC - Model-View-Controller + +A pattern describing separation of concerns in a complex application. The +C represents the domain or business logic. The C represents the +user interface. The C mediates the interaction between the two. + +Opinions vary between how closely Maypole adheres to this pattern. + +Here's one opinion: + +=over 4 + +=item view + +This is represented in Maypole by the view class (L, +L, or L), and by the templates. + +=item controller + +An abstract concept in Maypole, i.e. there is no specific controller class. + +The main sequence of events that occur during the processing of a request is +controlled by methods in C. Thus, the controller logic is in the +same class as the request object. This may seem a bit strange, but in practice +it works well. + +More detailed events within the processing of a request are actually handled by +methods in the Maypole 'model'. For instance, switching from one template to +another - the "Template Switcheroo" referred to in L. + +Be aware that occasionally authors refer to the C when they are +describing the C. + +=item model + +In Maypole, the 'model' is the set of classes representing individual tables in +the database. Tables are related to each other in a more or less complex way. +Each table class inherits from a Maypole model class, such as +L or L. + +The functionality provided by the Maypole model class is more accurately +described as a Presentation Model (see below). In complex Maypole applications, +it is good practise to separate the domain model (the 'heart' of the +application) into a separate class hierarchy (see +L). + +The distinction is relatively unimportant when using Maypole in 'default' mode - +i.e. using L, and allowing Maypole to autogenerate the +'model' classes straight out of the database. + +However, in many applications, a more complex domain model is required, or may +already exist. In this case, the Maypole model is more clearly seen as a layer +that sits on top of the domain model, mediating access to it from the web UI, +via the controller. + +This conceptualisation helps developers maintain a separation between the +Maypole model classes (presentation model), and the domain model. Without this +distinction, developers may add domain-specific code to the Maypole model +classes. To a certain extent, in simple applications, this is fine. But if you +find yourself adding lots of non-Exported methods to your Maypole model classes, +and these methods are not there to directly support Exported methods, consider +whether you could separate out the domain model into a separate hierarchy of +classes - see L. + +Otherwise, the 'model' classes may develop into two quite uncoupled code bases, +but which co-exist in the same files. They will interact through a relatively +small number of methods. These methods should in fact become the public API of +the domain model, which should be moved to a separate class hierarchy. At some +point, the convenience of dropping new methods into the 'shared' classes will be +outweighed by the heuristic advantage of separating different layers into +separate class hierarchies. + +=back + +=head3 Presentation Model + +This pattern more accurately describes the role of the Maypole model. +Martin Fowler describes I in L +and L. + +The user sends an event (e.g. an HTTP request) to the Controller. The Controller +translates the request into a method call on the Presentation Model. The +Presentation Model interacts with the underlying Domain Model, and stores the +results in a bunch of variables, which I +(that's why it's a Presentation Model, not a Domain Model). The View then +queries the Presentation Model to retrieve these new values. In Maypole, this is +the role of the C method on L, which transmits the +new values to the templates. + +=head1 AUTHOR + +David Baird, C<< >> + +=head1 COPYRIGHT & LICENSE + +Copyright 2005 David Baird, All Rights Reserved. + +This text is free documentation; you can redistribute it and/or modify it +under the same terms as the Perl documentation itself. + +=cut diff --git a/lib/Maypole/Model/CDBI/AsForm.pm b/lib/Maypole/Model/CDBI/AsForm.pm index d76ecb4..8a7f06c 100644 --- a/lib/Maypole/Model/CDBI/AsForm.pm +++ b/lib/Maypole/Model/CDBI/AsForm.pm @@ -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 ); } diff --git a/lib/Maypole/View/TT.pm b/lib/Maypole/View/TT.pm index c966a7d..433309e 100644 --- a/lib/Maypole/View/TT.pm +++ b/lib/Maypole/View/TT.pm @@ -8,7 +8,7 @@ use Template::Constants qw( :all ); our $error_template; { local $/; $error_template = ; } -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__ Maypole error page -

Maypole application error

+

Maypole Application Error

This application living at [%request.config.uri_base%], [%request.config.application_name || "which is unnamed" %], has @@ -427,31 +425,31 @@ the path "[% request.path %]". The error text returned was:

Request details

- +
[% FOR attribute = ["model_class", "table", "template", "path", "content_type", "document_encoding", "action", "args", "objects"] %] - [% END %] [% FOREACH param IN request.params %] - + [% END %]
[% attribute %] [% +
[% attribute %] [% request.$attribute.list.join(" , ") %]
CGI Parameters
[% param.key %] [% param.value %]
[% param.key %] [% param.value %]

Website / Template Paths

- - + +
Base URI [% request.config.uri_base %]
Paths [% paths %]
Base URI [% request.config.uri_base %]
Paths [% paths %]

Application configuration

- - - - + + + +
Model [% request.config.model %]
View [% request.config.view %]
Classes [% request.config.classes.list.join(" , ") %]
Tables [% request.config.display_tables.list.join(" , ") %]
Model [% request.config.model %]
View [% request.config.view %]
Classes [% request.config.classes.list.join(" , ") %]
Tables [% request.config.display_tables.list.join(" , ") %]
diff --git a/t/crud.t b/t/crud.t new file mode 100755 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¬es=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¬es=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¬es=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=¬es=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=¬es=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¬es=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 index 0000000..ceee5d2 --- /dev/null +++ b/t/templates/custom/edit @@ -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 index 0000000..385eb99 --- /dev/null +++ b/wishlist.txt @@ -0,0 +1,56 @@ +2.12/3.0 wishlist +================= + +Not for inclusion in the MANIFEST. + +2.11 +==== +Fix factory roots, document and explain behaviour +send_output() should return a status code +Move template test out of process() and into handler_guts() - maybe +Fix bug 14570 - returning error codes breaks CGI::Maypole +Write Maypole::Manual::Exceptions +Test and refactor external_redirect() + +Fix Mp::P::USC. + +2.12 +==== +Maypole::instance() +Better plugin architecture, for models and bits of models. +Investigate problems reported with adopt() - rt 15598 +Re-implement Maypole::Cache as Maypole::Plugin::Cache, probably using + start_request_hook, and not overriding handler_guts() +Handle repeat form submissions. +Implement internal_redirect(). +Build a more sophisticated app for testing. +Move class_of() to the controller - need to do this to support multiple models + - maybe +Multiple model support - URLs like /$base/$model/$table/$action/$id. +Refactor M-P-USC and M-P-Session into M-P-User, M-P-Session, and M-P-Cookie + + +3.0 +==== +Encapsulate all request data in HTTP::Request object, and all response data +in HTTP::Response object + +Look at HTTP::Body + +Easier file uploads - look at incorporating Mp::P::Upload + +Add email handling - like Rails - via model plugins. + +An e-commerce model plugin would be nice - or proof of concept - maybe look +at Handel. + +Add validation layer(s), or just an API + +killer apps: SVN model; mitiki; Pet Shop; adventure builder + +Multiple views - HTML, text-only, PDF, SOAP, XML - use request data to switch +to an alternate view_object - switch via a factory method. + +Maybe rename the model to PModel (Presentation Model)? + +Pseudo-continuations...