The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

Name

Gantry::Docs::Cookbook - Gantry How Tos

Intro

This document is set up like a cookbook, but all the recipes are fully implemented in Gantry::Samples. The first recipe explains how to run the samples.

You might also be interested in Gantry::Docs::FAQ which answers a different set of questions that are not covered in Gantry::Samples. Bigtop has its own Bigtop::Docs::Cookbook and full documentation suite, see Bigtop::Docs::TOC.

The questions are:

How do I run the samples?

To run the samples, you need sqlite 3 and Gantry. Once those are in place just change to the samples directory of the Gantry distribution and type:

    ./app.server

The stand alone server will print a list of available URLs like this:

  Available urls:
    http://localhost:8080/
    http://localhost:8080/ajaxrequest
    http://localhost:8080/authcookie
    http://localhost:8080/authcookie/sqlite
    http://localhost:8080/authcookie/sqlite/closed
    http://localhost:8080/fileupload
    http://localhost:8080/table_perms
    http://localhost:8080/table_perms_crud
    http://localhost:8080/user
    http://localhost:8080/user/group

These locations will be mentioned again in the appropriate sections below.

How do I upload files?

You need three things to upload a file...

  1. A controller method expecting the file, which knows what to do with it.

  2. A form for the user to supply the file info from their browser.

  3. The file_upload method supplied by all engines.

Note that there is no special plugin to load. Gantry engines all know how to upload files and are happy to do so at any time.

Gantry::Samples::FileUpload provides an example. It has a single do_ method, do_main, which uses fileupload.tt supplied in the samples html/templates sub directory.

Once the form validates, do_main says:

    my $upload = $self->file_upload( 'file' );

where file is the name of the form field containing the file name. This method returns a hash ref with thses keys:

unique_key

A unique identifier for the file based on the current time and a random number.

name

Base name of user's file.

suffix

File suffix (e.g. txt).

fullname

Name of user's file including suffix.

size

Number of bytes in file.

mime

Mime type of file.

filehandle

The handle from which you read the file.

To see how to catch and store the file, see do_main in Gantry::Samples::FileUpload.

Note that Bigtop does not help with file uploads, since the actual upload is done by a single method call and the details of processing the received file vary too much for a generic scheme.

How should I configure an app?

The short answer is: not like the samples. The samples are set up to run exclusively in a stand aloner server environment, so that people trying them can more easily run them.

For configuration best practice advice, see the Gantry::Docs::FAQ question "How should I configure an app?" It shows how we prefer to configure apps in a normal life cycle.

How do I authenticate users?

While you could authenticate users in a variety of ways, our prefered scheme is with a cookie. Gantry provides Gantry::Plugins::AuthCookie to manage those cookies. To see a demonstration, run the samples and visit http://localhost:8080/authcookie/sqlite/closed.

You need two pieces to make authentication work. First, you need to modify the config information. This is best done in its Bigtop file. Second, you need to set up a database to keep track of the users and their passwords. I'll explain the Bigtop changes first.

Config Adjustments

In the top level config block, where the engine statement is, add:

    plugins AuthCookie;

This will make every controller in the application use the authentication plugin, but you still have to ask nicely for it to keep people out. By default, everyone is still allowed in to all pages. To keep people out, set config variables at that controller level. For example:

    controller AuthCookie::SQLite::Closed {
        page_link_label `AuthCookie w/ SQLite Closed`;
        rel_location `authcookie/sqlite/closed`;
        config {
            auth_deny yes => no_accessor;
            auth_require `valid-user` => no_accessor;
        }
    }

This creates a controller which denies access unless the user is valid. The auth_require `valid-user` syntax is meant to mimic Apache basic auth syntax.

This controller doesn't have any methods. Rather, it inherits them from this one:

    controller AuthCookie {
        page_link_label AuthCookie;
        rel_location authcookie;
        method do_open is stub {
        }
        method do_closed is stub {
        }
    }

