Home | Wiki | OI 1.x Docs | OI 2.x Docs |
OpenInteract2::Manual::Tutorial - Learn how to create and modify a package
This tutorial will show you the different methods for creating a package and how to maintain them.
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.
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
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
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.
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
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.
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.
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
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.
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.
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.
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.)
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.
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.
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:
You never know where in the filesystem your site or package will be, so why tie yourself down?
Templates may be located in the package directory or in the sitewide package template directory. Using this syntax to refer to a template hides us from this location distinction. It also makes it possible for us to have them be loaded from the database or other storage mechanism.
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!
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
.
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.
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.
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.
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.
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.
Now let's get it installed to a website, first however...
Before you go any further put your info in the relevant areas of
book/package.conf
(next to 'author' and 'url').
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.
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.
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
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.
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!
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.
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.
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.)
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!
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.
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:
OpenInteract2::Action::CommonSearch - Provides 'search_form' and 'search' tasks. (Sound familiar?)
OpenInteract2::Action::CommonAdd - Provides the 'display_add' and 'add' tasks so you can add new objects.
OpenInteract2::Action::CommonDisplay - Provides the 'display' task so you can display an object in a non-editable format.
OpenInteract2::Action::CommonUpdate - Provides the 'display_form' and 'update' tasks so you can modify an existing object.
OpenInteract2::Action::CommonRemove - Provides the 'remove' task to permanently delete an existing object.
Using one or more of these is a three-step process:
Subclass the right class.
Add the right configuration keys and values
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.
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.
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.
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.
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:
We're displaying an error message if it occurs. This is useful because the 'update' task will redisplay our editing form if it encounters any errors.
We have set of 'form_begin' and 'form_end' around our table.
We're using 'label_form_text_row' instead of 'label_text_row' for our field displays.
We're passing the text in a 'value' attribute rather than a 'text' attribute. (TODO: change form_text_row to use 'value'?)
We added a submit button using the widget 'form_submit_row'
We have a new widget, 'form_hidden', to let the server know which object we're editing.
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.
Now you should be getting the drill. Adding a common action to your class means:
Adding entries to your action configuration.
Adding templates, if necessary.
Subclassing the common action.
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.
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!
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:
Enabling and using action security
Enabling and using security for individual SPOPS objects
Adding functionality to your SPOPS objects
Setting up 'lookup' actions
Setting up 'template_only' actions
Using multiple datasources
Distribution of the 'book' package, version 0.08.
http://openinteract.sourceforge.net/cgi-bin/twiki/view/OI/OpenInteract2Tutorial
Copyright (c) 2002-2003 Chris Winters. All rights reserved.
Chris Winters <chris@cwinters.com>
Generated from the OpenInteract 1.99_03 source.