Skip to main content

By clicking Submit, you agree to the developerWorks terms of use.

The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

All information submitted is secure.

  • Close [x]

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerworks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

By clicking Submit, you agree to the developerWorks terms of use.

All information submitted is secure.

  • Close [x]

Build Web apps with Maypole

A Perl framework for quick and easy database-backed applications

Simon Cozens (simon@simon-cozens.org), Freelance author and programmer, Freelance
Simon Cozens is a Perl programmer and author. He has released more than 90 Perl modules. His books include Beginning Perl, Extending and Embedding Perl, and the forthcoming second edition of Advanced Perl Programming. He also maintains the Perl.com site for O'Reilly & Associates. When not stuck in front of a computer, he enjoys making music, playing the Japanese game of Go, and teaching at his local church. You can reach Simon at simon@simon-cozens.org.

Summary:  Simon Cozens turns a love of beer into a Perl application server -- going from a simple front end to database servers, and developing into a social-network Web application. He begins, however, with the beer.

Date:  25 May 2004
Level:  Introductory

Activity:  9953 views
Comments:  

The development of my latest project, Maypole, began over 4000 years ago, when, so the story goes, the goddess Ninkasi taught the Sumerian mortals a recipe for a magical drink. Her concoction of hops, barley, yeast, and water (and a few other special ingredients), was an instant hit, and has remained the cornerstone of a university education ever since.

As a student, I subdivided the entire world of beer into Guinness, fizzy lager, and proper English bitter. It was at the Open Source Conference 2000, when Adam Turoff presented me with a lovely bottle of Chimay Blue, that the scales fell from my eyes and I became introduced to the wide variety of different tastes and subtle flavors of the beers of the world.

The big problem with Ninkasi's recipe is that the nicer the end result, the more of it you consume, and, for some reason, the less likely you are to remember how good it was in the morning, and so you never know whether or not you want to buy that particular beer again. So you have to buy it anyway to try to work out whether or not you liked it. This is enjoyable, but not particularly economical. I found myself needing some kind of database to keep track of my tastings.

Not one to do things by halves (or should that be 284 ml?), what started as a simple database ended up as a complete framework for writing Web-based, database-backed applications in Perl.

Building the database

The first bit was easy. I created an SQL schema that linked beers to their breweries and the pubs that served them:


Listing 1. Beer database schema
        create table brewery (
            id int not null auto_increment primary key,
            name varchar(30),
            url varchar(50),
            notes text,
        );

        create table beer (
            id int not null auto_increment primary key,
            brewery integer,
            style integer,
            name varchar(30),
            ...
        );

        create table handpump (
            id int not null auto_increment primary key,
            beer integer, pub integer
        );

        create table pub (
            id int not null auto_increment primary key,
            name varchar(60),
            url varchar(120),
            notes text
        );

        create table style (
            id int not null auto_increment primary key,
            name varchar(60),
            notes text
        );

And then what? I didn't particularly want to be sitting in the mysql shell typing precarious INSERT INTO statements, especially after the fourth pint of Hook Norton Generation. Of course, I could write some command-line Perl scripts to read little text files and put them into the database, or something similar, but the ideal way of keeping the database up to date, visible, and easy to edit would be to have a Web interface to it.

At this point I realized that the project had suddenly become vastly more generic than just a way of keeping track of my favorite tipples, and I could create a useful framework that put a Web front end onto any database. This was beginning to sound like work, so I left the idea alone for a couple of years.

Then there was a surge in interest in the Perl community in conglomeration of ideas: the Template Toolkit, the Class::DBI object database mapping layer, and the idea of an MVC (Model-View-Controller)-style framework for Web applications. Andy Wardley, the creator of the Template Toolkit, explains what this means:

What the MVC-for-the-Web crowd is really trying to achieve is a clear separation of concerns. Put your database code in one place, your application code in another, your presentation code in a third place. That way, you can chop and change different elements at will, hopefully without affecting the other parts (depending on how well your concerns are separated, of course). This is common sense and good practice. MVC achieves this separation of concerns as a by-product of clearly separating inputs (controls) and outputs (views).

