App::Office::Contacts - A web-based contacts manager


The scripts discussed here, contacts.cgi and contacts.psgi, are shipped with this module.

A classic CGI script, contacts.cgi:


        use strict;
        use warnings;

        use CGI;
        use CGI::Application::Dispatch;

        # ---------------------

        my($cgi) = CGI -> new;

        CGI::Application::Dispatch -> dispatch
                args_to_new => {QUERY => $cgi},
                prefix      => 'App::Office::Contacts::Controller',
                table       =>
                ''              => {app => 'Initialize', rm => 'display'},
                ':app'          => {rm => 'display'},
                ':app/:rm/:id?' => {},

A Plack script, contacts.psgi:


        use strict;
        use warnings;

        use CGI::Application::Dispatch::PSGI;

        use Plack::Builder;

        # ---------------------

        my($app) = CGI::Application::Dispatch -> as_psgi
                prefix => 'App::Office::Contacts::Controller',
                table  =>
                ''              => {app => 'Initialize', rm => 'display'},
                ':app'          => {rm => 'display'},
                ':app/:rm/:id?' => {},

                enable "Plack::Middleware::Static",
                path => qr!^/(assets|favicon|yui)/!,
                root => '/var/www';

For more on Plack, see My intro to Plack.


App::Office::Contacts implements a web-based, private and group, contacts manager.

App::Office::Contacts uses Moose.

Once such a structure is in place, then we can have multiple sites per organization, or multiple occupations per person, or multiple donations per entity (person or organization).

For the latter, see App::Office::Contacts::Donations.


This module is available as a Unix-style distro (*.tgz).

See for help on unpacking and installing distros.

Installation Pre-requisites

A note to beginners

At various places I refer to a file, lib/App/Office/Contacts/.htoffice.contacts.conf, shipped in this distro.

Please realize that if you edit this file, you must ensure the copy you are editing is the one used by the code at run-time.

After a module such as this is installed, the code will look for that file in the directory where Build.PL or Makefile.PL has installed the code.

The module which reads the file is App::Office::Contacts::Util::Config.

Both Build.PL or Makefile.PL install .htoffice.contacts.conf along with the Perl modules.

So, if you unpack the distro and edit the file within the unpacked code, you'll still need to copy the patched version into the installed code's directory structure.

There is no need to restart your web server after updating this file.

The Yahoo User Interface (YUI)

This module does not ship with YUI. You can get it from:

Most development was done using V 2.8.0r4. The original work was done with an earlier version of YUI.

Currently, I have no plans to port this code to V 3 of YUI.

See lib/App/Office/Contacts/.htoffice.contacts.conf, around line 70, where it specifies the URL used by the code to access the YUI.

The database server

I use Postgres.

So, I create a user and a database, via psql, using:

        shell>psql -U postgres
        psql>create role contact login password 'contact';
        psql>create database contacts owner contact encoding 'UTF8';

Then, to view the database after using the shipped Perl scripts to create and populate it:

        shell>psql -U contact contacts

If you use another server, patch lib/App/Office/Contacts/.htoffice.contacts.conf, around lines 22 and 36, where it specifies the database DSN and the CGI::Session driver.

Installing the module

Install App::Office::Contacts as you would for any Perl module:


        cpanm App::Office::Contacts

or run

        sudo cpan App::Office::Contacts

or unpack the distro, and then either:

        perl Build.PL
        ./Build test
        sudo ./Build install


        perl Makefile.PL
        make (or dmake)
        make test
        make install

Either way, you need to install all the other files which are shipped in the distro.

Install the HTML::Template files

Copy the distro's htdocs/assets/ directory to your web server's doc root.

Specifically, my doc root is /var/www/, so I end up with /var/www/assets/.

Install the FAQ web page

In lib/App/Office/Contacts/.htoffice.contacts.conf there is a line:


This page is displayed when the user clicks FAQ on the About tab.

A sample page is shipped in docs/html/contacts.faq.html. It has been built from docs/pod/contacts.faq.pod.

So, copy the latter into your web server's doc root, or generate another version of the page, using docs/pod/contacts.faq.pod as input.

Install the trivial CGI script and the Plack script

Copy the distro's httpd/cgi-bin/office/ directory to your web server's cgi-bin/ directory, and make contacts.cgi executable.

My cgi-bin/ dir is /usr/lib/cgi-bin/, so I end up with /usr/lib/cgi-bin/office/contacts.cgi.

Now I can run (but not yet!).

Creating and populating the database

The distro contains a set of text files which are used to populate constant tables. All such data is in the data/ directory.

