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

NAME

OpenInteract2::Manual::TutorialAdvanced - Tutorial for advanced OpenInteract functionality

SYNOPSIS

This document will walk through some of the more advanced (but still common) actions you can do with OpenInteract2.

For the examples in this document we'll rely on the 'book' application created in OpenInteract2::Manual::Tutorial.

ACTIONS: SETTING UP A LOOKUP TABLE

Create the data structure

One of the immediate problems with our 'book' record is that there's no way to categorize them. Sure, most books cross categories -- my favorite: 'Computer Programming' + 'Self-Help' -- but our books don't even have one.

But we don't want the user to type in any old category. We should be able to define a list of known categories then allow her to choose one of them when creating or updating a book.

So, first we'll create a table to hold these categories. It's commonly known as a 'lookup' table because it's so simple, all we do is find values:

 CREATE TABLE book_category (
     category_id   %%INCREMENT%%,
     category      varchar(25) not null,
     primary key( category_id )
 );

Store this in struct/book_category.sql and if you're using Oracle/PostgreSQL add a sequence in struct/book_category_sequence.sql:

 CREATE SEQUENCE book_category_seq 

Now we'll need to add the table and sequence to our SQL installer class. In OpenInteract2/SQLInstall/Book.pm change:

 my %FILES = (
    oracle  => [ 'book.sql', 'book_sequence.sql' ],
    pg      => [ 'book.sql', 'book_sequence.sql' ],
    default => [ 'book.sql' ],
 );

to:

 my @tables    = qw( book.sql book_category.sql );
 my @sequences = qw( book_sequence.sql book_category_sequence.sql );
 my %FILES = (
    oracle  => [ @tables, @sequences ],
    pg      => [ @tables, @sequences ],
    default => [ @tables ],
 );

Also add an SPOPS class to conf/spops_book_category.ini:

 [book_category]
 class           = OpenInteract2::BookCategory
 isa             = 
 field           = 
 field_discover  = yes
 id_field        = category_id
 increment_field = yes
 sequence_name   = book_category_seq
 is_secure       = no
 no_insert       = category_id
 no_update       = category_id
 base_table      = book_category
 name            = category
 object_name     = Book Category
 
 [book_category display]
 ACTION = lookups
 TASK   = 

Setup lookup action

And here's the interesting part -- to add simple editing functionality to your lookup table you can just add the following action to conf/action.ini:

 [book_category]
 action_type  = lookup
 object_key   = book_category
 title        = Book Categories
 field_list   = category
 label_list   = Category
 size_list    = 25
 order        = category
 url_none     = yes

The 'action_type' setting is the important one. OI2 references a list of action types in the server configuration (key: 'action_type'). Think of these as parent actions to yours -- all they require is configuration and you're set. In this case you need to set:

object_key

SPOPS name of your object.

title

Title of the values to edit.

field_list

