.\" Automatically generated by Pod::Man v1.37, Pod::Parser v1.32
.\"
.\" Standard preamble:
.\" ========================================================================
.de Sh \" Subsection heading
.br
.if t .Sp
.ne 5
.PP
\fB\\$1\fR
.PP
..
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" Set up some character translations and predefined strings. \*(-- will
.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
.\" double quote, and \*(R" will give a right double quote. | will give a
.\" real vertical bar. \*(C+ will give a nicer C++. Capital omega is used to
.\" do unbreakable dashes and therefore won't be available. \*(C` and \*(C'
.\" expand to `' in nroff, nothing in troff, for use with C<>.
.tr \(*W-|\(bv\*(Tr
.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
.ie n \{\
. ds -- \(*W-
. ds PI pi
. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
. ds L" ""
. ds R" ""
. ds C` ""
. ds C' ""
'br\}
.el\{\
. ds -- \|\(em\|
. ds PI \(*p
. ds L" ``
. ds R" ''
'br\}
.\"
.\" If the F register is turned on, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.Sh), items (.Ip), and index
.\" entries marked with X<> in POD. Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.if \nF \{\
. de IX
. tm Index:\\$1\t\\n%\t"\\$2"
..
. nr % 0
. rr F
.\}
.\"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.hy 0
.if n .na
.\"
.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
.\" Fear. Run. Save yourself. No user-serviceable parts.
. \" fudge factors for nroff and troff
.if n \{\
. ds #H 0
. ds #V .8m
. ds #F .3m
. ds #[ \f1
. ds #] \fP
.\}
.if t \{\
. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
. ds #V .6m
. ds #F 0
. ds #[ \&
. ds #] \&
.\}
. \" simple accents for nroff and troff
.if n \{\
. ds ' \&
. ds ` \&
. ds ^ \&
. ds , \&
. ds ~ ~
. ds /
.\}
.if t \{\
. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
.\}
. \" troff and (daisy-wheel) nroff accents
.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
.ds ae a\h'-(\w'a'u*4/10)'e
.ds Ae A\h'-(\w'A'u*4/10)'E
. \" corrections for vroff
.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
. \" for low resolution devices (crt and lpr)
.if \n(.H>23 .if \n(.V>19 \
\{\
. ds : e
. ds 8 ss
. ds o a
. ds d- d\h'-1'\(ga
. ds D- D\h'-1'\(hy
. ds th \o'bp'
. ds Th \o'LP'
. ds ae ae
. ds Ae AE
.\}
.rm #[ #] #H #V #F C
.\" ========================================================================
.\"
.IX Title "Gantry::Docs::Tutorial 3"
.TH Gantry::Docs::Tutorial 3 "2008-01-18" "perl v5.8.8" "User Contributed Perl Documentation"
.SH "Name"
.IX Header "Name"
Gantry::Docs::Tutorial \- The Gantry Tutorial
.SH "Introduction"
.IX Header "Introduction"
Gantry is a mature web framework, released in late 2005 onto
an unsuspecting world. For more information on the framework, its
features and history, see Gantry::Docs::About.
.PP
Here we will explore the basic workings of Gantry by constructing a
very simple application. Don't let the simplicity of this example
fool you \*(-- this framework has extreme flexibility in delivering
applications with web and scripted components. The example in
this document is only to get you started.
.PP
This document begins by describing a simple one-table management application.
It walks through the process of building the application. Then,
it shows a tool \*(-- called Bigtop \*(-- which can be used to build the application
from a relatively small configuration file. Finally, it shows how
to add another table and regenerate the app via Bigtop.
.SH "Sample App Description"
.IX Header "Sample App Description"
I'm worried about my wife's address book. There is only one copy and
without it, we would lose track of many of our friends and some of
our relatives. I want to put my wife's address book into a database,
but allow her to use it through a web interface.
.PP
Here are the things that Lisa tracks:
.IP "name" 4
.IX Item "name"
the name of a person or nuclear family
.IP "address" 4
.IX Item "address"
postal address, so we can send toys to the kids etc.
.IP "phone" 4
.IX Item "phone"
one or more numbers (email addresses are in the margin, but that
will have to wait for version 1.1)
.PP
This leads to one table:
.PP
.Vb 10
\& CREATE SEQUENCE address_seq;
\& CREATE TABLE address (
\& id int4 PRIMARY KEY DEFAULT NEXTVAL( 'address_seq' ),
\& name varchar,
\& street varchar,
\& city varchar,
\& state varchar,
\& zip varchar,
\& phone varchar
\& );
.Ve
.PP
The application needs to show all the addresses in a single table,
allow for adding new ones and editing or deleting existing ones. To make
it easier to accomodate Lisa's international family and friends, we won't
do any validation of the data \*(-- except to make sure she enters
some. For example, this will allow her to wedge several numbers
(home, cell, etc.) into the phone field.
.SH "Hand-writing the Sample App"
.IX Header "Hand-writing the Sample App"
After creating a directory called Apps\-Address, I made a lib subdirectory
for the code. (You could use h2xs to help with the initial steps. Or,
you could use Bigtop, as I did, see \*(L"Using Bigtop\*(R" below.)
.PP
There are four key modules in this application:
.IP "Apps::Address" 4
.IX Item "Apps::Address"
the base module
.IP "Apps::Address::Model" 4
.IX Item "Apps::Address::Model"
the DBIx::Class schema which controls the model. In a bigger app, it would
control all models.
.IP "Apps::Address::Address" 4
.IX Item "Apps::Address::Address"
the controller for the address table
.IP "Apps::Address::Model::address" 4
.IX Item "Apps::Address::Model::address"
the object relational mapper class, which inherits from DBIx::Class
.PP
We'll walk through each of these in a subsection, showing the code
with commentary interspersed. After our tour I'll show the modules
again without the commentary, so you can see how they look when whole,
in \*(L"Complete Code Listings\*(R".
.Sh "Apps::Address"
.IX Subsection "Apps::Address"
The job of the base module is to provide a home for shared code and
a common place to hold site navigation links.
.PP
Here is our module (without its documentation but with commentary
interspersed):
.PP
.Vb 1
\& package Apps::Address;
.Ve
.PP
.Vb 1
\& use strict;
.Ve
.PP
.Vb 1
\& our $VERSION = '0.01';
.Ve
.PP
It begins like any other module...
.PP
.Vb 1
\& use Gantry qw{ -TemplateEngine=TT };
.Ve
.PP
.Vb 1
\& our @ISA = ( 'Gantry' );
.Ve
.PP
\&...but, it uses Gantry with a template engine (Template Toolkit).
.PP
Note that somewhere you need to use Gantry with the \-Engine option.
I'll do that in the stand alone server script. You could also do it
in a \s-1CGI\s0 dispatching script or in httpd.conf for mod_perl deployments.
.PP
.Vb 1
\& use Apps::Address::Address;
.Ve
.PP
For the convenience of future readers, the base module has an explicit use
for the single controller Apps::Address::Address (which we will see below).
This is purely for documentation.
.PP
.Vb 5
\& #-----------------------------------------------------------------
\& # $self->site_links( )
\& #-----------------------------------------------------------------
\& sub site_links {
\& my ( $self ) = @_;
.Ve
.PP
.Vb 4
\& return [
\& { link => $self->app_rootp() . 'address', label => 'Address' },
\& ];
\& } # END site_links
.Ve
.PP
The \f(CW\*(C`site_links\*(C'\fR method provides a common place for all (or most) app
pages to look for site navigation links. The only link here takes users
to the default page in the address table's controller.
.PP
.Vb 5
\& #-----------------------------------------------------------------
\& # $self->do_main( )
\& #-----------------------------------------------------------------
\& sub do_main {
\& my ( $self ) = @_;
.Ve
.PP
.Vb 2
\& $self->stash->view->template( 'main.tt' );
\& $self->stash->view->title( 'Main Listing' );
.Ve
.PP
.Vb 6
\& $self->stash->view->data( {
\& pages => [
\& { link => 'address', label => 'Address' },
\& ],
\& } );
\& } # END do_main
.Ve
.PP
.Vb 1
\& 1;
.Ve
.PP
\&\f(CW\*(C`do_main\*(C'\fR is one (of two) default methods Gantry dispatches to. If you
hit the controller on its base \s-1URL\s0, Gantry will try to dispatch to \f(CW\*(C`do_main\*(C'\fR.
If you don't have one of those, it will fall back to \f(CW\*(C`do_default\*(C'\fR (which
we use sparingly, usually to accept \s-1URL\s0 parameters without having to use
the query string).
.PP
This main method merely displays the site links in \f(CW\*(C`main.tt\*(C'\fR which ships
with Gantry. It shows a bulleted list of all navigation links.
.PP
There is one other commonly useful method in the base controller: \f(CW\*(C`init\*(C'\fR.
Gantry.pm handles a set of standard configuration parameters.
If you need to handle others, implement an init sub and accessors for them.
First, dispatch to \s-1SUPER\s0, so it can handle the standard parameters. Then
handle your app specific ones. For example, an init to catch an \s-1SMTP\s0 host
name might look like this:
.PP
.Vb 2
\& sub init {
\& my ( $self ) = @_;
.Ve
.PP
.Vb 2
\& # process SUPER's init code
\& $self->SUPER::init( );
.Ve
.PP
.Vb 2
\& $self->smtp_host( $self->fish_conf( 'smtp_host' ) || '' );
\& } # END init
.Ve
.PP
Using fish_conf has two advantages over a more direct approach like this:
.PP
.Vb 1
\& $self->smtp_host( $self->r->dir_config( 'smtp_host' ) || '' );
.Ve
.PP
First, using dir_config ties you to mod_perl. Second, directly fishing
from the request object prevents a more general solution, like
Gantry::Conf (see Gantry::Conf::Tutorial for how to use that).
.Sh "Apps::Address::Model"
.IX Subsection "Apps::Address::Model"
The Model might better be called Apps::Address::Schema, since it inherits from
DBIx::Class::Schema. But we call it the model. It has two purposes.
First, is to load the actual model classes. Second, it sets the \s-1DBI\s0
options for the database connections.
.PP
.Vb 2
\& package Apps::Address::Model;
\& use strict; use warnings;
.Ve
.PP
.Vb 1
\& use base 'DBIx::Class::Schema';
.Ve
.PP
.Vb 3
\& __PACKAGE__->load_classes( qw/
\& address
\& / );
.Ve
.PP
.Vb 3
\& sub get_db_options {
\& return { AutoCommit => 1 };
\& }
.Ve
.PP
.Vb 1
\& 1;
.Ve
.PP
See \f(CW\*(C`DBIx::Class::Schema\*(C'\fR for a discussion of \f(CW\*(C`load_classes\*(C'\fR and the
other things you can set up in your schema.
.PP
This schema only loads \f(CW\*(C`address\*(C'\fR, since that is our only table. Even
for complex apps, there is rarely any more complexity to this module.
But, it will have more classes to load.
.Sh "Apps::Address::Address"
.IX Subsection "Apps::Address::Address"
This is the workhorse for this application. It manages the \s-1CRUD\s0 (create,
retrieve, update, and delete) for address book rows. Again, I'll include
it a piece at a time with running commentary.
.PP
.Vb 1
\& package Apps::Address::Address;
.Ve
.PP
.Vb 1
\& use strict;
.Ve
.PP
.Vb 1
\& use base 'Apps::Address';
.Ve
.PP
It begins like any subclass. Note that it is a subclass of Apps::Address
which is itself a subclass of Gantry. The only \f(CW\*(C`handler\*(C'\fR sub is in Gantry.pm
(unless you count user authentication, but that's way ahead of our little
story about the vulnerable address book with the flowers on the cover).
.PP
.Vb 1
\& use Gantry::Plugins::DBIxClassConn qw( get_schema );
.Ve
.PP
.Vb 2
\& use Apps::Address::Model;
\& use Apps::Address::Model::address qw( $ADDRESS );
.Ve
.PP
To ease DBIx::Class use, Gantry provides a
plugin: \f(CW\*(C`Gantry::Plugins::DBIxClassConn\*(C'\fR. That plugin exports \f(CW\*(C`get_schema\*(C'\fR,
which controllers need to read their database. We'll see how to use it below.
.PP
In addition to loading the plugin, our controller also needs to use the
base model (a.k.a., the schema) and which ever models it actually needs.
.PP
Each table has a model in the Model namespace with the same name as the
table (note the case \*(-- this exactly matches the sql shown in the previous
section). The model exports an alias to its full name as \f(CW$ADDRESS\fR to
save us some typing when we use it. It uses uc on the table's name to
make the alias more visible.
.PP
.Vb 1
\& use Gantry::Plugins::AutoCRUD;
.Ve
.PP
This is the real key to avoiding work. AutoCRUD handles create, update
and delete (we'll see retrieval in a minute). This module is more of a
mixin than a plugin. It exports five methods to us: \f(CW\*(C`do_add\*(C'\fR, \f(CW\*(C`do_edit\*(C'\fR,
\&\f(CW\*(C`do_delete\*(C'\fR, \f(CW\*(C`form_name\*(C'\fR, and \f(CW\*(C`write_file\*(C'\fR. The \f(CW\*(C`form_name\*(C'\fR is just
the name of the template to use for add/edit input. If you don't want
the standard \f(CW\*(C`form.tt\*(C'\fR, that comes with Gantry, don't import that method.
Instead, implement that method so it returns the name of your template file.
.PP
The \f(CW\*(C`write_file\*(C'\fR method handles file uploads, which we don't need for
this app.
.PP
In Gantry, the handler calls methods named do_* where the star is replaced
with a string from the url. So the \s-1URL\s0 for adding an entry to the
address book would be something like:
.PP
.Vb 1
\& http://somehost.example.com/address/add
.Ve
.PP
where somehost.example.com is our host (or virtual host) and /address/add
is the requested page. address is a Location in our apache conf and
add becomes do_add, the name of the method to execute. Using the do_ prefix
has two advantages. First, since \s-1URL\s0 pieces are used directly, it keeps
people from running non-handlers by clever url spoofing. Second, and
for our company more importantly, it makes it clear which methods
are accessible, and which are not. This aids us when we are modifying
a controller. If it starts with do_ it can be reached via url.
.PP
So, we are mixing in \f(CW\*(C`do_add\*(C'\fR, \f(CW\*(C`do_edit\*(C'\fR, and \f(CW\*(C`do_delete\*(C'\fR. We need
to implement a few methods to complete our controller.
.PP
We need a small sub so the DBIx::Class plugin can find our schema:
.PP
.Vb 1
\& sub schema_base_class { return 'Apps::Address::Model'; }
.Ve
.PP
Now we are coming to the real code. The default action for a Location
in Gantry is do_main. We usually use it to display a table with one
summary row for each database row like this. It looks like this:
.PP
.Vb 5
\& #-----------------------------------------------------------------
\& # $self->do_main( )
\& #-----------------------------------------------------------------
\& sub do_main {
\& my ( $self ) = @_;
.Ve
.PP
.Vb 2
\& $self->stash->view->template( 'results.tt' );
\& $self->stash->view->title( 'Address' );
.Ve
.PP
The \f(CW\*(C`do_main\*(C'\fR controller uses the \f(CW\*(C`results.tt\*(C'\fR default main listing
template which ships with Gantry. If you change templates, you'll probably
need to substantially modify the rest of \f(CW\*(C`do_main\*(C'\fR.
.PP
.Vb 5
\& my $real_location = $self->location() || '';
\& if ( $real_location ) {
\& $real_location =~ s{/+$}{};
\& $real_location .= '/';
\& }
.Ve
.PP
Some care is required to avoid missing or doubled slashes when forming URLs.
But, that's nothing a little string work can't address. With a clean
address in place we are ready to build the output.
.PP
.Vb 12
\& my $retval = {
\& headings => [
\& 'Name',
\& 'Street',
\& ],
\& header_options => [
\& {
\& text => 'Add',
\& link => $real_location . "add",
\& },
\& ],
\& };
.Ve
.PP
The template always receives a hash reference. I frequently call mine
\&\f(CW$retval\fR, short for return value. For \f(CW\*(C`results.tt\*(C'\fR, the hash describes
the main listing table. The are two parts to that: the heading row and
the body rows. There is one heading row for the table. This one has labels:
\&'Name' and 'Street.' There is one option the user can invoke for the whole
table: Add. Clicking that will lead to the same \s-1URL\s0 with 'add' appended.
That \s-1URL\s0 will be dispatched to the \f(CW\*(C`do_add\*(C'\fR we mixed in from the AutoCRUD
plugin.
.PP
Now we need the data for the main listing. For simplicity, I'll get all the
data. It is not hard to get pages of data, but I'll leave that for later
documents (look for \f(CW\*(C`rows\*(C'\fR or \f(CW\*(C`paged_conf\*(C'\fR in bigtop or tentmaker's docs).
.PP
.Vb 2
\& my $schema = $self->get_schema();
\& my @rows = $ADDRESS->get_listing( { schema => $schema } );
.Ve
.PP
First, I asked for the DBIx::Class schema, but calling the \f(CW\*(C`get_schema\*(C'\fR
accessor mixed in for us by the \f(CW\*(C`DBIxClassConn\*(C'\fR plugin. Next, I called
\&\f(CW\*(C`get_listing\*(C'\fR on the address model, through its alias. This sugar method
returns the rows for our main listing. Note that it expects named arguments
in a hash reference and \f(CW\*(C`schema\*(C'\fR is required.
.PP
Now, it's a fairly simple matter to loop over each database row making
a table row in the template data.
.PP
.Vb 21
\& foreach my $row ( @rows ) {
\& my $id = $row->id;
\& push(
\& @{ $retval->{rows} }, {
\& data => [
\& $row->name,
\& $row->street,
\& ],
\& options => [
\& {
\& text => 'Edit',
\& link => $real_location . "edit/$id",
\& },
\& {
\& text => 'Delete',
\& link => $real_location . "delete/$id",
\& },
\& ],
\& }
\& );
\& }
.Ve
.PP
First, I fished the row id out of the DBIx::Class object, and stored it
in a scalar. This allows direct interpolation into a couple of \s-1URL\s0 link
strings. Then I pushed the data and the row options into the \f(CW\*(C`rows\*(C'\fR
key of the return value hash. The data are just the family's name and
street address. You could add any other columns from the underlying
table. For instance, to add phone, add:
.PP
.Vb 1
\& $row->phone,
.Ve
.PP
to the data list after \f(CW\*(C`$row\-\*(C'\fRstreet>. The order and contents of the
data are up to you.
.PP
Just as the header row has an Add option, each data row has an Edit
and a Delete option. Note that their URLs include the row id to work on.
The only thing I have left to do in \f(CW\*(C`do_main\*(C'\fR is to set the return value data
in place:
.PP
.Vb 2
\& $self->stash->view->data( $retval );
\& } # END do_main
.Ve
.PP
The only other large piece is the form, in which users enter new addresses
or edit existing ones. AutoCRUD calls this method for you when the users
visits do_add and do_edit pages. Call this method form. If an edit triggered
the call, it will pass in the row as it stands in the database.
.PP
The following code produces this on the screen:
.PP
.Vb 4
\& #-----------------------------------------------------------------
\& # $self->form( $row )
\& #-----------------------------------------------------------------
\& sub form {
.Ve
.PP
.Vb 4
\& return {
\& row => $row,
\& legend => $self->path_info =~ /edit/i ? 'Edit' : 'Add',
\& fields => [
.Ve
.PP
The default template is called \f(CW\*(C`form.tt\*(C'\fR. Among other things, it expects
the return value hash to contain \f(CW\*(C`row\*(C'\fR (if editing), \f(CW\*(C`legend\*(C'\fR (legend
of form's fieldset), and \f(CW\*(C`fields\*(C'\fR (what the user will see and enter or edit).
If the \f(CW\*(C`row\*(C'\fR is supplied, its values are used for initial form population.
The \f(CW\*(C`legend\*(C'\fR is set based on the \f(CW\*(C`path_info\*(C'\fR which contains part of the
\&\s-1URL\s0. If that \s-1URL\s0 fragment includes 'edit,' the legend is 'Edit.' Otherwise,
it is 'Add.'
.PP
The \f(CW\*(C`fields\*(C'\fR are an array of the entry elements the user will see. The
order of the array controls the on screen order. Each field is a little
hash. While there are other keys, the four most common are used over and
over, not just in this example.
.PP
.Vb 6
\& {
\& name => 'name',
\& optional => 0,
\& label => 'Name',
\& type => 'text',
\& },
.Ve
.PP
The \f(CW\*(C`name\*(C'\fR must be the name of the column in the database and will also
be used as the name of the html form element.
.PP
If \f(CW\*(C`optional\*(C'\fR is true, the field is optional. Otherwise, it is required.
I could have omitted optional from the Name hash, since required is the
default.
.PP
The \f(CW\*(C`label\*(C'\fR is displayed in the left hand column of the form input table.
.PP
The \f(CW\*(C`type\*(C'\fR is the \s-1HTML\s0 form element type. See \f(CW\*(C`form.tt\*(C'\fR in Gantry's
templates for a complete list of types is understands. That will also
explain how to include other field hash keys to specify things like
pull down options.
.PP
The other \f(CW\*(C`fields\*(C'\fR hashes are all of the same form. Only the field names
and labels change. Here is one example:
.PP
.Vb 10
\& {
\& name => 'city',
\& optional => 1,
\& label => 'City',
\& type => 'text',
\& },
\& #...
\& ],
\& };
\& } # END form
.Ve
.PP
Finally, there are some small subs which return strings used by the
AutoCRUD plugin at various points.
.PP
.Vb 6
\& #-----------------------------------------------------------------
\& # get_model_name( )
\& #-----------------------------------------------------------------
\& sub get_model_name {
\& return $ADDRESS;
\& }
.Ve
.PP
Gantry::Plugins::AutoCRUD uses get_model_name to find out which model
class to use for create, update, delete, and lookups.
.PP
.Vb 6
\& #-----------------------------------------------------------------
\& # get_orm_helper( )
\& #-----------------------------------------------------------------
\& sub get_orm_helper {
\& return 'Gantry::Plugins::AutoCRUDHelper::DBIxClass';
\& }
.Ve
.PP
For historical reasons, the AutoCRUD plugin defaults to using Class::DBI.
We no longer use that. So, we have to provide \f(CW\*(C`get_orm_helper\*(C'\fR to
identify our \s-1CRUD\s0 helper.
.PP
.Vb 6
\& #-----------------------------------------------------------------
\& # text_descr( )
\& #-----------------------------------------------------------------
\& sub text_descr {
\& return 'address';
\& }
.Ve
.PP
Gantry::Plugins::AutoCRUD uses text_descr to fill in the blank in things
like:
.PP
.Vb 1
\& Delete _____?
.Ve
.PP
That's the whole controller (save the #... where the other fields go
\&\*(-- see below for \*(L"Complete Code Listing\*(R").
.Sh "Apps::Address::Model::address"
.IX Subsection "Apps::Address::Model::address"
To separate sql from the controller (and view) we use Gantry with an
Object-Relational Mapper (\s-1ORM\s0). For this example I will show
DBIx::Class, since it the one we've settled on. You could also
use Class::DBI or Gantry's native models, but I won't show you how.
.PP
Gantry provides its own base class to add to \f(CW\*(C`DBIx::Class\*(C'\fR it is
Gantry::Utils::DBIxClass. Each model subclasses it and represents one table
in the database. These classes are standard \f(CW\*(C`DBIx::Class\*(C'\fR subclasses. Here
is the own for my address table:
.PP
.Vb 2
\& package Apps::Address::Model::address;
\& use strict; use warnings;
.Ve
.PP
.Vb 1
\& use base 'Gantry::Utils::DBIxClass', 'Exporter';
.Ve
.PP
.Vb 1
\& our $ADDRESS = 'Apps::Address::Model::address';
.Ve
.PP
.Vb 1
\& our @EXPORT_OK = ( '$ADDRESS' );
.Ve
.PP
Note that we export the alias for controllers to use when referring to
the model class. This mitigates the length of the name. Gantry does not
require you to do this. If you prefer to type the name, feel free.
.PP
.Vb 15
\& __PACKAGE__->load_components( qw/ PK::Auto Core / );
\& __PACKAGE__->table( 'address' );
\& __PACKAGE__->add_columns( qw/
\& id
\& name
\& street
\& created
\& modified
\& city
\& state
\& zip
\& phone
\& / );
\& __PACKAGE__->set_primary_key( 'id' );
\& __PACKAGE__->base_model( 'Apps::Address::Model' );
.Ve
.PP
All of these calls are common when using \f(CW\*(C`DBIx::Class\*(C'\fR, except \f(CW\*(C`base_model\*(C'\fR.
Gantry uses it to hide the connection inside \f(CW\*(C`Gantry::Plugins::DBIxClassConn\*(C'\fR.
.PP
Various parts of Gantry use other methods I should define here. They are
all simple.
.PP
.Vb 2
\& sub foreign_display {
\& my $self = shift;
.Ve
.PP
.Vb 1
\& my $name = $self->name() || '';
.Ve
.PP
.Vb 2
\& return "$name";
\& }
.Ve
.PP
The \f(CW\*(C`foreign_display\*(C'\fR controls the default sort order of \f(CW\*(C`get_listing\*(C'\fR
which I called in \f(CW\*(C`do_main\*(C'\fR of the controller for the address table. It
also controls how rows from the table will be summarized when other tables
refer to this one via a foreign key.
.PP
.Vb 3
\& sub get_foreign_display_fields {
\& return [ qw( name ) ];
\& }
.Ve
.PP
This tells anyone who is interested the names in the foreign display string
in their order of appearance there. \f(CW\*(C`get_foreign_display_fields\*(C'\fR is what
actually controls \f(CW\*(C`get_listing\*(C'\fR sort order.
.PP
.Vb 4
\& sub get_foreign_tables {
\& return qw(
\& );
\& }
.Ve
.PP
This returns a list of table names for which this table has foreign keys.
.PP
.Vb 3
\& sub table_name {
\& return 'address';
\& }
.Ve
.PP
Finally, this returns the name of the table. This becomes important if you
are using Postgres schemas which preface the table name with its schema
name and a dot (for instance: my_schema.my_table). This table name will
have the dot, even though the dot is converted to an underscore for the
package name and in other places where Perl objects to dots.
.PP
See the perldoc for \f(CW\*(C`DBIx::Class\*(C'\fR and \f(CW\*(C`DBIx::Class::ResultSet\*(C'\fR for more
details.
.Sh "Complete Code Listings"
.IX Subsection "Complete Code Listings"
Note that all \s-1POD\s0 sections have been omitted for brevity.
.PP
\&\s-1SQL\s0 for database creation
.PP
.Vb 10
\& CREATE SEQUENCE address_seq;
\& CREATE TABLE address (
\& id int4 PRIMARY KEY DEFAULT NEXTVAL( 'address_seq' ),
\& name varchar,
\& street varchar,
\& city varchar,
\& state varchar,
\& zip varchar,
\& phone varchar
\& );
.Ve
.PP
Apps::Address
.PP
.Vb 1
\& package Apps::Address;
.Ve
.PP
.Vb 1
\& use strict;
.Ve
.PP
.Vb 1
\& our $VERSION = '0.01';
.Ve
.PP
.Vb 1
\& use Apps::Address::Address;
.Ve
.PP
.Vb 1
\& use Gantry qw{ -TemplateEngine=TT };
.Ve
.PP
.Vb 1
\& our @ISA = qw( Gantry );
.Ve
.PP
.Vb 5
\& #-----------------------------------------------------------------
\& # $self->do_main( )
\& #-----------------------------------------------------------------
\& sub do_main {
\& my ( $self ) = @_;
.Ve
.PP
.Vb 2
\& $self->stash->view->template( 'main.tt' );
\& $self->stash->view->title( 'Main Listing' );
.Ve
.PP
.Vb 6
\& $self->stash->view->data( {
\& pages => [
\& { link => 'address', label => 'Address' },
\& ],
\& } );
\& } # END do_main
.Ve
.PP
.Vb 5
\& #-----------------------------------------------------------------
\& # $self->site_links( )
\& #-----------------------------------------------------------------
\& sub site_links {
\& my ( $self ) = @_;
.Ve
.PP
.Vb 4
\& return [
\& { link => $self->app_rootp() . 'address', label => 'Address' },
\& ];
\& } # END site_links
.Ve
.PP
.Vb 1
\& 1;
.Ve
.PP
Apps::Address::Model
.PP
.Vb 2
\& package Apps::Address::Model;
\& use strict; use warnings;
.Ve
.PP
.Vb 1
\& use base 'DBIx::Class::Schema';
.Ve
.PP
.Vb 3
\& __PACKAGE__->load_classes( qw/
\& address
\& / );
.Ve
.PP
.Vb 3
\& sub get_db_options {
\& return { AutoCommit => 1 };
\& }
.Ve
.PP
.Vb 1
\& 1;
.Ve
.PP
Apps::Address::Address
.PP
.Vb 1
\& package Apps::Address::Address;
.Ve
.PP
.Vb 1
\& use strict;
.Ve
.PP
.Vb 1
\& use base 'Apps::Address';
.Ve
.PP
.Vb 1
\& use Gantry::Plugins::DBIxClassConn qw( get_schema );
.Ve
.PP
.Vb 2
\& use Apps::Address::Model;
\& use Apps::Address::Model::address qw( $ADDRESS );
.Ve
.PP
.Vb 7
\& use Gantry::Plugins::AutoCRUD qw(
\& do_add
\& do_edit
\& do_delete
\& form_name
\& write_file
\& );
.Ve
.PP
.Vb 1
\& sub schema_base_class { return 'Apps::Address::Model'; }
.Ve
.PP
.Vb 5
\& #-----------------------------------------------------------------
\& # $self->do_main( )
\& #-----------------------------------------------------------------
\& sub do_main {
\& my ( $self ) = @_;
.Ve
.PP
.Vb 2
\& $self->stash->view->template( 'results.tt' );
\& $self->stash->view->title( 'Address' );
.Ve
.PP
.Vb 5
\& my $real_location = $self->location() || '';
\& if ( $real_location ) {
\& $real_location =~ s{/+$}{};
\& $real_location .= '/';
\& }
.Ve
.PP
.Vb 12
\& my $retval = {
\& headings => [
\& 'Name',
\& 'Street',
\& ],
\& header_options => [
\& {
\& text => 'Add',
\& link => $real_location . "add",
\& },
\& ],
\& };
.Ve
.PP
.Vb 2
\& my $schema = $self->get_schema();
\& my @rows = $ADDRESS->get_listing( { schema => $schema } );
.Ve
.PP
.Vb 21
\& foreach my $row ( @rows ) {
\& my $id = $row->id;
\& push(
\& @{ $retval->{rows} }, {
\& data => [
\& $row->name,
\& $row->street,
\& ],
\& options => [
\& {
\& text => 'Edit',
\& link => $real_location . "edit/$id",
\& },
\& {
\& text => 'Delete',
\& link => $real_location . "delete/$id",
\& },
\& ],
\& }
\& );
\& }
.Ve
.PP
.Vb 2
\& $self->stash->view->data( $retval );
\& } # END do_main
.Ve
.PP
.Vb 5
\& #-----------------------------------------------------------------
\& # $self->form( $row )
\& #-----------------------------------------------------------------
\& sub form {
\& my ( $self, $row ) = @_;
.Ve
.PP
.Vb 43
\& return {
\& row => $row,
\& legend => $self->path_info =~ /edit/i ? 'Edit' : 'Add',
\& fields => [
\& {
\& name => 'name',
\& optional => 0,
\& label => 'Name',
\& type => 'text',
\& },
\& {
\& name => 'street',
\& optional => 1,
\& label => 'Street',
\& type => 'text',
\& },
\& {
\& name => 'city',
\& optional => 1,
\& label => 'City',
\& type => 'text',
\& },
\& {
\& name => 'state',
\& optional => 1,
\& label => 'State',
\& type => 'text',
\& },
\& {
\& name => 'zip',
\& optional => 1,
\& label => 'Zip',
\& type => 'text',
\& },
\& {
\& name => 'phone',
\& optional => 1,
\& label => 'Phone',
\& type => 'text',
\& },
\& ],
\& };
\& } # END form
.Ve
.PP
.Vb 6
\& #-----------------------------------------------------------------
\& # get_model_name( )
\& #-----------------------------------------------------------------
\& sub get_model_name {
\& return $ADDRESS;
\& }
.Ve
.PP
.Vb 6
\& #-----------------------------------------------------------------
\& # get_orm_helper( )
\& #-----------------------------------------------------------------
\& sub get_orm_helper {
\& return 'Gantry::Plugins::AutoCRUDHelper::DBIxClass';
\& }
.Ve
.PP
.Vb 6
\& #-----------------------------------------------------------------
\& # text_descr( )
\& #-----------------------------------------------------------------
\& sub text_descr {
\& return 'address';
\& }
.Ve
.PP
.Vb 1
\& 1;
.Ve
.PP
Apps::Address::Model::address
.PP
.Vb 2
\& package Apps::Address::Model::address;
\& use strict; use warnings;
.Ve
.PP
.Vb 1
\& use base 'Gantry::Utils::DBIxClass', 'Exporter';
.Ve
.PP
.Vb 1
\& our $ADDRESS = 'Apps::Address::Model::address';
.Ve
.PP
.Vb 1
\& our @EXPORT_OK = ( '$ADDRESS' );
.Ve
.PP
.Vb 15
\& __PACKAGE__->load_components( qw/ PK::Auto Core / );
\& __PACKAGE__->table( 'address' );
\& __PACKAGE__->add_columns( qw/
\& id
\& name
\& street
\& created
\& modified
\& city
\& state
\& zip
\& phone
\& / );
\& __PACKAGE__->set_primary_key( 'id' );
\& __PACKAGE__->base_model( 'Apps::Address::Model' );
.Ve
.PP
.Vb 3
\& sub get_foreign_display_fields {
\& return [ qw( name ) ];
\& }
.Ve
.PP
.Vb 4
\& sub get_foreign_tables {
\& return qw(
\& );
\& }
.Ve
.PP
.Vb 2
\& sub foreign_display {
\& my $self = shift;
.Ve
.PP
.Vb 1
\& my $name = $self->name() || '';
.Ve
.PP
.Vb 2
\& return "$name";
\& }
.Ve
.PP
.Vb 3
\& sub table_name {
\& return 'address';
\& }
.Ve
.PP
.Vb 1
\& 1;
.Ve
.SH "Deploying the Application"
.IX Header "Deploying the Application"
After coding the above modules we only need to do two more things:
create the database and add our application to httpd.conf.
.PP
In Postgres, you can merely say something like
.PP
.Vb 2
\& createdb address
\& psql address -U apache < schema.postgres
.Ve
.PP
(supplying passwords as requested) where schema.postgres is the one shown above
in \*(L"Sample App Description\*(R".
.PP
Assuming you are using mod_perl 1.3, you can add the following to your
httpd.conf:
.PP
.Vb 2
\&
\& #!/usr/bin/perl
.Ve
.PP
.Vb 1
\& use lib '/home/me/Apps-Address/lib';
.Ve
.PP
.Vb 3
\& use Address;
\& use Address::Address;
\&
.Ve
.PP
.Vb 7
\&
\& PerlSetVar dbconn dbi:Pg:dbname=address
\& PerlSetVar dbuser apache
\& PerlSetVar dbpass secret
\& PerlSetVar template_wrapper wrapper.tt
\& PerlSetVar root /home/me/Apps-Address/html:/home/me/srcgantry/root
\&
.Ve
.PP
.Vb 4
\&
\& SetHandler perl-script
\& PerlHandler Apps::Address::Address
\&
.Ve
.PP
Adjust the dbconn, dbuser, and dbpass PerlSetVars for your database. The root
needs to include the directory where wrapper.tt lives. You can copy one
from the sample_wrapper.tt that ships with gantry (look in the directory
named root).
.PP
Now all that remains is to restart the server.
.PP
If you are using Gantry::Conf (which we prefer, but didn't discuss above),
you need to set one var:
.PP
.Vb 1
\& PerlSetVar GantryConfInstance addressbook
.Ve
.PP
Then create a config file for the set vars shown above. See
Gantry::Conf::Tutorial for details.
.PP
If you are using \s-1CGI\s0 you need to make a script instead of adjusting apache
locations. Here is ours:
.PP
.Vb 1
\& #!/usr/bin/perl
.Ve
.PP
.Vb 1
\& use CGI::Carp qw( fatalsToBrowser );
.Ve
.PP
.Vb 1
\& use lib '/home/me/Apps-Address/lib';
.Ve
.PP
.Vb 1
\& use Apps::Address qw{ -Engine=CGI -TemplateEngine=TT };
.Ve
.PP
.Vb 1
\& use Gantry::Engine::CGI;
.Ve
.PP
.Vb 13
\& my $cgi = Gantry::Engine::CGI->new( {
\& config => {
\& dbconn => 'dbi:Pg:dbname=address',
\& dbuser => 'apache',
\& template_wrapper => 'wrapper.tt',
\& root => '/home/me/Apps-Address/html:',
\& '/home/me/srcgantry/root',
\& },
\& locations => {
\& '/' => 'Apps::Address',
\& '/address' => 'Apps::Address::Address',
\& },
\& } );
.Ve
.PP
.Vb 1
\& $cgi->dispatch();
.Ve
.PP
If you are using Gantry::Conf with \s-1CGI\s0, use the single config hash key:
.PP
.Vb 6
\& my $cgi = Gantry::Engine::CGI->new( {
\& config => {
\& GantryConfInstance => 'address',
\& }
\& # locations as above
\& } );
.Ve
.PP
If you want to deploy the app as a stand alone server (most useful
during testing), change the above cgi script to this:
.PP
.Vb 1
\& #!/usr/bin/perl
.Ve
.PP
.Vb 1
\& use Gantry::Server;
.Ve
.PP
.Vb 1
\& use lib '/home/me/Apps-Address/lib';
.Ve
.PP
.Vb 2
\& use Apps::Address qw{ -Engine=CGI -TemplateEngine=TT };
\& use Gantry::Engine::CGI;
.Ve
.PP
.Vb 13
\& my $cgi = Gantry::Engine::CGI->new( {
\& config => {
\& dbconn => 'dbi:Pg:dbname=address',
\& dbuser => 'apache',
\& template_wrapper => 'wrapper.tt',
\& root => '/home/me/Apps-Address/html:',
\& '/home/me/srcgantry/root',
\& },
\& locations => {
\& '/' => 'Apps::Address',
\& '/address' => 'Apps::Address::Address',
\& },
\& } );
.Ve
.PP
.Vb 2
\& my $port = shift || 8080;
\& my $server = Gantry::Server->new( $port );
.Ve
.PP
.Vb 2
\& $server->set_engine_object( $cgi );
\& $server->run();
.Ve
.PP
That is, trade use CGI::Carp for use Gantry::Server and \f(CW\*(C`<$cgi\-\*(C'\fRdispatch>>
for the last four lines shown above. Running the script will start a
server on port 8080 (or whatever port was supplied on the command line).
.SH "Using Bigtop"
.IX Header "Using Bigtop"
Now I have a confession. I never coded the example in the previous section.
I let Bigtop do it.
.PP
Bigtop is a code generator which can safely regenerate as thing change (like
the data model). The bigtop script reads a Bigtop file to produce apps
like the one shown above. There is a more detailed example in the tutorial
for Bigtop.
.PP
Bigtop uses its own little language to describe web applications. The language
is designed for simplicity of structure. There are basically only two
constructs: semi-colon terminated statements and brace delimited blocks.
.PP
The easiest way to edit bigtop files is to use tentmaker, a browser
delivered editor. It saves a lot of typing. If you really want to
see what the bigtop file looks like, see \*(L"Complete Bigtop Code Listings\*(R"
below. If you want to just build the app from that listing, use
\&\f(CW\*(C`address\-new.bigtop\*(C'\fR from the examples directory of the Bigtop distribution.
Type:
.PP
.Vb 1
\& bigtop -c address-new.bigtop all
.Ve
.PP
If you want to build that bigtop file, keep reading.
.PP
First, type:
.PP
.Vb 1
\& tentmaker -n Apps::Address address
.Ve
.PP
This will start tentmaker, tell it to make a new app called \f(CW\*(C`Apps::Address\*(C'\fR
and give it a single table \f(CW\*(C`address\*(C'\fR. Once it starts, tentmaker will
print a \s-1URL\s0 on your screen like this:
.PP
.Vb 1
\& ...You can connect to your server at http://localhost:8080/
.Ve
.PP
Go to the \s-1URL\s0 indicated with a \s-1DOM\s0 compliant browser like Firefox or Safari.
There are five tabs in tentmaker. We need to change things only in the
App Body, so click it.
.PP
Scroll down to edit the tables called 'address' (it should be the only
table). After clicking 'edit,' scroll down further until you see the
\&'Field Quick Edit' table. Change the 'Column Name' ident to 'name.' Click
\&'Apply Quick Edit.' (Actually, you can click anywhere in the browser outside
fo the input box to update it.) Change 'description' to 'street'. Then,
under 'Create Field(s),' enter a single string:
.PP
.Vb 1
\& city state zip phone email
.Ve
.PP
Then press 'Create.' You should see the new fields in the quick edit table.
.PP
Click optional in the quick edit heading row to make all fields
optional. Finally, uncheck optional for the name.
.PP
Now click the 'Bigtop Config' table. Enter a file name next to 'Save As:'
After you enter a name, click 'Save As:'. tentmaker will print a little
message under the the buttons telling you whether save your file or not.
If it saved successfully, press 'Stop Server' and confirm that you want
to stop the server.
.PP
In the same shell where you launched tentmaker, you should have your prompt
back. Type:
.PP
.Vb 1
\& bigtop -c address.bigtop all
.Ve
.PP
Change \f(CW\*(C`address.bigtop\*(C'\fR to whatever you called the bigtop file. Bigtop will
build the application and give you instructions on how to start it. Follow
those. For example, since I have an executable 'sqlite' in my path,
bigtop said this:
.PP
.Vb 3
\& I have generated your 'Apps::Address' application. I have also
\& taken the liberty
\& of making an sqlite database for it to use. To run the application:
.Ve
.PP
.Vb 2
\& cd Apps-Address
\& ./app.server [ port ]
.Ve
.PP
.Vb 1
\& The app.server runs on port 8080 by default.
.Ve
.PP
.Vb 2
\& Once the app.server starts, it will print a list of the urls it can serve.
\& Point your browser to one of those and enjoy.
.Ve
.PP
.Vb 1
\& If you prefer to run the app with Postgres or MySQL type one of these:
.Ve
.PP
.Vb 2
\& bigtop --pg_help
\& bigtop --mysql_help
.Ve
.PP
If you don't have sqlite, it will add a step for building the database.
Do type:
.PP
.Vb 1
\& bigtop --pg_help
.Ve
.PP
or
.PP
.Vb 1
\& bigtop --mysql_help
.Ve
.PP
if you use one of those databases.
.Sh "Generating with bigtop"
.IX Subsection "Generating with bigtop"
There are about 100 lines in the example bigtop file built above.
Here is a complete list of what bigtop built for you from that file
(with directory levels shown by indentation):
.PP
.Vb 35
\& Apps-Address/ - a directory where everything in the app lives
\& app.cgi CGI script
\& app.db sqlite database, if you sqlite in your path
\& app.server stand alone server script
\& Build.PL
\& Changes ready for use
\& MANIFEST complete as of the initial generation
\& MANIFEST.SKIP
\& README in need of heavy editing
\& docs/
\& address.bigtop - the original bigtop file
\& schema.mysql - ready for use with mysql
\& schema.postgres - ready for use with psql
\& schema.sqlite - ready for use with sqlite
\& html/
\& templates/
\& genwrapper.tt - a simple site look
\& lib/
\& Apps/
\& Address.pm - base module stub for the app
\& GENAddress.pm - generated base module for the app
\& Address/
\& Address.pm - controller stub for the address table
\& GEN/
\& Address.pm - generated code for Address.pm above
\& Model.pm - DBIx::Class schema to all models
\& Model/
\& address.pm - model stub for the address table
\& GEN/
\& address.pm - generated code for address.pm above
\& t/
\& 01_use.t - tests whether each controller compiles
\& 02_pod.t - if you have Test::Pod, validates all pod in all modules
\& 03_podcover.t - if you have Test::Pod::Coverage, looks for missing pod
\& 10_run.t - hits the default page of each controller
.Ve
.PP
Note that there are more modules than in the hand written version. This
allows you to change the data model and regenerate without fear of losing
hand coded changes. So, Address.pm, Address::Model, Address::Address, and
Address::Model::address are stubs providing a place for you to add
your customized code as needed; while Address::GEN::Address,
Address::Model::GEN::address, etc. are generated each time you run
bigtop. If you need to do something other than what the generated code does,
simply redefine the behavior in the non-generated code stubs and that will be
used. Do not edit the \s-1GEN\s0 modules, instead only add code to the stubs as
needed.
.Sh "Revisions"
.IX Subsection "Revisions"
Suppose that you want some validation of the input.
.PP
Further, suppose my wife wants us to add a birth day table so she can
send cards.
.PP
We'll see how to add those things here, by manually editing the bigtop file.
You could do these things with tentmaker as well. But sometimes it is
easier to work with your favorite text editor. Do what makes sense.
.PP
\fIConstraining things\fR
.IX Subsection "Constraining things"
.PP
No data in the sample address book is validated (because Lisa has
too many friends and relatives living in too many places for meaningful
validation).
.PP
But, if you want validation, you can include it like so:
.PP
.Vb 7
\& field zip {
\& is varchar;
\& label Zip;
\& html_form_type text;
\& html_form_optional 1;
\& html_form_constraint `qr{^\ed{5}$}`;
\& }
.Ve
.PP
The constraint could be a valid Perl regex. You could also call a sub which
returns a regex. If you include a uses statement in your controller like
this:
.PP
.Vb 1
\& uses Data::FormValidator::Constraints => `qw(:closures)`;
.Ve
.PP
You can set the constraint like so:
.PP
.Vb 1
\& html_form_constraint `zip_or_postcode()`;
.Ve
.PP
See perldoc Data::FormValidator::Constraints for details of the closures
available. All of them return a regex suitable for use as shown.
.PP
\fIEmail address field\fR
.IX Subsection "Email address field"
.PP
It is particularly easy to add a new field to the address table:
.PP
.Vb 6
\& field email {
\& is varchar;
\& label `Email Address`;
\& html_form_type text;
\& html_form_optional 1;
\& }
.Ve
.PP
Note that I put the label for this field in backquotes, since its name
contains a space.
.PP
We don't have to change the Address controller block, because the
only thing affected is the form. tentmaker already specified that the form
should have all_fields_but id. So, email will show up upon regeneration.
.PP
\fIBirthday table\fR
.IX Subsection "Birthday table"
.PP
The most interesting change is adding birthdays. In my mind, this leads
to a new table with this schema:
.PP
.Vb 7
\& CREATE SEQUENCE birth_seq;
\& CREATE TABLE birth (
\& id int4 PRIMARY KEY DEFAULT NEXTVAL( 'birth_seq' ),
\& name varchar,
\& family int4,
\& birthday date
\& );
.Ve
.PP
To generate this sql, its model and controller we can add this
to our bigtop file (again, I'll show it a bit at a time with commentary):
.PP
.Vb 7
\& table birth {
\& field id { is int4, primary_key, auto; }
\& field name {
\& is varchar;
\& label Name;
\& html_form_type text;
\& }
.Ve
.PP
This will be the name of one person in a nuclear family.
.PP
.Vb 6
\& field family {
\& is int4;
\& label Family;
\& html_form_type select;
\& refers_to address;
\& }
.Ve
.PP
This field becomes a foreign key pointing to the address table, since it
uses the \f(CW\*(C`refers_to\*(C'\fR statement. When the user enters a value for this
field, they must choose one family defined in the address table.
.PP
.Vb 8
\& field birthday {
\& is date;
\& label Birthday;
\& html_form_type date;
\& date_select_text `Popup Calendar`;
\& }
\& foreign_display `%name`;
\& }
.Ve
.PP
I've chosen to store the actual date of birth (which leads to recording
women's ages, shame on me). This is to show how date selection works
smoothly for your users. There are three steps to this process. The
first one is shown here: use the date_select_text statement. Its value
becomes the link text the user clicks to popup the calendar selection
mini\-window. See, the controller below for the other two steps.
.PP
.Vb 4
\& controller Birth is AutoCRUD {
\& controls_table birth;
\& rel_location birthday;
\& uses Gantry::Plugins::Calendar;
.Ve
.PP
Step two in easy dates is to use Gantry::Plugins::Calendar which provides
javascript code generation routines.
.PP
.Vb 2
\& text_description birthday;
\& page_link_label `Birth Day`;
.Ve
.PP
This page will show up in site navigation with its page_link_label
.PP
.Vb 6
\& method do_main is main_listing {
\& title `Birth Day`;
\& cols name, family, birthday;
\& header_options Add;
\& row_options Edit, Delete;
\& }
.Ve
.PP
The main listing is just like the one for the address table, except for
the names of the displayed fields.
.PP
.Vb 8
\& method form is AutoCRUD_form {
\& form_name birthday_form;
\& all_fields_but id;
\& extra_keys
\& legend => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`,
\& javascript => `$self->calendar_month_js( 'birthday_form' )`;
\& }
\& }
.Ve
.PP
Now the name of the form becomes important. The calendar_month_js
method (mixed in by Gantry::Plugins::Calendar) generates the javascript
for the popup and its callback, which populates the date fields.
Note that we don't tell it which fields to handle. It will work on
all fields that have date_select_text statements.
.PP
Once these changes are made, we can regenerate the application:
.PP
.Vb 1
\& bigtop docs/address.bigtop all
.Ve
.PP
Execute this command while in the build directory (the one with the Changes
file in it).
.PP
For the app to work successfully, you will need to alter the existing
database so it has the new columns and birth day table. Either throw out
the old database or alter it at your option. Bigtop has data statements
which allow you to specify initial data for tables. This makes discarding
a database less painful.
.PP
Again, I confess that I used tentmaker to get me started with the changes
above, then cleaned its output until it became the
\&\*(L"Complete Bigtop Code Listing\*(R" below.
.PP
You can continue to edit the bigtop file with a text editor or tentmaker
and regenerate as the app matures. We have regenerated production apps
months after deployment.
.Sh "Complete Bigtop Code Listing"
.IX Subsection "Complete Bigtop Code Listing"
.Vb 139
\& config {
\& engine CGI;
\& template_engine TT;
\& Init Std { }
\& SQL SQLite { }
\& SQL Postgres { }
\& SQL MySQL { }
\& CGI Gantry { gen_root 1; with_server 1; flex_db 1; }
\& Control Gantry { dbix 1; }
\& Model GantryDBIxClass { }
\& SiteLook GantryDefault { }
\& }
\& app Apps::Address {
\& config {
\& dbconn `dbi:SQLite:dbname=app.db` => no_accessor;
\& template_wrapper `genwrapper.tt` => no_accessor;
\& }
\& controller is base_controller {
\& method do_main is base_links {
\& }
\& method site_links is links {
\& }
\& }
\& table address {
\& field id {
\& is int4, primary_key, auto;
\& }
\& field name {
\& is varchar;
\& label Name;
\& html_form_type text;
\& html_form_optional 0;
\& }
\& field street {
\& is varchar;
\& label Street;
\& html_form_type text;
\& html_form_optional 1;
\& }
\& foreign_display `%name`;
\& field city {
\& is varchar;
\& label City;
\& html_form_type text;
\& html_form_optional 1;
\& }
\& field state {
\& is varchar;
\& label State;
\& html_form_type text;
\& html_form_optional 1;
\& }
\& field zip {
\& is varchar;
\& label Zip;
\& html_form_type text;
\& html_form_optional 1;
\& html_form_constraint `qr{^\ed{5}$}`;
\& }
\& field country {
\& is varchar;
\& label Country;
\& html_form_type text;
\& html_form_optional 1;
\& }
\& field email {
\& is varchar;
\& label Email;
\& html_form_type text;
\& html_form_optional 1;
\& }
\& field phone {
\& is varchar;
\& label Phone;
\& html_form_type text;
\& html_form_optional 1;
\& }
\& }
\& controller Address is AutoCRUD {
\& controls_table address;
\& rel_location address;
\& text_description address;
\& page_link_label Address;
\& method do_main is main_listing {
\& cols name, street;
\& header_options Add;
\& row_options Edit, Delete;
\& title Address;
\& }
\& method form is AutoCRUD_form {
\& all_fields_but id, created, modified;
\& extra_keys
\& legend => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`;
\& }
\& }
\& table birth {
\& field id {
\& is int4, primary_key, auto;
\& }
\& field name {
\& is varchar;
\& label Name;
\& html_form_type text;
\& }
\& field family {
\& is int4;
\& label Family;
\& refers_to address;
\& html_form_type select;
\& }
\& field birthday {
\& is date;
\& label Birthday;
\& html_form_type text;
\& date_select_text `Popup Calendar`;
\& }
\& foreign_display `%name`;
\& }
\& controller Birth is AutoCRUD {
\& controls_table birth;
\& rel_location birthday;
\& uses Gantry::Plugins::Calendar;
\& text_description birthdays;
\& page_link_label `Birth Days`;
\& method do_main is main_listing {
\& title `Birth Day`;
\& cols name, family, birthday;
\& header_options Add;
\& row_options Edit, Delete;
\& }
\& method form is AutoCRUD_form {
\& form_name birthday_form;
\& all_fields_but id;
\& extra_keys
\& legend => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`,
\& javascript => `$self->calendar_month_js( 'birthday_form' )`;
\& }
\& }
\& }
.Ve
.SH "Summary"
.IX Header "Summary"
In this document we have seen how a simple Gantry app can be written
and deployed. While building a simple app with bigtop can take just
a few minutes, interesting parts can be fleshed out as needed. Our
goal is to provide a framework that automates the 50\-80% of most apps
which is repetitive, allowing us to focus our time on the more interesting
bits that vary from app to app.
.PP
If you want to see a more realistic app, see Bigtop::Docs::Tutorial
which builds a basic freelancer's billing app.
.PP
There are other documents you might also want to read.
.IP "Gantry::Docs::FAQ" 4
.IX Item "Gantry::Docs::FAQ"
categorized questions and answers explaining how to do common tasks
.IP "Gantry::Docs::About" 4
.IX Item "Gantry::Docs::About"
marketing document listing the features of Gantry and telling its history
.PP
The modules have their own docs which is where would be gantry developers
should look for more information.
.SH "Author"
.IX Header "Author"
Phil Crow
.SH "Copyright and License"
.IX Header "Copyright and License"
Copyright (c) 2006\-7, Phil Crow.
.PP
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.6 or,
at your option, any later version of Perl 5 you may have available.