But with so many people talking about MVC Web applications, I found it a little odd that nobody had really put together a really good generic framework for one in Perl and released it. Java™ programmers have Struts, which makes it nice and easy to deploy such applications, and that's quite the rage at the moment, but Perl programmers have been forced to write their own -- over, and over again. I imagine that each large Perl company has its own big application framework. Let's look at how they usually work.


Dramatis personae

Kake Pugh's article on writing Web applications without writing much code (see Resources for a link) introduced a common set of components used to develop MVC frameworks, and I've used many of her ideas in Maypole, including the same components.

We'll start at the model end -- this is a set of classes that define the behavior of the application: when someone clicks search, there's got to be some code that does the searching and returns the results so they can be handed off to the view.

Because most applications use a relational database as their data store, and because objects are a handy way of representing, well, objects, the model class is usually based on an object-database mapping system. Amongst the many Perl libraries that do this, Class::DBI came out as my favorite due to its sheer simplicity, and it has a bunch of extension classes that make it even simpler. For instance, if I start with:


Listing 2a. Creating the classes with CDBI::Loader
        use Class::DBI::Loader;
        Class::DBI::Loader->new( namespace => "BeerDB", dsn => "dbi:mysql:beerdb" );

Then, magically, the BeerDB::Beer, BeerDB::Brewery, BeerDB::Pub, and BeerDB::Handpump classes come into existence, and I can say:


Listing 2b. Creating the classes with CDBI::Loader
        my $hooky      = BeerDB::Beer->retrieve(12); # Retrieve by ID

        my $generation = BeerDB::Beer->search( name => "Generation" );
        $generation->score(9); # Good beer!

If I then specify relationships between tables, I can say things like:


Listing 2c. Creating the classes with CDBI::Loader
        BeerDB::Beer->has_a(brewery => "BeerDB::Brewery");

        my $rch        = BeerDB::Brewery->find_or_create( name => "RCH" );
        my $pitchfork  = BeerDB::Beer->create({
            name    => "Pitchfork",
            brewery => $rch
        });

        print $pitchfork->brewery->name;

Now, this is a reasonable amount of what we want a model class to do, all in two lines of code. I like things that don't take up very much code, so Maypole currently uses a model class system based on Class::DBI::Loader. There's no reason why someone couldn't come along and write his own Maypole::Model::Tangram, or a wrapper around any of the many other database-object mapping libraries in Perl, since Maypole is designed to allow you to do precisely that, but we have to start somewhere, and I started with Maypole::Model::CDBI.

The Template Toolkit does a fantastic job of abstracting out presentation logic from an application, and so Maypole::View::TT is a thin wrapper around that, presenting it with the objects returned from the model class. Similarly, someone might want to write Maypole::View::HTMLTemplate, but Template Toolkit does what I need.

On the controller side, Apache's mod_perl interface is a good way of interacting with Perl-based Web applications. We could use CGI, and I expect to write a CGI::Maypole sometime soon, but Maypole is currently best front-ended by Apache::MVC. (In fact, Maypole was Apache::MVC before the CDBI and Apache components were more properly factored out.)


Putting it all together

Now we have the components. How do we put them all together to form our Web application? To use Apache::MVC, we need to write a driver package that Apache can talk to. For applications that just want to do search, editing, adding, deleting, and viewing data, this is very short indeed -- the whole point of Maypole is that you shouldn't be writing any code for all the common operations.

So first, we start by inheriting from Apache::MVC, and telling it what database we plan to use:


Listing 3. The simplest Maypole application
        package BeerDB;
        use base 'Apache::MVC';
        BeerDB->setup("dbi:mysql:beerdb");

That single call to setup will connect to the database, set up a single class for each table, and cause those classes to inherit from Maypole::Model::CDBI.

Next we define some configuration parameters for our application: the base URL of the site, so that we can construct links to other commands; the maximum number of rows per page in a listing before we go to paged output; and the tables we want to display -- in our example, we need to exclude the handpump table, since that is only a link table used to join the pubs and beers tables together. If we were okay displaying all the tables in the database, we wouldn't need that last line.

BeerDB->config->{uri_base} = "http://neo.trinity-house.org.uk/beerdb/";
BeerDB->config->{rows_per_page} = 10;
BeerDB->config->{display_tables} = [qw[beer brewery pub style]];

We need to tell the various packages what kind of data they're expecting, so that Class::DBI::FromCGI can automatically update the database from CGI values:


