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. 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. 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.
+As you will recall from the packages tutorial, the Request
+Processor (RP) and Package Manager (APM) 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]} { + permission::require_permission -object_id $note_id -privilege write + + set context_bar [ad_context_bar "Edit Note"] +} else { + permission::require_permission -object_id $note_id -privilege 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.
+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.
+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 +5.9.0 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.
+In OpenACS 5.9.0, 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.