package DBIx::Frame; # -*- Perl -*- Tue Oct 21 13:49:39 CDT 2003 ############################################################################### # Written by Tim Skirvin # Copyright 2001-2002, Tim Skirvin and UIUC Board of Trustees. # Redistribution terms are below. ############################################################################### use vars qw(@EXPORT @EXPORT_OK @ISA $VERSION); $VERSION = "1.00"; =head1 NAME DBIx::Frame::CGI - tools for web-based use of DBIx::Frame databases =head1 SYNOPSIS use DBIx::Frame::CGI; DBIx::Frame->init('server', 'dbtype') || exit(0); my $DB = DBIx::Frame->new('database', 'user', 'pass') or die("Couldn't connect to database: ", DBIx->errstr); my $CGI = new CGI || die "Couldn't open CGI"; my $params = {}; foreach ($cgi->param) { $$params{$_} = $cgi->param($_); } my $action = $cgi->param('action') || ""; my $table = $cgi->param('table') || ""; print $cgi->header(), $cgi->start_html(-title => "YOUR TITLE"); print $DB->make_html( $action, $table, $params, {} ) or die "Couldn't run script"; print $cgi->end_html(); exit(0); More functions, and detailed descriptions, are below. =head1 DESCRIPTION DBIx::Frame::CGI is an extension of the DBIx::Frame module to allow for web use and administration of DBIx::Frame databases. It provides a common set of HTML functions for creating, modifying, viewing, and deleting entries in the database. These tools allow for simple administration scripts, as well as a decent API for creating more complex user scripts. =cut use strict; use DBIx::Frame; use HTML::FormRemove qw(RemoveFormValues); use Exporter; use CGI; use vars qw( @ACTIONS %ACTION ); @ACTIONS = qw( create list search ); %ACTION = ( 'edit' => \&html_edit, 'view' => \&html_view, 'create' => \&html_create, 'search' => \&html_search, 'update' => \&html_update, 'delete' => \&html_delete, 'insert' => \&html_insert, 'list' => \&html_list_banner, '' => \&html_actions, 'actions' => \&html_actions, ); use SelfLoader; SelfLoader->load_stubs(); 1; __DATA__ # Comment me out to test the functions without SelfLoader; =head1 USAGE All of these functions must be invoked on a fully created C object, as discussed in its manual page. There are four type of functions in this package - Full-Layout, HTML Form Layout, and Actions. =head2 Full-Layout Functions Note that these layout functions are fairly specific - they will make fully laid out HTML, based on the design goals of the author. These goals may not mesh exactly with what you want to do; if this is so, then it should be a fairly simple matter to write new functions based on these for your own CGI scripts. Specifically, each of these can be overridden by adding a section to your .cgi files that looks like this: package YOUR::PACKAGE; sub html_menu { # insert your own code here } Then, when you're using your own package, it will use this version of C (or any other piece of code) instead of the system-default code. You can therefore use this as a template to make your own web designs. The defaults, though, are actually fairly decent, or at least a fair bit of effort has been put into helping them be that way. Suggestions are, as always, encouraged. =over 4 =item make_html ( TABLE, ACTION, PARAMS, OPTIONS [, OTHER] ) Returns a whole formatted HTML page, using the subfunctions from the package, based on the input from C. At the bottom of the page is a centered C. The page still needs headers and footers. C, C, C, and C are passed into the sub- functions as appropriate. Default Cs (case-insensitive): ACTION Called Function Function Type [none] html_actions() Full-Layout Options create html_create() HTML Form Layout list html_list() HTML Form Layout search html_search() HTML Form Layout edit html_edit() HTML Form Layout view html_view() HTML Form Layout delete html_delete() HTML Form Layout + Actions update html_update() Actions insert html_insert() Actions More actions can be added with C, and actions can be removed with C. All actions are invoked with the following parameters: C
, C, C, C Valid C: nomenu If set, don't include the bottom menu C quiet If set, print as little information as possible with the HTML tables; not yet fully implemented =cut sub make_html { my ($self, $table, $action, $params, $options, @other) = @_; $params = {} unless ($params && ref $params); $options = {} unless ($options && ref $options); $action ||= 'actions'; my @return; if ( my $code = $self->action(lc $action) ) { push @return, &{$code}($self, $table, $params, $options, @other); } else { push @return, "Invalid action: $action" } unless ( $$options{'nomenu'} ) { push @return, "