Listing 4. Describing the acceptable data
        BeerDB::Brewery->untaint_columns( printable => [qw/name notes url/] );
        BeerDB::Style->untaint_columns( printable => [qw/name notes/] );
        BeerDB::Beer->untaint_columns(
            printable => [qw/abv name price notes/],
            integer => [qw/style brewery score/],
            date =>[ qw/date/],
        );

Finally, we need to tell the classes how they relate to each other. For instance, when we view a beer and look at its brewery, we don't want to just get the numeric ID for the row in the brewery table; instead we want the name of the brewery and a link to a page that views that brewery. Similarly, we need to tell the BeerDB::Brewery class that it has many beers attached to it, so it should display a list of all its beers.

We can do this with the has_a and has_many methods in Class::DBI, but I always forget how they work, so I wrote a little module to do it for me:


Listing 5. Class::DBI::Loader::Relationship
        use Class::DBI::Loader::Relationship;
        BeerDB->config->{loader}->relationship($_) for (
            "a brewery produces beers",
            "a style defines beers",
            "a pub has beers on handpumps");

        1;

And that's all the programming that we need to do for our beer database -- roughly twenty lines of Perl code.

Of course, there's going to be a lot of templating required to present this mass of logic to the user, but fortunately, almost of all of what we need is provided by the Maypole factory default templates. So let's get the application up and running, and see what it looks like.


Plugging in and switching on

We have our driver class written. We now need to tell Apache about it, and put the templates in place. First, we put this into the Apache configuration:


Listing 6. Apache configuration for BeerDB
        <Location /beerdb>
            SetHandler perl-script
            PerlHandler BeerDB
        </Location>

And then we make two directories under the beerdb subdirectory of the Web root: custom and factory. factory should contain the templates shipped with Maypole, which we don't need to modify. We can override those templates specific to our application (there won't be that many) in custom. For instance, our custom/header will be called first by all of the main pages:


Listing 7. The custom header
        <HTML>
            <HEAD>
                <TITLE> Beer Database </TITLE>
        <META http-equiv=Content-Type content="text/html; charset=utf-8">
        <LINK title=myStyle href="/beerdb.css" type=text/css rel=stylesheet>
        </HEAD>
        <BODY>
        <DIV class="content">

We also provide a custom page called frontpage to serve as an entry point to the application. In a moment, we'll see how this comes into play.


Listing 8. The front page template
        [% INCLUDE header %]

        <h2> The beer database </h2>

        <TABLE BORDER="0" ALIGN="center" WIDTH="70%">
        [% FOR table = config.display_tables %]
        <TR>
        <TD>
        <A HREF="[%table%]/list">List by [% table %]</A>
        </TD>
        </TR>
        [% END %]
        </TABLE>

        <BR>

This simply provides links to the *class*/link page for every table in that list of displayable tables we configured earlier.

Now we are all set up, and we can point our browser at http://localhost/beerdb/beer/list and see something like this:


Figure 1. Listing of all beers
Figure 1. Listing of all beers

Our default templates provide a list page with a paged view of the rows; links to view individual entries; the ability to view related data, such as the links to the style and brewery pages; a search box; and a box for adding a new entry. If we select a brewery, we get a list of the beers produced by that brewery:


Figure 2. Hook Norton
Figure 2. Hook Norton

We can also edit records, delete records, and so on. We have a fully featured interface to our database, with only twenty lines of code.


Buttering the bread

Of course, it's only short because this is a reasonably small simple application -- we're just putting a Web front end to a database, what is sometimes called a CRUD interface, for Creating, Reading, Updating, and Deleting database records.

Real-life applications require a bit more than just browsing a database. As a sample real-life application, I developed a social networking tool, similar to Friendster or Orkut, based on Maypole. It's called Flox, partially because it flocks people together and partially because it's localised for my home town of Oxford and its university.

We'll first look at how Maypole does its stuff, then what we're going to need it to do in order to make Flox work, before finally getting into the implementation details.

Maypole is based around the idea of a request object. This is a little like the mod_perl Apache request object, but at a much higher level -- it knows about tables, classes, templates, and methods. So, for instance, the request process begins with handling the URL. This is something that Maypole itself knows nothing about; getting the URL is a function of how Maypole is being run, and so the abstract parse_location method is actually implemented in Apache::MVC as follows:


