From: Chris W. <la...@us...> - 2005-03-04 03:03:26
|
Update of /cvsroot/openinteract/OpenInteract2/doc/Manual In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv13491/Manual Modified Files: TutorialAdvanced.pod Log Message: OIN-92: add more information... Index: TutorialAdvanced.pod =================================================================== RCS file: /cvsroot/openinteract/OpenInteract2/doc/Manual/TutorialAdvanced.pod,v retrieving revision 1.5 retrieving revision 1.6 diff -C2 -d -r1.5 -r1.6 *** TutorialAdvanced.pod 26 Feb 2005 05:04:04 -0000 1.5 --- TutorialAdvanced.pod 4 Mar 2005 03:03:15 -0000 1.6 *************** *** 9,14 **** common) actions you can do with OpenInteract2. ! For the examples in this document we'll rely on the 'books' ! application created in L<OpenInteract2::Manual::Tutorial>. =head1 ACTIONS: SETTING UP A LOOKUP TABLE --- 9,14 ---- common) actions you can do with OpenInteract2. ! For the examples in this document we'll rely on the 'book' application ! created in L<OpenInteract2::Manual::Tutorial>. =head1 ACTIONS: SETTING UP A LOOKUP TABLE *************** *** 17,21 **** One of the immediate problems with our 'book' record is that there's ! no way to categorize them. Sure, most books are cross-category -- my favorite: 'Computer Programming' + 'Self-Help' -- but our books don't even have B<one>. --- 17,21 ---- 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 B<one>. *************** *** 132,137 **** =back ! See the documentation for the C<lookup> package for more information ! and additional options you can set. Add these files to your C<MANIFEST>, modify your C<Changes> and --- 132,137 ---- =back ! See L<OpenInteract2::App::Lookup> for more information and additional ! options you can set. Add these files to your C<MANIFEST>, modify your C<Changes> and *************** *** 163,171 **** =head2 Add field to table ! So now we need to add our category to the book table. Using whatever ! database tool you're comfortable with add the following definition: 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 --- 163,176 ---- =head2 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 *************** *** 185,189 **** "_display_form_customize", etc. ! So in our action L<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 --- 190,194 ---- "_display_form_customize", etc. ! So in our action C<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 *************** *** 198,202 **** } ! And reference it: sub _display_add_customize { --- 203,207 ---- } ! And reference it from your callbacks: sub _display_add_customize { *************** *** 221,225 **** first_label = '---Categories---' ) -%] ! Assuming you have categrories: category_id category --- 226,230 ---- first_label = '---Categories---' ) -%] ! Assuming you have the following categrories: category_id category *************** *** 230,255 **** 4 Vegetarian Cooking ! You'll see something like this (remember that we sorted the categories ! when we assigned them to the template parameters): ! <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> =head2 Add fields to action configuration ! Finally, since we configured our SPOPS object with 'field_discover' we ! didn't have to add the field to our SPOPS configuration. But since ! common action configurations don't have this feature yet you'll have ! to add to your C<conf/action.ini> under '[book]' these keys and ! values: c_update_fields = category_id c_add_fields = category_id =head1 ACTIONS: A TEMPLATE CAN BE AN ACTION --- 235,267 ---- 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> =head2 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 ! C<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! + =head1 ACTIONS: A TEMPLATE CAN BE AN ACTION *************** *** 312,317 **** The only differences are that when we call C<OI.action_execute()> we go through the action's security checks as well as use its cached ! content if available. The C<INCLUDE> call does not -- but it's also ! faster. =head1 ACTIONS: ADDING VALIDATION TO COMMON TASKS --- 324,331 ---- The only differences are that when we call C<OI.action_execute()> we go through the action's security checks as well as use its cached ! content if available. The C<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.) =head1 ACTIONS: ADDING VALIDATION TO COMMON TASKS *************** *** 319,324 **** --- 333,530 ---- =head1 ACTIONS: USING MULTIPLE TABLES WITH COMMON SEARCHING + =head2 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 C<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. + + =head2 Create a link table + + 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. + + =head2 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 + C<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. + + =head2 Configuring the search + + In your C<conf/action.ini> you'll need to add these fields to your + C<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<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! + =head1 ACTIONS: SECURING AN ACTION + =head2 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. + + =head2 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 L<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. + + =head2 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 + L<OpenInteract2::Manage::Website::CreateSecurityForAction>, or using + C<oi2_manage>: + + oi2_manage secure_action --action=book --scope=group --scope_id=5 + =head1 SPOPS: ADDING OBJECT BEHAVIORS *************** *** 333,336 **** --- 539,629 ---- =head2 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. + =head1 SEE ALSO |