This data is loaded into the 'contacts' database using programs in the distro. All such programs are in the scripts/ directory.

After unpacking the distro, create and populate the database:

        shell>cd App-Office-Contacts-1.00
        # Naturally, you only drop /pre-existing/ tables :-),
        # so use later, when re-building the db.
        #shell>perl -Ilib scripts/ -v
        shell>perl -Ilib scripts/ -v
        shell>perl -Ilib scripts/ -v
        shell>perl -Ilib scripts/ -v
        shell>perl -Ilib scripts/ -v

Note: The '-Ilib' means 2 things:

Perl looks in the current directory structure for the modules

That is, Perl does not use the installed version of the code, if any.

The code looks in the current directory structure for .htoffice.contacts.conf

That is, it does not use the installed version of this file, if any.

So, if you leave out the '-Ilib', Perl will use the version of the code which has been formally installed, and then the code will look in the same place for .htoffice.contacts.conf.

Start testing

Point your broswer at

Your first search can then be just 'a', without the quotes.

Files not shipped with this distro


This code has been written, and runs in a private module, Local::Contacts.

Local::Contacts actually contains all of App::Office::Contacts, including donations, importing vCards, occupations, sites and a start to sticky label printing.

Local::Contacts has been re-written to split it into several modules, which are being released one at a time, and to remove the Apache-specific code, and to start using REST [1] as a way of structuring path infos.


App::Office::Contacts::Donations will be released shortly.


Much of this code has already been written, but is not yet grafted in from Local::Contacts.

See scripts/ (not yet shipped) for details. This program creates data/label_brands.txt and data/label_codes.txt.

These text files are then imported when running scripts/

App::Office::Contacts::Export::StickyLabels will be released shortly.


This code has also been written, but is not yet grafted in from Local::Contacts.

App::Office::Contacts::Import::vCards will be released shortly.


This code has also been written, but is not yet grafted in from Local::Contacts.

The country/state/locality/postcode (zipcode) data will be shipped in SQLite format, as part of App::Office::Contacts::Sites.

Data for Australia and America with be included in the distro.

Note: The country/etc data is imported into whatever database you choose to use for your contacts database, even if that's another SQLite database.

App::Office::Contacts::Sites will be released shortly.

Lastly, the occupations per person code is not being shipped yet.


I found a bug! Some subs are called twice! See the log!

Nope, wrong again. These subs are meant to be called twice.

        ron@zoe:~/perl.modules/App-Office-Contacts$ ack build_notes_js
        104: my($organization_notes_js)  = $self -> param('view') -> notes -> build_notes_js('organization');
        105: my($person_notes_js)        = $self -> param('view') -> notes -> build_notes_js('person');

This applies to build_donations_js and build_notes_js, at lease.

The Report Type menu contains the wrong entries

Perhaps you have not dropped and created the tables properly.

You should edit these files (all in the App::Office::Contacts::Donations scripts/ directory), to suit yourself, and then run them in this order:


This last one is for testing only, of course.

The thing to note is that scripts/populate.all, when processing the reports table, only adds records which are not there already. It can do this because data/reports.txt for App::Office::Contacts contains 1 record ('Records'), but data/reports.txt for App::Office::Contacts::Donations only contains records pertaining to donations.

This still means that the entries in the reports table must be names exactly as expected by the if statement in report.js, function report_onsubmit(). You have been warned.

The corresponding Perl code is in App::Office::Contacts::View::Report and App::Office::Contacts::Donations::View::Report.

Yes, but Contacts can now see the Donations report types

Ahh, yes. That is a design fault.

How is the code structured?

MVC (Model-View-Controller).

The sample scripts contacts.cgi and contacts use

        prefix => 'App::Office::Contacts::Controller'

so the files in lib/App/Office/Contacts/Controller are the modules which are run to respond to http requests.

Files in lib/App/Office/Contacts/View implement views, and those in lib/App/Office/Contacts/Database implement the model.

Files in lib/App/Office/Contacts/Util are a mixture:

This is used by all code.

This is just used to create tables, populate them, and drop them.

Hence it won't be used by CGI scripts, unless you write such a script yourself.

This is used to validate CGI form data.

Why did you use Sub::Exporter?

The way I wrote the code, various pairs of classes, e.g. App::Office::Contacts::Controller::Notes and App::Office::Contacts::Donations::Controller::Notes, could share a lot of code, but they had incompatible parents. Sub::Exporter solved this problem.

It may happen that one day the code is restructured to solve this differently.

It seems you use singular words for the names of arrays and array refs.

Yes I do. I think in terms of the nature of each element, not the storage mechanism.