Listing 9. How the URL is parsed
        sub parse_location {
            my $self = shift;
            $self->{path} = $self->{ar}->uri;
            my $loc = $self->{ar}->location;
            $self->{path} ||= "frontpage";
            my @pi = split /\//, $self->{path};
            shift @pi while @pi and !$pi[0];
            $self->{table} = shift @pi;
            $self->{action} = shift @pi;
            $self->{args} = \@pi;

            $self->{params} = { $self->{ar}->content };
            $self->{query}  = { $self->{ar}->args };
        }

This retrieves the path from the Apache object stashed in $self->{ar}, and tries to fill the table, action, and args slots. (@pi stands for path info.)

So a URL of /brewery/view/1 would represent a call to the view method (the action) on the class controlling the brewery table, which Maypole knows is BeerDB::Brewery, and pass in the argument 1 to the args array. Additionally, any query parameters in the URL are stuck into $r->{query} and POSTed-in data is put into $r->{params}.

Now that we know a little about what the request is intended to do, we need to make sure that it's allowed to do it. First, we check that the request is "applicable" -- that is, that we're allowed to call that method over the Web. Since the view method is defined with the Perl attribute :Exported, this signifies that we can call it via a Maypole URL. BeerDB::Brewery::view inherits from Maypole::Model::Base::View, and is defined like this:

sub view :Exported {}

That's right, it has no code. We'll come back to this in a moment.

So for any action we want to perform on our database, we can simply create a method, add the :Exported attribute, and it'll be accessible by Maypole.


Listing 10. Creating a new action
        package BeerDB::Beer;

        sub drink :Exported {
            my ($self, $r) = @_;
            # Implementation is left as an exercise
            # for the interested reader.
        }

With this in place, we can go to /beer/drink/1, Maypole will pick up drink as the action, and the drink method will be called. That is, of course, if we're authenticated to do so.

The next phase in the request's journey from your browser back to your browser involves checking that access to that particular request is allowed -- you may not want everyone being able to drink all your beer. The authenticate method in Maypole is permissive by default, but can be overridden in your own class. We'll see how to do that when we develop a full-sized application.

Now that the request has cleared all the checks and hurdles, it's ready to be handed off to the model class for processing. The default process action on a model class, before it calls the action, looks at the first argument in the args slot. With /beer/view/1, we have a request object that looks a little like this:


Listing 11. Filling the request object from the URL
        $r = {
            path => "/beer/view/1",
            table => "beer",
            model_class => "BeerDB::Beer",
            action => "view",
            args => [ 1 ],
            ...
        }

This first argument is used as the row of the table to inspect. Maypole will look for a row in the beer table with the ID 1 and create a BeerDB::Beer object representing that row. This will be placed in the objects slot. process also sets the template slot to the name of the action, so our object now looks like this:


Listing 12. Adding the data to the request
        $r = {
            path => "/beer/view/1",
            table => "beer",
            model_class => "BeerDB::Beer",
            action => "view",
            args => [ ],
            objects => [ bless { id => 1, name => ... }, "BeerDB::Beer" ],
            template => "view",
            ...
        }

To simply view an object, we don't need to do any more work than have the object accessible; the actual displaying will be handled by the template. That's why there didn't need to be any code for our view method, although it did need to exist so that it could be :Exported.

How does the templating work, then? Once the request has been processed and the action method view called, it is handed off to the template class; by default, this is Maypole::View::TT, which uses the Template Toolkit. This first finds the template: it looks first in a directory specific to the class. If there's a file called beer/view, this will be used first; next, a custom template that is generic to all classes, custom/view; and finally, the factory-shipped do-the-right-thing template, factory/view.

The objects are passed into the template along with the request and a load of metadata about the class, so that you can create totally generic templates. However, the objects are also passed in with an alias: in the case of beer/view, the object is available as beer, so you can say:


Listing 13. Using an object by name
        <h2> [% beer.name %] </h2>

        Style: [% beer.style %]

But a brewery would be passed in as brewery:

        <h2> [% brewery.name %] </h2> 
Address: [% brewery.address %]

