tutorial-advanced.xml

Delivered as text/xml

[ hide source ] | [ make this the default ]

File Contents

<?xml version='1.0' ?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN"
               "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd" [
<!ENTITY % myvars SYSTEM "../variables.ent">
%myvars;
]>
<chapter id="tutorial-advanced">
  <title>Advanced Topics</title>
  <authorblurb>
    <para>by <ulink url="mailto:joel@aufrecht.org">Joel Aufrecht</ulink></para>
  </authorblurb>
    <para>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&#39;ve completed the basic tutorial.</para>

    <sect1 id="tutorial-specs">
      <title>Write the Requirements and Design Specs</title>
      <para>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 <xref linkend="docbook-primer"/>.</para>
      <para>It&#39;s time to document.  For the tutorial we&#39;ll use
      pre-written documentation.  When creating a package
      from scratch, start by copying the documentation template from
	<computeroutput>/var/lib/aolserver/openacs-dev/packages/acs-core-docs/xml/docs/xml/package-documentation-template.xml</computeroutput>
	to
	<computeroutput>myfirstpackage/www/docs/xml/index.xml</computeroutput>.</para>
      <para>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 <computeroutput>www/doc/xml</computeroutput>
      directory, and store png or jpg versions of supporting files in the
	<computeroutput>www/doc</computeroutput> directory.</para>
      <para>For this tutorial, you should instead install the
	pre-written documentation files for the tutorial app.  Log in
      as <replaceable>$OPENACS_SERVICE_NAME</replaceable>, create the standard
      directories, and copy the prepared documentation:</para>
      <screen>[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ <userinput>cd /var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/</userinput>
[$OPENACS_SERVICE_NAME myfirstpackage]$ <userinput>mkdir -p www/doc/xml</userinput>
[$OPENACS_SERVICE_NAME myfirstpackage]$ <userinput>cd www/doc/xml</userinput>
[$OPENACS_SERVICE_NAME xml]$ <userinput>cp /var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/acs-core-docs/www/files/myfirstpackage/* .</userinput>
[$OPENACS_SERVICE_NAME xml]$</screen>
      <para> 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.</para>
      <para>Open the file <computeroutput>index.xml</computeroutput>
      in emacs.  Examine the file.  Find the version history (look for the tag
        <computeroutput>&lt;revhistory&gt;</computeroutput>).  Add a
        new record to the document version history.  Look for the
        <computeroutput>&lt;authorgroup&gt;</computeroutput> tag and
        add yourself as a second author.  Save and exit.</para>
      <para>Process the XML file to create HTML documentation.  The
      HTML documentation, including supporting files such as pictures,
      is stored in the <computeroutput>www/docs/</computeroutput>
      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:</para>
      <screen>[$OPENACS_SERVICE_NAME xml]$<userinput> make</userinput>
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
[$OPENACS_SERVICE_NAME xml]$</screen>
      <para>Verify that the documentation was generated and reflects
      your changes by browsing to <computeroutput>http://<replaceable>yoursite</replaceable>:8000/myfirstpackage/doc</computeroutput></para>
    </sect1>
    <sect1 id="tutorial-cvs">
      <title>Add the new package to CVS</title>
      <para>Before you do any more work, make sure that your work is
      protected by putting it all into cvs.  The <computeroutput>cvs
      add</computeroutput> command is not recursive, so you&#39;ll have to
      traverse the directory tree manually and add as you go.  (<ulink
      url="http://www.piskorski.com/docs/cvs-conventions.html">More on
      CVS</ulink>)</para>
      <screen>[$OPENACS_SERVICE_NAME xml]$ <userinput>cd ..</userinput>
[$OPENACS_SERVICE_NAME doc]$ <userinput>cd ..</userinput>
[$OPENACS_SERVICE_NAME www]$ <userinput>cd ..</userinput>
[$OPENACS_SERVICE_NAME myfirstpackage]$ <userinput>cd ..</userinput>
[$OPENACS_SERVICE_NAME packages]$ <userinput>cvs add myfirstpackage/</userinput>
Directory /cvsroot/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage added to the repository
[$OPENACS_SERVICE_NAME packages]$ <userinput>cd myfirstpackage/</userinput>
[$OPENACS_SERVICE_NAME myfirstpackage]$ <userinput>cvs add www</userinput>
Directory /cvsroot/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www added to the repository
[$OPENACS_SERVICE_NAME myfirstpackage]$ <userinput>cd www</userinput>
[$OPENACS_SERVICE_NAME www]$ <userinput>cvs add doc</userinput>
Directory /cvsroot/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/doc added to the repository
[$OPENACS_SERVICE_NAME www]$ <userinput>cd doc</userinput>
[$OPENACS_SERVICE_NAME doc]$ <userinput>cvs add *</userinput>
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/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/doc/xml added to the repository
cvs add: use 'cvs commit' to add these files permanently
[$OPENACS_SERVICE_NAME doc]$ <userinput>cd xml</userinput>
[$OPENACS_SERVICE_NAME xml]$ <userinput>cvs add Makefile index.xml</userinput>
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
[$OPENACS_SERVICE_NAME xml]$<userinput> cd ../../..</userinput>
[$OPENACS_SERVICE_NAME myfirstpackage]$ <userinput>cvs commit -m "new package"</userinput>
cvs commit: Examining .
cvs commit: Examining www
cvs commit: Examining www/doc
cvs commit: Examining www/doc/xml
RCS file: /cvsroot/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/doc/admin-guide.html,v
done
Checking in www/doc/admin-guide.html;
/cvsroot/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/doc/admin-guide.html,v  &lt;--  admin-guide.html
initial revision: 1.1
done
<emphasis>(many lines omitted)</emphasis>
[$OPENACS_SERVICE_NAME myfirstpackage]$</screen>
        <figure>
          <title>Upgrading a local CVS repository</title>
          <mediaobject>
            <imageobject>
              <imagedata fileref="images/development-with-cvs.png" format="PNG" align="center"/>
            </imageobject>
          </mediaobject>
        </figure>
    </sect1>

  <sect1 id="tutorial-etp-templates" xreflabel="OpenACS ETP Templates">
    <title>OpenACS Edit This Page Templates</title>

    <authorblurb>
      <para>by <ulink url="mailto:ncarroll@ee.usyd.edu.au">Nick Carroll</ulink></para>
    </authorblurb>
    
    <sect2 id="goals">
      <title>Goals</title>
      <itemizedlist>
        <listitem>
          <para>Learn about the OpenACS templating system.</para>
        </listitem>
        <listitem>
          <para>Learn about subsites and site-map administration.</para>
        </listitem>
      </itemizedlist>
    </sect2>
    
    <sect2 id="introduction">
      <title>Introduction</title>
      <para>
        The OpenACS templating system allows you to give your site a consistent look and feel. It also promotes code maintainability in the presentation layer, by allowing presentation components to be reused across multiple pages. If you need to change the layout for some reason, then you only need to make that change in one location, instead of across many files.
      </para>
      <para>
        In this problem set you will familiarise yourself with the templating system in openacs. This will be achieved through customising an existing edit-this-page application template.
      </para>
      <para>
        Before proceeding, it is strongly advised to read the templating documentation on your OpenACS installation (http://localhost:8000/doc/acs-templating). The documentation lists the special tags available for ADP files.
      </para>
    </sect2>
    
    <sect2 id="exercise1">
      <title>Exercise 1: Create a Subsite</title>
      <itemizedlist>
        <listitem>
          <para>Create a subsite called pset3.</para>
        </listitem>
        <listitem>
          <para>A subsite is simply a directory or subdirectory mounted at the end of your domain name. This can be done in one of two places:</para>
          <itemizedlist>
            <listitem>
              <para>http://localhost:8000/admin/site-map</para>
            </listitem>
            <listitem>
              <para>or the subsite admin form on the main site, which is available when you login to your OpenACS installation.</para>
            </listitem>
          </itemizedlist>
        </listitem>
      </itemizedlist>
    </sect2>
    
    <sect2 id="exercise2">
      <title>Exercise 2: Checkout and Install edit-this-page (ETP)</title>
      <itemizedlist>
        <listitem>
          <para>Checkout ETP from CVS:</para>
          <screen>cd ~/openacs/packages
            cvs -d:pserver:anonymous@openacs.org:/cvsroot login
            cvs -d:pserver:anonymous@openacs.org:/cvsroot co edit-this-page</screen>
        </listitem>
        <listitem>
          <para>Go to the package manager at http://yoursite/acs-admin/apm. And install  the new package: edit-this-page.</para>
        </listitem>
        <listitem>
          <para>Or use the "Add Application" form available on the Main site.</para>
        </listitem>
      </itemizedlist>
    </sect2>
    
    <sect2 id="exercise3">
      <title>Change ETP Application</title>
      <itemizedlist>
        <listitem>
          <para>Work out how to change the ETP application.</para>
        </listitem>
        <listitem>
          <para>Investigate each of the available ETP templates:</para>
          <itemizedlist>
            <listitem><para>Default</para></listitem>
            <listitem><para>News</para></listitem>
            <listitem><para>FAQ</para></listitem>
          </itemizedlist>
        </listitem>
      </itemizedlist>
    </sect2>
    
    <sect2 id="exercise4">
      <title>Exercise 4: Create a New ETP Template</title>
      <itemizedlist>
        <listitem>
          <para>Browse the files for each of the above ETP templates at:</para>
          <screen>cd ~/openacs/packages/edit-this-page/templates</screen>
        </listitem>
        <listitem>
          <para>Use the article template as the basis of our new col2 template.</para>
          <screen>cp article-content.adp col2-content.adp
            cp article-content.tcl col2-content.tcl
            cp article-index.adp col2-index.adp
            cp article-index.tcl col2-index.tcl</screen>
        </listitem>
        <listitem>
          <para>The template should provide us with the following ETP layout:</para>
          <table frame="all" border="1" cellpadding="5" cellspacing="0">
              <title>table showing ETP layout</title>
            <?dbhtml table-width="250" ?>
            <?dbfo table-width="250" ?>
            <tgroup cols="2" align="left" colsep="1" rowsep="1">
              <colspec colname="c1"/>
              <colspec colname="c2" colwidth="2"/>
              <tbody>
                <row>
                  <entry namest="c1" nameend="c2" align="center">Header</entry>
                </row>
                <row>
                  <?dbhtml row-height="200"?>
                  <?dbfo row-height="200"?>
                  <entry>Sidebar</entry>
                  <entry>Main Content Pane</entry>
                </row>
              </tbody>
            </tgroup>
          </table>
        </listitem>
        <listitem>
          <para>The "Main Content" pane should contain the editable content that ETP provides.</para>
        </listitem>
        <listitem>
          <para>The "Header" should display the title of the page that you set in ETP.</para>
        </listitem>
        <listitem>
          <para>The "Sidebar" should display the extlinks that you add as a content item in ETP.</para>
        </listitem>
      </itemizedlist>
    </sect2>
    
    <sect2 id="exercise5">
      <title>Exercise 5: Register the col2 Template with ETP</title>
      <itemizedlist>
        <listitem>
          <para>Need to register your template with ETP so that it appears in the drop-down menu that you would have seen in Exercise 3.</para>
          <screen>cd ~/openacs/packages/edit-this-page/tcl
            emacs etp-custom-init.tcl</screen>
        </listitem>
        <listitem>
          <para>Use the function etp::define_application to register your template with ETP</para>
          <itemizedlist>
            <listitem><para>Uncomment the "asc" definition</para></listitem>
            <listitem><para>Set allow_extlinks to true, the rest should be false.</para></listitem>
          </itemizedlist>
        </listitem>
        <listitem>
          <para>Restart your server for the changes to take effect.</para>
        </listitem>
      </itemizedlist>
    </sect2>
    
    <sect2 id="exercise6">
      <title>Exercise 6: Configure ETP to use the col2 Template</title>
      <itemizedlist>
        <listitem>
          <para>Configure your ETP instance at /lab4/index to use the col2 template.</para>
        </listitem>
        <listitem>
          <para>Create external links to link to other mounted ETP instances.</para>
        </listitem>
        <listitem>
          <para>Check that your external links show up in the sidebar when you view your ETP application using the col2 template.</para>
        </listitem>
      </itemizedlist>
    </sect2>
    
    <sect2 id="end">
      <title>Who Wrote This and When</title>
      <para>This problem set was originally written by Nick Carroll in August 2004 for the <ulink url="http://www.usyd.edu.au">University of Sydney</ulink> Course EBUS5002.</para>
      <para>This material is copyright 2004 by Nick Carroll.  It may be copied, reused, and modified, provided credit is given to the original author.</para>
      <para><phrase role="cvstag">($Id: tutorial-advanced.xml,v 1.60.2.11 2022/04/27 12:22:29 gustafn Exp $)</phrase></para>
    </sect2>
    
  </sect1>

  <sect1 id="tutorial-comments">
     <title>Adding Comments</title>
     <para>You can track comments for any ACS Object.  Here we&#39;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.</para>
     <para>First, we need to generate a URL for adding comments.  In note-edit.tcl:</para>
     <programlisting>
 set comment_add_url [export_vars -base [general_comments_package_url]comment-add {
  { object_id $note_id } 
  { object_name $title } 
  { return_url "[ad_conn url]?[ad_conn query]"} 
 }]
 </programlisting>
     <para>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.</para>
     <para>We need to create html that shows any existing comments.
     We do this with another general_comments function:</para>
     <programlisting>set comments_html [general_comments_get_comments
     -print_content_p 1 $note_id]</programlisting>
     <para>First, we pass in an optional parameter 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.</para>
     <para>We put our two new variables in the note-edit.adp
     page.</para>
     <programlisting>&lt;a href="@comment_add_url@"&gt;Add a comment&lt;/a&gt;
 @comments_html@</programlisting>
   </sect1>
   <sect1 id="tutorial-admin-pages">
     <title>Admin Pages</title>
     <para>
     There are at least two flavors of admin user interface:
     </para>
     <itemizedlist>
       <listitem><para>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.
       </para></listitem>
       <listitem><para>Dedicated admin pages.  If you want admins to have
       access to data that users aren&#39;t interested in or aren&#39;t allowed
       to see you will need dedicated admin pages.  The conventional
       place to put those dedicated admin pages is in the
 <computeroutput>/var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/admin</computeroutput>
 directory.
      </para>
<screen>[$OPENACS_SERVICE_NAME www]$ <userinput>mkdir admin</userinput></screen>
<screen>[$OPENACS_SERVICE_NAME www]$ <userinput>cd admin</userinput></screen>
      <para>
      Even if your application doesn&#39;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.
      </para>
<screen>[$OPENACS_SERVICE_NAME admin]$ <userinput>vi index.adp</userinput></screen>
<programlisting>
&lt;master&gt;
&lt;property name="title"&gt;@title;literal@&lt;/property&gt;
&lt;property name="context"&gt;@context;literal@&lt;/property&gt;

&lt;ul class="action-links"&gt;
  &lt;li&gt;&lt;a href="@parameters_url@" title="Set parameters" class="action_link"&gt;Set parameters&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</programlisting>
<screen>[$OPENACS_SERVICE_NAME admin]$ <userinput>vi index.tcl</userinput></screen>
<programlisting>
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] }
}]

</programlisting>

<para>
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&#39;t have to type in the
<computeroutput>/admin</computeroutput> every time they need to reach it.  You
could put a static link to the top-level
<computeroutput>index.adp</computeroutput> 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&#39;t have permission to access this
page" message.
</para>
<para>
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
<computeroutput>/var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/admin/index.tcl</computeroutput>:
</para>
<programlisting>

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
}
</programlisting>
<para>
In 
<computeroutput>/var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/admin/index.adp</computeroutput> put:
</para>
<programlisting>
&lt;if @admin_p@ ne nil&gt;
  &lt;a href="@admin_url@"&gt;@admin_title@&lt;/a&gt;
&lt;/if&gt;
</programlisting>

      </listitem>
    </itemizedlist>

  </sect1>

  <sect1 id="tutorial-categories">
    <title>Categories</title>
  <authorblurb>
    <para>extended by <ulink url="mailto:nima.mazloumi@gmx.de">Nima Mazloumi</ulink></para>
  </authorblurb>
    <para>You can associate any ACS Object with one or more categories.
    In this tutorial we&#39;ll show how to equip your application with user
    interface to take advantage of the Categories service.
    </para>
    <para>
    We&#39;ll start by installing the Categories service.  Go to
    <computeroutput>/acs/admin</computeroutput> and install it.  This step
    won&#39;t be necessary for the users of your applications because you&#39;ll create
    a dependency with the Package Manager which will take care that the
    Categories service always gets installed when your application gets
    installed.
    </para>
    <para>
    Now that we have installed the Categories service we can proceed to
    modifying our application so that it can take advantage of it.  We&#39;ll do it
    in three steps:
    </para>
    <orderedlist>
        <listitem><para>
          The Categories service provides a mechanism to associate one or
          more <emphasis>category trees</emphasis> 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.
          </para>
          <para>
          The way to achieve this is to provide a link
          to the Category Management pages.  Add the following snippet to your
            <computeroutput>/var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/admin/index.tcl</computeroutput>
          file:
          </para>
          <programlisting>
		  set category_map_url [export_vars -base "[site_node::get_package_url -package_key categories]cadmin/one-object" { { object_id $package_id } }]
          </programlisting>
          <para>
          and the following snippet to your
            <computeroutput>/var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www/admin/index.adp</computeroutput>
          file:
          </para>
          <programlisting>
   	          &lt;a href="@category_map_url@"&gt;#&shy;categories.Site_wide_Categories#&lt;/a&gt;
          </programlisting>     
          <para>The link created by the above code (<computeroutput>category_map_url</computeroutput>) 
          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
          <computeroutput>package_id</computeroutput> so that category trees
          will be associated with the object identified by this
          <computeroutput>package_id</computeroutput>.  The categorization
          service is actually more general than that: instead of
          <computeroutput>package_id</computeroutput> 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 <computeroutput>forum_id</computeroutput> to
          associate category trees with just that one forum rather than the
          entire application instance.
        </para></listitem>
      <listitem><para>
          Once the category trees have been selected users need a way
          to categorize items.  The easiest way to do this is by adding the
          <computeroutput>category</computeroutput> widget type of the
          form builder to <computeroutput>note-edit.tcl</computeroutput>.
          To achieve this we&#39;ll need to use the <computeroutput>-extend</computeroutput>
          switch to the <computeroutput>ad_form</computeroutput> command. Here&#39;s the "meat" of the
          <computeroutput>note-edit.tcl</computeroutput> page:</para>
          <programlisting>
			# extend the form to support categories
			set package_id [ad_conn package_id]
			    
    			category::ad_form::add_widgets -form_name note -container_object_id $package_id -categorized_object_id [expr {[info exists item_id] ? $item_id : ""}]

    			ad_form -extend -name note -on_submit {
        			set category_ids [category::ad_form::get_categories -container_object_id $package_id]
    			} -new_data {
    				....
					category::map_object -remove_old -object_id $item_id $category_ids
	    		} -edit_data {
            		....
        			category::map_object -remove_old -object_id $item_id $category_ids
    			} -after_submit {
        				ad_returnredirect "."
        				ad_script_abort
    			}
			</programlisting>
			<para>While the <computeroutput>category::ad_form::add_widgets</computeroutput> proc is taking 
			care to extend your form with associated categories you need to ensure that your items are mapped 
			to the corresponding category object yourself.</para>
    <para><computeroutput>note-edit.tcl</computeroutput> requires a
<computeroutput>note_id</computeroutput> 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
<computeroutput>entry-edit.tcl</computeroutput> we used <computeroutput>ad_form</computeroutput> to access the Form Template
commands; here, we call them directly because we don&#39;t need the extra
features of ad_form.  The form calls itself, but
with hidden variables carrying both
<computeroutput>note_id</computeroutput> and
<computeroutput>confirm_p</computeroutput>.  If confirm_p is present,
we delete the record, set redirection back to the index, and abort
script execution.</para>

      <para>The database commands:</para>
      <screen>[$OPENACS_SERVICE_NAME@yourserver www]$ <userinput>emacs note-delete.xql</userinput></screen>
      <programlisting>&lt;?xml version="1.0"?&gt;
&lt;queryset&gt;
  &lt;fullquery name="do_delete"&gt;
    &lt;querytext&gt;
      select samplenote__delete(:note_id)
    &lt;/querytext&gt;
  &lt;/fullquery&gt;
  &lt;fullquery name="get_name"&gt;
    &lt;querytext&gt;
      select samplenote__name(:note_id)
    &lt;/querytext&gt;
  &lt;/fullquery&gt;
&lt;/queryset&gt;</programlisting>
      <para>And the adp page:</para>
      <screen>[$OPENACS_SERVICE_NAME@yourserver www]$ <userinput>emacs note-delete.adp</userinput></screen>
      <programlisting>
&lt;master&gt;
&lt;property name="title"&gt;@title@&lt;/property&gt;
&lt;property name="context"&gt;{@title@}&lt;/property&gt;
&lt;h2&gt;@title@&lt;/h2&gt;
&lt;formtemplate id="note-del-confirm"&gt;&lt;/formtemplate&gt;
&lt;/form&gt;</programlisting>
    <para>The ADP is very simple.  The
<computeroutput>formtemplate</computeroutput> 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.</para>
      </listitem>
      <listitem><para>We will now make categories optional on package instance level and 
 		  also add a configuration page to allow the package admin to enable/disable 
 		  categories for his package.
 		  </para>
 		  <para>Go to the APM and create a number parameter with the name "<computeroutput>EnableCategoriesP</computeroutput>" 
 		  and the default value "<computeroutput>0</computeroutput>".</para>
 		  <para>Add the following lines to your <computeroutput>index.tcl</computeroutput>:</para>
 		  <programlisting>
          set return_url [ns_conn url]
          set use_categories_p [parameter::get -parameter "EnableCategoriesP"]
          </programlisting>
          <para>Change your to this:</para>
          <programlisting>
			&lt;a href=configure?&lt;%=[export_vars -url {return_url}]%&gt;&gt;Configure&lt;/a&gt;
			&lt;if @use_categories_p@&gt;
   			&lt;a href="@category_map_url@"&gt;#&shy;categories.Site_wide_Categories#&lt;/a&gt;
   			&lt;/if&gt;
          </programlisting>
          <para>Now create a configure page</para>
          <programlisting>
          	ad_page_contract {
    			This page allows an admin to change the categories usage mode.
			} {
    			{return_url ""}
			}

			set title "Configure category mode"
			set context [list $title]
			set use_categories_p [parameter::get -parameter "EnableCategoriesP"]

			ad_form -name categories_mode -form {
    			{enabled_p:text(radio)
        			{label "Enable Categories"}
        			{options {{Yes 1} {No 0}}}
        			{value $use_categories_p}
    			}
    			{return_url:text(hidden) {value $return_url}}
    			{submit:text(submit) {label "Set Mode"}}
			} -on_submit {
    			parameter::set_value  -parameter "EnableCategoriesP" -value $enabled_p
    			if {$return_url ne ""} {
        			ns_returnredirect $return_url
    			}
			}
           </programlisting>
           <para>and add this to its corresponding ADP page</para>
           <programlisting>
          	&lt;master&gt;
			&lt;property name="title"&gt;@title@&lt;/property&gt;
			&lt;property name="context"&gt;@context@&lt;/property&gt;

			&lt;formtemplate id="categories_mode"&gt;&lt;/formtemplate&gt;
	      </programlisting>
		<para>Reference this page from your admin page</para>
		<programlisting>
		#TCL:
		set return_url [ad_conn url]

		#ADP:
		&lt;a href=configure?&lt;%=[export_vars -url {return_url}]%&gt;&gt;Configure&lt;/a&gt;
		</programlisting>
		<para>Change the <computeroutput>note-edit.tcl</computeroutput>:</para>
		<programlisting>
		# Use Categories?
		set use_categories_p [parameter::get -parameter "EnableCategoriesP" -default 0]
		if { $use_categories_p == 1 } {
			# YOUR NEW FORM DEFINITION
		} else {
    		# YOUR OLD FORM DEFINITION
		}
	</programlisting>
 	  </listitem>
 	  <listitem><para>You can filter your notes using categories. The below example does not support multiple 
 	  filters and displays a category in a flat format.</para><para>The first step is to 
 	  define the optional parameter <computeroutput>category_id</computeroutput> for 
 	  <computeroutput>index.tcl</computeroutput>:</para>
 	  <programlisting>
 	  	ad_page_contract {
  		YOUR TEXT
		} {
			YOURPARAMS
    		{category_id:integer,optional {}}
		}
 	  </programlisting>
 	  <para>Now you have to check whether categories are enabled or not. If this is the case and a 
 	  category id is passed you need to extend your sql select query to support filtering. One 
 	  way would be to extend the <computeroutput>mfp::note::get</computeroutput> proc to 
 	  support two more swiches <computeroutput>-where_clause</computeroutput> and
 	  <computeroutput>-from_clause</computeroutput>.</para>
 	  <programlisting>
 	  	set use_categories_p [parameter::get -parameter "EnableCategoriesP" -default 0]

		if { $use_categories_p == 1 &amp;&amp; $category_id ne "" } {

			set from_clause "category_object_map com"
			set_where_clause "com.object_id = qa.entry_id and com.category_id = :category_id"
			
			...
								
    		mfp::note::get \
    		-item_id $item_id \
    		-array note_array \
    		-where_clause $where_clause \
    		-from_clause $from_clause
    		
    		...
		} else {
    		# OLD STUFF
		}
 	  </programlisting>
 	  <para>Also you need to make sure that the user can see the corresponding categories. Add the following 
 	  snippet to the end of your index page:</para>
 	  <programlisting>
 	  # Site-Wide Categories
		if { $use_categories_p == 1} {
    		set package_url [ad_conn package_url]
    		if { $category_id ne "" } {
        		set category_name [category::get_name $category_id]
        		if { $category_name eq "" } {
            		ad_return_exception_page 404 "No such category" "Site-wide \
          			Category with ID $category_id doesn&#39;t exist"
            		return
        		}
        		# Show Category in context bar
        		append context_base_url /cat/$category_id
        		lappend context [list $context_base_url $category_name]
        		set type "all"
    		}

    		# Cut the URL off the last item in the context bar
    		if { [llength $context] > 0 } {
        		set context [lreplace $context end end [lindex $context end end]]
    		}

    		db_multirow -unclobber -extend { category_name tree_name } categories categories {
        		select c.category_id as category_id, c.tree_id
        		from   categories c, category_tree_map ctm
        		where  ctm.tree_id = c.tree_id
        		and    ctm.object_id = :package_id
    		} {
        		set category_name [category::get_name $category_id]
        		set tree_name [category_tree::get_name $tree_id]
    		}
		}
		</programlisting>
		<para>and to the corresponding index ADP page:</para>
		<programlisting>
		&lt;if @use_categories_p@&gt;
 			&lt;multiple name="categories"&gt;
           		&lt;h2&gt;@categories.tree_name@
           		&lt;group column="tree_id"&gt;
             		&lt;a href="@package_url@cat/@categories.category_id@?@YOURPARAMS@&amp;category_id=@categories.category_id@"&gt;@categories.category_name@
           		&lt;/group&gt;
         	&lt;/multiple&gt;
		&lt;a href="@package_url@view?@YOURPARAMS@"&gt;All Items&lt;/if&gt;
 	  </programlisting>
 	  <para>Finally you need an <computeroutput>index.vuh</computeroutput> in your 
 	  www folder to rewrite the URLs correctly, <xref linkend="tutorial-vuh"/>:</para>
 	  <programlisting>
 	  set url /[ad_conn extra_url]

	  if {[regexp {^/+cat/+([^/]+)/*} $url ignore_whole category_id]} {
              rp_form_put category_id $category_id
	  }
	  rp_internal_redirect "/packages/YOURPACKAGE/www/index" 	  
 	  </programlisting>
 	  <para>Now when ever the user select a category only notes that belong to this category are displayed.</para>
 	  </listitem>  
    </orderedlist>
  </sect1>

  <sect1 id="profile-code">
    <title>Profile your code</title>

    <authorblurb>
      <para>by <ulink url="mailto:jade@rubick.com">Jade Rubick</ulink></para>
    </authorblurb>

      <para>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:
      <ulink 
      url="http://openacs.org/forums/message-view?message_id=161324">profiling
      your code using ds_profile</ulink>
      </para>
  </sect1>

      <sect1 id="tutorial-distribute">
        <title>Prepare the package for distribution.</title>
        <para>Browse to the package manager.  Click on
        <computeroutput><guilabel>tutorialapp</guilabel></computeroutput>.</para>
        <para>Click on <computeroutput><guilabel>Generate a distribution file
        for this package from the
        filesystem</guilabel></computeroutput>.
        </para>
        <para>Click on the file size
        (<computeroutput><guilabel>37.1KB</guilabel></computeroutput>)
        after the label <computeroutput><guilabel>Distribution
        File:</guilabel></computeroutput> and save the file to
        /var/tmp.</para>
        <para><indexterm>
            <primary>The publish point for new packages should be
        fixed.</primary>
          </indexterm>
</para>

    <para><ulink url="http://openacs.org/forums/message-view?message_id=192919">Package development guidelines</ulink></para>

      </sect1>

  <sect1 id="tutorial-upgrades">
    <title>Distributing upgrades of your package</title>
    <authorblurb>
      <para>by Jade Rubick</para>
    </authorblurb>
    <para>The OpenACS Package Repository builds a list of packages
    that can be installed on OpenACS installations, and can be used by
    administrators to update their packages. If you are a package
    developer, there are a couple of steps you need to take in order
    to release a new version of your package. </para>

    <para>For the sake of this example, let&#39;s assume you are the
    package owner of the <computeroutput>notes</computeroutput>
    package. It is currently at version 1.5, and you are planning on
    releasing version 1.6. It is also located in OpenACS&#39;s CVS.</para>

    <para>To release your package:</para>
    <screen>cd /path/to/notes
cvs commit -m "Update package to version 1.6."
cvs tag notes-1-6-final
cvs tag -F openacs-5-1-compat
</screen>

    <para>Of course, make sure you write upgrade scripts 
      (<xref linkend="tutorial-upgrade-scripts"/>)</para>
  </sect1>


  <sect1 id="tutorial-notifications">
    <title>Notifications</title>
    <authorblurb>
      <para>by <ulink url="mailto:dave@student.usyd.edu.au">David Bell</ulink> and <ulink url="mailto:simon@collaboraid.net">Simon Carstensen</ulink></para>
    </authorblurb>

    <para>The notifications package allows you to send notifications through any 
    defined communications medium (e.g. email, sms) upon some event occurring within 
    the system.</para>
    <para>This tutorial steps through the process of integrating the notifications 
    package with your package.</para>
   
    <para>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).</para>

    <programlisting>
    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();
    </programlisting>

    <para>You also need a drop script. This is untested for
    compatibility with the above script.</para>
    <programlisting>
      -- @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&#39;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();
    </programlisting>

    <para>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</para>

    <programlisting>
        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
    </programlisting>

    <para>This code is placed in the Tcl procedure that creates blog
    entries, right after the entry gets created in the code. The
    <computeroutput>$blog(package_id)</computeroutput> is the OpenACS
    object_id of the Weblogger instance to which the entry has been
    posted to and the <computeroutput>$new_content</computeroutput> 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.</para>

    <para>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.</para>

    <para>Such a link can be created using the <computeroutput>notification::display::request_widget</computeroutput> 
    proc:</para>
    <programlisting>
    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] \
    ]
    </programlisting>

    <para>which will return something like

    <programlisting>
    You may &lt;a href="/notifications/request-new?..."&gt;request notification&lt;/a&gt; for Weblogger.</programlisting>

    which can be readily put on the blog index page. The <computeroutput>pretty_name</computeroutput> 
    parameter is what appears at the end of the text returned (i.e. "... request notification&lt;/a&gt; for pretty_name"), 
    The <computeroutput>url</computeroutput> parameter should be set to the address we want the user 
    to be redirected to after they have finished the subscription process.</para>

    <para>This should be all you need to implement a notification system. For more examples
    look at the forums package.</para>
  </sect1>

<sect1 id="tutorial-hierarchical">
    <title>Hierarchical data</title>
    <authorblurb>
      <para>by <ulink url="http://web.archive.org/web/20151128111517/http://www.rubick.com:8002/">Jade Rubick</ulink>
      with help from many people in the OpenACS community</para>
    </authorblurb>

    <para>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
    <computeroutput>tree_sortkey.</computeroutput></para>

    <para>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
    <computeroutput>context_id</computeroutput> column of
    acs_objects (Note that there is talk of adding in a
    <computeroutput>parent_id</computeroutput> column instead, because
    the use of <computeroutput>context_id</computeroutput> 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:</para>

    <programlisting>
      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"
    </programlisting>

    <para>Note the use of the
    <computeroutput>tree_level()</computeroutput> function, which
    gives you the level, starting from 1, 2, 3... </para>

    <para>Here&#39;s an example, pulling all of the children for a given
  parent:</para> 

    <programlisting>
      SELECT 
      children.*,
      tree_level(children.tree_sortkey) -
        tree_level(parent.tree_sortkey) as level
      FROM 
      some_table parent, 
      some_table children
      WHERE 
      children.tree_sortkey between parent.tree_sortkey and tree_right(parent.tree_sortkey)
      and parent.tree_sortkey &lt;&gt; children.tree_sortkey
      and parent.key = :the_parent_key;
      </programlisting>

    <para>The reason we subtract the parent&#39;s tree_level from the
    child&#39;s tree_level is that the tree_levels are global, so if you
    want the parent&#39;s tree_level to start with 0, you&#39;ll want the
    subtraction in there. This is a reason you&#39;ll commonly see magic
    numbers in tree_sortkey SQL queries, like
    <computeroutput>tree_level(children.tree_sortkey) -
    4</computeroutput>. That is basically an incorrect way to do it,
    and subtracting the parent&#39;s tree_level is the preferred method.</para>

    <para>This example does not include the parent. To return the entire subtree including the parent, leave out the non-equals clause:</para>

    <programlisting>
      SELECT
      subtree.*,
      tree_level(subtree.tree_sortkey) -
        tree_level(parent.tree_sortkey) as level
      FROM some_table parent, some_table subtree
      WHERE 
      subtree.tree_sortkey between parent.tree_sortkey and tree_right(parent.tree_sortkey)
      and parent.key = :the_parent_key;
    </programlisting>

    <para>If you are using the Content Repository, you get a similar
      facility, but the <computeroutput>parent_id</computeroutput>
      column is already there. Note you can do joins with
      <computeroutput>tree_sortkey</computeroutput>:</para>
      
    <programlisting>
      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
    </programlisting>

    <para>This rather long thread explains <ulink
    url="http://openacs.org/forums/message-view?message_id=16799">How
    tree_sortkeys work</ulink> and this paper <ulink
    url="http://www.yafla.com/papers/sqlhierarchies/sqlhierarchies2.htm">describes
    the technique for tree_sortkeys</ulink>, although the <ulink
    url="http://openacs.org/forums/message-view?message_id=112943">OpenACS
    implementation has a few differences in the
    implementation</ulink>, to make it work for many languages and the
    LIKE construct in PostgreSQL.
    </para>

  </sect1>

  <sect1 id="tutorial-vuh">
    <title>Using .vuh files for pretty URLs</title>
    <para>.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 <computeroutput>note-edit?item_id=495</computeroutput>, we can use <computeroutput>note/495</computeroutput>.  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:</para>
      <screen>[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ <userinput>cd /var/lib/aolserver/<replaceable>$OPENACS_SERVICE_NAME</replaceable>/packages/myfirstpackage/www</userinput>
[$OPENACS_SERVICE_NAME www]$ <userinput>emacs note.vuh</userinput>
</screen>
    <para>Paste this into the file:</para>
    <programlisting><xi:include href="../../files/tutorial/note.vuh" xi:parse="text" xmlns:xi="http://www.w3.org/2001/XInclude"><xi:fallback>example missing</xi:fallback></xi:include></programlisting>
    <para>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.</para>
    <para>We use <computeroutput>rp_form_put</computeroutput> 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).</para>
    <para>Next, modify note-list so that its link is of the new form.:</para>
      <screen>[$OPENACS_SERVICE_NAME www]$ <userinput>emacs ../lib/note-edit.tcl</userinput></screen>
    <programlisting>
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
    } {
	<emphasis role="strong">set edit_url [export_vars -base "note/$item_id"]</emphasis>
	set delete_url [export_vars -base "note-delete" {item_id}]
    }
</programlisting>

    <para>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.</para>

  </sect1>


  <sect1 id="tutorial-css-layout">
    <title>Laying out a page with CSS instead of tables</title>
    <sect2>
      <title>.LRN home page with table-based layout</title>
    <mediaobject>
      <imageobject>
        <imagedata fileref="images/dotlrn-style-1.png" format="PNG" align="center"/>
      </imageobject>
    </mediaobject>
    <para>A sample of the HTML code (<ulink url="files/dotlrn-style-1.html">full source</ulink>)</para>
      <programlisting>&lt;table border="0" width="100%"&gt;
  &lt;tr&gt;
    &lt;td valign="top" width="50%"&gt;
      &lt;table class="element" border="0" cellpadding="0" cellspacing="0" width="100%"&gt;
        &lt;tr&gt; 
          &lt;td colspan="3" class="element-header-text"&gt;
            &lt;bold&gt;Groups&lt;/bold&gt;
         &lt;/td&gt;
       &lt;/tr&gt;
       &lt;tr&gt;
         &lt;td colspan="3" class="dark-line" height="0"&gt;&lt;img src="/resources/acs-subsite/spacer.gif"&gt;&lt;/td&gt;&lt;/tr&gt;
          &lt;tr&gt;
            &lt;td class="light-line" width="1"&gt;
              &lt;img src="/resources/acs-subsite/spacer.gif" width="1"&gt;
            &lt;/td&gt;
            &lt;td class="element-text" width="100%"&gt;
            &lt;table cellspacing="0" cellpadding="0" class="element-content" width="100%"&gt;
              &lt;tr&gt;
                &lt;td&gt;
                  &lt;table border="0" bgcolor="white" cellpadding="0" cellspacing="0" width="100%"&gt;
                    &lt;tr&gt;
                      &lt;td class=element-text&gt;
                        MBA 101</programlisting>
    </sect2>
    <sect2>
      <title>.LRN Home with CSS-based layout</title>
      <mediaobject>
        <imageobject>
          <imagedata fileref="images/dotlrn-style-3.png" format="PNG" align="center"/>
        </imageobject>
      </mediaobject>
    <para>A sample of the HTML code (<ulink url="files/dotlrn-style-2.html">full source</ulink>)</para>
    <programlisting>&lt;div class="left"&gt;
  &lt;div class="portlet-wrap-shadow"&gt;
    &lt;div class="portlet-wrap-bl"&gt;
      &lt;div class="portlet-wrap-tr"&gt;
        &lt;div class="portlet"&gt;
          &lt;h2&gt;Groups&lt;/h2&gt;
          &lt;ul&gt;
            &lt;li&gt;
              &lt;a href="#"&gt;Class MBA 101&lt;/a&gt;</programlisting>
    <para>If the CSS is removed from the file, it looks somewhat different:</para>
    <mediaobject>
      <imageobject>
        <imagedata fileref="images/dotlrn-style-2.png" format="PNG" align="center"/>
      </imageobject>
    </mediaobject>
    </sect2>
  </sect1>

  <sect1 id="tutorial-html-email">
    <title>Sending HTML email from your application</title>
    <authorblurb>
      <para>by <ulink url="mailto:jade@rubick.com">Jade Rubick</ulink></para>
    </authorblurb>

    <para>Sending email is fairly simple using the acs-mail-lite
    package. Sending HTML email is only slightly more complicated.</para>

    <programlisting>
    set subject "my subject"

    set message "&lt;b&gt;Bold&lt;/b&gt; not bold"

    set from_addr "me@myemail.com"

    set to_addr "me@myemail.com"

    # the from to html closes any open tags.
    set message_html [ad_html_text_convert -from html -to html $message]

    # some mailers chop off the last few characters.
    append message_html "   "
    set message_text [ad_html_text_convert -from html -to text $message]
        
    set message_data [ad_build_mime_message $message_text $message_html]
    
    set extra_headers [ns_set new]

    ns_set put $extra_headers MIME-Version [ns_set get $message_data MIME-Version]
    ns_set put $extra_headers Content-ID [ns_set get $message_data Content-ID]
    ns_set put $extra_headers Content-Type [ns_set get $message_data Content-Type]
    set message [ns_set get $message_data body]
    
    acs_mail_lite::send \
        -to_addr $to_addr \
        -from_addr $from_addr \
        -subject $subject \
        -body $message \
        -extraheaders $extra_headers
    </programlisting>
  </sect1>

  <sect1 id="tutorial-caching">
    <title>Basic Caching</title>
    <authorblurb>
      <para>Based on <ulink url="http://openacs.org/forums/message-view?message_id=157448">a post by Dave Bauer</ulink>.</para>
    </authorblurb>
    <para>Caching using the database API is described in the database API tutorial.</para>
    <para>Caching using util_memoize</para>
    <orderedlist>
      <listitem>
        <para>Implement your proc as <computeroutput>my_proc_not_cached</computeroutput></para>
      </listitem>
      <listitem>
        <para>Create a version of your proc called <computeroutput>my_proc</computeroutput> which wraps the non-cached version in the caching mechanism.  In this example, my_proc_not_cached takes one argument, -foo, so the wrapper passes that on.  The wrapper also uses the list command, to ensure that the arguments get passed correctly and to prevent commands passed in as arguments from being executed.</para>
        <programlisting>ad_proc my_proc {-foo} {
        Get a cached version of my_proc.
} {
    return [util_memoize [list my_proc_not_cached -foo $foo]]
}</programlisting>
      </listitem>
      <listitem>
        <para>In your code, always call my_proc.  There will be a separate cache item for each unique call to my_proc_not_cached so that calls with different arguments are cached separately. You can flush the cache for each cache key by calling util_memoize_flush my_proc_not_cached args.</para>
      </listitem>
      <listitem>
        <para>
          The cached material will of course become obsolete over time.  There are two ways to handle this.</para> 

        <itemizedlist>
          <listitem>
            <para>Timed Expiration: pass in max_age to util_memoize.  If the content is older than max_age, it will be re-generated.</para>
          </listitem>
          <listitem>
            <para>
              Direct Flushing.  In any proc which invalidates the cached content, call  util_memoize_flush my_proc_not_cached args.</para>
          </listitem>
        </itemizedlist>
      </listitem>
      <listitem>
        <para>If you are correctly flushing the cached value, then it will need to be reloaded.  You may wish to pre-load it, so that the loading delay does not impact users.  If you have a sequence of pages, you could call the cached proc in advance, to increase the chances that it&#39;s loaded and current when the user reaches it.  Or, you can call (and discard) it immediately after flushing it.</para>
      </listitem>
    </orderedlist>
  </sect1>

  <sect1 id="tutorial-schedule-procs">
    <title>Scheduled Procedures</title>
    <para>Put this proc in a file <computeroutput>/packages/<replaceable>myfirstpackage</replaceable>/tcl/scheduled-init.tcl</computeroutput>.  Files in /tcl with the -init.tcl ending are sourced on server startup.  This one executes my_proc every 60 seconds:</para>
    <programlisting>ad_schedule_proc 60 myfirstpackage::my_proc
</programlisting>
<para>This executes once a day, at midnight:</para>
    <programlisting>ad_schedule_proc \
    -schedule_proc ns_schedule_daily \
    [list 0 0] \
    myfirstpackage::my_proc
</programlisting>
    <para>See <ulink url="/api-doc/proc-view?proc=ad%5fschedule%5fproc">ad_schedule_proc</ulink> for more information.</para>
  </sect1>

  <sect1 id="tutorial-wysiwyg-editor">
    <title>Enabling WYSIWYG</title>
     <authorblurb>
      <para>by <ulink url="mailto:nima.mazloumi@gmx.de">Nima Mazloumi</ulink></para>
     </authorblurb>
    <para>Most of the forms in OpenACS are created using the form builder, see <xref linkend="form-builder"/>. For detailed information on the 
    API take a look <ulink url="/api-doc/proc-view?proc=ad_form">here</ulink>.</para>
    <para>The following section shows how you can modify your form to allow WYSIWYG functionalities.</para>
	<para>Convert your page to use <code>ad_form</code> (some changes but worth it)</para>
	<para>Here an examples. From:</para>
	<programlisting>
	template::form create my_form
	template::element create my_form my_form_id -label "The ID" -datatype integer -widget hidden
	template::element create my_form my_input_field_1 -html { size 30 } -label "Label 1" -datatype text -optional
	template::element create my_form my_input_field_2 -label "Label 2" -datatype text -help_text "Some Help" -after_html {&lt;a name="#"&gt;Anchor&lt;/a&gt;}
	</programlisting>
	<para>To:</para>
	<programlisting>
	ad_form -name my_form -form {
		my_form_id:key(acs_object_id_seq)
 		{my_input_field_1:text,optional
               {label "Label 1"}
               {html {size 30}}}
      	{my_input_field_2:text
               {label "Label 2"}
               {help_text "Some Help"}
	       	   {after_html
               {&lt;a name="#"&gt;Anchor&lt;/a&gt;}}}
	} ...
	</programlisting>
	<warning>
	<para>You must not give your form the same name that your page has. Otherwise HTMLArea won&#39;t load.</para>
	</warning>
	<para>Convert your textarea widget to a richtext widget and enable htmlarea.</para>
	<para>The <code>htmlarea_p</code>-flag can be used to prevent 
	WYSIWYG functionality. Defaults to true if left away.</para>
	<para>From:</para>
	<programlisting>
	{my_input_field_2:text
	</programlisting>
	<para>To:</para>
	<programlisting>
	{my_input_field_2:richtext(richtext)
			{htmlarea_p "t"}
	</programlisting>
	<para>The richtext widget presents a list with two elements: text and content type.
	To learn more on existing content types search in Google for "MIME-TYPES" or 
	take a look at the <code>cr_mime_types</code> table.</para>
	<para>Make sure that both values are passed as a list to your 
	<code>ad_form</code> or you will have problems 
	displaying the content or handling the data manipulation correctly.</para>
	<para>Depending on the data model of your package you either support a content format 
	or don&#39;t. If you don&#39;t you can assume <code>"text/html"</code> or 
	<code>"text/richtext"</code> or <code>"text/enhanced"</code>.</para>
	<para>The relevant parts in your <code>ad_form</code> definition are the 
	switches <code>-new_data</code>, <code>-edit_data</code>, 
	<code>-on_request</code> and <code>-on_submit</code>.</para>
	<para>To allow your data to display correctly you need to add an <code>-on_request</code> block. 
	If you have the format stored in the database pass this as well else use <code>"text/html"</code>:</para>
	<programlisting>
	set my_input_field_2 [template::util::richtext::create $my_input_field_2 "text/html"]
	</programlisting>
	<para>Now make sure that your SQL queries that do the data manipulation retrieve the correct value. 
	If you simply use <code>my_input_field_2</code> you will store a list. 
	Thus you need to add an <code>-on_submit</code> block:</para>
	<programlisting>
	set my_input_field_2 [ template::util::richtext::get_property contents $my_input_field_2]
	set format [ template::util::richtext::get_property format $my_input_field_2] #This is optional
	</programlisting>
	<para>Now the correct values for <code>my_input_field_2</code> and 
	<code>format</code> are passed to the <code>-new_data</code> and 
	<code>-edit_data</code> blocks which don&#39;t need to get touched.</para>
	<para>To make HTMLArea optional per package instance define a string parameter 
	<code>UseWysiwygP</code> which defaults <code>0</code> for your 
	package using the APM.</para>
	<para>In your edit page make the following changes</para>
	<programlisting>
	# Is WYSIWYG enabled?
	set use_wysiwyg_p [parameter::get -parameter "UseWysiwygP" -default "f"]
	
	...
	
	{htmlarea_p $use_wysiwyg_p}
	</programlisting>
	<para>The <code>-on_request</code> switch should set this value for your form.</para>
	<programlisting>
	set htmlarea_p $use_wysiwyg_p
	</programlisting>
	<para>All you need now is a configuration page where the user can change this setting. Create a 
	<code>configure.tcl</code> file:</para>
	<programlisting>
ad_page_contract {

    This page allows a faq admin to change the UseWysiwygP setting

} {
    {return_url ""}
}

    set title "Should we support WYSIWYG?"
    set context [list $title]

    set use_wysiwyg_p

    ad_form -name categories_mode -form {
        {enabled_p:text(radio)
       	    {label "Enable WYSIWYG"}
            {options {{Yes t} {No f}}}
            {value $use_wysiwyg_p}
        }
        {return_url:text(hidden) {value $return_url}}
        {submit:text(submit) {label "Change"}}
    } -on_submit {
        parameter::set_value  -parameter "UseWysiwygP" -value $enabled_p
        if {$return_url ne ""} {
            ns_returnredirect $return_url
        }
    }
</programlisting>
	<para>In the corresponding ADP file write</para>
	<programlisting>
	&lt;master&gt;
	&lt;property name="title"&gt;@title@&lt;/property&gt;
	&lt;property name="context"&gt;@context@&lt;/property&gt;

	&lt;formtemplate id="categories_mode"&gt;&lt;/formtemplate&gt;
	</programlisting>
	<para>And finally reference this page from your admin page</para>
	<programlisting>
	#TCL:
	set return_url [ad_conn url]

	#ADP:
	&lt;a href=configure?&lt;%=[export_vars -url {return_url}]%&gt;&gt;Configure&lt;/a&gt;
	</programlisting>

</sect1>
 
  <sect1 id="tutorial-parameters">
    <title>Adding in parameters for your package</title>
    <para>Each instance of a package can have parameters associated
    with it. These are like preferences, and they can be set by the
    administrator for each application to change the behavior of your
    application. </para>

    <para>To add parameters for your package, go to the Automatic
    Package Manager (/acs-admin/apm)</para>

    <para>Click on your package</para>

    <para>Under the Manage section, click on Parameters</para>

    <para>It&#39;s fairly self-explanatory at this point. Create the
    parameters you want, and then access them in your code using the
    parameter::get procedure.</para>

  </sect1>

  <sect1 id="tutorial-upgrade-scripts">
    <title>Writing upgrade scripts</title>
    <authorblurb>
      <para>by <ulink url="mailto:jade@rubick.com">Jade Rubick</ulink></para>
    </authorblurb>
    <para>If your package changes its data model, you have to write an
    upgrade script. This is very easy in OpenACS. </para>
    
    <para>First, you want to make sure you change the original .sql file so that new installation will have the new data model.</para>

    <para>Next, check what version your package is currently at. For
    example, it may be at version 1.0b1. Create a file in
    yourpackage/sql/postgres/upgrade called packagename-1.0b1-1.0b2.sql and put
    the SQL code that will update the data model. For example, if you
    add in a column, you would have an alter table add column
    statement in this file. Test this out very well, because data
    model changes are more serious and fundamental changes than the
    program .tcl files. </para>

    <para>Now use the APM to create a new package version
      1.0b2. Commit all your changes, tag the release 
      (<xref linkend="tutorial-upgrades" />), 
      and both new installations and upgrades
      will be taken care of.</para>
  </sect1>

  <sect1 id="tutorial-second-database">
    <title>Connect to a second database</title>
    <para>It is possible to use the OpenACS Tcl database API with
    other databases.  In this example, the OpenACS site uses a
    PostgreSQL database, and accesses another PostgreSQL database called
    legacy.</para>
    <orderedlist>
      <listitem>
        <para>Modify config.tcl to accommodate the legacy database, and to
        ensure that the legacy database is not used for standard
        OpenACS queries:</para>
        <programlisting>ns_section ns/db/pools
ns_param   pool1              "Pool 1"
ns_param   pool2              "Pool 2"
ns_param   pool3              "Pool 3"
ns_param   legacy             "Legacy"

ns_section ns/db/pool/pool1
<emphasis>#Unchanged from default</emphasis>
ns_param   maxidle            1000000000
ns_param   maxopen            1000000000
ns_param   connections        5
ns_param   verbose            $debug
ns_param   extendedtableinfo  true
ns_param   logsqlerrors       $debug
if { $database eq "oracle" } {
    ns_param   driver             ora8
    ns_param   datasource         {}
    ns_param   user               $db_name
    ns_param   password           $db_password
} else {
    ns_param   driver             postgres
    ns_param   datasource         ${db_host}:${db_port}:${db_name}
    ns_param   user               $db_user
    ns_param   password           ""
}

ns_section ns/db/pool/pool2
<emphasis>#Unchanged from default, removed for clarity</emphasis>

ns_section ns/db/pool/pool3
<emphasis>#Unchanged from default, removed for clarity</emphasis>

ns_section ns/db/pool/legacy
ns_param   maxidle            1000000000
ns_param   maxopen            1000000000
ns_param   connections        5
ns_param   verbose            $debug
ns_param   extendedtableinfo  true
ns_param   logsqlerrors       $debug
ns_param   driver             postgres
ns_param   datasource         ${db_host}:${db_port}:legacy_db
ns_param   user               legacy_user
ns_param   password           legacy_password


ns_section ns/server/${server}/db
ns_param   pools              *
ns_param   defaultpool        pool1

ns_section ns/server/${server}/acs/database
ns_param database_names [list main legacy]
ns_param pools_main [list pool1 pool2 pool3]
ns_param pools_legacy [list legacy]</programlisting>
      </listitem>
      <listitem>
        <para>To use the legacy database, use the
          <code>-dbn</code> flag for any of the
          <code>db_</code> API calls.  For
          example, suppose there is a table called "foo" in the legacy
          system, with a field "bar".  List "bar" for all records with
          this Tcl file:</para>
        <programlisting>db_foreach -dbn legacy get_bar_query {
  select bar from foo
  limit 10
} {
  ns_write "&lt;br/&gt;$bar"
}</programlisting>

      </listitem>
    </orderedlist>
  </sect1>

  <sect1 id="tutorial-future-topics">
  <title>Future Topics</title>
    <itemizedlist>
      <listitem><para>How to enforce security so that users can&#39;t
      change other users records</para>
      </listitem>
      <listitem><para>How to use the content management tables so that
      ... what?</para></listitem>
      <listitem><para>How to change the default stylesheets for Form
      Builder HTML forms.</para></listitem>
      <listitem><para>How to make your package searchable with OpenFTS/Oracle</para></listitem>
      <listitem><para>How to prepare pagelets for inclusion in other pages</para></listitem>
      <listitem><para>How and when to put procedures in a Tcl procedure library</para></listitem>
      <listitem><para>More on ad_form - data validation, other stuff.
      (plan to draw from Jon Griffin&#39;s doc)</para></listitem>
      <listitem><para>partialquery in xql</para></listitem>
      <listitem><para>How to use the html/text entry widget to get the
      "does this look right" confirm page </para></listitem>
      <listitem><para>APM package dependencies</para></listitem>
    </itemizedlist>
  <para>See also the <ulink url="http://openacs.org/faq/one-faq?faq_id=43841">OpenACS Programming FAQ</ulink></para>
  </sect1>
</chapter>