Home | Wiki | OI 1.x Docs | OI 2.x Docs OI logo

NAME

OpenInteract2::Manual::Tutorial - Learn how to create and modify a package

SYNOPSIS

This tutorial will show you the different methods for creating a package and how to maintain them.

PREREQUISITES

What's a package?

Before you start developing one you should really know what a package is. Go read OpenInteract2::Manual::Packages and come back when you're done.

Install OI2

Not only must OI2 be installed, but you also need to either have access to the distribution source or your friendly local sysadmin needs to create a source directory. This is as simple as:

  $ oi2_manage create_source_dir \
       --distribution_dir=/private/path/to/OpenInteract-2.00 \
       --source_dir=/public/path/to/oi_source

Create a website

If you haven't created a website yet do so now. Check out OpenInteract2::Manual::QuickStart for a guide to getting a site up and running in just a few minutes.

Once it's created, go ahead and set the OPENINTERACT2 environment variable to the full path to the site. For instance, in bash you would use:

  $ export OPENINTERACT2=/path/to/mysite

CREATING THE PACKAGE

A word on the example

For our example we're going to create a 'book' package. This will keep track of all our books and allow us to search our library, add new books, update existing ones and remove old ones. It won't be the backbone for a massive e-commerce website to make you lots of money. It does not attempt to best model the relationships for all the data about a book.

Generating the skeleton

OpenInteract comes with tools to create a skeleton package -- we don't want to do all this from scratch! The skeleton package has the directory structure, metadata and a number of files to get you going on your new package. Here's how to create one -- be sure to first go to the directory under which the package will be created:

  $ oi2_manage create_package \
       --source_dir=/public/path/to/oi_source \
       --package=book

And here's what you'll see:

  PROGRESS: Starting task
  PROGRESS: Task complete
  ACTION: Create package book
      OK:     Package book created ok in /path/to/my/book

And now let's see what it created:

  $ find book/
  book/
  book/conf
  book/conf/spops.ini
  book/conf/action.ini
  book/data
  book/doc
  book/doc/book.pod
  book/struct
  book/template
  book/template/sample.tmpl
  book/script
  book/html
  book/html/images
  book/OpenInteract2
  book/OpenInteract2/Action
  book/OpenInteract2/Action/Book.pm
  book/OpenInteract2/SQLInstall
  book/OpenInteract2/SQLInstall/Book.pm
  book/package.conf
  book/MANIFEST.SKIP
  book/Changes
  book/MANIFEST

These files and directories are explained in OpenInteract2::Manual::Packages.

You will normally need to edit/add the following:

  book/package.conf              # Add name, version, author information
  book/MANIFEST                  # Add names of distribution files
  book/conf/spops.ini            # Describe the objects your package uses
  book/conf/action.ini           # Map URLs to handlers in your package
  book/data                      # Specify the initial data and security
  book/struct                    # Describe the tables used to store your objects
  book/template                  # HTML to display and manipulate your objects
  book/OpenInteract2             # Optional Perl modules defining object behavior
  book/OpenInteract2/Action      # Manipulate objects for desired functionality
  book/OpenInteract2/SQLInstall  # Tell the installer about your tables, data, security
  book/doc/book.pod              # Last but not least, tell the world about it

Short sidebar: Creating a MANIFEST

Notice that we create a MANIFEST file for you when the package is created. As you add more files to your package you'll need to add them to your book/MANIFEST. Fortunately, it can be created automatically:

  $ cd /path/to/mypackage
  $ perl -MExtUtils::Manifest -e 'ExtUtils::Manifest::mkmanifest()'

That's it! If you have an old 'MANIFEST' file in the directory it will be copied to 'MANIFEST.bak'. Also note that files matching patterns in the book/MANIFEST.SKIP file will not be included.

DEFINE STRUCTURES

I don't know about you but I feel more grounded in reality when I start with data structures. Some people like to work the other way around and start with the views of the data. If you're like that you can read that part first.

Our table

We'll just make this very easy to start out with. Unlike the venerable intro database shipped with Sybase products, we're not defining separate tables for authors, publishers, book artists, etc. We'll just define a single table and make some assumptions. Here's the table that we'll save to book/struct/book.sql:

  CREATE TABLE book (
     book_id          %%INCREMENT%%,
     author_last      varchar(30) not null,
     author_first     varchar(30) null,
     title            varchar(100) not null,
     publisher        varchar(50) null,
     publish_year     varchar(4) not null,
     isbn             varchar(25) null,
     primary key( book_id )
  )

Note that funny %%INCREMENT%% key. That's a sign for OpenInteract to rewrite the SQL before passing it to the database. In this case we'll substitute what's appropriate for this database to create an auto-incrementing key. For sequence-based schemes we just use the sequence datatype and rely on someone else to create the sequence for us. In fact, we'll go ahead and create a sequence so we can run on PostgreSQL. Save the following to book/struct/book_sequence.sql:

  CREATE SEQUENCE book_seq

Letting the installer know

Now that we've defined the table and sequence, how do we let OI2 know what to install with our package? Every package that installs data structures (tables, sequences, indexes, etc.), security information and/or initial data will have a SQL installer class. In our case it's found in book/OpenInteract2/SQLInstall/Book.pm. Here's what it looks like when it's first generated:

   1: package OpenInteract2::SQLInstall::Book;
   2: 
   3: # This is a simple example of a SQL installer you might write. It uses
   4: # your package name as the base and assumes you want to create a
   5: # separate table for Oracle users and include a sequence for Oracle
   6: # and PostgreSQL users.
   7: 
   8: # It also assumes that you're installing security of some type, either
   9: # default handler or default object.
  10: 
  11: use strict;
  12: use base qw( OpenInteract2::SQLInstall );
  13: 
  14: my %FILES = (
  15:    oracle  => [ 'book_oracle.sql',
  16:                 'book_sequence.sql' ],
  17:    pg      => [ 'book.sql',
  18:                 'book_sequence.sql' ],
  19:    default => [ 'book.sql' ],
  20: );
  21: 
  22: sub get_structure_set {
  23:     return 'book';
  24: }
  25: 
  26: sub get_structure_file {
  27:     my ( $self, $set, $type ) = @_;
  28:     return $FILES{oracle} if ( $type eq 'oracle' );
  29:     return $FILES{pg}     if ( $type eq 'Pg' );
  30:     return $FILES{default};
  31: }
  32: 
  33: # Uncomment this if you're passing along initial data
  34: 
  35: #sub get_data_file {
  36: #    return 'initial_data.dat';
  37: #}
  38: 
  39: # Uncomment this if you're using security
  40: 
  41: #sub get_security_file {
  42: #    return 'install_security.dat';
  43: #}
  44: 
  45: 1;

Cool, it's pretty much already done for us! Since we're lazy we should probably change the name of the Oracle table:

  14: my %FILES = (
  15:    oracle  => [ 'book.sql',
  16:                 'book_sequence.sql' ],
  17:    pg      => [ 'book.sql',
  18:                 'book_sequence.sql' ],
  19:    default => [ 'book.sql' ],
  20: );

We'll return to this file in OpenInteract2::Manual::TutorialAdvanced when we deal with initial data and again with action security.

Installer Process

What happens is that the main installer process will create an instance of our class. It will ask that object to perform certain actions: create structures, install data, install security. The defaults for these actions are no-ops and are defined in OpenInteract2::SQLInstall.

So when we defined get_structure_file() the main installer process knows what type of datasource our 'book' object will be and will pass that type (e.g., 'Oracle', 'Pg', 'MySQL') to our object's method. We can then decide which structure(s) to pass back.