And if you went to brewery/list, where you have a whole set of breweries, these are available, naturally, as breweries:


Listing 14. Plural objects in templates
        <TABLE>
        [% FOR brewery = breweries %]
        <TR>
        <TD>[% brewery.name %] </TD>
        <TD>[% brewery.address %] </TD>
        <TD>[% brewery.url %] </TD>
        <TD>[% brewery.notes %] </TD>
        </TR>
        [% END %]
        </TABLE>

Once the request has been through the templating system, the output is then returned to the browser. Here's a summary of the process:


Figure 3. Flowchart: A user request
Figure 3. Flowchart

Fundamentals of Flox

Back to Flox. For the core of any social networking site, there are three concepts: the user, the "friendship" connection between two users, and an invitation between an existing user and one who's not yet on the system. Sure, there are communities and messages and lots of other things we could add in, but we'll start with the core of it, and we'll model these concepts with database tables.

A user will be identified by e-mail address, have a name, a profile, a password, and a status. We'll use the e-mail address as a login name. To make the invitation process easier, we'll have two groups of users: those with a status of real are real users who have accepted an invitation to the system, and those with a status of invited haven't yet taken up an invite.


Listing 15. The schema for Flox
        CREATE TABLE user (
            id int not null auto_increment primary key,
            email varchar(255),
            first_name varchar(50),
            last_name varchar(50),
            profile text,
            password varchar(255),
            status ENUM("real", "invitee")
        );

Invites can now run from one entry in the user table to another, and have an additional column for when the invite is deemed to have been rejected by force of apathy:


Listing 16. The invitation table
        CREATE TABLE invitation (
            id char(32) not null primary key,
            issuer int,
            recipient int,
            expires date
        );

When an invite is accepted, the recipient's status changes to real, and the invite is removed from the table.

For the friendship links, we'll model them in a really lossy but simple way. A link happens between one entry in the user table and another, as before, but it is either "offered" or "confirmed." If it is accepted, a reciprocal "confirmed" link will be created. As with the invitations, if it's rejected, it's simply removed from the table.

Authentication

The first thing we're going to need is authentication and the concept of a current user. Maypole has a hook for authentication that defaults to allowing everyone access to everything; we can change this by overriding the authenticate method in our subclass and examining the request to determine whether or not we should let it through.

For instance, we're going to need to allow access for people to accept and reject invitations if they're not logged in, since evidently they're accepting an invitation because they don't already have an account. So we can start our authenticate method like so:


Listing 17. A basic authentication method
        sub authenticate {
            my ($self, $r) = @_;
            return OK if $r->table eq "invitation" and
                ($r->action eq "accept" or $r->action eq "reject");
            return;
        }

Next, we want to find a cookie or some authentication tokens to identify a user. Let's assume we have a get_user method that works out the current user, if there is one, and puts it in $r->{user}. If we had this, we would be able to say:


Listing 18. Authentication with a user
        sub authenticate {
            my ($self, $r) = @_;
            return OK if $r->table eq "invitation" and
                ($r->action eq "accept" or $r->action eq "reject");
            $r->get_user;
            return OK if $r->{user};
            return;
        }

This is almost what we want, except that it would rudely dump the user to a standard Apache 403 page, instead of giving the option to log in. We want to actually proceed with the request, but redirect the user to a login form. We do this like so:


Listing 19. Redirecting to a log-in form
        sub authenticate {
            my ($self, $r) = @_;
            return OK if $r->table eq "invitation" and
                ($r->action eq "accept" or $r->action eq "reject");
            $r->get_user;
            return OK if $r->{user};

            $r->template("login");
            return OK;
        }

Thankfully, Maypole is smart enough not to do the action if something else has set the template for the request: /user/delete/2 will not delete user 2 first and then later pop up a login box to ask questions about whether you are allowed to do that. The login template itself looks something like this:


Listing 20. The login form template
        [% INCLUDE header %]

        <H2> You need to log in </H2>

        <DIV class="login">
            [% IF login_error %]
               <FONT COLOR="#FF0000"> [% login_error %] </FONT>
            [% END %]
           <FORM ACTION="/[% request.path %]" METHOD="post">
                Email address: <INPUT TYPE="text" NAME="email"> <BR>
                Password: <INPUT TYPE="password" NAME="password"> <BR>
                <INPUT TYPE="submit">
           </FORM>
        </DIV>