List of fields you want to edit. (We've only got one.)

label_list

List of labels for each of the fields in field_list, in the same order.

size_list

Size of text fields to edit for each of the fields in field_list, in the same order.

order

Field/ORDER-BY clause used to retrieve the data.

See OpenInteract2::App::Lookup for more information and additional options you can set.

Add these files to your MANIFEST, modify your Changes and package.ini and update your site. You can install the SQL structures like this (assuming OPENINTERACT2 environment variable is set to your website):

 $ oi2_manage install_sql_structure --package=book \
           --file=book_category.sql --file=book_category_sequence.sql

Back in the original tuturial we ran the 'install_sql' management task. That's really just a wrapper around three separate tasks: 'install_sql_structure', 'install_sql_data' and 'install_sql_security'. Additionally, since we don't want to try to reinstall our 'book.sql' we're giving it an argument to limit the files it will operate on.

Once you restart your server go to the 'Lookup Tables' link in your 'Admin Tools' box. (Or just go to the '/lookups/' URL.) You'll see 'Book Categories' there. Click on the link and you'll see a set of text entry fields, one per table row.

Each entry will become a new category, so enter a few and click 'Save'. You'll get a status message with your entries and get taken back to the initial lookup tables page. Click on 'Book Categories' again and you'll see your categories. You can now add, edit and remove at will.

Add field to table

So now we need to add our category our table of books. Using whatever database tool you're comfortable with add the following definition to the 'book' table:

  category_id  int null

For instance, when I use PostgreSQL I just do:

 psql> ALTER TABLE book ADD category_id INT NULL;

When you restart the server all your 'book' objects will have the new property 'category_id'. Since we set 'field_discover' to 'yes' in the previous tutorial we didn't need to tell SPOPS or OI2 about it.

But we still can't assign the category from our book editing form...

Source values for field from lookup

What we want to do is present a list of all the available categories to the user when she's editing a book. But how do we do that? Remember, we're using one of the Common actions and didn't actually write any code to do inserts and updates.

Fortunately the Common actions have a callback just for this. It's always named "customize" -- "_display_add_customize", "_display_form_customize", etc.

So in our action OpenInteract2/Action/Book.pm we'll implement these callbacks. This is very similar to how we added our list of publishers to the search form but instead of plain strings as the source we're using objects:

So add the following method:

 sub _add_categories {
     my ( $self, $template_params ) = @_;
     $template_params->{categories} =
         OpenInteract2::BookCategory->fetch_group({ order => 'category' });
 }

And reference it from your callbacks:

 sub _display_add_customize {
     my ( $self, $template_params ) = @_;
     $self->_add_categories( $template_params );
 }

 sub _display_form_customize {
     my ( $self, $template_params ) = @_;
     $self->_add_categories( $template_params );
 }

Then add a reference to those categories in your data editing form:

 [%- count = count + 1 -%]
 [% INCLUDE label_form_select_row( label       = 'Category'
                                   name        = 'category_id',
                                   picked      = book.category,
                                   list        = categories,
                                   value_field = 'category_id',
                                   label_field = 'category',
                                   first_label = '---Categories---' ) -%]

Assuming you have the following categrories:

  category_id  category
  ---------------------
  1            Self-Help
  2            Perl
  3            Regular Expressions
  4            Vegetarian Cooking 

You'll see something like this -- remember that we sorted the categories by 'category' when we assigned them to the template parameters:

 <tr>
   <td><b>Category</b></td>
   <td><select name="category_id">
          <option value="">---Categories---</option>
          <option value="2">Perl</option>
          <option value="3">Regular Expressions</option>
          <option value="1">Self-Help</option>
          <option value="4">Vegetarian Cooking</option>
      </select>
   </td>
 </tr>

Add fields to action configuration

Finally, while we didn't have to add the field to our SPOPS configuration we do have to add it to our actions. (Common action configurations don't have an field discovery feature yet.) Add to your conf/action.ini under '[book]' these keys and values:

 c_update_fields = category_id
 c_add_fields    = category_id

After you do so you should be able to add and update your books with new categories. Give it a whirl!

ACTIONS: A TEMPLATE CAN BE AN ACTION

Another action type: template_only

Another way to create an action without any code is to point it directly at a template. This is very useful for embeddable components. For instance, many actions come with a 'box' that has pointers to common actions. Say we wanted to create a 'Book Actions Box' that had links like 'Search', 'Add' and (when you're on a book record), 'Edit' and 'Remove'.

We'll go backwards first this time, defining the action first. In your conf/action.ini add the following:

 [book_box]
 action_type  = template_only
 template     = book::toolbox
 title        = Book Tools
 url_none     = yes

We can now reference 'book_box' just like any other action. But first we need to create a simple template in template/toolbox.tmpl:

 [%- search_url = OI.make_url( ACTION = 'book', TASK = 'search' );
     add_url    = OI.make_url( ACTION = 'book', TASK = 'display_add' ); -%]
 - <a href="[% search_url %]">Search</a> <br />
 - <a href="[% add_url %]">Add</a> <br />
 [% IF book -%]
   [%- edit_url   = OI.make_url( ACTION = 'book', TASK = 'display_form',
                                 book_id = book.id );
       remove_url = OI.make_url( ACTION = 'book', TASK = 'remove',
                                 book_id = book.id ) -%]
 - <a href="[% search_url %]">Edit</a> <br />
 - <a href="[% add_url %]">Remove</a><br />
 [% END %]

We're done. Just add the new file to your MANIFEST and install.

You can execute the content from within a template too. For instance, just add the following to any template:

 [% OI.action_execute( 'book_box' ) %]

Once it's processed you'll see your content. You do something similar when you place the action content in a box:

 [% OI.box_add( 'book_box' ) %]

The 'box_add' method stores the action away for execution later (by another action, OpenInteract2::Action::Box). But since we've wrapped our template as an action we can now treat it like every other component.

It's also worth mentioning that we can still reference the template with Template Toolkit directives, like:

 [% INCLUDE book::toolbox %]

The only differences are that when we call OI.action_execute() we go through the action's security checks as well as use its cached content if available. The INCLUDE call does not do this extra work so it's a little faster to process. It's up to you whether the speed is worth it. (And you can always change between the two, so a decision won't be permanent.)