In our case we have the main table and, for a couple databases, a separate sequence.

Let the configuration know

The class we're using as a SQL installer is held in the book/package.conf file. Peek into the file and you should see:

  ...
  sql_installer    OpenInteract2::SQLInstall::Book
  ...

Excellent, it's already set for us. Let's move on.

DECLARE THE OBJECT

Walkthrough SPOPS configuration

Now it's time to configure our persistent objects. As mentioned above we're only going to use a single 'book' object for this example. So let's look at conf/spops.ini as it's created for us:

  # This is a sample spops.ini file. Its purpose is to define the
  # objects that will be used in your package. Each of the keys is
  # commented below.
  
  # If you do not plan on defining any objects for your package, then
  # you can skip this discussion and leave the file as-is.
  
  # Note that you must edit this file by hand -- there is no web-based
  # interface for editing a package's spops.perl (or other)
  # configuration files.
  
  # You can have any number of entries in this file, although they
  # should all be members of the single hashref (any name is ok) in the
  # file.
  #
  # Finally, you can retrieve this information as a perl data structrure
  # at anytime by doing:
  #
  #   my $hashref = $object_class->CONFIG;
  # or
  #   my $hashref = CTX->lookup_object( 'object_alias' )->CONFIG;
  #
  # For more information about the SPOPS configuration process, see
  # 'perldoc SPOPS::Configure' and 'perldoc SPOPS::Configure::DBI'
  
  
  # 'book' - Defines how you can refer to the object class
  # within OpenInteract2. For portability and a host of other reasons,
  # OI sets up aliases for the SPOPS object classes so you can refer to
  # them from the context. For instance, if you are in an application
  # 'MyApp':
  #
  #  my book_class = CTX->lookup_object( 'book' );
  #  print ">> My Class: [book_class]
  #
  #  Output: '>> My Class: [OpenInteract2::Book]'
  #
  # This way, your application can do:
  #
  #  my $object = CTX->lookup_object( 'book' )->fetch( $object_id );
  #
  # and not care about the different implementations of the '' class and such.
  #
  # Note that the 'alias' key allows you to setup additional aliases for
  # this object class.
  
  #[book]
  
  # class - Defines the class this object will be known by. This is
  # almost always 'OpenInteract2::Blah'
  
  #class           = OpenInteract2::Book
  
  # code_class - Perl module from which we read subroutines into the
  # namespace of this class. This is *entirely optional*, only needed if
  # you have additional behaviors to program into our object.
  
  #code_class      = OpenInteract2::Book
  
  # isa - Define the parents of this class, generally used for enhanced
  # functionality like full-text indexing.
  #
  # Note that you DO NOT have to add OpenInteract2::SPOPS::DBI,
  # SPOPS::DBI, SPOPS::DBI::Pg, or any of the other ones that you
  # entered in OI 1.x. This is done at runtime for you, so your objects
  # will always be in sync with what databaes you're
  # using. Additionally, you specify whether the object is secured using
  # 'is_secure', below:
  
  #isa             = 
  
  # field - List of fields/properties of this object. If this is a
  # DBI-based object and you specify 'yes' for 'field_discover' below,
  # you can leave this blank
  
  #field           = id
  #field           = name
  #field           = type
  
  # field_discover - Whether to discover the fields for this object at
  # startup. (Recommended.)
  
  #field_discover  = yes
  
  # id_field - Name of primary key field -- this only identifies the
  # object uniquely. You still need to deal with generating new values,
  # either by an auto-incrementing mechanism (in which case you need to
  # use the appropriate SPOPS::DBI class) or something else.
  
  #id_field        = book_id
  
  # increment_field - Whether to use (or be aware of) auto-incrementing
  # features of your database driver.
  
  #increment_field = yes
  
  # sequence_name - If we're using a sequence (Oracle, Postgres) this is
  # the name to use.
  
  #sequence_name   = book_seq
  
  # is_secure - 'yes' if the object is protected by security, anything
  # else if not
  
  #is_secure       = yes
  
  # no_insert - Fields for which we should not try to insert
  # information, ever. If you're using a SPOPS implementation (e.g.,
  # 'SPOPS::DBI::MySQL') which generates primary key values for you, be
  # sure to put your 'id_field' value here.
  
  #no_insert       = book_id
  
  # no_update - Fields we should never update
  
  #no_update       = book_id
  
  # skip_undef - Values for these fields will not be inserted/updated at
  # all if the value within the object is undefined. This, along with
  # 'sql_defaults', allows you to specify default values. 
  
  #skip_undef      =
  
  # sql_defaults - List fields for which a default is defined. Note that
  # SPOPS::DBI will re-fetch the object after first creating it if you
  # have fields listed here to ensure that the object always reflects
  # what's in the database.
  
  #sql_defaults    =
  
  # base_table - Name of the table we store the object tinformation
  # in. Note that if you have 'db_owner' defined in your application's
  # 'server.perl' file (in the 'db_info' key), then SPOPS will prepend
  # that (along with a period) to the table name here. For instance, if
  # the db_owner is defined to 'dbo', we would use the table name
  # 'dbo.book'
  
  #base_table      = book
  
  # alias - Additional aliases to use for referring to this object
  # class. For instance, if we put 'project_book' here we'd be able to
  # retrieve this class name by using 'CTX->lookup_object( 'book )'
  # AND 'CTX->lookup_object( 'project_book' ).
  
  #alias           = 
  
  # name - Either a field name or some other method name called on your
  # object to generate a name for a particular object.
  
  #name            =
  
  # object_name - Name of this class of objects
  
  #object_name     = Book
  
  # is_searchable = 'yes' if you'd like this object to be indexed by the
  # full_text package.
  
  # is_searchable = no
  
  # fulltext_field - if you've set 'is_searchable' to 'yes' you'll need
  # to list one or more fields of your object from which to pull text to
  # index
  
  # fulltext_field = 
  # has_a - Define a 'has-a' relationship between objects from this
  # class and any number of other objects. Each key in the hashref is an
  # object class (which gets translated to your app's class when you
  # apply the package to an application) and the value is an arrayref of
  # field names. The field name determines the name of the routine
  # created: if the field name matches up with the 'id_field' of that
  # class, then we create a subroutine named for the object's
  # 'object-alias' field. If the field name does not match, we append
  # '_{object_alias}' to the end of the field. (See 'perldoc
  # SPOPS::Manual::Relationships' for more.)
  
  #[book has_a]
  #OpenInteract2::Theme = theme_id
  
  # links_to - Define a 'links-to' relationship between objects from
  # this class and any number of other objects. This may be modified
  # soon -- see 'perldoc SPOPS::Configure::DBI' for more.
  
  #[book links_to]
  #OpenInteract2::Foo = foo_book_link
  
  # creation_security - Determine the security to apply to newly created
  # objects from this class. (See 'SPOPS::Secure')
  
  #[book creation_security]
  #user  = WRITE
  #group = site_admin_group:WRITE
  #world = READ
  
  # track - Which actions should we log? Value of 'yes' does, anything
  # else does not.
  
  #[book track]
  #create = no
  #update = yes
  #remove = yes
  
  # display - Allow the object to be able to generate a URL to display
  # itself. OI2 has hooks so that you can refer to an ACTION and TASK
  # and have OI2::URL use these to create a local URL, properly
  # localized to your deployment context:
  
  #[book display]
  #ACTION = book
  #TASK   = display
  