Notice how we use request.path to redirect the user back to where they were originally going; if the login is successful, then the second time they make this request, the authentication method will be successful and pass the request on unmolested.

The final piece of the puzzle is that get_user method, and it is provided by the Maypole plug-in module Maypole::Authentication::UserSessionCookie; this checks for a session cookie and uses Apache::Session to associate a session cookie with a logged-in user name. It also takes care of dispensing cookies to clients that don't have them, if they also provide a valid username and password.

Invitations

Most of the user viewing and editing is now relatively trivial manipulating of the templates; the hard work comes in setting up invitations and friend connections. We've already gone on a long time, so we'll only look at the invitations here; the connections are more or less a subset of the same behavior.

When a logged-in user clicks the Invite a friend button, they're taken to /invitation/create; we want this to be a form that lets them put in the name and e-mail address of a friend to whom they want to issue an invitation. This doesn't require any additional data, so we use another Maypole trick: if we request a method like create, which is not exported for the Flox::Invitation class, it doesn't immediately give up. In fact, it checks to see if there's a valid create template, and if so, just passes all that it has so far to the templater. The flowchart above was a slight simplification, and should really have looked like this:


Figure 4. Flowchart: Calling templates without action
Figure 4. Flowchart: Calling templates without action

If we need to display something on the request form that's specific to the current user, we can make use of the fact that the authentication process has already defined $r->{user} for us, and this is accessible inside the template through the variable [% request.user %].

The "invite user" form submits to /invitation/do_edit/ -- this is the standard "back-end" record creation and editing method that applies the changes and then returns the user back to a sensible page.

Now we need to issue the invitation. The form has posted us in forename, surname, and email parameters, so we use the wonderful CGI::Untaint module to ensure that they're what we expect: forename and surname should be plain strings, whereas email needs to look like a valid e-mail address:


Listing 21. Untainting the data
        my $h = CGI::Untaint->new(%{$r->{params}});
        my (%errors, %ex);
        my %map = (email => "email",
                   forename => "printable",
                   surname => "printable");
        for (keys %map) {
            $ex{$_} = $h->extract("-as_$map{$_}" => $_)
                or $errors{$_} = $h->error;
        }

If there were any errors, we send the form back to the user with the errors in a template variable so that they can be used to highlight the problem:


Listing 22. Returning errors to the user
        if (keys %errors) {
            $r->{template_args}{message} = "There was something wrong with that...";
            $r->{template_args}{errors} = \%errors;
            $r->{template} = "issue";
            return;
        }

We need to make sure that the invited user is not already in the system. If he does exist, then there are two possibilities: first, he could be a "real" user, in which case we redirect the browser to viewing his profile:


Listing 23. When an invited user has already accepted
        my ($user) = Flox::User->search({ email => $ex{email} });
        if ($user) {
            if ($user->status eq "real") {
                $r->objects([ $user ]);
                $r->template("view");
                $r->model_class("Flox::User");
                $r->{template_args}{message} =
                    "That user already seems to exist on Flox.
                     Is this the one you meant?";

Notice that we're essentially changing the nature of the request. We could do this by redirecting the browser to /user/view/ and the ID of the user we found, but doing it this way allows us to add an additional message to the template.

The other possibility is if they're already an invited user, but haven't accepted the invitation from someone else yet.


Listing 24. When an invited user has not yet accepted
            } else {
                # Put it back to the form
                $r->{template_args}{message} = "That user has already been invited;
                                                please wait for them to accept";
                $r->{template} = "issue";
            }
            return;
        }

Now we know we have a new user, so we can create a record in the user table with the status of invitee:


Listing 25. Creating a new user
        my $new_user = Flox::User->create({
            email => $ex{email},
            first_name => $ex{forename},
            last_name  => $ex{surname},
            status => "invitee"
        });

We then send off a mail asking them to visit /invitation/accept/$ID, where ID is a relatively secure 32-bit cookie for the ID of the invitation; we also create the row in the invitation table:


Listing 26. Adding the new invitation object
        Flox::Invitation->create({
            id => $cookie
            issuer => $r->{user},
            recipient => $new_user,
            expires => Time::Piece->new(time + LIFETIME)->datetime
        });