ACTIONS: ADDING VALIDATION TO COMMON TASKS

ACTIONS: USING MULTIPLE TABLES WITH COMMON SEARCHING

Another table with searchable information

Our book empire is growing. We now have multiple locations where our books are stored and we need to track which books are stored in which locations.

For our purposes we'll assume a location table like this:

 CREATE TABLE location (
   location_id      %%INCREMENT%%,
   name             varchar(50) not null,
   city             varchar(25) null,
   state            varchar(2) null,
   primary key( location_id )
 )

And we'll also assume that we're getting a feed of these data from our shipping company, so we don't need to create code to edit the data.

For the sake of this exercise we'll also assume we don't need to manipulate individual 'location' records. Since the search mechanism relies entirely on database tables and joins we don't require that all tables are mapped to SPOPS objects.

Our main reason we want the additional table is for searching -- so we can find all books in a particular location, or all books with 'Food' in their title in a particular city.

Since there can be the same book at many different locations, and many different books at a single location, we have a many-to-many relationship. For this we need a separate table (aka, 'join table') to hold the linking data. It'll look like this:

 CREATE TABLE book_at_location (
   book_id          %%INCREMENT_TYPE%% not null,
   location_id      %%INCREMENT_TYPE%% not null,
   primary key( book_id, location_id )
 )

If you want you can add information specific to this link to the table (count, date last book received, etc.). For our purposes here it doesn't matter.

Feed the search from your form

We want to be able to search by location name, city and state. First we'll add the fields to the search form -- in template/search_form.tmpl add:

   [% INCLUDE label_form_text_row( label = 'Location',
                                   name  = 'loc.name', size  = 30 ) %]
 
   [% INCLUDE label_form_text_row( label = 'City',
                                   name  = 'loc.city', size  = 30 ) %]
 
   [% INCLUDE label_form_text_row( label = 'State',
                                   name  = 'loc.state', size  = 5 ) %]

Note that we prefixed the field names from our 'location' table with 'loc.'. That's because we need to need to be able to tell our action for which fields we should join to another table.

The string 'loc' doesn't mean anything. It just needs to be consistent between your form field declarations and the configuration we're about to do.

In your conf/action.ini you'll need to add these fields to your c_search_fields* listings. We'll assume the location name and city are 'LIKE' matches while the state is an exact match. So under 'book' add:

 c_search_fields_like  = loc.name
 c_search_fields_like  = loc.city
 c_search_fields_exact = loc.state

Note that we kept the 'loc.' prefix on all fields.

Next, we need to tell OI2 how to match up our book records with these 'loc.' fields. We do that with the parameter c_search_table_links. It will look like this:

 [book c_search_table_links]
 loc = book.book_id
 loc = book_at_location.book_id
 loc = book_at_location.location_id
 loc = location.location_id

The 'loc' field matches our prefix and its values represent (in pairs) how tables are joined. OI will step through these parameters and construct a SQL JOIN clause like this:

 WHERE ...
       AND book.book_id = book_at_location.book_id
       AND book_at_location.location_id = location.location_id

It will only generate this join if any of the fields with a 'loc' prefix are searched. So if a user just searches for a book title OI2 will put together a query like this:

 WHERE book.title LIKE '%Charlotte%'

But if a user searches for a book title in a particular city this query will change:

 WHERE book.title LIKE '%Charlotte%'
       AND location.city LIKE '%burgh%'
       AND book.book_id = book_at_location.book_id
       AND book_at_location.location_id = location.location_id

Once you've made these changes you're ready to search!

ACTIONS: SECURING AN ACTION

What is action security?

