Writing OpenACS &version; Application Pages
By Rafael H. Schloming
and Pete SuOverview
In this document, we'll examine the user interface pages of the Notes
application in more detail, covering two separate aspects of page
development in OpenACS &version;. First, we'll talk about the code needed to make
your pages aware of which application instance they are running
in. Second, we'll talk about using the form builder to develop
form-based user interfaces in OpenACS &version;. While these seem like unrelated
topics, they both come up in the example page that we are going to
look at, so it makes sense to address them at the same time.
Application Instances and Subsites
As you will recall from the packages tutorial, the Request
Processor (RP) and Package Manager (APM) in OpenACS &version; allow site
administrators to define an arbitrary mapping from URLs in the site to
objects representing content. These objects may represent single
files, or entire applications. The APM uses the site map to map
application instances to particular URLs within a site. We call
creating such a mapping mounting the application instance at a
particular URL. The tutorial also showed how a given URL is
translated into a physical file to serve using the site map. We'll
repeat this description here, assuming that you have mounted an
instance of Notes at the URL /notes as we did in the packages-example:
AOLserver receives your request for the URL /notes/somepage.
This URL is passed to the request processor.
The RP looks up the URL in the site map, and sees that the object
mounted at that location is an instance of the notes
application.
The RP asks the package manager where in the file system the Notes
package lives. In the standard case, this would be
ROOT/packages/notes.
The RP translates the URL to serve a page relative to the page root of
the application, which is
ROOT/packages/notes/www/. Therefore, the page that is
finally served is ROOT/packages/notes/www/hello.html,
which is what we wanted.
What is missing from this description is a critical fact for
application developers: In addition to working out what file to serve,
the RP also stores information about which package instance the file
belongs to into the AOLserver connection environment. The following
ad_conn interfaces can be used to extract this
information:
[ad_conn package_url]
If the URL refers to a package instance, this is the URL to the root
of the tree where the package is mounted.
[ad_conn package_id]
If the URL refers to a package instance, this is the ID of that
package instance.
[ad_conn package_key]
If the URL refers to a package instance, this is the unique key name
of the package.
[ad_conn extra_url]
If we found the URL in the site map, this is the tail of the URL
following the part that matched a site map entry.
In the Notes example, we are particularly interested in the
package_id field. If you study the data model and code,
you'll see why. As we said before in the data modeling tutorial, the Notes application points the
context_id of each Note object that it creates to the
package instance that created it. That is, the context_id
corresponds exactly to the package_id that comes in from
the RP. This is convenient because it allows the administrator and the
owner of the package to easily define access control policies for all
the notes in a particular instance just my setting permissions on the
package instance itself.
The code for adding and editing notes, in
notes/www/add-edit.tcl, shows how this works. At the top
of the page, we extract the package_id and use it to do
permission checks:
set package_id [ad_conn package_id]
if {[info exists note_id]} {
ad_require_permission $note_id write
set context_bar [ad_context_bar "Edit Note"]
} else {
ad_require_permission $package_id create
set context_bar [ad_context_bar "New Note"]
}
This code figures out whether we are editing an existing note or
creating a new one. It then ensures that we have the right privileges
for each action.
Later, when we actually create a note, the SQL that we run ensures
that the context_id is set the right way:
db_dml new_note {
declare
id integer;
begin
id := note.new(
owner_id => :user_id,
title => :title,
body => :body,
creation_user => :user_id,
creation_ip => :peeraddr,
context_id => :package_id
);
end;
}
The rest of this page makes calls to the form builder part of the
template system. This API allows you to write forms-based pages
without generating a lot of duplicated HTML in your pages. It also
encapsulates most of the common logic that we use in dealing with
forms, which we'll discuss next.
Using Forms
The forms API is pretty simple: You use calls in the
template::form namespace in your Tcl script to create
form elements. The final template page then picks this stuff up and
lays the form out for the user. The form is set up to route submit
buttons and whatnot back to the same Tcl script that set up the
form, so your Tcl script will also contain the logic needed to process
these requests.
So, given this outline, here is a breakdown of how the forms code
works in the add-edit.tcl page. First, we create a form object
called new_note:
template::form create new_note
All the forms related code in this page will refer back to this
object. In addition, the adp part of this page does
nothing but display the form object:
<master>
@context_bar@
<hr>
<center>
<formtemplate id="new_note"></formtemplate>
</center>
The next thing that the Tcl page does is populate the form with form
elements. This code comes first:
if {[template::form is_request new_note] && [info exists note_id]} {
template::element create new_note note_id \
-widget hidden \
-datatype number \
-value $note_id
db_1row note_select {
select title, body
from notes
where note_id = :note_id
}
}
The if_request call returns true if we are asking the
page to render the form for the first time. That is, we are rendering
the form to ask the user for input. The tcl part of a
form page can be called in 3 different states: the initial request,
the initial submission, and the validated submission. These states
reflect the typical logic of a forms based page in OpenACS:
First render the input form.
Next, control passes to a validation page that checks and confirms the
inputs.
Finally, control passes to the page that performs the update in the
database.
The rest of the if condition figures out if we are
creating a new note or editing an existing note. If
note_id is passed to us from the calling page, we assume
that we are editing an existing note. In this case, we do a database
query to grab the data for the note so we can populate the form with
it.
The next two calls create form elements where the user can insert or
edit the title and body of the Note. The interface to
template::element is pretty straightforward.
Finally, the code at the bottom of the page performs the actual
database updates when the form is submitted and validated:
if [template::form is_valid new_note] {
set user_id [ad_conn user_id]
set peeraddr [ad_conn peeraddr]
if [info exists note_id] {
db_dml note_update {
update notes
set title = :title,
body = :body
where note_id = :note_id
}
} else {
db_dml new_note {
declare
id integer;
begin
id := note.new(
owner_id => :user_id,
title => :title,
body => :body,
creation_user => :user_id,
creation_ip => :peeraddr,
context_id => :package_id
);
end;
}
}
ad_returnredirect "."
}
In this simple example, we don't do any custom validation. The nice
thing about using this API is that the forms library handles all of
the HTML rendering, input validation and database transaction logic on
your behalf. This means that you can write pages without duplicating
all of that code in every set of pages that uses forms.
How it All Fits
To watch all of this work, use the installer to update the Notes
package with the new code that you grabbed out of CVS or the package
repository, mount an instance of Notes somewhere in your server and
then try out the user interface pages. It should become clear that in
a real site, you would be able to, say, create a custom instance of
Notes for every registered user, mount that instance at the user's
home page, and set up the permissions so that the instance is only
visible to that user. The end result is a site where users can come
and write notes to themselves.
This is a good example of the leverage available in the OpenACS &version;
system. The code that we have written for Notes is not at all more
complex than a similar application without access control or site map
awareness. By adding a small amount of code, we have taken a small,
simple, and special purpose application to something that has the
potential to be a very useful, general-purpose tool, complete with
multi-user features, access control, and centralized administration.
Summary
In OpenACS &version;, application pages and scripts can be aware of the package
instance, or subsite in which they are executing. This is a powerful
general purpose mechanism that can be used to structure web services
in very flexible ways.
We saw how to use this mechanism in the Notes application and how it
makes it possible to easily turn Notes into an application that
appears to provide each user in a system with their own private notes
database.
We also saw how to use the templating system's forms API in a simple
way, to create forms based pages with minimal duplication of code.
($Id: subsites.xml,v 1.3.2.1 2002/05/15 23:26:19 vinodk Exp $)