These stubs are filled inside Gantry::Samples::AuthCookie. They are not particularly interesting, but here they are:

    sub do_open {
        my ( $self ) = @_;

        return( "you're in" );

    } # END do_open

    sub do_closed {
        my ( $self ) = @_;

        my @lines;
        push( @lines, "you're in: " . $self->user );
        push( @lines,
            ht_br(), ht_br(),
            ht_a(
                ( $self->app_rootp . "/login?logout=1" ),
                ('logout ' . $self->user),
            ),
        );
        return( join( "", @lines ) );

    } # END do_closed

Some pages display differently for logged in users than they do for others. For instance the front page of perlmonks always shows the Seekers of Perl Wisdom section for anyone not logged in, but a page of the user's choice for those logged in. This requires looking at the cookie, to find out who is logged in, without denying access. These modules in the samples demonstrate this:

    Gantry::Samples::AuthCookie::SQLite
    Gantry::Samples::TablePermissions
    Gantry::Samples::TablePermCRUD

They do this by setting:

    auth_optional yes => no_accessor;

in their controller level config blocks. This sets the user_row attribute of the site object, without keeping anyone out of the page for failure to have a valid cookie. Now that the config tells the auth cookie plugin where to look for user data and which pages to restrict, we must have database tables for that user data.

Authentication Database

For Gantry::Plugins::AuthCookie to work, you need to set up a database for it to use, or add tables to your app's existing database. Using a separate database is good in a corporate setting where users have various access to many different apps. Combining the auth tables into an existing database is better for self standing sites. The samples use a single database, so I'll show that approach first.

There are three essential columns in the user table needed for authentication: id, user name, and pass word. The id is for the benifit of the ORM. The other fields hold the user's credentials. The names of these columns is not fixed by the AuthCookie plugin. The defaults are ident and password. We'll see how to control the names the plugin uses below.

You are welcome to put additional information in the user rows. The user table from the samples has this schema (which was generated by bigtop):

    CREATE TABLE user (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        active INTEGER,
        username varchar,
        password varchar,
        email varchar,
        fname varchar,
        lname varchar,
        created datetime,
        modified datetime
    );

When you use authentication, with either auth_require or auth_optional, you can get the ORM row for the logged in user by calling user_row on your Gantry site object. If auth is optional, and the user is not logged in, you will still get a user row, but it won't have data in it.

To tell the plugin about the table, the samples use these variables in the app level config block:

    config {
        dbconn `dbi:SQLite:dbname=app.db` => no_accessor;
        auth_table user => no_accessor;
        auth_user_field username;
        #...
    }

To use a separate database for auth, set auth_dbconn like dbconn, but pointing to the other database. We need to tell the plugin the name of our user table with the auth_table config parameter. Since sqlite allows us to, we call it user. Since our user names are stored in the username column, and not in ident, we must set the auth_user_field config parameter to username. To change the pass word column away from password, we would use auth_password_field, but we don't need to in this case.

That's all you need to set up cookie base user log-ins. If you want to further restrict pages to subsets of logged-in users, see the next question.

How do I authorize users?

Once you have mastered authenticating users, it is usually a short step to wanting to divide them into groups with different access rights. For instance, a message board like slashdot needs special access for editors.

As with authentication, there are two parts to authorization. First, you need to change the config info (or code). Then, you need to include the groups and their member lists in the database. I'll take them in that order.

Note well, that the samples do not use group authorization at the controller level. They do use groups at the row level (see the next question). Thus, the tables and models are in place to handle groups. You could alter the examples as described below to convert the valid-user requirement into a group membership requirement.

To restrict the closed controller to a specific group, first change the auth_require value from valid-user to group. Then, add auth_groups with the name of the group whose members are allowed to reach the page.

    controller AuthCookie::SQLite::Closed {
        page_link_label `AuthCookie w/ SQLite Closed`;
        rel_location `authcookie/sqlite/closed`;
        config {
            auth_deny yes => no_accessor;
            auth_require `group` => no_accessor;
            auth_groups `admin` => no_accessor;
        }
    }

Note that the value for the auth_groups config statement may be a comma separated list of group names (with optional internal whitespace). Logged in users who are members of any listed group, will be allowed to access the page.

Database additions for authorized groups

In addition to the user table described above, you need two other tables to make groups work. The first table names the groups. The second lists the members.