Table Options

"; push @return, "
" . $self->html_menu($table) . "
"; } return wantarray ? @return : join("\n", @return); } =item html_actions ( [ TABLE [, PARAMS, OPTIONS ]] ) Returns a table of table/action pairs available to the user. Gets the information from C and C<@DBIx::Frame::ACTIONS>. Each column of the table is a different action; each row is a table. =cut sub html_actions { my ($self, $table, $params, $options, @other) = @_; my @list = sort $self->tables; my @return = "
"; my $list = scalar @list; foreach my $action ( @ACTIONS ) { my $act = ucfirst $action; push @return, " "; } push @return, " "; foreach my $table (@list) { next unless $table; foreach my $action ( @ACTIONS ) { next unless $action; push (@return, " "); } push (@return, " "); } push (@return, "
$act
" . "$table" . "
\n"); wantarray ? @return : join("\n", @return); } =item html_menu ( [TABLE] ) Returns a menu in HTML to navigate the various tables and actions available to the user. Gets the information from C and C<@DBIx::Frame::ACTIONS>. The menu is an HTML form that reinvokes the calling program, using the fields 'action' and 'table'. =cut sub html_menu { my ($self, $table, @other) = @_; my $cgi = new CGI; my @return = $cgi->start_form; my @list = sort $self->tables; push @return, $cgi->popup_menu('table', \@list, $table); foreach ( @ACTIONS ) { push @return, $cgi->submit('action', ucfirst $_ ); } push @return, $cgi->end_form; wantarray ? @return : join("\n", @return); } =back =head2 HTML Form Layout The HTML Form Layout functions are generally based around each table's C function, which is defined in its class. Note that this function must be properly created if you expect these functions to actually do anything. =over 4 =item html_create ( TABLE, PARAMS, OPTIONS ) Returns an HTML form containing the code necessary to insert an item into the database. Submitting the form should invoke C. =cut sub html_create { my ($self, $table, $params, $options, @other) = @_; my $html = $self->html->{$table}; my @return; push @return, "
Add to '$table'

