Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

Managing the House with Perl


October, 2003: Managing the House with Perl

Managing the House with Perl

The Perl Journal October, 2003

By Simon Cozens

Simon is a freelance programmer and author, whose titles include Beginning Perl (Wrox Press, 2000) and Extending and Embedding Perl (Manning Publications, 2002). He's the creator of over 30 CPAN modules and a former Parrot pumpking. Simon can be reached at simon@ simon-cozens.org.


These days, a lot of the code that I write for myself, out of work time, comes as a result of changes in my life situation. When I went to Japan for a month, I wrote some code that helped me maintain a diary and newsletter. Recently, I've moved house, and now have the joy of housemates again.

On top of everything else, this means all sorts of daily tasks require additional administration—bills need to be divided up, the house network needs better organization, there needs to be a shopping list for communally bought items, and so on. Being a lazy hacker, I shun additional administration and code around it. And since there are quite a few overly geeky houseshares around who might benefit from automating their admin, I took the time to write HouseShare.pm.

What It Does

HouseShare is a web-based administration system for a shared house. When it's completely finished, it will look after the network, the phone bill, the shopping list, and pass messages and information between housemates. At the moment, it does a reasonable chunk of those things.

When you first connect to HouseShare with your web browser, you'll see a menu like Figure 1.

Here, you see the front page for the Trinity House (that's my geekhouse) installation of HouseShare. At the bottom of the page are the latest blog entries—notes that all housemates should see.

Let's add a new computer to the house network by following the link on the computer icon. This presents us with a list of the currently configured computers, and prompts us for information about a new one. (See Figure 2.)

You'll notice that the system also suggests the next available IP address for us. Hosts on the network can be renamed, reconfigured, or deleted; changes to the network will be reflected in the DNS server, which is controlled by the whole HouseShare application.

HouseShare is a modular system and additional components can easily be added and updated. The phone bill and communal- shopping modules haven't yet been written, although they have been designed and I'll talk about their operation later on, but they will slot in with one additional database table and an additional Perl module each.

How It Does It

The heart of the HouseShare system is a combination of two of the Perl modules I talk about most in these columns: Class::DBI and the Template Toolkit. As Kake Pugh points out (http://www.perl.com/pub/a/2003/07/15/nocode.html), these two modules are almost made for each other, allowing you to go straight from a database to HTML with very little Perl in the middle.

Most of the magic that runs HouseShare is done in the appropriately named HouseShare::Magic class. This is a subclass of Class::DBI, which all the HouseShare classes inherit from. Its job is to provide all the bridging code necessary to get from the database to HTML output.

One of the most important methods it provides, for instance, is list. The various listing pages for computers, users, and so on is all provided by this one list method in HouseShare::Magic. Even more interestingly, most of the pages are produced by exactly the same Template Toolkit template. This raises an obvious question: How does the template know whether or not it's dealing with a user, a computer, or something else?

Class::DBI helps with some of this, providing methods like columns which return a list of a database table's columns. If we tell the template the names of each table's columns, we can write code like this to turn a list of objects into a table:

[% FOR item = objects;
    "<tr>";
    FOR col = classmetadata.columns;
        NEXT IF col == "id";
        "<td>";item.$col;"</td>";
    END; 
    button(item, "edit");
    button(item, "delete");
    "</tr>";
END %]

Another very useful piece of code is UNIVERSAL::moniker, which adds two methods to every class: moniker and plural_moniker. These methods transform a class name like HouseShare::Computer into computer and computers, respectively.

Now code like:

<h2> Listing of all [% classmetadata.plural %]</h2>

will say "Listing of all computers" and "listing of all users." If we have a class like HouseShare::PhoneNumber, which represents numbers that users have registered as having called recently, we can override the moniker and plural_moniker methods appropriately:

package HouseShare::PhoneNumber;
sub plural_moniker { "recently called phone numbers" }
sub moniker { "phone number" }

and the same template will still make sense.

HouseShare::Magic contains a do-everything templating method, process, which finds the templates, sets up the Template object, and creates a default set of arguments for it to use. The more interesting of these are classmetadata. We've already seen the use of columns and plural; here's the classmetadata argument in full:

$args->{classmetadata} = {
    name => $class,
    columns => [ $class->columns ],
    colnames => { $class->column_names },
    moniker => $class->moniker,
    plural  => $class->plural_moniker,
    description => $class->description
};

Two methods in that metadata section may not be immediately recognizable: description and column_names are provided by HouseShare::Magic itself, and are supposed to be overridden in child classes. column_names maps a database table's columns to names that are sensible for display; the default implementation just uppercases the first character:

sub column_names { 
    my $class = shift; 
    map { $_ => ucfirst $_ } $class->columns 
}

However, for some classes, you'll want to specify more human-readable column names. For instance, in the computer table, the column for the IP address is ip. With the default version of column_names, this will come out as "Ip," which is horrific. Instead, we provide a better mapping:

sub column_names {
    ip => "IP Address",
    hostname => "Hostname",
    owner => "Owner",
    comment => "Comment"
}

Now our table can have a nice, friendly header:

<TR>
[% FOR col = classmetadata.columns.list;
    NEXT IF col == "id";
    "<TH>"; classmetadata.colnames.$col; "</TH>";
END %]

Similarly, description provides a human-readable description of what the class represents.

This more or less covers what process does, and everything else that spits out HTML is implemented in terms of that. For instance, the list method that produces the lists of things just looks like this:

sub list {
    my $class = shift;
    $class->process("list", { objects => [$class->retrieve_all] });
}

This code looks for a template called "list," and passes as additional arguments to an array called objects, which are all the table's rows.

As we've seen with our list template, we then go through all the columns of this class's database table, and ask each object for its details. This works perfectly for things such as comments and IP addresses, but when I asked a computer for its owner, I was surprised to see that my computers had an owner of "1", rather than "simon."

This is because, in the database schema, the owner is stored as a foreign key into the users table. We've set up a Class::DBI has-a relationship to say that each computer has an owner, and therefore, quite correctly, calling owner on the HouseShare::Computer object that returns a HouseShare::User.

Unfortunately, this object stringifies to the ID, which is not what we want. (At least it doesn't stringify to HouseShare::User=HASH(0xgarbage), which would be even less useful.) We want to display the actual username.

There are two ways we could do this. I did it first a good way, and this helped me to see a better way. The good way is to allow each class to override the default template. We do this in the magic template processing method by providing a series of template search paths:

my ($class, $name, $args) = @_;
my $template = Template->new({ INCLUDE_PATH => [
    File::Spec->catdir($HouseShare::templatehome, $class->moniker),
    File::Spec->catdir($HouseShare::templatehome, "custom"),
    File::Spec->catdir($HouseShare::templatehome, "factory")
]});

This means, if we call HouseShare::Computer->list, Template Toolkit will first look for templates in the /opt/houseshare/templates/computer directory, then in /opt/houseshare/templates/custom, (where installation-specific customizations can be made), and finally, in /opt/houseshare/templates/factory, where the factory settings are found. This allowed me to put code into templates/computer/list to fiddle with the owner column:

IF col == "owner"; item.owner.username; ELSE; item.$col; END;

Now we can have our HouseShare::Computer class-specific templates in one location, out of the way. That was the good way.

The better way is to realize that Class::DBI is only trying to be helpful when it stringifies a HouseShare::User object to the ID, and it could easily be persuaded to stringify it to something more useful instead. So, putting the following code in HouseShare::User:

__PACKAGE__->columns(Stringify => qw[ username ]);

solves the problem without having to mess with specific templates.

Editing Records

So much for displaying things. What about editing them? Well, there's the wonderful Class::DBI::FromCGI method, which turns a set of posted CGI form parameters into a Class::DBI object in your specified class, handling untainting via CGI::Untaint. That solves half the CGI problem, allowing you to create and update objects just by receiving form field values—the other half of the problem involves creating the CGI form in the first place. As it turns out, there's a nice, generic way we can do this, too.

In the process of writing my HouseShare application, I found myself writing the Class::DBI::AsForm module. This provides a to_cgi method, returning hash mapping columns to HTML form elements.

If we feed this hash to our template too, we can create a generic form for adding entries to a database table like so:

<h3>Add a new [%classmetadata.moniker%]</h3>
<FORM METHOD="post">
    <INPUT TYPE="hidden" NAME="action" VALUE="crate">
    <INPUT TYPE="hidden" NAME="class" VALUE="[%classmetadata.name%]">
     [% FOR col = classmetadata.columns;
        NEXT IF col == "id";
        "<b>";classmetadata.colnames.$col;"</b> : "; 
        classmetadata.cgi.$col;
        "<BR>";
        END;
    %]
    <INPUT TYPE="submit" NAME="create" VALUE="create">
</FORM>

Editing objects is very similar: Just replace the relevant row in the list table with a set of calls to to_field($col) on the object. This produces an HTML snippet for the column in question, optionally taking notice of has-a relationships. For instance, when we edit a computer, at some point, our template will do the equivalent of

object.to_field("owner")

The owner of a computer is a HouseShare::User, and to_field knows this, so it produces a drop-down box of the users, with the current owner selected:

<select name="owner">
    <option value=1 selected> simon </option>
    <option value=2> heth </option>
    ...
</select>

Hence, the add box we used to add a new computer to the network was generated completely generically, using a generic template and no special code in the computer class.

We've mentioned briefly the FromCGI module that is used to process these forms when the data is returned. Here's the code which does this, again in the generic Magic class:

sub do_edit {
    my $class = shift;
    my $r = Apache->request;
    my $obj = $class->retrieve(shift);
    my $h = CGI::Untaint->new(%{Apache::Request->new($r)->parms});
    $obj->update_from_cgi($h);
    $class->list;
}

I've removed some of the error-checking code for the purposes of clarity: We'll be passed in an object ID by the front-end handler, and then CGI::Untaint reads and verifies the CGI form parameters. Sending this CGI::Untaint object to the update_from_cgi method, as provided by Class::DBI::FromCGI does the rest, and we direct the user back to the list page.

Identifying Users

What other information do we feed to our magical process method? You'll notice that at the top right-hand corner of our page, there was a little box with our name, demonstrating that the system had recognized the current user. This is done by passing in a HouseShare::User instance into the template arguments:

$args->{me} = HouseShare::User->me;

The me method tries to work out which of the housemates the remote user viewing the page actually is. How can we do that? Well, given that we know about all of their computers, and we can determine which IP address their browser is connected from, we can tell who owns the computer making the request. This is obviously a weak form of authentication, but in a house share situation where everyone has physical access to each other's kneecaps—sorry, I mean, computers—there's not much point in having any stronger authentication.

On the other hand, it is important to ensure that this request is actually coming from inside the house's network. The last thing you want is some random stranger out there on the Internet messing with your milk budget. To demonstrate the HouseShare system to the world at large, I added a demo mode which means that people can access and view the web site, but not change anything.

To work out who the user is, we start by knowing their IP address — information we get from the environment:

sub my_ip {
    return $ENV{'REMOTE_ADDR'} || 
    inet_ntoa(scalar gethostbyname(hostname() || "localhost"));
}

The first line checks the REMOTE_ADDR as set by the web server; the second line assures that this function will still work properly when HouseShare routines are called from the command line. As well as helping with debugging, we'll see later that this overcomes an interesting problem.

Now we can ask the main HouseShare module for the house's network and construct a NetAddr::IP representing the network range:

sub me {
    my $class = shift;

    my $net = NetAddr::IP->new(HouseShare->config->{network});

Now, if our IP address is not in this range, we switch to demo mode and don't return a user:

if (!$net->contains(NetAddr::IP->new($class->my_ip))) {
    $HouseShare::demo = 1; 
    return;
}

and now we can see if we have a computer in the house registered to this IP:

if (my @computer = HouseShare::Computer->search({ip => $class->my_ip})) {
    return $computer[0]->owner;
}

The owner method will, quite properly, return a HouseShare::User, so our work is done.

Now comes the interesting problem. Suppose, you've just installed HouseShare, and you go to the web site and want to start adding computers and users. Unfortunately, the computer doesn't currently know who you are—and it can't look you up by computer, because you don't have any computers registered either! To bootstrap the system, the installation program prompts for the first user's information and creates a HouseShare::User object for them. From then on, any access from an unregistered IP address inside the network is assumed to be from this first "administrator" user:

my @users = $class->retrieve_all;
return $users[0];

And that, basically, is the HouseShare::User class.

The Front End

Finally, in our tour of the HouseShare classes, let's look at the front-end mod_perl handler, which ties the whole system together.

Here's the entire handler:

sub handler {
    my $r = shift;
    my @pi = split /\//, $r->uri();
    shift @pi while @pi and !$pi[0];
    @pi = qw(user process frontpage) unless @pi; 
    my $class = "HouseShare::".ucfirst(shift(@pi));
    my $method = shift @pi || "present";
    return DECLINED if !$class->require || !$class->can($method);
    $r->send_http_header("text/html");
    $class->$method(@pi);
    return OK; 
}    

I will admit that this code is a little insecure, although it's not easy to see how to exploit it—you'd have to find a dangerous class method in a HouseShare class. This is not the way I would recommend you code, but it was a neat hack. The idea is that the URL http://houseshare.my.house/computer/edit/5 gets turned into HouseShare::Computer->edit(5);.

If there isn't a method, we call the generic method present; this means that things like the HouseShare::Blog class (based on Bryar, the subject of some of my previous articles) can be accessed from http://houseshare.my.house/blog.

And if there isn't even a class, such as when we hit the front page, we're effectively sent to HouseShare::User->present("frontpage"), which displays the frontpage template. (The choice of the user class to do this is somewhat arbitrary.)

One thing you might notice about that handler routine is that it's simple and compact, a theme that runs through the whole system—in fact, the system currently weighs in at only 250 lines of actual Perl code, and 300 lines in the templates. All the heavy lifting is either done with existing modules or abstracting tasks away to a generic layer, such as the Magic class.

Doing Real Work

So far, we've discussed a lot of infrastructure — a framework for doing neat things with databases and the Web. However, it's now very trivial to build on top of this framework to add "real work" functionality to HouseShare.

For instance, one piece of real work we can do with HouseShare::Computer is to update the DNS tables when a Computer object is added or modified. Thankfully, with Class::DBI's trigger support, this is a very simple matter. Assuming we have a subroutine called build_zonefile which does the equivalent of:

print $_->hostname, " IN A ", $_->ip, "\n"
    for HouseShare::Computer->retrieve_all;

(except, naturally, with a little more smarts...) we can trivially arrange for this to be called when anything changes:

__PACKAGE__->add_trigger(after_update => \&build_zone	file);
__PACKAGE__->add_trigger(after_create => \&build_zone	file);

Now new hosts will automatically be added to the DNS server, and updates will automatically be reflected—as usual, with only a tiny bit of code.

What It Will Do Soon

HouseShare currently does around 50 percent of what I would like to see it do. It was very simple to plug in HouseShare::Blog as a 20-line subclass of Bryar to add a blog to the site, and an Apache::MP3 instance to share music around the house; I also plan to add a CGI::Wiki wiki to share information about who to call when the power fails, and so on.

The next big step will be integrating Tony Bowden's Data::BT::PhoneBill to download and parse phone-bill data. The methodology here is similar to what we've seen so far: A Phonenumbers class and database table will register known numbers and associate them with people who are likely to have called them, and then a method on that class will grab the data, share out the cost, and process a template displaying the results.

The final piece (for now) will be something to record purchases of house essentials and share the cost between the housemates; again, this will be a simple class with a database table, and a class method to do the work. This simple approach can be used to extended the system to add all kinds of functionality—in fact, the framework we've drawn up can be used in a huge number of applications, and we use a (admittedly, more complex) variant of the idea at work as a basis for all kinds of e-commerce sites.

HouseShare is available from my CVS repository at http://cvs.simon-cozens.org/viewcvs.cgi/?cvsroot=HouseShare; it's currently a little underdocumented but e-mail me if you want to install it and hack on it and I'll help you through it.

I sometimes think that HouseShare is a little bit overkill for the job it does; it's currently slightly more useful than a whiteboard in the hall. But it's been a lot of fun, which is the main thing, and in the course of writing it, I've learned a lot about Class::DBI, Template Toolkit, abstracting functionality away, and making generic templates do a wide range of tasks. I hope through reading this, you have, too.

TPJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.