Here is the definition of the group table (it was generated by bigtop):

    CREATE TABLE user_group (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        ident varchar,
        description varchar,
        created datetime,
        modified datetime
    );

The short name of the group is ident. This is the name you list in auth_groups values. The usually longer description is meant to be a more verbose and therefore understandable description of the group. Only the ident field is used by the AuthCookie plugin.

The group membership table represents a three way join between the user and user_group tables. (See "What is a three way join?" if you aren't familiar with the three way join concept.) Each row in this table links one user to one group in classic many-to-many fashion (again, bigtop generated the table layout):

    CREATE TABLE user_user_group (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user INTEGER,
        user_group INTEGER
    );

If you generate these tables with bigtop, it will make controllers to manage them. You will almost surely want to secure those with group level authorization. You will probably need to add one user manually through the database command line tool to bootstrap the management process. The samples allow anyone to update the user information, a method suitable only for development, if ever there was one.

How do I authorize based at the row level?

In order to answer this question, let's begin by talking about how the samples of this work. The Table Permissions controller represents a one table 'For Sale' bulletin board (so does the Table Permissions with Manual CRUD, see below). Note that this is not a fully functional site. It just demonstrates row level permissions.

Any user may visit the Table Permissions main listing and see all of the items for sale. But what else you may do there is governed by whether you are logged in and whether your logged in user is n the admin authorization group.

What is a three way join?

A three way join is a many-to-many relationship between two tables. For example, the relationship between authors and books. An author of one book likely has written other books. For each author, there are many books. But, in the other direction a good number of books have multiple authors. For each book there could be many authors.

Generally, you need a special extra table to hold the relationship:

    +--------+             +------+
    | author |             | book |
    +--------+             +------+
           ^                 ^
            \               /
          +-------------------+
          |    author_book    |
          +-------------------+

Here the author_book table has only two (or three fields if you give it an id). Both are foriegn keys to the tables on the end of the relationship.

How do I work with a simple three way join?

You need two things for easy use of a three way structure, once your SQL is in place. First, you need your model to understand it. Second, you need an easy way to perform CRUD on the join table rows.

Making and using the model directly

Making the model understand your three way preferences is easy. Start with the two tables on the ends of the relationship as normal. Then add this to the bigtop file:

    join_table author_book {
        joins author => book;
    }

You can do this with kickstart syntax when you make or augment the bigtop file:

    bigtop -n BookStore 'author(name)<->book(title,year:int4)'

That will make the two regular tables and the joining table.

The Threeway utils module

Once you have a three way structure, you can use Gantry::Utils::Threeway to manage the rows in the joining table (the one in the middle). You can do this from the controller for the table on either end of the many-to-many relationship, or for both of them.

To show this, I'll pull code from Gantry::Samples::User.

First, use the module:

    use Gantry::Utils::Threeway;

Then provide a do_ method. The one in the user example manages group membership for users. In kickstart syntax, the relationship is user<->groups. The full method is:

 #-----------------------------------------------------------------
 # $self->do_groups(  )
 #-----------------------------------------------------------------
 sub do_groups {
     my ( $self, $user_id ) = @_;

     my $threeway = Gantry::Utils::Threeway->new( {
         self            => $self,
         primary_id      => $user_id,
         primary_table   => 'user',
         join_table      => 'user_user_group',
         secondary_table => 'user_group',
         legend          => 'Assign Groups to User'        
     } );

     $threeway->process();

 } # END do_groups

All you have to do is construct the three way object and call process on it. This displays a form with a check box for each group. The current memberships are already checked. Clicking in the boxes and submitting the form updates them.

The keys needed by new are:

self

The Gantry site object.

primary_id

The value of the foreign key in the joining table that points to this controller.

primary_table

The name of the controller's table.

join_table

The name of the joining table.

secondary_table

The name of the table on the other end of the many-to-many.

legend

HTML fieldset legend around the form where new joining table rows are created from check box values.

Using the three way manually

If you want to access rows from the table on the other end of the many-to-many relationship, use the many_to_many relationship in the model:

    my @groups = $user->user_groups();

That will return an array of groups to which the current user belongs. You can turn that around through a group row:

    my @members = $group->users();

If you need the rows from the joining table, use the has_many relationship from the model:

    my @joining_rows = $user->user_user_groups();

Making a three way manually

By far, the easiest way to create a three way joining structure is with a bigtop join_table block as shown above. But you can do it yourself. In your author model, add calls like these:

    __PACKAGE__->has_many(
        author_books => 'YourApp::Model::author_book',
        'author' # your table name
    );
    __PACKAGE__->many_to_many(
        books => 'author_books',  # value matches the has many above
        'book' # the other table name
    );

Then do the same in the book model. Finally, make sure you have a model for the joining rows with a normal foreign key belongs_to for each of the end point tables:

    __PACKAGE__->belongs_to( user => 'YourApp::Model::author' );
    __PACKAGE__->belongs_to( user => 'YourApp::Model::book' );

How do I make my Gantry app respond to SOAP requests?

There are two types or 'styles' of SOAP requests. Gantry can help with either, but it is better at the document style, so that is what I'll discuss here.

To see a sample of this approach, run the samples app.server in one shell and samples/bin/soap_client in another. Give the client a Farenheit temperature on the command line. You should see a SOAP request packet. The client will send that packet to the server immediately after printing it for you. Then, you should see a SOAP response packet with the temperature in Celcius.

Here's what you need to do to make your own server.

Make a controller. For instance, you could add this to your bigtop file:

    controller SOAP {
        rel_location GantrySoapService;
        skip_test 1;
        plugins SOAP::Doc;
        method do_f2c is stub {
        }
    }

When you regenerate, you'll have a new SOAP.pm in which to place your code. It will inherit from a GEN module that uses the document style SOAP plugin. Then you'll have to fill in the code for the do_f2c routine. Bigtop made this stub:

    #-----------------------------------------------------------------
    # $self->do_f2c(  )
    #-----------------------------------------------------------------
    sub do_f2c {
        my ( $self ) = @_;
    }

All we need to do is fill it in.

If all the SOAP request parameters are at the same level in their packet (a fairly common case), you can take advantage of the plugin's automated conversion of the SOAP packet into form parameters. If your SOAP packet has nested tags, you'll need to parse the XML with a module like XML::LibXML or XML::Twig. The sample's packets are simple.

Here is the finished routine (less comments offering advice on XML::LibXML):

  1   sub do_f2c {
  2     my ( $self ) = @_;
  3     my $time     = $self->soap_current_time();
  4     my $params   = $self->params();  # easy way
  5
  6     my $f_temp   = $params->{ farenheit };
  7     my $celcius  = 5.0 * ( $f_temp - 32 ) / 9.0;
  8
  9     my $ret_struct = [
 10         {
 11             GantrySoapServiceResponse => [
 12                 { currentUTCTime => $time },
 13                 { celcius        => $celcius },
 14             ]
 15         }
 16     ];
 17
 18     $self->soap_namespace_set(
 19         'http://usegantry.org/soapservice'
 20     );
 21
 22     return $self->soap_out( $ret_struct, 'internal', 'pretty' );
 23 } # END do_f2c

If you need to tell your client the UTC time of your response in valid SOAP time format, call soap_current_time, as I did on line 3.

Since my server's SOAP requests are simple, I can call params on line 5, just as I would to handle form parameters. The input parameter is in the farenheit key (line 6). A grade school formula does the conversion on line 8.

Lines 9-16 build the structure of the return packet. The top level tag is GantrySoapServiceResponse. Inside it will be a list of tags (order often matters to DTDs), one for the time, the other for the converted temperature.

To control the namespace of GantrySoapServiceResponse and its children, I called soap_namespace_set (line 18).

Finally, line 22 uses soap_out to send the packet back to the client. It expects:

return structure

See the example above. If you need a empty tag like <empty />, use

    { empty => undef }
namespace position

This must be a string chosen from 'prefix' or 'internal'. The default is prefix. This governs where the namespace is defined, and therefore has a cosmetic effect on the SOAP packet. A prefix namespace is defined in the SOAP Envelope tag where it is given the prefix tns. That prefix appears on all tags in the returned packet.

If you use internal instead, the namespace is defined in the top level tag:

    <GantrySoapServiceResponse
        xmlns="http://usegantry.org/soapservice">

Then the elements in the body of the response have no explicit namespace prefix.

pretty print

If this has a true value, the resulting XML packet will have various whitespace added to it to improve human readability. No whitespace will be added anywhere that would affect parsing the result.

That's all there is to a document style SOAP server in Gantry.

How do I use Gantry to build a SOAP client?

There is a sample SOAP client in samples/bin/soapclient. There are three parts to a SOAP client: build the XML for the request, send that XML to the proper URL, parse the response (this list leaves out coming up with the data for the request). Gantry can help with the first one, but you need LWP or something similar for the other two.

Here is commentary on the soapclient sample.

    se strict; use warnings;

    use lib qw( lib ../lib );

This lib directory makes sure that code comes from the samples or from distribution and not from installed locations. This is useful for developers working on the samples.

    use LWP::UserAgent;
    use Gantry::Plugins::SOAP::Doc;

LWP will handle the actual http request/response. The SOAP::Doc plugin will make the XML to send.

    my $f_temp = shift || 68;
    my $url    = 'http://localhost:8080/GantrySoapService/f2c';

These set the URL or the web service. You must be running the samples app.server on port 8080 of the local host for this to work.

    my $site = {
        action_url       => $url,
        post_to_url      => $url,
        target_namespace => 'http://sunflower.com/soapns',
    };

This structure will be passed to helper routines below. The namespace is not particularly important, but it services as documentation for users of the service.

    my $request_args = [
        {
            temperature => [
                { farenheit => $f_temp },
            ]
        },
    ];

This structure is the data for the request. It has an outer XML tag called temperature. Inside that tag is one parameter for the remote method called farenheit. To add other parameters, add more hashes with keys for the parameters and legal values. If you need an empty tag, use { key_name => undef }

Which will generate:

            <key_name />

Note that ensuring valid parameters is totally up to you.

    my $soap_obj    = Gantry::Plugins::SOAP::Doc->new( $site );

Instantiate a SOAP::Doc plugin object. If you happened to have a Gantry object with the SOAP plugin like a server, you could skip this step and just use the Gantry object as the invocant of soap_out in the next step.

    my $request_xml = $soap_obj->soap_out(
            $request_args, 'internal', 'pretty'
    );

Calling soap_out on a SOAP::Doc plugin object (or a Gantry site object) returns a valid XML SOAP packet.

    warn "request:\n$request_xml\n";

    transact_via_xml( $site, $request_xml );

The packet is first printed for the user's benefit, then sent to the server.

The transact_via_xml sub is not that interesting, but I'll include it for completeness. Mostly it makes sure to get everything aligned for proper LWP functioning.

    sub transact_via_xml {
        my ( $site, $request_xml ) = @_;

        # make the request
        my $user_agent = LWP::UserAgent->new();
        $user_agent->agent( 'Sunflower/1.0' );

        my $request = HTTP::Request->new(
            POST => $site->{ post_to_url }
        );

        $request->content_type( 'text/xml; charset=utf-8' );
        $request->content_length( length $request_xml );
        $request->header( 'Host' => $site->{host} );
        $request->header( 'SoapAction' => $site->{ action_url } );
        $request->content( $request_xml );

        my $response = $user_agent->request( $request );

        warn $response->content . "\n";
    }

That's a complete stand alone client. You could do the same three steps in a Gantry controller to contact a foreign web service while serving a page request, depending on the service throughput and your users' patience.

What is Submit and Add Another? How do I turn it on?

When a user is adding a row to a database table, they often need to add more than one. Gantry provides a little feature to make this easier called 'Submit and Add Another'. If you turn it on, the user will see it as a button between 'Submit' and 'Cancel'. Clicking it will first validate the form. If it validates, the row will be created. But, instead of going back to a main listing, the user will be returned to the add form.

To turn on this feature in a manual form method, add:

    submit_and_add_another => 1,

to the returned hash.

Bigtop and tentmaker can do this for you. Simply add:

    submit_and_add_another => 1

to the extra_keys for the form. Note that there is no specific keyword for this. You can set any key in the forms hash by adding it to extra_keys.