There are two layers of security in OpenInteract. The first, action security, is the most widely used and determines who can do what in your application. Action security can be segmented by task. So you may have certain tasks within an action that all users can do (such as search and view items) but other tasks only users of a particular group can do (such as create, modify and remove items).

Security is always specified by group. While the underlying mechanism for storing and retrieving security can be used with individual users it's strongly discouraged.

Configure your action

Assigning security to your action is very simple -- all the changes are in your action's configuration file.

First, you need to tell OI2 that your action is secure with the 'is_secure' key:

 [book]
 class     = OpenInteract2::Action::Book
 is_secure = yes

If this is set to anything but 'yes' the action processor will ignore any security settings. (In OpenInteract2::Manual::Tutorial we had this set to 'no'.)

Now you need to define the security required for your action. You have three options to choose from: 'NONE', 'READ' and 'WRITE'. (There's a fourth option, 'SUMMARY', but it's rarely used.) These levels are additive so if a user has 'WRITE' permission she also has 'READ'. Also, note that if you don't specify a requirement we assume 'WRITE'.

If you want a single security requirement for all tasks in our book action then the job is easy:

 [book]
 class     = OpenInteract2::Action::Book
 is_secure = yes
 security  = WRITE

However, many times you'll want to have separate requirements for separate tasks. For instance, in our book action we want everyone to be able to search and display book records. But maybe we only want groups with WRITE permission to the action to modify the book records. So we might have:

 [book security]
 DEFAULT      = WRITE
 search       = READ
 search_form  = READ
 display      = READ

Here we have a new key, 'DEFAULT'. This is a special task name that acts as a catch-all: every task not explicitly gets this security. Of course, since a task not listed gets 'WRITE' security anyway this is technically redundant. But it better to communicate your intentions explicitly.

That's it -- restart the server and your action will now be secured. Of course, you need to assign security to groups.

Assigning security

Assigning security to actions is typically done through the website -- click on the 'Security' link from the 'Admin Tools' box, or just go to the '/security/' URL.

You can also modify action security through a management task OpenInteract2::Manage::Website::CreateSecurityForAction, or using oi2_manage:

 oi2_manage secure_action --action=book --scope=group --scope_id=5

SPOPS: ADDING OBJECT BEHAVIORS

Define the class

SPOPS objects provide simple persistence behavior. But you may want them to have other types of behavior as well. The recommended way of doing this is to create your class and have it subclass the generated SPOPS class.

A typical use of this is to encapsulate common queries. Say you have a 'recipe' object:

 [recipe]
 class = OpenInteract2::Recipe
 ...

After using it for a while you find that you're one of your standard queries is to find recipes with a particular ingredient posted in the last month. It's executed from multiple actions so it makes sense to have this query live in the same object that actually retrieves the objects. To hold it you'd create something like this:

 package OpenInteract2::Recipe;
 
 use strict;
 @OpenInteract2::Recipe::ISA = qw( OpenInteract2::RecipePersist );
 
 sub by_ingredient_in_last_month {
     my ( $class, $ingredient ) = @_;
     my $where = qq{
         ingredient LIKE ? '
           AND date_part( 'epoch', CURRENT_DATETIME ) - date_part( 'epoch', posted_on ) <= 25920000
     };
     return $class->fetch_group({
         where => $where,
         value => [ $ingredient ],
     });
 }

Hey, where did that OpenInteract2::RecipePersist come from?

Add to declaration

The last step is to change the name of your generated class. This is so people can use the name they'd expect to see ('OpenInteract2::Recipe' for a 'recipe' object). Changing this is simple:

 OLD:
 [recipe]
 class = OpenInteract2::Recipe
 ...
 
 NEW:
 [recipe]
 class = OpenInteract2::RecipePersist
 ...

One other task: when we want to get the object class name from the context (using lookup_object()) we don't want to get 'OpenInteract2::RecipePersist'. So we need to alias the class:

 [recipe]
 class = OpenInteract2::RecipePersist
 alias_class = OpenInteract2::Recipe
 ...

So now when we get the class name like this we'll get the right thing:

 my $recipe_class = CTX->lookup_object( 'recipe' );
 my $recent_recipes =
     $recipe_class->by_ingredient_in_last_month( 'ketchup' );
 print "Stuff you've made recently:\n";
 foreach my $recipe ( @{ $recent_recipes } ) {
     print "    ", $recipe->name, "\n";
 }