I have switched to plurals for the names of database tables though.

What's the database schema?

See docs/contacts.schema.png.

The file was created with ships with GraphViz::DBI. I patched it to use GraphViz::DBI::General.

Does the database server have pre-requisites?

The code is DBI-based, of course.

Also, the code assumes the database server supports $dbh -> last_insert_id(undef, undef, $table_name, undef).

How do I add tables to the schema?

Do all of these things:

Choose a new name which does not conflict with names used by Ron's add-on packages!
Add the table's name to data/table_names.txt

The table names in this file are stored in the table called table_names, in the order read in from this file.

Do not change the order of records in this file if you are going to update the table_names table without recreating the database.

The code has to know which table has which id in that table (table_names), so that donations, notes and sites can be associated with the correct table, and with the correct id within that table.

Add the table's initialization code to App::Office::Contacts::Util::Create

You'll need code to create, drop and (perhaps) populate your new table.

There are many examples already in that module.

Add the table's name to the table called table_names

Do this with a one-off SQL statement, or by following the instructions above about creating and populating the database.

Add your code to utilize the new table
Please explain the program, text file, and database table names

Programs are shipped in scripts/, and data files in data/.

I prefer to use '.' to separate words in the names of programs.

However, for database table names, I use '_' in case '.' would case problems.

Programs such as and, use table names for their data files' names. Hence the '_' in the names of their data files.

What do I need to know about the Close tab/Delete/Notes/Sites/Update buttons?

These buttons are at the bottom of the detail forms for entities (i.e. People and Organizations).

They are deliberately in (English) alphabetical order, left-to-right.

So, if the Donations add-on is installed, the Donations button will be between the Delete and Notes buttons.

Where do I get data for Localities and Postcodes?

In Australia, a list of localities and postcodes is available from

In America, you can buy a list from companies such as, who are an official re-seller of US Mail's database.

Is printing supported?

Subsets of the entities can be selected for printing to sticky labels.

A huge range of labels is supported via PostScript::MailLabels.

Printing will be shipped as App::Office::Contacts::Export::StickyLabels.

Will you re-write it to use a different Javascript library?

No, that would be an unproductive use of my time.

Other such libraries might do a good job, but I don't believe they'll do a better job.

I have published a review of various Javascript libraries [1], and IMHO YUI is the best.


What's with user_id and creator_id?

Ahhh, you've been reading the source code, eh? Well done!

Originally (i.e. in my home-use module Local::Contacts), users had to log on to use this code.

So, there was a known user at all times, and the modules used user_id to identify that user.

Then, when records in (some) tables were created, the value of user_id was stored in the creator_id field.

Now I take the view that you should implement Single Sign-on, meaning this set of modules is never responsible to tracking who's logged on.

Hence this line in App::Office::Contacts::Controller:

        $self -> param(user_id => 0); # 0 means we don't have anyone logged on.

That in turn means there is now no knowledge of the id of the user who is logged on, if any.

To match this, various table definitions have been changed, so that instead of App::Office::Contacts::Util::Create using:

        creator_id integer not null, references people(id),

the code says:

        creator_id integer not null,

This allows a user_id of 0 to be stored in those tables.

Also, the transaction logging code (since deleted) could identify the user who made each edit.

What's special about Person id == 1?

Originally, I stored my own name in the people table, with an id of 1. Well, this was good for testing, if nothing else.

And there was code in App::Office::Contacts::Database::Person, sub get_people(), to ensure searches would return my name, when it matched the search key.

In other places, updates and deletes of the person with id == 1 were forbidden.

Also, code in App::Office::Contacts::Database::Organization, sub get_organizations(), ensured that when I was logged on, my searches would return all organizations which matched the search key.

The effect was to override the code which implemented private address books. And this was for the purpose of providing support for users of the code.

Such code has been commented out. I did not delete it in case it needs to be re-activated in certain circumstances.

I suggest, now, that the Help tab should point to a web page giving details of whatever support you offer.

What about Organization id == 1?

In a similar manner (to Person id == 1), there is a special organization with id == 1, whose name is '-'.

Code relating to this organization has not been commented out.

Do not delete this organization! It is needed.

You can search for all such special code with 'ack Special'. ack is part of App::Ack.

What data files have fake data in them?


Email the author, or log a bug on RT:


App::Office::Contacts was written by Ron Savage <> in 2009.

Home page:


Australian copyright (c) 2009, Ron Savage. All Programs of mine are 'OSI Certified Open Source Software'; you can redistribute them and/or modify them under the terms of The Artistic License, a copy of which is available at: