%myvars; ]> Advanced Topics by Joel Aufrecht This tutorial covers topics which are not essential to creating a minimal working package. Each section can be used independently of all of the others; all sections assume that you've completed the basic tutorial. Write the Requirements and Design Specs Before you get started you should make yourself familiar with the tags that are used to write your documentation. For tips on editing SGML files in emacs, see . It's time to document. For the tutorial we'll use pre-written documentation. When creating a package from scratch, start by copying the documentation template from /var/lib/aolserver/openacs-dev/packages/acs-core-docs/xml/docs/xml/package-documentation-template.xml to myfirstpackage/www/docs/xml/index.xml. You then edit that file with emacs to write the requirements and design sections, generate the html, and start coding. Store any supporting files, like page maps or schema diagrams, in the www/doc/xml directory, and store png or jpg versions of supporting files in the www/doc directory. For this tutorial, you should instead install the pre-written documentation files for the tutorial app. Log in as service0, create the standard directories, and copy the prepared documentation: [service0 service0]$ cd /var/lib/aolserver/service0/packages/myfirstpackage/ [service0 myfirstpackage]$ mkdir -p www/doc/xml [service0 myfirstpackage]$ cd www/doc/xml [service0 xml]$ cp /var/lib/aolserver/service0/packages/acs-core-docs/www/files/myfirstpackage/* . [service0 xml]$ OpenACS uses DocBook for documentation. DocBook is an XML standard for semantic markup of documentation. That means that the tags you use indicate meaning, not intended appearance. The style sheet will determine appearance. You will edit the text in an xml file, and then process the file into html for reading. Open the file index.xml in emacs. Examine the file. Find the version history (look for the tag <revhistory>). Add a new record to the document version history. Look for the <authorgroup> tag and add yourself as a second author. Save and exit. Process the xml file to create html documentation. The html documentation, including supporting files such as pictures, is stored in the www/docs/ directory. A Makefile is provided to generate html from the xml, and copy all of the supporting files. If Docbook is set up correctly, all you need to do is: [service0 xml]$ make cd .. ; /usr/bin/xsltproc ../../../acs-core-docs/www/xml/openacs.xsl xml/index.xml Writing requirements-introduction.html for chapter(requirements-introduction) Writing requirements-overview.html for chapter(requirements-overview) Writing requirements-cases.html for chapter(requirements-cases) Writing sample-data.html for chapter(sample-data) Writing requirements.html for chapter(requirements) Writing design-data-model.html for chapter(design-data-model) Writing design-ui.html for chapter(design-ui) Writing design-config.html for chapter(design-config) Writing design-future.html for chapter(design-future) Writing filename.html for chapter(filename) Writing user-guide.html for chapter(user-guide) Writing admin-guide.html for chapter(admin-guide) Writing bi01.html for bibliography Writing index.html for book [service0 xml]$ Verify that the documentation was generated and reflects your changes by browsing to http://yoursite:8000/myfirstpackage/doc Add the new package to CVS Before you do any more work, make sure that your work is protected by putting it all into cvs. The cvs add command is not recursive, so you'll have to traverse the directory tree manually and add as you go. (More on CVS) [service0 xml]$ cd .. [service0 doc]$ cd .. [service0 www]$ cd .. [service0 myfirstpackage]$ cd .. [service0 packages]$ cvs add myfirstpackage/ Directory /cvsroot/service0/packages/myfirstpackage added to the repository [service0 packages]$ cd myfirstpackage/ [service0 myfirstpackage]$ cvs add www Directory /cvsroot/service0/packages/myfirstpackage/www added to the repository [service0 myfirstpackage]$ cd www [service0 www]$ cvs add doc Directory /cvsroot/service0/packages/myfirstpackage/www/doc added to the repository [service0 www]$ cd doc [service0 doc]$ cvs add * cvs add: cannot add special file `CVS'; skipping cvs add: scheduling file `admin-guide.html' for addition cvs add: scheduling file `bi01.html' for addition cvs add: scheduling file `data-model.dia' for addition cvs add: scheduling file `data-model.png' for addition cvs add: scheduling file `design-config.html' for addition cvs add: scheduling file `design-data-model.html' for addition cvs add: scheduling file `design-future.html' for addition cvs add: scheduling file `design-ui.html' for addition cvs add: scheduling file `filename.html' for addition cvs add: scheduling file `index.html' for addition cvs add: scheduling file `page-map.dia' for addition cvs add: scheduling file `page-map.png' for addition cvs add: scheduling file `requirements-cases.html' for addition cvs add: scheduling file `requirements-introduction.html' for addition cvs add: scheduling file `requirements-overview.html' for addition cvs add: scheduling file `requirements.html' for addition cvs add: scheduling file `sample-data.html' for addition cvs add: scheduling file `sample.png' for addition cvs add: scheduling file `user-guide.html' for addition cvs add: scheduling file `user-interface.dia' for addition cvs add: scheduling file `user-interface.png' for addition Directory /cvsroot/service0/packages/myfirstpackage/www/doc/xml added to the repository cvs add: use 'cvs commit' to add these files permanently [service0 doc]$ cd xml [service0 xml]$ cvs add Makefile index.xml cvs add: scheduling file `Makefile' for addition cvs add: scheduling file `index.xml' for addition cvs add: use 'cvs commit' to add these files permanently [service0 xml]$ cd ../../.. [service0 myfirstpackage]$ cvs commit -m "new package" cvs commit: Examining . cvs commit: Examining www cvs commit: Examining www/doc cvs commit: Examining www/doc/xml RCS file: /cvsroot/service0/packages/myfirstpackage/www/doc/admin-guide.html,v done Checking in www/doc/admin-guide.html; /cvsroot/service0/packages/myfirstpackage/www/doc/admin-guide.html,v <-- admin-guide.html initial revision: 1.1 done (many lines omitted) [service0 myfirstpackage]$
Upgrading a local CVS repository
Adding Comments You can track comments for any ACS Object. Here we'll track comments for notes. On the note-edit.tcl/adp pair, which is used to display individual notes, we want to put a link to add comments at the bottom of the screen. If there are any comments, we want to show them. First, we need to generate a url for adding comments. In note-edit.tcl: set comment_add_url "[general_comments_package_url]comment-add?[export_vars { { object_id $note_id } { object_name $title } { return_url "[ad_conn url]?[ad_conn query]"} }]" This calls a global, public tcl function that the general_comments package registered, to get its url. You then embed in that url the id of the note and its title, and set the return_url to the current url so that the user can return after adding a comment. We need to create html that shows any existing comments. We do this with another general_comments function: set comments_html [general_comments_get_comments -print_content_p 1 $note_id] First, we pass in an optional parameter that that says to actually show the contents of the comments, instead of just the fact that there are comments. Then you pass the note id, which is also the acs_object id. We put our two new variables in the note-edit.adp page. <a href="@comment_add_url@">Add a comment</a> @comments_html@ Admin Pages There are at least two flavors of admin user interface: Admins use same pages as all other users, except that they are offered admin links and buttons where appropriate. For example, if admins have privilege to bulk-delete items you could provide checkboxes next to every item seen on a list and the Delete Selected button on the bottom of the list. Dedicated admin pages. If you want admins to have access to data that users aren't interested in or aren't allowed to see you will need dedicated admin pages. The conventional place to put those dedicated admin pages is in the /var/lib/aolserver/service0/packages/myfirstpackage/www/admin directory. [service0 www]$ mkdir admin [service0 www]$ cd admin Even if your application doesn't need any admin pages of its own you will usually need at least one simple page with a bunch of links to existing administration UI such as Category Management or standard Parameters UI. Adding the link to Category Management is described in the section on categories. The listing below adds a link to the Parameters UI of our package. [service0 admin]$ vi index.adp <master> <property name="title">@title;noquote@</property> <property name="context">@context;noquote@</property> <ul class="action-links"> <li><a href="@parameters_url@" title="Set parameters" class="action_link">Set parameters</a></li> </ul> [service0 admin]$ vi index.tcl ad_page_contract {} { } -properties { context_bar } set package_id [ad_conn package_id] permission::require_permission \ -object_id $package_id \ -privilege admin] set context [list] set title "Administration" set parameters_url [export_vars -base "/shared/parameters" { package_id { return_url [ad_return_url] } }] Now that you have the first admin page it would be nice to have a link to it somewhere in the system so that admins don't have to type in the /admin every time they need to reach it. You could put a static link to the toplevel index.adp but that might be distracting for people who are not admins. Besides, some people consider it impolite to first offer a link and then display a nasty "You don't have permission to access this page" message. In order to display the link to the admin page only to users that have admin privileges add the following code near the top of /var/lib/aolserver/service0/packages/myfirstpackage/www/admin/index.tcl: set package_id [ad_conn package_id] set admin_p [permission::permission_p -object_id $package_id \ -privilege admin -party_id [ad_conn untrusted_user_id]] if { $admin_p } { set admin_url "admin" set admin_title Administration } In /var/lib/aolserver/service0/packages/myfirstpackage/www/admin/index.adp put: <if @admin_p@ ne nil> <a href="@admin_url@">@admin_title@</a> </if> Categories You can associate any ACS Object with one or more categories. In this tutorial we'll show how to equip your application with user interface to take advantage of the Categories service. We'll start by installing the Categories service. Go to /acs/admin and install it. This step won't be necessary for the users of your applications because you'll create a dependency with the Package Manager which will take care that the Categories service always gets installed when your application gets installed. Now that we have installed the Categories service we can proceed to modifying our application so that it can take advantage of it. We'll do it in three steps: The Categories service provides a mechanism to associate one or more category trees that are relevant to your application. One example of such tree is a tree of geographical locations. Continents are on the top of such tree, each continent containing countries etc. Another tree might contain market segments etc. Before users of your application can take advantage of the Categories service there needs to be a way for administrators of your application to choose which category trees are applicable for the application. The way to achieve this is is to provide a link to the Category Management pages. Add the following snippet to your /var/lib/aolserver/service0/packages/myfirstpackage/www/admin/index.tcl file: set category_map_url [export_vars -base \ "[site_node::get_package_url -package_key categories]cadmin/one-object" \ { { object_id $package_id } }] and the following snippet to your /var/lib/aolserver/service0/packages/myfirstpackage/www/admin/index.adp file: <li><a href="@category_map_url@" class="action_link">Site-Wide Categories</a> The link created by the above code will take the admin to the generic admin UI where he can pick category trees that make sense for this application. The same UI also includes facilities to build and edit category trees. Notice that the only parameter in this example is package_id so that category trees will be associated with the object identified by this package_id. The categorization service is actually more general than that: instead of package_id you could use an ID of some other object that serves as a "container" in your application. For example, if your discussion forums application supports multiple forums you would use forum_id to associate category trees with just that one forum rather than the entire application instance. Once the category trees have been selected users need a way to categorize items. The easiest way to do this is by adding the category widget type of the form builder to note-edit.tcl. To achieve this we'll need to use the -extend switch to the ad_form command. Here's the "meat" of the note-edit.tcl page: ad_form -name note -form { {item_id:key} {title:text {label Title}} } set package_id [ad_conn package_id] set category_trees [category_tree::get_mapped_trees $package_id] foreach tree $category_trees { foreach { tree_id name subtree_id } $tree {} ad_form -extend -name note -form \ [list [list category_id_${tree_id}:integer(category),optional \ {label $name} \ {html {single single}} \ {category_tree_id $tree_id} \ {category_subtree_id $subtree_id} \ {category_object_id {[value_if_exists entry_id]}}]] } ad_form -extend \ -name note \ -new_request { permission::require_permission -object_id [ad_conn package_id] -privilege create set page_title "Add a Note" set context [list $page_title] } -edit_request { permission::require_write_permission -object_id $item_id mfp::note::get \ -item_id $item_id \ -array note_array set title $note_array(title) set page_title "Edit a Note" set context [list $page_title] } -new_data { mfp::note::add \ -title $title } -after_submit { ad_returnredirect "." ad_script_abort } This page requires a note_id to determine which record should be deleted. It also looks for a confirmation variable, which should initially be absert. If it is absent, we create a form to allow the user to confirm the deletion. Note that in entry-edit.tcl we used ad_form to access the Form Template commands; here, we call them directly because we don't need the extra features of ad_form. The form calls itself, but with hidden variables carrying both note_id and confirm_p. If confirm_p is present, we delete the record, set redirection back to the index, and abort script execution. The database commands: [service0@yourserver www]$ emacs note-delete.xql <?xml version="1.0"?> <queryset> <fullquery name="do_delete"> <querytext> select samplenote__delete(:note_id) </querytext> </fullquery> <fullquery name="get_name"> <querytext> select samplenote__name(:note_id) </querytext> </fullquery> </queryset> And the adp page: [service0@yourserver www]$ emacs note-delete.adp <master> <property name="title">@title@</property> <property name="context">{@title@}</property> <h2>@title@</h2> <formtemplate id="note-del-confirm"></formtemplate> </form> The ADP is very simple. The formtemplate tag outputs the HTML form generated by the ad_form command with the matching name. Test it by adding the new files in the APM and then deleting a few samplenotes. Categories You can associate any ACS Object with one or more categories. In this tutorial we'll show how to equip your application with user interface to take advantage of the Categories service. We'll start by installing the Categories service. Go to /acs/admin and install it. This step won't be necessary for the users of your applications because you'll create a dependency with the Package Manager which will take care that the Categories service always gets installed when your application gets installed. Now that we have installed the Categories service we can proceed to modifying our application so that it can take advantage of it. We'll do it in three steps: The Categories service provides a mechanism to associate one or more category trees that are relevant to a particular application instance. One example of such tree is a tree of geographical locations. Continents are on the top of such tree, each continent containing countries etc. Another tree might contain market segments etc. Before users of your application can take advantage of the Categories service there needs to be a way for administrators of your application to choose which category trees are applicable for the application. The way to achieve this is is to provide a link to the Category Management pages. Add the following snippet to your /var/lib/aolserver/service0/packages/myfirstpackage/www/admin/index.tcl file: set category_map_url [export_vars -base \ "[site_node::get_package_url -package_key categories]cadmin/one-object" \ { { object_id $package_id } }] and the following snippet to your /var/lib/aolserver/service0/packages/myfirstpackage/www/admin/index.adp file: <li><a href="@category_map_url@" class="action_link">Site-Wide Categories</a> The link created by the above code will take the admin to the generic admin UI where he can pick category trees that make sense for this application. The same UI also includes facilities to build and edit category trees. Notice that the only parameter in this example is package_id so that category trees will be associated with the object identified by this package_id. The categorization service is actually more general than that: instead of package_id you could use an ID of some other object that serves as a "container" in your application. For example, if your discussion forums application supports multiple forums you would use forum_id to associate category trees with just that one forum rather than the entire application instance. Once the category trees have been selected users need a way to categorize items. The easiest way to do this is by adding the category widget type of the form builder to note-edit.tcl. To achieve this we'll need to use the -extend switch to the ad_form command. Here's the note-edit.tcl page with added sections emphasized. ad_page_contract { This is the view-edit page for notes. @author Your Name (you@example.com) @cvs-id $Id$ @param item_id If present, assume we are editing that note. Otherwise, we are creating a new note. } { item_id:integer,optional } ad_form -name note -form { {item_id:key} {title:text {label Title}} } category::ad_form::add_widgets \ -form_name note \ -container_object_id [ad_conn package_id] \ -categorized_object_id [value_if_exists item_id] ad_form -extend -name note \ -new_request { permission::require_permission -object_id [ad_conn package_id] -privilege create set page_title "Add a Note" set context [list $page_title] } -edit_request { permission::require_write_permission -object_id $item_id mfp::note::get \ -item_id $item_id \ -array note_array set title $note_array(title) set page_title "Edit a Note" set context [list $page_title] } -on_submit { set category_ids [category::ad_form::get_categories \ -container_object_id [ad_conn package_id]] } -new_data { mfp::note::add \ -title $title \ -item_id $item_id category::map_object \ -remove_old \ -object_id $item_id \ $category_ids set message "Note $title added" } -edit_data { mfp::note::edit \ -item_id $item_id \ -title $title category::map_object \ -remove_old \ -object_id $item_id \ $category_ids set message "Note $title changed" } -after_submit { ad_returnredirect -message $message "." ad_script_abort } Note how we have replaced what was a single ad_form invocation with two. The -extend flag is used to build a form incrementally. We had to do it so that we can insert the call to category::ad_form::add_widgets. This procedure will add as many category widgets as there are trees associated with our package_id. The complementary proc category::ad_form::get_categories will take care of collecting the values after the form has been submitted. The block -on_submit will get executed at this time, followed by execution of either -new_data or -edit_data, depending on whether we are adding a new note or editing an existing one. Profile your code There are several facilities for profiling your code in OpenACS. The first thing to do is to install the developer-support package and play around with it. But there is also support in the API for profiling your code: profiling your code using ds_profile Prepare the package for distribution. Browse to the package manager. Click on tutorialapp. Click on Generate a distribution file for this package from the filesystem. Click on the file size (37.1KB) after the label Distribution File: and save the file to /tmp. The publish point for new packages should be fixed. Notifications by David Bell and Simon Carstensen The notifications package allows you to send notifications through any defined communications medium (e.g. email, sms) upon some event occuring within the system. This tutorial steps through the process of integrating the notifications package with your package. First step is to create the notification types. To do this a script similar to the one below needs to be loaded into Postgresql. I create this script in a package-name/sql/postgresql/package-name-notifications-init.sql file. I then load this file from my create sql file. The following code snippet is taken from Weblogger. It creates a lars_blogger_notif notification type (which was created above). create function inline_0() returns integer as ' declare impl_id integer; v_foo integer; begin -- the notification type impl impl_id := acs_sc_impl__new ( ''NotificationType'', ''lars_blogger_notif_type'', ''lars-blogger'' ); v_foo := acs_sc_impl_alias__new ( ''NotificationType'', ''lars_blogger_notif_type'', ''GetURL'', ''lars_blogger::notification::get_url'', ''TCL'' ); v_foo := acs_sc_impl_alias__new ( ''NotificationType'', ''lars_blogger_notif_type'', ''ProcessReply'', ''lars_blogger::notification::process_reply'', ''TCL'' ); PERFORM acs_sc_binding__new ( ''NotificationType'', ''lars_blogger_notif_type'' ); v_foo:= notification_type__new ( NULL, impl_id, ''lars_blogger_notif'', ''Blog Notification'', ''Notifications for Blog'', now(), NULL, NULL, NULL ); -- enable the various intervals and delivery methods insert into notification_types_intervals (type_id, interval_id) select v_foo, interval_id from notification_intervals where name in (''instant'',''hourly'',''daily''); insert into notification_types_del_methods (type_id, delivery_method_id) select v_foo, delivery_method_id from notification_delivery_methods where short_name in (''email''); return (0); end; ' language 'plpgsql'; select inline_0(); drop function inline_0(); You also need a drop script. This is untested for comptability with the above script. -- @author gwong@orchardlabs.com,ben@openforce.biz -- @creation-date 2002-05-16 -- -- This code is newly concocted by Ben, but with significant concepts and code -- lifted from Gilbert's UBB forums. Thanks Orchard Labs. -- Lars and Jade in turn lifted this from gwong and ben. create function inline_0 () returns integer as ' declare row record; begin for row in select nt.type_id from notification_types nt where nt.short_name in (''lars_blogger_notif_type'',''lars_blogger_notif'') loop perform notification_type__delete(row.type_id); end loop; return null; end;' language 'plpgsql'; select inline_0(); drop function inline_0 (); -- -- Service contract drop stuff was missing - Roberto Mello -- create function inline_0() returns integer as ' declare impl_id integer; v_foo integer; begin -- the notification type impl impl_id := acs_sc_impl__get_id ( ''NotificationType'', -- impl_contract_name ''lars_blogger_notif_type'' -- impl_name ); PERFORM acs_sc_binding__delete ( ''NotificationType'', ''lars_blogger_notif_type'' ); v_foo := acs_sc_impl_alias__delete ( ''NotificationType'', -- impl_contract_name ''lars_blogger_notif_type'', -- impl_name ''GetURL'' -- impl_operation_name ); v_foo := acs_sc_impl_alias__delete ( ''NotificationType'', -- impl_contract_name ''lars_blogger_notif_type'', -- impl_name ''ProcessReply'' -- impl_operation_name ); select into v_foo type_id from notification_types where sc_impl_id = impl_id and short_name = ''lars_blogger_notif''; perform notification_type__delete (v_foo); delete from notification_types_intervals where type_id = v_foo and interval_id in ( select interval_id from notification_intervals where name in (''instant'',''hourly'',''daily'') ); delete from notification_types_del_methods where type_id = v_foo and delivery_method_id in ( select delivery_method_id from notification_delivery_methods where short_name in (''email'') ); return (0); end; ' language 'plpgsql'; select inline_0(); drop function inline_0(); The next step is to setup our notification creation. A new notification must be added to the notification table for each blog entry added. We do this using the notification::new procedure notification::new \ -type_id [notification::type::get_type_id \ -short_name lars_blogger_notif] \ -object_id $blog(package_id) \ -response_id $blog(entry_id) \ -notif_subject $blog(title) \ -notif_text $new_content This code is placed in the tcl procedure that creates blog entries, right after the entry gets created in the code. The $blog(package_id) is the OpenACS object_id of the Weblogger instance to which the entry has been posted to and the $new_content is the content of the entry. This example uses the package_id for the object_id, which results in setting up notifications for all changes for blogger entries in this package. However, if you instead used the blog_entry_id or something like that, you could set up per-item notifications. The forums packages does this -- you can look at it for an example. The final step is to setup the notification subscription process. In this example we want to let a user find out when a new entry has been posted to the blog. To do this we put a link on the blog that allows them to subscribe to notifications of new entries. The notifications/requests-new page is very handy in this situation. Such a link can be created using the notification::display::request_widget proc: set notification_chunk [notification::display::request_widget \ -type lars_blogger_notif \ -object_id $package_id \ -pretty_name [lars_blog_name] \ -url [lars_blog_public_package_url] \ ] which will return something like You may <a href="/notifications/request-new?...">request notification</a> for Weblogger. which can be readily put on the blog index page. The pretty_name parameter is what appears at the end of the text returned (i.e. "... request notification</a> for pretty_name"), The url parameter should be set to the address we want the user to be redirected to after they have finished the subscription process. This should be all you need to implement a notification system. For more examples look at the forums package. Hierarchical data by Jade Rubick with help from many people in the OpenACS community One of the nice things about using the OpenACS object system is that it has a built-in facility for tracking hierarchical data in an efficient way. The algorithm behind this is called tree_sortkey. Any time your tables are subclasses of the acs_objects table, then you automatically get the ability to structure them hierarchically. The way you do this is currently via the context_id column of acs_objects (Note that there is talk of adding in a parent_id column instead, because the use of context_id has been ambiguous in the past). So when you want to build your hierarchy, simply set the context_id values. Then, when you want to make hierarchical queries, you can do them as follows: db_multirow categories blog_categories " SELECT c.*, o.context_id, tree_level(o.tree_sortkey) FROM blog_categories c, acs_objects o WHERE c.category_id = o.object_id ORDER BY o.tree_sortkey" Note the use of the tree_level() function, which gives you the level, starting from 1, 2, 3... Here's an example, pulling all of the children for a given parent: SELECT children.*, tree_level(children.tree_sortkey) - tree_level(parent.tree_sortkey) as level FROM some_table parent, some_table children WHERE children.tree_sorktey between parent.tree_sortkey and tree_right(parent.tree_sortkey) and parent.tree_sortkey <> children.tree_sortkey and parent.key = :the_parent_key; The reason we substract the parent's tree_level from the child's tree_level is that the tree_levels are global, so if you want the parent's tree_level to start with 0, you'll want the subtraction in there. This is a reason you'll commonly see magic numbers in tree_sortkey SQL queries, like tree_level(children.tree_sortkey) - 4. That is basically an incorrect way to do it, and subtracting the parent's tree_level is the preferred method. This example does not include the parent. To return the entire subtree including the parent, leave out the non-equals clause: SELECT subtree.*, tree_level(children.tree_sortkey) - tree_level(parent.tree_sortkey) as level FROM some_table parent, some_table subtree WHERE subtree.tree_sorktey between parent.tree_sortkey and tree_right(parent.tree_sortkey) and parent.key = :the_parent_key; If you are using the Content Repository, you get a similar facility, but the parent_id column is already there. Note you can do joins with tree_sortkey: SELECT p.item_id, repeat(:indent_pattern, (tree_level(p.tree_sortkey) - 5)* :indent_factor) as indent, p.parent_id as folder_id, p.project_name FROM pm_projectsx p, cr_items i WHERE p.project_id = i.live_revision ORDER BY i.tree_sortkey This rather long thread explains How tree_sortkeys work and this paper describes the technique for tree_sortkeys, although the OpenACS implementation has a few differences in the implementation, to make it work for many languages and the LIKE construct in Postgres. Using .vuh files for pretty urls .Vuh files are special cases of .tcl files, used for rewriting incoming urls. We can use a vuh file to prettify the uri for our notes. Instead of note-edit?item_id=495, we can use note/495. To do this, we will need a new .vuh file for redirection and we will need to change the referring links in note-list. First, add the vuh: [service0 service0]$ cd /var/lib/aolserver/service0/packages/myfirstpackage/www [service0 www]$ emacs note.vuh Paste this into the file: example missing We parse the incoming request and treat everything after the final / as the item id. Note that this simple redirection will lose any additional query parameters passed in. Many OpenACS objects maintain a pretty-name, which is a unique, human-readable string, usually derived from title, which makes an even better 'pretty url' than a numeric id; this requires that your display page be able to look up an item based on pretty id. We use rp_form_put to store the item id in the internal register that the next page is expecting, and then redirects the request in process internally (ie, without a browser refresh). Next, modify note-list so that its link is of the new form.: [service0 www]$ emacs ../lib/note-edit.tcl db_multirow \ -extend { edit_url delete_url } notes notes_select { select ci.item_id, n.title from cr_items ci, mfp_notesx n where n.revision_id = ci.live_revision } { set edit_url [export_vars -base "note/$item_id"] set delete_url [export_vars -base "note-delete" {item_id}] } You may also need to change some of the links in your package. Commonly, you would use ad_conn package_url to build the URL. Otherwise, some of your links may be relative to the virtual directory (note/) instead of the actual directory that the note is being served from Future Topics How to enforce security so that users can't change other users records How to use the content management tables so that ... what? How to change the default stylesheets for Form Builder HTML forms. How to make your package searchable with OpenFTS/Oracle How to prepare pagelets for inclusion in other pages How and when to put procedures in a tcl procedure library More on ad_form - data validation, other stuff. (plan to draw from Jon Griffin's doc) How and when to implement caching partialquery in xql How to use the html/text entry widget to get the "does this look right" confirm page APM package dependencies See also the OpenACS Programming FAQ