Using security

First: are you sure you need per-object security? Checking security can add significant overhead to object retrieval, so if your security requirements can be met by application security instead you should use it.

So if you need it, adding security is as simple as setting 'is_secure' to 'yes' in your object configuration:

 [myobject]
 class = OpenInteract2::Myobject
 ...
 is_secure = yes

This will add SPOPS::Secure to your class's @ISA and automatically ensure that users have READ security to retrieve an object and WRITE security to store or remove it.

That leaves a gap: how do we know what the security should be to create an object? You cover this with the 'creation_security' configuration section. It looks like this:

 [myobject creation_security]
 user  = WRITE
 group = site_admin_group:WRITE
 world = READ

This means that we create a security object for the user who creates the object as WRITE, one for the 'site_admin_group' as WRITE, and one for the world as READ. You can skip a scope if you like -- if you only want users in the 'public' group to be able to see your object you'd use:

 [myobject creation_security]
 group = public_group:READ

The 'site_admin_group' and 'public_group' used above is the name of any group found in your server configuration's 'default_objects' configuration section. That section typically looks like this:

 [default_objects]
 superuser        = 1
 supergroup       = 1
 theme            = 1
 public_group = 2
 site_admin_group = 3

The 'groupname:PERMISSION' is a syntax enhancement: when the server starts up we lookup 'groupname' in 'default_objects' and substitute that group's ID. So with the above definitions when you create a new 'myobject' we'll also create a security object giving WRITE permission to group with ID 3.

MISC

Using Multiple Datasources

OI2 can support multiple datasources. Most applications do not have need for this, but some may need to present legacy data alongside current data.

OI2 ships with a single configured DBI datasource called 'main'. Assuming a configuration for PostgreSQL it looks like this:

 [datasource main]
 type     = DBI
 dbi_type = Pg
 dsn      = dbname=current_data
 username = pguser
 password = pgpass

Say we have a MySQL database with legacy data. We'd just add it to the configuration file like this:

 [datasource main]
 type     = DBI
 dbi_type = Pg
 dsn      = dbname=current_data
 username = pguser
 password = pgpass
 
 [datasource legacy]
 type     = DBI
 dbi_type = mysql
 dsn      = database=legacy
 username = mysqluser
 password = mysqlpass

We can reference the datasource in code through the context and use it as a straight DBI handle:

 my $dbh = CTX->datasource( 'legacy' );
 my $sql = "SELECT count(*) FROM old_table WHERE foo = ?";
 my ( $sth );
 eval {
     $sth = $dbh->prepare( $sql );
     $sth->execute( $foo );
 };
 my ( $count ) = $sth->fetchrow_array;

We can also use the datasource to back our SPOPS objects. Currently, the easiest way to associate an SPOPS class with a specific datasource is through its configuration.

Assuming we had a read-only SPOPS object declaration for old invoices:

 [legacy_invoice]
 class           = OpenInteract2::LegacyInvoice
 isa             = SPOPS::Tool::ReadOnly
 field           = 
 field_discover  = yes
 id_field        = invoice_id
 is_secure       = no
 base_table      = invoice_old
 name            = invoice_num
 object_name     = Invoice

we'd just add a 'datasource' key with the name of our legacy datasource:

 [legacy_invoice]
 class           = OpenInteract2::LegacyInvoice
 isa             = SPOPS::Tool::ReadOnly
 datasource      = legacy
 ...

And that's it. You can use it just like any other SPOPS class:

 my $customer_id = $request->param( 'customer_id' );
 my $invoices = OpenInteract2::LegacyInvoice->fetch_group({
     where => 'customer_id = ?',
     value => $customer_id,
     order => 'invoice_date DESC'
 });
 foreach my $inv ( @{ $invoices } ) {
     print "Date: $inv->{invoice_date} ($inv->{num_items})\n";
 }

SPOPS will pull the data from the separate database, but when you're accessing the data you won't know (or care) where it's from.

This means it's also easy to swap datasources behind the scenes -- for instance, to point to a backup database server if the main one goes down.

SEE ALSO

OpenInteract2::Manual::Management - Creating management tasks

COPYRIGHT

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

AUTHORS

Chris Winters <chris@cwinters.com>