" unless $$options{'quiet'}; return <
@{[ $self->_replace($params || {}, $html->( $self, undef, 'create', $options )) ]}
EOL } =item html_list ( TABLE, PARAMS, OPTIONS ) =item html_list_banner ( TABLE, PARAMS, OPTIONS ) =item html_list_nosearch ( TABLE, PARAMS, OPTIONS, ENTRIES ) Returns an HTML table containing data selected with C, using C. (Note that this does not use the C function.) The table also includes links to a perform more actions on the items - by default, it's 'view', but 'edit' and 'delete' can be added. Valid options for C: nodetail Doesn't offer 'view' action admin Offers 'edit' and 'delete' action count Total entries to print. Defaults to 50. first First entry to print. Defaults to 0. last Last entry to print. Defaults to (first + count) nocount Don't offer 'next' and 'last' options, offer a search dialogue instead (if necessary to narrow the search) nodelsearch Don't include the html_search() dialogue box if no matches are found. tdopts The options to use for each of the tags in the table. Defaults to 'align=center'. useropts See below. The trickiest of the above options is 'useropts'. This must be an array reference; it contains a list of additional actions to offer for each item. Each array item must be either the name of the function you want to invoke (see B for information on how to add these), or another array reference; this reference must contain first the name of the function you want to invoke, and then a list of tables that it affects. B is the same as B, except that it returns a small banner at the top. B is therefore more easily embedded in other code. B actually does the work of B using an array of selected datahashes (B). It may be invoked by other programs that want to select based on their own criteria. =cut sub html_list { my ($self, $table, $params, $options, @other ) = @_; return "" unless $table; $self->html_list_nosearch($table, $params, $options, $self->select($table, $self->_html_select($params), @other) ); } sub html_list_nosearch { my ($self, $table, $params, $options, @entries) = @_; return "" unless $table; my $entrycount = scalar @entries; $options ||= {}; my $total = $$options{'count'} || 50; my $first = $$params{'first'} || 0; my $last = $$params{'last'} || $first + $total; my $tdopts = $$options{'tdopts'} || "align=center"; # Get the list of actions to perform my @actions; push @actions, 'view' unless ( $$options{nodetail} ); push @actions, 'edit' if ($$options{admin}) ; push @actions, 'delete' if ($$options{admin}) ; # Parse 'useropts' if ( $$options{useropts} ) { next unless ref $$options{useropts}; my @useropts = @{$$options{useropts}}; foreach my $option (@useropts) { next unless $option; if (ref $option) { my $probation = shift @{$option}; my %tables; foreach ( @{$option} ) { $tables{lc $_}++ } next unless $tables{lc $table}; push @actions, $probation; } } } my $printed = 0; my @return = "\n"; # Iterate through the proper entries for (my $i = $first; $i < $last; $i++) { my $entry = $entries[$i]; next unless ($entry && ref $entry); # Print the HTML headers from list_head(), if not done yet unless ($printed++) { push @return, " "; foreach my $item ( $self->list_head($table) ) { push @return, " "; } push @return, " " if scalar(@actions); push @return, " "; } push @return, " "; foreach my $item ($self->make_list($table, $entry)) { # push @return, " "; push @return, " "; } if (scalar @actions) { my @list; foreach my $option (@actions) { my $action = $self->_action($table, $option, $entry, $0); push @list, "" . ucfirst lc $option . ""; } push @return, " "; } push @return, " "; } if (! $printed ) { # No entries matched, go back to 'search' push @return, "
No matches

"; push @return, "

$itemActions
$item $item ", join(" | \n", @list), "
\n"; push @return, $self->html_search($table, $params, $options) unless $$options{nodelsearch}; } else { my $action = $self->_action($table, 'list', $params, $0); if (!$$options{'nocount'}) { push @return, ""; push @return, " Entries ", $first + 1, " - ", ($entrycount < $last) ? $entrycount : $last, " of $entrycount
"; push @return, join(" \n", $self->_firstentry($action, $params, $first, $total, $entrycount) ); push @return, " "; push @return, "\n"; # Print the same info under the table push @return, "
", join("\n", $self->_firstentry($action, $params, $first, $total, $entrycount) ), "
"; } else { if ($entrycount > $total) { # Don't print count information, offer a 'search' option instead push @return, "", " Entries ", $first + 1, " - ", ($entrycount < $last) ? $entrycount : $last, " of $entrycount
", ""; push @return, ""; push @return, "
Narrow your search
"; push @return, $self->html_search($table, $params, $options); } else { push @return, "" } } } wantarray ? @return : join("\n", @return); } sub html_list_banner { my ($self, $table, $params, $options, @other ) = @_; my @ret = "
Matching entries in $table
"; push @ret, html_list(@_); wantarray ? @ret : join("\n", @ret); } =item html_search( TABLE, PARAMS, OPTIONS ) Returns an HTML form containing the code necessary to search the database. Submitting the form should invoke C. Valid options for C: nosearchname Don't include the 'search TABLE' bit at the top of the search =cut sub html_search { my ($self, $table, $params, $options, @other) = @_; my $html = $self->html->{$table} || return undef; my @return; push @return, "
Search $table
" unless $$options{'nosearchname'}; push @return, <
@{[ $self->_replace($params || {}, $html->( $self, undef, 'search', $options )) ]}
EOL wantarray ? @return : join("\n", @return); } =item html_edit ( TABLE, PARAMS, OPTIONS ) Returns an HTML form containing the code necessary to edit a database entry. Submitting the form should invoke C. =cut sub html_edit { my ($self, $table, $params, $options, @other) = @_; my $cgi = new CGI; return "" unless $table; my $html = $self->html->{$table} || return ""; my $datahash = {}; my @return; foreach my $entry ($self->select($table, $params, undef) ) { $$entry{'replace'} = 1; my @list; foreach ( keys %{$entry} ) { next unless $_ && $$entry{$_}; push @list, $cgi->hidden("Old.$_", $$entry{$_}); } my $id = $$entry{'ID'} || 0; push @return, <Edit item in $table
@{[ $cgi->hidden('ID', $$entry{ID}) ]} @list @{[ $self->_replace($entry, $html->( $self, $entry, 'update', $options )) ]}
EDIT } wantarray ? @return : join("\n", @return); } =item html_view ( TABLE, PARAMS, OPTIONS ) Returns an HTML form containing the code necessary to view an item in without the formatting of the form - thus making it printable. Relies on HTML::FormRemove. =cut sub html_view { my ($self, $table, $params, $options, @other) = @_; my $html = $self->html->{$table} || return ""; my @html = "
"; foreach my $entry ($self->select($table, $params, undef) ) { push @html , $self->_replace( $entry, $html->( $self, $entry, 'view', $options ) ); } push @html, "
"; my @return = RemoveFormValues(@html); wantarray ? @return : join("\n", @return); } =back =head2 HTML Form Layout + Actions =over 4 =item html_delete ( TABLE, PARAMS, OPTIONS ) Offer a method of deleting items from the database. Operates on two levels: either returns an HTML form containing the code necessary to delete an item from the database, or actually does the work and returns some basic searching information. The difference between the two actions is the value of the 'CONFIRM' parameter. If it's set, then delete, otherwise return message asking whether you want to continue. (Note that this is the least tested part of the code.) =cut sub html_delete { my ($self, $table, $params, $options, @other) = @_; my $id = $$params{'ID'} || 0; my @return; if ($$params{'CONFIRM'}) { # Do the deletion if ( $self->delete($table, $params) ) { # Deletion Successful push @return, "

ID \#$id from $table deleted

"; push @return, $self->html_search($table, {}, $options, @other); } else { # Deletion Unsuccessful push @return, "

Deletion from $table failed

"; push @return, DBI->errstr; push @return, $self->html_actions($table, @other); } } else { # Confirm the deletion first my @table = $id ? $self->select($table, { 'ID' => $id } ) : $self->select($table, $params); foreach my $entry ( @table ) { push @return, "

Are you sure you want to delete this entry?

"; push @return, "
    "; foreach (sort keys %{$entry}) { push @return, "
  • $_: $$entry{$_}"; } push @return, "
"; if ($id) { push @return, "Yes"; } else { my @items = "action=Delete"; foreach ($self->key($table) ) { push @items, join('=', $_, $$entry{$_} ) if $$entry{$_}; } push @items, "CONFIRM=1"; my $list = join '&', @items; push @return, "Yes"; } push @return, "No", "

"; } } wantarray ? join ("\n", @return) : @return; } =back =head2 Actions Actions actually do work on the database =over 4 =item html_update ( TABLE, PARAMS, OPTIONS ) Updates the items selected with C. The original items are selected with "Old.FIELD" fields in C. Returns an C of the appropriate values. =cut sub html_update { my ($self, $table, $params, $options, @other) = @_; my $id = $$params{'ID'} || 0; $options ||= {}; my $admin = $$options{'admin'} ? 1 : 0; my $checkhash = {}; foreach ($self->key($table)) { $$checkhash{$_} = $$params{"Old.$_"} } my $hash = $id ? { 'ID' => $id } : $checkhash; my @return = "

Updating $table
"; foreach my $entry ($self->select($table, $hash, undef) ){ my $checkhash = {}; foreach ($self->key($table)) { $$checkhash{$_} = $$params{"Old.$_"} } $$checkhash{'ID'} = $id ; if ( $self->update($table, $params, $checkhash, $admin) ) { push @return, "
Success! Database updated
"; push @return, $self->html_list($table, $params, $options, @other); } else { push @return, "

Couldn't update $table - " . $self->error || "Unknown error", "

" ; push @return, $self->html_list($table, $checkhash, $options, @other); } } return wantarray ? @return : join("\n", @return); } =item html_insert ( TABLE, PARAMS, OPTIONS ) Adds an item into C based on C. Returns an C of the same values, allowing you to add more entries easily. =cut sub html_insert { my ($self, $table, $params, $options, @other) = @_; $options ||= {}; my $admin = $$options{'admin'} ? 1 : 0; my (@missing, $hash, @return); if ($self->insert($table, $params, $admin)) { push @return, "
Success! Database updated with new entry
"; push @return, $self->html_list($table, $params, $options, @other); } else { push @return, "

Couldn't insert into $table - " . $self->error || "Unknown error" . "

" ; } push @return, $self->html_create($table, {}, @other); return wantarray ? @return : join("\n", @return); } =back =cut =head2 Local Behaviour Management One of the main goals of this module is to allow for programmers to manipulate the behaviours of these HTML forms fairly easily. This is primarily taken care of through the use of the C and C hash references, which are referenced throughout the code. There are also a few functions to help manipulate the behaviour of the code: =over 4 =item action ( ACTION [, CODEREF] ) This function handles the list of actions available within C. The actions are contained by a private hash, where the keys are the action names and the values are the code references to make the HTML and/or perform the actions. If C is not offered, returns undef. If it doesn't exist, and no C is offered, returns an empty string. If C is offered, sets the code refence value to C (regardless of whether the action previously existed). Regardless, returns the new code reference. Note, all code references should take the standard parameters: CODEREF($self, $table, $params, $options, @other); =cut sub action { my ($self, $action, $code) = @_; return undef unless $action; $action = lc $action; if ($code) { return undef unless ref $code; $ACTION{$action} = $code; } $ACTION{$action} || ""; } =item remove_action ( ACTION [, ACTION [, ACTION [...]]] ) Removes C from the list of actions. If multiple Cs are offered, removes them all. Returns the number of successful removals. =cut sub remove_action { my $self = shift; my $count = 0; foreach (@_) { if ($_ && $ACTION{$_}) { delete $ACTION{$_} && $count++ } } $count; } =back =cut ############################################################################### ### INTERNAL FUNCTIONS ######################################################## ############################################################################### ### _html_select ( PARAMS, OPTIONS ) # Creates the hash for passing into select(). sub _html_select { my ($self, $params, $options, @other) = @_; my $datahash = {}; if ($params && ref $params) { foreach (keys %{$params}) { next unless defined($$params{$_}); $$datahash{$_} = join("", "%", $$params{$_}, "%") if ( $$params{$_} =~ /\S/ ) ; $$datahash{$_} =~ s/^%\^//g if defined($$datahash{$_}); $$datahash{$_} =~ s/\$%$//g if defined($$datahash{$_}); } } $datahash; } ### _action ( TABLE, ACTION [, ENTRY [, PROG ]] ) # Returns the HTML code necessary to invoke PROG to do ACTION on ENTRY in # TABLE. sub _action { my ($self, $table, $action, $entry, $prog) = @_; return undef unless ($table && $action); $prog ||= $0; $entry ||= {}; my $select = $$entry{ID} ? "ID=$$entry{ID}" : $self->_make_select_keys($table, $entry) || ""; "$prog?action=$action&table=$table&$select"; } ### _firstentry ( ACTION, PARAMS, FIRST, TOTAL, COUNT ) # Create the "First 25 | Prev 25 | Next 25 | Last 25" links. sub _firstentry { my ($self, $action, $params, $first, $total, $count) = @_; return "" if ($count <= $total); return "" unless ($params && ref $params); my @options = ( 0, $first - $total, $first + $total, $count - $total ); my @labels = qw( First Previous Next Last ); my @return; push @return, "
", ""; for (my $i = 0; $i < scalar @options; $i++) { if ( ( $labels[$i] eq 'First' && ! $first > 0 ) || ( $labels[$i] eq 'Previous' && $first - $total < 0 ) || ( $labels[$i] eq 'Next' && $first + $total > $count ) || ( $labels[$i] eq 'Last' && $first + $total == $count ) ) { push @return, ""; } else { push @return, ""; } } push @return, "", "
", " $labels[$i] $total", ""; push @return, "
"; foreach (sort keys %{$params}) { push @return, CGI->hidden($_, $$params{$_}) unless lc $_ eq 'first'; } push @return, CGI->hidden('first', $options[$i]); push @return, CGI->submit( 'submit-number', "$labels[$i] $total" ) ; push @return, "
", "
"; wantarray ? @return : join(" ", @return); } ### _make_select_keys ( TABLE, ENTRY ) # Help make the URL for searching for ENTRY, based on the keys for that # TABLE. sub _make_select_keys { my ($self, $table, $entry) = @_; my @items; foreach ($self->key($table) ) { next unless $_; push @items, join('=', $_, $$entry{$_} ) if $$entry{$_}; } join '&', @items; } 1; __DATA__ =head1 SHARED DATA STRUCTURES The following data structions are considered public, and may be modified by the running program as appropriate. =over 4 =item @DBIx::Frame::ACTIONS Determines which actions should be offered in C. =back =cut =head1 NOTES None of these items do any work to determine whether the user is allowed to perform the actions that he's trying to do - this should be taken care of when writing the CGI scripts and choosing which user can connect. Good class design is also important. The C values that you use must guarantee that each item is unique! If this is not done, then functions like C can wreak havoc on your =head1 REQUIREMENTS Perl 5 or better, DBIx::Frame, and HTML::FormRemove. =head1 SEE ALSO B, B, B. =head1 TODO Fixing up the HTML::FormRemove thing would be nice, or else doing something else with C (which I'm not altogether happy with). =head1 AUTHOR Written by Tim Skirvin . =head1 LICENSE University of Illinois Open Source License Copyright (c) 2002 University of Illinois Board of Trustees All rights reserved Developed by: Theoretical Biophysics Group University of Illinois, Beckman Institute http://www.ks.uiuc.edu/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software, DBIx::Frame, and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution. * Neither the names of the Theoretical Biophysics Group, the University of Illinois, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. =head1 COPYRIGHT Copyright 2000-2001 by the University of Illinois Board of Trustees and Tim Skirvin . =cut ############################################################################### ### Version History ########################################################### ############################################################################### # v0.9 Fri Jul 13 10:51:18 CDT 2001 ### Release candidate. Internal documentation written, it seems modular. # v0.91 Tue Jul 17 11:48:09 CDT 2001 ### Added 'Table Options' to the html_menu() in make_html() # v0.92 Fri Jul 27 09:34:10 CDT 2001 ### Removed quotes from around the 'table'. Fixed a bug in html_list() # involving HTML tables. # v0.93 Thu Aug 16 12:09:47 CDT 2001 ### Added actions() and its related functions, and modified make_html() for it ### Added support for 'useropts' in the list of OPTIONS for html_list() ### Standardized the 'actions' list in html_list() ### Added html_list_banner() for increased actions() support # v0.94 Fri Jan 25 13:58:07 CST 2002 ### Fixed the "Next 25" thing to work on searches. # v0.95 Thu Feb 21 11:18:04 CST 2002 ### Changed to UIUC/NCSA Open Source License - essentially the BSD license. # v0.96 Tue Apr 2 13:35:03 CST 2002 ### Fixed to work with with DBI::Frame 1.04 # v0.97 Wed Aug 14 11:12:04 CDT 2002 ### html_list_nosearch() split off to let other functions use it. Added ### 'tdopts' as an option. Updated _firstentry() to use whole forms instead ### of links; this looks much better, and works better too. # v0.98 Mon Oct 7 16:20:45 CDT 2002 ### Actually using 'options' for various fields. ### Minor typographical fixes. # v0.98.1 Wed Jan 15 14:56:45 CST 2003 ### Added an 'option' of 'quiet', which is meant for displaying only the ### necessary information for the tables - ie, no 'Add to Register' text. ### Only html_create uses it so far, we'll do more soon. # v0.98.2 Fri Jan 17 13:03:45 CST 2003 ### Using SelfLoader. It might help. ### Trying to use the 'admin' field more appropriately. Need more docs ### first. ### Still need to match DBI::Frame's ADMIN, ORDER, REQUIRED fields # v0.98.3 Thu Mar 27 14:19:29 CST 2003 ### Set 'nowrap' on the actions table field # v0.98.4 Mon Oct 20 10:08:49 CDT 2003 ### html_search() now has a 'nosearchname' field. Updated the ### documentation a bit for prettiness # v1.00 Tue Oct 21 13:49:49 CDT 2003 ### Updated to be DBIx::Frame. Updated for prettiness.