Since we're in a tutorial we don't need the comments. So we'll strip those out and work with something more manageable:

   1: [book]
   2: class           = OpenInteract2::Book
   3: #code_class      = OpenInteract2::Book
   4: isa             = 
   5: field           = 
   6: field_discover  = yes
   7: id_field        = book_id
   8: increment_field = yes
   9: sequence_name   = book_seq
  10: is_secure       = no
  11: no_insert       = book_id
  12: no_update       = book_id
  13: skip_undef      =
  14: sql_defaults    =
  15: base_table      = book
  16: alias           = 
  17: name            = title
  18: object_name     = Book
  19: is_searchable   = no
  20: fulltext_field  = title
  21: fulltext_field  = author_first
  22: fulltext_field  = author_last
  23: 
  24: [book track]
  25: create = yes
  26: update = yes
  27: remove = yes
  28: 
  29: [book display]
  30: ACTION = book
  31: TASK   = display

On line 1 we have the declaration for this object and the next line is the class we'll create for it. Line 3 is still commented out because we don't have any custom behavior to implement yet. (We'll do so later)

Lines 4 and 5 are blank because OI fills them in for us at server startup. In particular the 'isa' field being empty here is a big change from OI 1.x if you've ever developed a package under it. You don't need to add the SPOPS implementation classes (e.g., SPOPS::DBI::Pg and SPOPS::DBI if you're using Postgres), the OI SPOPS classes or any security classes.

Line 5 is blank because we've declared 'field_discover' to 'yes' on the next line. If we hadn't we'd have to list all the fields in the object. Not listing them means we don't need to change the configuration if we add a field to the table.

Line 7 tells SPOPS what our ID field is, and line 8 says that it's an auto-incrementing field. Line 9 tells databases who use sequences (like PostgreSQL and Oracle) the name of the sequence to use for our ID field values.

Line 10 says that this object is not protected by security. If we want to protect it we just need to change this to 'yes'.

Lines 11 and 12 tell SPOPS not to insert or update the ID field. We can add more fields to this if we like -- for instance, if we have an user and timestamp automatically set by the database we'd add those fields here.

Line 13 gives SPOPS a list of fields we should skip insert/update if they're undef. This is typically used in conjunction with line 14 to have the database fill in default values for us rather than try to insert a NULL. We don't have any defaults on our table so we'll leave it as-is.

Line 15 just names our table.

Line 16 could hold an alias for our object so we could refer to it as another name (e.g., 'libro') as well.

Line 17 tells SPOPS what field or method we should use to identify this object. So when we're displaying a list of generic objects (books and blog entries, for instance) we'll display what's in the specified field or returned by the specified method.

Line 18 is just a generic name for this object. Combined with the previous line this allows us to create a listing of generic objects like:

  Type           Description
  ----------------------------------------
  Blog Entry     Why your language sucks
  Book           Programming Perl
  Blog Entry     Why my language sucks
  Book           Kiln People

Lines 19-22 tell OI whether this object is searchable and if so what fields should be searched. For now we'll turn indexing off but we've indicated that the object once the object is searchable OI will index the 'title', 'author_first' and 'author_last' fields.

Lines 24-27 tell OI what actions we want to track along with the user who performed them and the time they were done. You can get a listing of these through the browser by clicking the 'Object Activity' link in the 'Admin Tools' box.

Lines 29-31 allow us to create a URL that will display a particular object. We've added some enhancements to the basic SPOPS behavior with the 'ACTION' and 'TASK' keywords. Just as in other URL generation methods this allows you to not be tied to a particular URL, especially if you're deploying it under a URL space. (See OpenInteract2::URL for more.)

CREATE THE ACTION AND TEMPLATES

Now that we've got our persistent object defined we want to do something with it. This entails creating an action. We'll create the code first and then do the configuration.

Initial action definition

First, let's check out what the skeleton creator whipped up for us:

   1: package OpenInteract2::Action::Book;
   2: 
   3: # This is a sample action. It exists only to provide a template for
   4: # you and some notes on what these configuration variables mean.
   5: 
   6: use strict;
   7: 
   8: # All actions subclass OI2::Action or one of its subclasses
   9: 
  10: use base qw( OpenInteract2::Action );
  11: 
  12: # You almost always use these next three lines -- the first imports
  13: # the logger, the second logging constants, the third the context
  14: 
  15: use Log::Log4perl            qw( get_logger );
  16: use OpenInteract2::Constants qw( :log );
  17: use OpenInteract2::Context   qw( CTX );
  18: 
  19: # Use whatever standard you like here -- it's always nice to let CVS
  20: # deal with it :-)
  21: 
  22: $OpenInteract2::Action::Book::VERSION = sprintf("%d.%02d", q$Revision: 1.1 $ =~ /(\d+)\.(\d+)/);
  23: 
  24: # Here's an example of the simplest response...
  25: 
  26: sub hello {
  27:     my ( $self ) = @_;
  28:     return 'Hello world!';
  29: }
  30: 
  31: # Here's a more complicated example -- this will just display all the
  32: # content types in the system.
  33: 
  34: sub list {
  35:     my ( $self ) = @_;
  36: 
  37:  # This will hold the data you're passing to your template
  38: 
  39:     my %params = ();
  40: 
  41:  # Retrieve the class corresponding to the 'content_type' SPOPS
  42:  # object...
  43: 
  44:     my $type_class = CTX->lookup_object( 'content_type' );
  45:     $params{content_types} = eval { $type_class->fetch_group() };
  46: 
  47:  # If we've encountered an error in the action, add the error message
  48:  # to it. The template has a component to find the errors encountered
  49:  # and display them
  50: 
  51:     if ( $@ ) {
  52:         $self->param_add( error_msg => "Failed to fetch content types: $@" );
  53:     }
  54: 
  55:  # The template also has a component to display a status
  56:  # message. (This is a silly status message, but it's just an
  57:  # example...)
  58: 
  59:     else {
  60:         my $num_types = scalar @{ $params{content_types} };
  61:         $self->param_add( status_msg => "Fetched $num_types types successfully" );
  62:     }
  63: 
  64:  # Every action should return content. It can do this by generating
  65:  # content itself or calling another action to do so. Here we're doing
  66:  # it ourselves.
  67: 
  68:     return $self->generate_content(
  69:                     \%params, { name => 'book::sample' } );
  70: }
  71: 
  72: 1;
  73: 
  74: __END__
  75: 
  76: =head1 NAME
  77: 
  78: OpenInteract2::Action::Book - Handler for this package
  79: 
  80: =head1 SYNOPSIS
  81: 
  82: =head1 DESCRIPTION
  83: 
  84: =head1 BUGS
  85: 
  86: =head1 TO DO
  87: 
  88: =head1 SEE ALSO
  89: 
  90: =head1 AUTHORS

Excellent. We'll just leave the 'hello()' method as-is for right now, add a 'search_form()' method, and replace 'list()' with 'search()'.

And since we're deleting 'list()' you might as well delete its accompanying template book/template/sample.tmpl. We won't be needing it.

Task: Show search form

Here's our task implementation for displaying the search form:

  31: sub search_form {
  32:     my ( $self ) = @_;
  33:     return $self->generate_content( {}, { name => 'book::search_form' } );
  34: }

You can't get simpler than this: all we're doing is specifying a template to process. Note that the template we're specifying doesn't have any concrete paths associated with it. Instead we're using a 'package::template' syntax. This is for a couple reasons:

Template: Define the search form

Here's what we'll use for our search form, saved to book/template/search_form.tmpl:

   1: [%- OI.page_title( 'Search for books' ) -%]
   2: 
   3: <h2>Book Search</h2>
   4: 
   5: [% INCLUDE form_begin( ACTION = 'book', TASK = 'search' ) %]
   6: 
   7: [% INCLUDE table_bordered_begin( table_width = '50%' ) %]
   8: 
   9: [% INCLUDE label_form_text_row( label = 'Title',
  10:                                 name  = 'title',
  11:                                 size  = 30 ) %]
  12: 
  13: [% INCLUDE form_submit_row( value = 'Search' ) %]
  14: 
  15: [% INCLUDE table_bordered_end %]
  16: 
  17: [% INCLUDE form_end %]

That doesn't look much like HTML, does it? You can use HTML with the same effects, but using these HTML template widgets makes it easy to customize the display. For instance, by adding a variable 'count' and incrementing it for every row we can get the typical alternating-color look for our table. In theory we can also change the implementation behind the scenes to use stylesheets instead of tables.

Line 1 invokes the OI plugin (see OpenInteract2::ContentGenerator::TT2Plugin) to set the title of the page. Line 5 outputs the HTML to start the form, using the 'ACTION' and 'TASK' keywords to construct a URL for the 'action' attribute rather than specifying the URL ourselves. Line 7 starts the table with a border around it for the form.

Lines 9-11 define the row and input for the title the user will search for, and line 13 defines the row with the 'submit' button, here labeled 'Search'. Line 15 closes the table, and line 17 closes the form. Easy!

Task: Run a search

Now here's the slightly more complicated implementation for searching our books. Note that we're restricting it to a LIKE search on the title for right now.

  36: sub search {
  37:     my ( $self ) = @_;
  38:     my %params = ();
  39:     my $request = CTX->request;
  40:     my $title = $request->param( 'title' );
  41:     unless ( $title ) {
  42:         $self->param_add( error_msg => 'No search parameters given' );
  43:         return $self->execute({ task => 'search_form' });
  44:     }
  45:     my $book_class = eval { CTX->lookup_object( 'book' ) };
  46:     if ( $@ ) {
  47:         $self->param_add( error_msg => "Cannot find book class: $@" );
  48:         return $self->execute({ task => 'search_form' });
  49:     }
  50:     my $results = eval {
  51:         $book_class->fetch_group({ where => 'title LIKE ?',
  52:                                    value => [ "%$title%" ] })
  53:     };
  54:     if ( $@ ) {
  55:         $self->param_add( error_msg => "Search failed: $@" );
  56:         return $self->execute({ task => 'search_form' });
  57:     }
  58:     $params{title}     = $title;
  59:     $params{book_list} = $results;
  60:     return $self->generate_content(
  61:                     \%params, { name => 'book::search_results' } );
  62: }

Here's what we do:

On lines 38 we initialize our template parameters hash for the process to fill up. This what we'll pass on to the content generator.

On line 39 we ask the context (which we've imported up on line 17) for the current request, and then on 40 ask the request for the 'title' parameter.

If the user didn't pass in a title we'll set an error message and redirect to the 'search_form' task on lines 41-44. We'll repeat this same pattern for other errors found during this process. It may be smart to refactor this by having one method exit point for all errors, but we'll worry about that later.

On line 45 we'll ask the context for the class corresponding to the 'book' key. This key is what we used in our SPOPS definition above. Lines 46 through 49 are error handling in case that method fails.

Finally, on lines 50-53 we execute the search, passing in the title given to us by the user between two wildcards. We collect the search results in the arrayref $results.

Line 54-57 are error handling for the search in case it fails.

On lines 58 and 59 we set the title searched for and results arrayref to the template parameter hashref, and on lines 60 and 61 we call the content generation method, passing along the parameters to the template book::search_results.

Template: Show search results

Now we'll define a template to display the search results. Save the following to book/template/search_results.tmpl:

   1: [%- OI.page_title( 'Book search results' ) -%]
   2: 
   3: <h2>Book Search Results</h2>
   4: 
   5: <p><b>Search</b>: [% title %]</p>
   6: 
   7: <p>Number of results: [% book_list.size %]</p>
   8: 
   9: [% IF book_list.size > 0 %]
  10: 
  11: [% INCLUDE table_bordered_begin %]
  12: 
  13: [% INCLUDE header_row(
  14:      labels = [ 'Title', 'Author', 'Published', 'Link' ] ) %]
  15: 
  16: [% FOREACH book = book_list %]
  17: <tr valign="middle" align="left">
  18:   <td>[% book.title %]</td>
  19:   <td>[% book.author_last %], [% book.author_first %]</td>
  20:   <td>[% book.publisher %] ([% book.publish_year %])</td>
  21:   <td><a href="http://www.amazon.com/exec/obidos/ASIN/[% book.isbn %]/">Amazon</a></td>
  22: </tr>
  23: [% END %]
  24: 
  25: [% INCLUDE table_bordered_end %]
  26: 
  27: [% END %]

This is still fairly simple. It looks more like HTML than the previous example, but there's still lots of templating going on.

Line 1 defines the page title, just like before. Line 5 shows how you can inline one of the parameters (a simple scalar) passed from the action. Similarly line 7 demonstrates getting a property (size) from another of the parameters passed from the action. In this case the property is provided to us by the Template Toolkit -- all array references have a number of properties we can query and actions we can perform; scalars and hashes do too, of course. (See Template::Manual::VMethods for a discussion.)

Line 9 is the beginning of a conditional: we only want to display the table if there are actually results to see. Otherwise you just see the table header with no content in the table, and that's ugly. (The conditional ends on line 27.)

Line 11 is just like the table beginning from the previous example except this time we're not setting an arbitrary width restriction. (Titles could get long.)

Lines 13-14 show us something new -- a template widget that will output a row of column labels given the labels themselves. This way all of our headers will look the same and if we change the one template, all our headers change.

Lines 16-23 comprise our loop for iterating through the book results. For each book we'll output the title, author name, publishing information and create a link to Amazon with more information about the book.

Line 25 closes the table, and line 27 closes the conditional started on line 9.

Configuring your action

Now that we've got the code and the templates, the only thing left for our action is to tell OpenInteract about it. We do that through the action configuration: another INI file. Here's what's generated for you:

  # This is a sample action.ini file. Its purpose is to define the
  # actions that OpenInteract2 can take based on the URL requested or
  # other means. The keys are commented below. (You can of course change
  # anything you like. I've only used your package name as a base from
  # which to start.)
  
  # 'book' - This is the published tag for this particular action
  # -- the tag specifies how the various pieces of OpenInteract will be
  # able to execute the action. See OpenInteract documentation under
  # 'Action Table'. Note that whatever your key is, it should *ALWAYS*
  # be lower-cased.
  
  [book]
  
  # class - The class that will execute the action. Can be blank if it's
  # a template-only action.
  
  class   = OpenInteract2::Action::Book
  
  # task_default - This is the task to assign when none specified in the
  # URL.
  
  task_default = list
  
  # is_secure - Check security for this action or not (normally used
  # only with components). Default is 'no', so if you do not specify
  # 'yes' your action will be unsecured.
  
  is_secure = no
  
  # method - The method in 'class' (above) that will execute the
  # action. If not specified we use the method defined in your
  # application's 'server.perl' file (normally 'handler').
  
  #method  = 
  
  # redir - Instead of specifying information for a particular action,
  # you can tell OI to lookup the information from another action. For
  # instance, if you wanted both the '/Person/' and '/People/' URLs to
  # run the same action, you could define the information in 'person'
  # and then put for '/People/' something like:
  #
  # [people]
  # redir = person
  
  #redir   = 
  
  # template - You can also specify just a template for an action. It
  # must be in the format 'package::template'.
  #template = 
  
  # Box parameters: if you are specifying a box with this action, you 
  # can also list the following, which should be intuitive:
  
  #weight = 5
  #title  = 
  
  # Use this if the action is defined only by a template, like a box
  #action_type = template_only
  
  # Use this if you'd like the 'lookups' action to handle editing the
  # data it defines (see 'lookup' package docs for more info)
  #action_type = lookup

After removing the comments and getting rid of all the extra stuff, here's what we're left with:

   1: [book]
   2: class        = OpenInteract2::Action::Book
   3: task_default = search_form
   4: is_secure    = no

That's quite a reduction! Line 1 defines the name of our action. The context uses this as an index so we can lookup the action information from anywhere in the system and create an action object from it. We also use it to create the URLs to which the action will respond. For instance, this action will respond to:

  /book
  /BOOK
  /Book

You can also use the url, url_none and url_alt properties to control this. See MAPPING URL TO ACTION under OpenInteract2::Action for more.

CREATE INITIAL DATA

We need something to search, and since we didn't define any tasks yet for inputting data we'll create a file with initial data that will get installed with the table structure we defined above.

Defining initial data

The most common way to define initial data is using the format from SPOPS::Import::Object. This is a serialized Perl data structure but don't worry, it's easy to type in.

Enter the following data into the file book/data/initial_books.dat:

   1: [
   2:   { import_type => 'object',
   3:     spops_class => 'OpenInteract2::Book',
   4:     field_order => [ qw/ author_last author_first title
   5:                          publisher publish_year isbn / ],
   6:   },
   7:   [ 'Brin', 'David', 'Kiln People',
   8:     'TOR', '2002', '0765342618' ],
   9:   [ 'Remnick', 'David', 'King of the World',
  10:     'Vintage', '1999', '0375702296' ],
  11:   [ 'Udell', 'Jon', 'Practical Internet Groupware',
  12:     "O'Reilly", '1999', '1565925378' ],
  13: ];

Let's walk through this:

On line 1 is the data structure initializer, an arrayref. The first element of the arrayref is a hashref of metadata and every successive element is data.

Line 2 opens up the metadata hashref. As required by SPOPS::Import::Object we declare the import type as 'object'. On the next line we specify the class as 'OpenInteract2::Book' -- this is the same value you listed in your SPOPS configuration above:

   1: [book]
   2: class           = OpenInteract2::Book
   3: ...

Lines 4 and 5 tell the import process in what order the field data will be given. You can use any order you like but the order of the columns must match the data or you'll have some funkily populated objects. Line 6 closes out the metadata hashref.

Line 7-8 define the first data element, an arrayref. As mentioned above the order of the data must match the order of the columns from 'field_order'. Lines 9-10 and 11-12 define the second and third data elements, respectively, and line 13 closes out the entire data structure.

Notify the installer

Now we need to let our installer class know that we've got some initial data to install. Change the get_data_file() subroutine in book/OpenInteract2/SQLInstall/Book.pm like this:

  33: # Uncomment this if you're passing along initial data
  34: 
  35: sub get_data_file {
  36:     return 'initial_books.dat';
  37: }

Easy enough. Now when we run the full installation process that data file will be processed and our 'book' table seeded with some data.

GET IT RUNNING

Now let's get it installed to a website, first however...

Take pride in your work

Before you go any further put your info in the relevant areas of book/package.conf (next to 'author' and 'url').

Check your package

We need to check our package to ensure the modules are at least syntactically correct and all the necessary files are around:

  $ cd book/
  $ oi2_manage check_package

And you should see something like:

  PROGRESS: Starting task
  PROGRESS: Task complete
  ACTION: Changelog check
      OK:     Changes
   
  ACTION: Files missing from MANIFEST
      FAILED: Files not found from MANIFEST: template/sample.tmpl
   
  ACTION: Extra files not in MANIFEST
      FAILED: Files not in MAIFEST found: data/initial_books.dat, struct/book.sql, \
  struct/book_sequence.sql, template/search_form.tmpl, template/search_results.tmpl
   
  ACTION: Config required fields
      OK:     package.conf
   
  ACTION: Config defined modules
      OK:     No modules defined, test skipped
   
  ACTION: Check ini file
      OK:     conf/action.ini
      OK:     conf/spops.ini
   
  ACTION: Check module
      OK:     OpenInteract2/Action/Book.pm
      OK:     OpenInteract2/SQLInstall/Book.pm
   
  ACTION: Template check
      FAILED: template/sample.tmpl
              File does not exist

Looks like we have a few things to deal with. Most of them we can fix with the MANIFEST generation scheme mentioned earlier (see CREATING A PACKAGE). Once you run that, run the oi2_manage check_package command again and you should see:

  PROGRESS: Starting task
  PROGRESS: Task complete
  ACTION: Changelog check
      OK:     Changes
   
  ACTION: Files missing from MANIFEST
      FAILED: Files not found from MANIFEST: template/sample.tmpl
   
  ACTION: Extra files not in MANIFEST
      OK:     No files not in MANIFEST found in package
   
  ACTION: Config required fields
      OK:     package.conf
   
  ACTION: Config defined modules
      OK:     No modules defined, test skipped
   
  ACTION: Check ini file
      OK:     conf/action.ini
      OK:     conf/spops.ini
   
  ACTION: Check module
      OK:     OpenInteract2/Action/Book.pm
      OK:     OpenInteract2/SQLInstall/Book.pm
   
  ACTION: Check data file
      OK:     data/initial_books.dat
   
  ACTION: Template check
      FAILED: template/sample.tmpl
              File does not exist
      OK:     template/search_form.tmpl
      OK:     template/search_results.tmpl

Looks like our manifest trick didn't remove the old entry. Delete it manually and run the command again. You should have 'OK' all the way down.

Export your package

Now we need to get ourpackage into a distributable format. It's oi2_manage to the rescue again:

  $ oi2_manage export_package

and you should see:

  PROGRESS: Starting task
  PROGRESS: Task complete
  ACTION: Export package book
      OK:     /home/cwinters/work/tmp/book/book-0.01.zip

This creates a new file book-0.01.zip in the current directory. Easy.

Install your package

Now you want to install the package to your website. If you haven't set the OPENINTERACT2 environment variable to your site's full path go ahead and do so, then run:

  $ oi2_manage install_package --package_file=book-0.01.zip

And you'll see something like:

  PROGRESS: Starting task
  PROGRESS: Finished with installation of book-0.01.zip
  PROGRESS: Task complete
  ACTION: install package
      OK:     book-0.01.zip

Now our website has a new directory, $WEBSITE_DIR/pkg/book-0.01 with all the files in our package. And the repository in $WEBSITE_DIR/conf/repository.ini has a new entry that looks like this:

  [book]
  version = 0.01
  installed = Thu Jul 10 02:24:39 2003
  directory = /path/to/mysite/pkg/book-0.01

Install the data structures and initial data

Yes, oi2_manage handles this too. When you run:

  $ oi2_manage install_sql --package=book

you'll see:

  PROGRESS: Starting task
  PROGRESS: Task complete
  ACTION: install SQL structure: book
      OK:     SQL installation successful
      OK:     book.sql
   
  ACTION: install object data: book
      OK:     initial_books.dat

Now the database has a 'book' table (customized for the type of database) and it's seeded with three objects.

Test it out!

Fire up your OI server and go to the /book/ URL. you should see a simple search form. Try to search for title 'world' and you should get a single result. Search for 'p' and you should get two. Hooray!

SMALL CHANGES

You may have noticed that if you search for an empty title you'll be brought silently back to the search form. What happened to our error message? Also, there's no way to get back to the search screen except for hitting 'back' in our browser. It would be nice to have a link in there.

Handling errors

The reason our error message didn't display is that we didn't tell it to. Let's make a small change to our book/template/search_form.tmpl file:

   1: [%- OI.page_title( "Search for books" ) -%]
   2: 
   3: [% PROCESS error_message %]
   4: 
   5: <h2>Book Search</h2>

You'll see we've added a 'PROCESS' line in there. This refers to the 'error_message' global template. It will look into our action parameters and display all of the values found for 'error_msg'. It could be one, it could be many. The template should display them all.

Adding a link

Next let's see about that link in the search results. Change the book/template/search_results.tmpl like so:

   1: [%- OI.page_title( 'Book search results' ) -%]
   2: 
   3: [%- search_form_url = OI.make_url( ACTION = 'book',
   4:                                    TASK   = 'search_form' ) -%]
   5: <p align="center"><a href="[% search_form_url %]">Run another search</a></p>
   6: 
   7: <h2>Book Search Results</h2>

This 'ACTION' and 'TASK' syntax for defining a link should look familar. We used it when defining the form action (in the form_begin global template) and in our SPOPS configuration. This ensures that the link is relative to whatever context we're deployed under, which means you're no longer forced to use OI under the root directory. (See OpenInteract2::URL for more about this.)

Installing the modified package

Now that we've modified the templates let's install the new package. First edit the book/package.conf file to reflect the new version number and add an entry to the top of the 'Changes' file. Next, export it and install the distribution to your website:

  $ oi2_manage export_package
  $ oi2_manage install_package --package_file=book-0.02.zip

Restart the server and try to run an empty search again. There's the message!

ADDING MORE TASKS

Searching is all well and good but we'd like to be able to add, update and remove objects as well. We could do this by creating separate methods for each of these in our action and they'd work fine. But we'd find ourselves writing the same code again and again. There's a better way.

Common Actions

There are a number of OpenInteract2::Action subclasses known as 'Common' actions. This is because they implement very common functionality with little to no code. In the common action family are:

Using a common action

Using one or more of these is a three-step process:

  1. Subclass the right class.

  2. Add the right configuration keys and values

  3. Add custom functionality through callbacks, as needed.

The first part is just putting the right class in your action's @ISA, simple. The second part depends on the action: different common actions have different configuration needs. (We'll walk through a couple below.)

The third part really depends on what you want to do. The interface for customizing the common actions is fairly simple and flexible enough to take care of many needs. It's also important to recognize when you should not use the common actions, whenever you're asking the customization to do tasks it was designed for it's probably a good bet that it would be easier to code yourself.

Translate searching tasks to common

So we'll first translate our 'search' and 'search_form' tasks to use the OpenInteract2::Common::Search class.

First, get rid of the 'search_form' and 'search' methods. Next, add the common action to the @ISA (or use base if you prefer). What you wind up with should look like this (we've taken out the comments but left the 'hello' method):

  package OpenInteract2::Action::Book;
  
  use strict;
  use base qw( OpenInteract2::Action::CommonSearch );
  
  use Log::Log4perl            qw( get_logger );
  use OpenInteract2::Constants qw( :log );
  use OpenInteract2::Context   qw( CTX );
  
  $OpenInteract2::Action::Book::VERSION = sprintf("%d.%02d", q$Revision: 1.1 $ =~ /(\d+)\.(\d+)/);
  
  sub hello {
      my ( $self ) = @_;
      return 'Hello world!';
  }
  
  1;

That's pretty short. Now we need to add some entries to your action configuration (it'll bulk up a little). After our additions it'll look like this:

   1: [book]
   2: class        = OpenInteract2::Action::Book
   3: task_default = search_form
   4: is_secure    = no
   5: c_object_type             = book
   6: c_search_form_template    = book::search_form
   7: c_search_results_template = book::search_results
   8: c_search_fields_like      = title
   9: c_search_results_order    = title

We've added only five lines to our previous configuration. You'll note in this and other examples that all common configuration keys begin with 'c_' to distinguish them from others.

Line 5 is the type of object the common action(s) work with. The value is the same that you'd pass to the lookup_object() method of the context object.

Line 6 is the name of the template to use for our search form. Note that we use the full 'package::template' syntax.

Line 7 is the name of the template to use for our search results. Line 8 defines the single field we want to search for, and line 9 orders the results for us.

That's pretty simple. Let's package it up and install it to the site. (Remember to first edit book/Changes and book/package.conf.)

  $ oi2_manage export_package
  $ oi2_manage install_package --package_file=book-0.03.zip

Restart the server and type in the '/book' URL again. Looks the same, doesn't it? Now run a search. Oops, no go. What's up?

Previously we'd placed our search results in an arrayref named 'book_list'. The CommonSearch class instead puts the results in an iterator named 'iterator'. So we'll have to change our search_results template. However, instead of modifying our package we'll first modify the template in the website itself so we can do tight iterations of template design, then we'll copy the template back into our package. Here's what the new template looks like; (full copy since it's got changes throughout):

   1: [%- OI.page_title( 'Book search results' ) -%]
   2: 
   3: [%- search_form_url = OI.make_url( ACTION = 'book',
   4:                                    TASK   = 'search_form' ) -%]
   5: <p align="center"><a href="[% search_form_url %]">Run another search</a></p>
   6: 
   7: <h2>Book Search Results</h2>
   8: 
   9: <p><b>Search</b>:<br>
  10: [% FOREACH search_field = search_criteria.keys %]
  11:    [% search_field %]: [% search_criteria.$search_field %]<br>
  12: [% END %]
  13: </p>
  14: 
  15: <p>Number of results: [% total_hits %]</p>
  16: 
  17: [% IF total_hits > 0 %]
  18: 
  19: [% INCLUDE table_bordered_begin %]
  20: 
  21: [% INCLUDE header_row(
  22:      labels = [ 'Title', 'Author', 'Published', 'Link' ] ) %]
  23: 
  24: [% WHILE ( book = iterator.get_next ) %]
  25: <tr valign="middle" align="left">
  26:   <td>[% book.title %]</td>
  27:   <td>[% book.author_last %], [% book.author_first %]</td>
  28:   <td>[% book.publisher %] ([% book.publish_year %])</td>
  29:   <td><a href="http://www.amazon.com/exec/obidos/ASIN/[% book.isbn %]/">Amazon</a></td>
  30: </tr>
  31: [% END %]
  32: 
  33: [% INCLUDE table_bordered_end %]
  34: 
  35: [% END %]

The first 10 lines are the same, but on lines 11-13 we see the first change. Instead of just outputting the title we now cycle through the search criteria given to us, displaying each with the value searched. You'll probably notice when you see the results that they're not exactly user-friendly. The CommonSearch class doesn't have the means to map human-readable names to fieldnames, but you can do so either in _search_customize() or in the template itself.

On line 16 there's another change: since we're using an iterator we can't ask it how many members it has. It could have ten or a thousand, it doesn't care and will keep unwinding as long as there are more entries. So CommonSearch provides another parameter to the template, 'total_hits', which we can use to display how many entries we found. (Note that 'total_hits' is only provided when you're using paged results, but since that's the default and we didn't change the setting we can use the parameter. See OpenInteract::Action::CommonSearch for more.)

Finally, on line 25 we had to modify our 'FOREACH' loop into a 'WHILE' loop. Fortunately that's the only line of the loop we had to change: since we assign to a variable of the same name for every cycle of the loop the rest of it doesn't care what type of loop we're in as long as that 'book' variable is defined.

So after you've seen that you can run a search with it, just copy the file back to your working package directory.

Add search fields

Now, let's say we wanted to add a search by author's last name and publisher. But instead of having the user type in a publisher's name we're going to present her with a list.

First, we'll modify the configuration file with the new fields:

   9: c_search_fields_like      = author_last
  10: c_search_fields_exact     = publisher

Next, we'll modify the template: h 15: [% INCLUDE label_form_text_row( label = "Author's Last Name", 16: name = 'author_last', 17: size = 30 ) %] 18: 19: [% INCLUDE label_form_select_row( label = 'Publisher', 20: name = 'publisher', 21: first_label = 'Publishers...', 22: value_list = publisher_list, 23: plain = 'yes', ) %]

You have not seen any use of 'label_form_select_row' yet. It creates a 'SELECT' input widget for you and displays it on the right-hand side of a two-celled table row. This template widget actually uses 'form_select' behind the scenes to create the HTML code for the drop-down box. You can pass 'form_select' lists of plain values and labels, or a list of objects and specify the object property to use for the value and label. For this use we're saying that we'll expect a plain list of publisher names in the variable 'publisher_list'.

Finally, we have to be able to step in during the presentation of the search form to give it our list of publishers. We do so with the _search_form_customize() callback method. This method is passed a hashref of the parameters that get passed to the template, so all we need to do is add our list of publishers to it:

  17: sub _search_form_customize {
  18:     my ( $self, $params ) = @_;
  19:     my $log = get_logger( LOG_APP );
  20: 
  21:     my $book_class = eval { CTX->lookup_object( 'book' ) };
  22:     return unless ( $book_class );
  23:     my $publishers = eval {
  24:         $book_class->db_select({ select_modifier => 'DISTINCT',
  25:                                  select          => [ 'publisher' ],
  26:                                  from            => $book_class->table_name,
  27:                                  order           => 'publisher',
  28:                                  return          => 'single-list' });
  29:     };
  30:     if ( $@ ) {
  31:         $log->error( "Caught error trying to fetch publishers: $@" );
  32:     }
  33:     else {
  34:         $log->is_debug &&
  35:             $log->debug( "Found publishers: ", join( ', ', @{ $publishers } ) );
  36:     }
  37:     $publishers ||= [];
  38:     $params->{publisher_list} = $publishers;
  39: }

You know the drill: edit book/Changes and book/package.conf, bundle up the package and install it, then restart the server.

Now when you open the '/book' URL the search form should be slightly different. And if you click on the drop-down list next the 'Publisher' you should see all the publishers in the system so far. Now search for one of them and you should get the correct results. Searching for multiple fields produces an 'AND' query, where every record must match all the criteria.

Static display

Now let's create a page to display the information for a book on a page by itself. We do this using the OpenInteract2::Action::CommonDisplay class which provides only one task, 'display'.

Looking at the class docs it looks like the only additional parameter we need is c_display_template so the class knows which template to feed our object. Let the configuration know that we'll be using a template called 'book::detail':

  12: c_display_template        = book::detail

Then create the template in book/template/detail.tmpl:

  [%- OI.page_title( "Book Details: $book.title" ) -%]
  
  <h2>Details: [% book.title %]</h2>
  
  [% INCLUDE table_bordered_begin %]
  [%- count = 0 -%]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_text_row( label = 'Title',
                             text  = book.title ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_text_row( label = 'Author',
                             text  = "$book.author_first $book.author_last" ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_text_row( label = 'Publisher',
                             text  = book.publisher ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_text_row( label = 'Year Published',
                             text  = book.publish_year ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_text_row( label = 'ISBN',
                             text  = book.isbn ) %]
  
  [% INCLUDE table_bordered_end %]

This is a pretty simple template. We're just setting up a table and passing in a label and text value for each field of the book object. We know we can use 'book' as the template parameter name from the documentation in OpenInteract2::Action::CommonDisplay.

Since this is a new file you'll need to add it to your book/MANIFEST, so do it now before you forget.

Next, add the class to the @ISA of our action:

   1: package OpenInteract2::Action::Book;
   2: 
   3: use strict;
   4: use base qw( OpenInteract2::Action::CommonSearch
   5:              OpenInteract2::Action::CommonDisplay );

That's it! But we need to be able to have a link somewhere so the user can actually see the object's details. The best place so far is in the search results where we can create a link around the book title, so modify the book/template/search_results.tmpl file like this:

  25: [% book_url = OI.make_url( ACTION = 'book', TASK = 'display',
  26:                            book_id = book.id ) -%]
  27: <tr valign="middle" align="left">
  28:   <td><a href="[% book_url %]">[% book.title %]</a></td>

This introduces a new method of the plugin, make_url. Again, it uses the familiar 'ACTION' and 'TASK' parameters to create the URL for us. We also pass it a key/value pair for 'book_id'; this will be appended to the URL as a GET query string.

Now edit your book/package.conf and book/Changes files to reflect the updates, bundle the package up and install it:

  $ oi2_manage export_package
  $ oi2_manage install_package --package_file=book-0.05.zip

Restart your server and run a search, then click on a title. There's your detailed record! There aren't many details to speak of, but we can add more later without modify the code, or even configuration, at all.

Update existing objects

Now let's setup a common action for editing your book objects. For this we use the OpenInteract2::Action::CommonUpdate class, which provides two tasks: 'display_form' and 'update'.

We need a few more parameters in our action configuration this time. For 'display_form' it looks like we just need c_display_form_template. For 'update' we'll need to list the fields we're updating in our object under c_update_fields. We don't have any toggled or date fields so we won't have to define any c_update_fields_toggled or c_update_fields_date. And instead of returning to the 'display_form' task after our update is complete we'll just return to the 'display' task so we'll set the parameter c_update_task accordingly. These are the parameters we'll add to our action configuration in book/conf/action.ini:

  13: c_display_form_template   = book::form
  14: c_update_fields           = title
  15: c_update_fields           = author_first
  16: c_update_fields           = author_last
  17: c_update_fields           = publisher
  18: c_update_fields           = publish_year
  19: c_update_fields           = isbn
  20: c_update_task             = display

We'll also need to create a template with our form. Add the following content to book/template/form.tmpl:

  [%- OI.page_title( "Edit a Book" ) -%]
  
  [% PROCESS error_message %]
  
  <h2>Edit a Book</h2>
  
  [% INCLUDE form_begin( ACTION = 'book', TASK = 'update' ) %]
  
  [% INCLUDE table_bordered_begin %]
  [%- count = 0 -%]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_form_text_row( label = 'Title',
                                  name  = 'title',
                                  value = book.title ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_form_text_row( label = 'Author First Name',
                                  name  = 'author_first',
                                  value = book.author_first ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_form_text_row( label = 'Author Last Name',
                                  name  = 'author_last',
                                  value = book.author_last ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_form_text_row( label = 'Publisher',
                                  name  = 'publisher',
                                  value = book.publisher ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_form_text_row( label = 'Year Published',
                                  name  = 'publish_year',
                                  value = book.publish_year,
                                  size  = 5 ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE label_form_text_row( label = 'ISBN',
                                  name  = 'isbn',
                                  value = book.isbn,
                                  size  = 15 ) %]
  
  [%- count = count + 1 -%]
  [% INCLUDE form_submit_row( value = 'Modify' ) %]
  
  [% INCLUDE table_bordered_end %]
  
  [% INCLUDE form_hidden( name = 'book_id', value = book.id ) %]
  
  [% INCLUDE form_end %]

This looks a lot like our 'detail' form above. The only differences are:

Since this is a new template go ahead and add it to book/MANIFEST right now.

Since we're showing the results of the 'display' task after our update and the CommonUpdate class populates the action parameters 'status_msg' with the status of our update, it would be a good idea to be able to display it on our 'detail' template.

Additionally we'll need to make our 'display_form' task available to the user. A link on the 'detail' template makes the most sense to do this, so modify the book/template/detail.tmpl file with both changes like this:

   1: [%- OI.page_title( "Book Details: $book.title" ) -%]
   2: 
   3: [% PROCESS status_message %]
   4: 
   5: [% IF OI.can_write( book ) %]
   6:    [% edit_url = OI.make_url( ACTION = 'book', TASK = 'display_form',
   7:                               book_id = book.id ) %]
   8: <p align="right"><a href="[% edit_url %]">Edit</a> this record.</p>
   9: [% END %]
  10: 
  11: <h2>Details: [% book.title %]</h2>

This introduces a new plugin method, 'can_write'. This method returns true if the current user has permission to edit the given object. We haven't discussed object security yet and you could easily skip the conditional. But it does no harm for when objects aren't secured (everything is writable) and if you decide to add security later on you'll be thankful.

Finally, we need to activate the CommonUpdate class in our action:

   1: package OpenInteract2::Action::Book;
   2: 
   3: use strict;
   4: use base qw( OpenInteract2::Action::CommonSearch
   5:              OpenInteract2::Action::CommonDisplay
   6:              OpenInteract2::Action::CommonUpdate );

Now edit your book/package.conf and book/Changes files to reflect the updates, bundle the package up and install it:

  $ oi2_manage export_package
  $ oi2_manage install_package --package_file=book-0.06.zip

Restart your server, run a search, and drill down to an object. You should see a 'Edit this record' above the detail form with 'Edit' as a link. Click the link and you should get an editable form with that object's information. Make any changes you like and save the object. You should see the detail form after a successful save.

Create new objects

Now you should be getting the drill. Adding a common action to your class means:

  1. Adding entries to your action configuration.

  2. Adding templates, if necessary.

  3. Subclassing the common action.

  4. Adding custom functionality to your action, if necessary.

So we'll breeze through the OpenInteract2::Action::CommonAdd class. Add the following to your configuration:

  21: c_display_add_template    = book::form
  22: c_add_task                = display
  23: c_add_fields              = title
  24: c_add_fields              = author_first
  25: c_add_fields              = author_last
  26: c_add_fields              = publisher
  27: c_add_fields              = publish_year
  28: c_add_fields              = isbn

We don't have to add a new template since we're reusing the book/template/form.tmpl template we created when adding update functionality. However, we do need to make a few modifications to it since we're doing double-duty by modifying the page title and, more importantly, the task to which the form data will be submitted:

   1: [%- title = ( book.is_saved ) ? 'Edit a Book' : 'Add a Book';
   2:     OI.page_title( title ) -%]
   3: 
   4: [% PROCESS error_message %]
   5: 
   6: <h2>[% title %]</h2>
   7: 
   8: [% form_task = ( book.is_saved ) ? 'update' : 'add' -%]
   9: [% INCLUDE form_begin( ACTION = 'book', TASK = form_task ) %]

Now add the class to your action:

   1: package OpenInteract2::Action::Book;
   2: 
   3: use strict;
   4: use base qw( OpenInteract2::Action::CommonSearch
   5:              OpenInteract2::Action::CommonDisplay
   6:              OpenInteract2::Action::CommonUpdate
   7:              OpenInteract2::Action::CommonAdd );

Slightly more tricky is where to add a link to this new functionality. It's not object-specific so we can't put it in this search results listing or when we're displaying an object. One option many OI actions take is to have a toolbox that displays whenever you're operating in that action. Since the toolbox displays no matter whether or not you're in the context of a specific data object it's perfect for this. We'll leave implemeting that up to you -- for now we'll put a link on the search form page:

   1: [%- OI.page_title( "Search for books" ) -%]
   2: 
   3: [% PROCESS error_message %]
   4: 
   5: [% add_url = OI.make_url( ACTION = 'book', TASK = 'display_add' ) -%]
   6: <p align="right"><a href="[% add_url %]">Add</a> a new book</p>

And do your normal book/package.conf and book/Changes editing, then put your new package in the website:

  $ oi2_manage export_package
  $ oi2_manage install_package --package_file=book-0.07.zip

Now try to add a new object then do a search, display and edit on it.

Remove

This one is really easy. Just add the c_remove_task parameter to book/conf/action.ini to tell the CommonRemove class where to go after removal:

  29: c_remove_task             = search_form

Inherit from the CommonRemove class in book/OpenInteract2/Action/Book.pm:

   1: package OpenInteract2::Action::Book;
   2: 
   3: use strict;
   4: use base qw( OpenInteract2::Action::CommonSearch
   5:              OpenInteract2::Action::CommonDisplay
   6:              OpenInteract2::Action::CommonUpdate
   7:              OpenInteract2::Action::CommonAdd
   8:              OpenInteract2::Action::CommonRemove );

And we'll go ahead and add a 'remove' link to the edit form in book/template/form.tmpl:

   1: [%- title = ( book.is_saved ) ? 'Edit a Book' : 'Add a Book';
   2:     OI.page_title( title ) -%]
   3: 
   4: [% PROCESS error_message %]
   5: 
   6: [% IF book.is_saved %]
   7: [% remove_url = OI.make_url( ACTION = 'book', TASK = 'remove',
   8:                              book_id = book.id ) -%]
   9: <p align="right"><a href="[% remove_url %]">Remove</a> this book</p>
  10: [% END %]

And since we're running the 'search_form' after a successful remove and CommonRemove sets the 'status_msg' action parameter, we'll add a directive to display the status of the last action to book/template/search_form.tmpl:

   1: [%- OI.page_title( "Search for books" ) -%]
   2: 
   3: [% PROCESS error_message;
   4:    PROCESS status_message %]

Now the standard book/package.conf and book/Changes editing, and then upgrade the website package:

  $ oi2_manage export_package
  $ oi2_manage install_package --package_file=book-0.08.zip

See how it works!

CLOSING THOUGHTS

That'll do it for this initial tutorial. We've gone from nothing and created a small but useful application to track our books. Each application bundle (the zip file you create) can be sent to someone else and installed on their OpenInteract server as easily it it was yours. In fact the last version of the package we've developed here is available on the OpenInteract wiki site. (See SEE ALSO.)

Hopefully you've caught hold of the OpenInteract approach to development and are ready to set your sights on larger applications. If you'd like to learn more there's another tutorial, OpenInteract2::Manual::TutorialAdvanced. It is more of a hodgepodge, adding discrete parts to what we've covered here. It covers:

SEE ALSO

Distribution of the 'book' package, version 0.08.

http://openinteract.sourceforge.net/cgi-bin/twiki/view/OI/OpenInteract2Tutorial

COPYRIGHT

Copyright (c) 2002-2003 Chris Winters. All rights reserved.

AUTHORS

Chris Winters <chris@cwinters.com>

Generated from the OpenInteract 1.99_03 source.


Home | Wiki | OI 1.x Docs | OI 2.x Docs
SourceForge Logo