Finally, we set the template back to view and the class back to Flox::User so that we can redirect the user to viewing his own profile again:


Listing 27. Sending the user back to his profile
        $r->objects([ $r->{user} ]);
        $r->template("view");
        $r->model_class("Flox::User");
        $r->{template_args}{message} = "Invitation sent to $ex{email}.";

The last piece of fun comes when the user goes to the URL to accept the invitation. After some basic checks to make sure that the invitation is valid, we then say:


Listing 28. Deleting the invitation and redirecting the user
            my $recipient = $invite->recipient;
            my $issuer = $invite->issuer;
            $invite->delete;
            $recipient->status("real");
            $r->{user} = $recipient;
            $r->model_class("Flox::User");
            $r->objects([ $recipient ]);
            $r->template("firsttime_edit");

This deletes the invitation, since we don't need it any more. We now set the recipient's status to real, and set things up so that we show the firsttime_edit template on the new user. This allows him to customize his profile and password, and then start making connections!


Conclusion

This article has outlined some of the principles of MVC development with Maypole, and shown specifically how Maypole can help you rapidly develop CRUD applications in Perl along with more complicated Web applications using the MVC model. Maypole is designed to allow you to put together complex enterprise database Web applications with a minimal amount of code -- it aims to do all of the structural work for you, and allow you to write the code and the templates you need to encapsulate what you want to do. And that is, I believe, how Web programming ought to be.


Resources

  • The Beer Church has a page on Ninkasi, the Mesopotamian goddess of brewing.

  • Simon maintains the Maypole home page. Read more about Maypole, the Beer database, Flox, and more at Simon's documentation page.

  • Simon's work on Maypole was funded in part by The Perl Foundation.

  • The article Rapid Web Application Deployment with Maypole (Perl.com, April 2004), also by Simon, gives more background on and examples of Maypole development.

  • Many of the components used in Kake Pugh's How to Avoid Writing Code (Perl.com, July 2003) are also used in Maypole.

  • Apache::MVC and the other Perl modules mentioned in this article all live at CPAN.

  • Learn more about the Model-View-Controller (MVC) framework from Wikipedia, the free encyclopedia.

  • You can use Maypole to allow your applications to access DB2® for Linux.

  • The IBM WebSphere® software platform is the core Web services and J2EE compatible application server with an array of deployment configurations for distributed multi-server and enterprise environments. You can download a free trial.

  • Simon's Flox social networking application is similar to Friendster and Orkut.

  • Find more Perl tips and expertise in the IBM developerWorks column Cultured Perl.

  • Find more resources for Linux developers in the developerWorks Linux zone.

  • Browse for books on these and other technical topics.

  • Develop and test your Linux applications using the latest IBM tools and middleware with a developerWorks Subscription: you get IBM software from WebSphere, DB2, Lotus, Rational, and Tivoli, and a license to use the software for 12 months, all for less money than you might think.

  • Download no-charge trial versions of selected developerWorks Subscription products that run on Linux, including WebSphere Studio Site Developer, WebSphere SDK for Web services, WebSphere Application Server, DB2 Universal Database Personal Developers Edition, Tivoli Access Manager, and Lotus Domino Server, from the Speed-start your Linux app section of developerWorks. For an even speedier start, help yourself to a product-by-product collection of how-to articles and tech support.

About the author

Simon Cozens is a Perl programmer and author. He has released more than 90 Perl modules. His books include Beginning Perl, Extending and Embedding Perl, and the forthcoming second edition of Advanced Perl Programming. He also maintains the Perl.com site for O'Reilly & Associates. When not stuck in front of a computer, he enjoys making music, playing the Japanese game of Go, and teaching at his local church. You can reach Simon at simon@simon-cozens.org.

Report abuse help

Report abuse

Thank you. This entry has been flagged for moderator attention.


Report abuse help

Report abuse

Report abuse submission failed. Please try again later.


developerWorks: Sign in


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

Choose your display name

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


Rate this article

Comments

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Linux, Web development, Open source
ArticleID=11397
ArticleTitle=Build Web apps with Maypole
publish-date=05252004
author1-email=simon@simon-cozens.org
author1-email-cc=tomyoung@us.ibm.com