0.00%
Search · Index

Weblog Page

Filtered by category developer, 1 - 10 of 22 Postings (all, summary)

Creating Web Pages

Created by Joel Aufrecht, last modified by Gustaf Neumann 02 May 2020, at 12:58 PM

As a workaround for missing content-repository functionality, copy a provided file into the directory for tcl files:
cp /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/acs-core-docs/www/files/note-procs.tcl /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/tcl/

To make this file take effect, go to the APM and choose "Reload changed" for "MyFirstPackage".

Our package will have two visible pages. The first shows a list of all objects; the second shows a single object in view or edit mode, and can also be used to add an object. The index page will display the list, but since we might reuse the list later, we'll put it in a seperate file and include it on the index page.

Figure 9.5. Page Map

tutorial-page-map.png

Each user-visible page in your package has, typically, three parts. The tcl file holds the procedural logic for the page, including TCL and database-independent SQL code, and does things like check permissions, invoke the database queries, and modify variables, and the adp page holds html. The -postgres.xql and -oracle.xql files contains database-specific SQL. The default page in any directory is index, so we'll build that first, starting with the tcl file:

[$OPENACS_SERVICE_NAME postgresql]$ cd /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackages/www
[$OPENACS_SERVICE_NAME www]$ emacs index.tcl

Paste this into the file.

ad_page_contract {
    This is the main page for the package.  It displays all of the Notes and provides links to edit them and to create new Notes.

    @author Your Name (you@example.com)
    @cvs-id $Id: tutorial-pages.html,v 1.37 2006/07/17 05:38:32 torbenb Exp $
}

set page_title [ad_conn instance_name]
set context [list]

Now index.adp:

<master>
  <property name="title">@page_title;noquote@</property>
  <property name="context">@context;noquote@</property>
<include src="/packages/myfirstpackage/lib/note-list">

You can test your work by viewing the page /myfirstpackage on your installation.

The index page includes the list page, which we put in /lib instead of /www to designate that it's available for reuse by other packages.

[$OPENACS_SERVICE_NAME www]$ mkdir /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/lib
[$OPENACS_SERVICE_NAME www]$ cd /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/lib
[$OPENACS_SERVICE_NAME lib]$ emacs note-list.tcl
template::list::create \
    -name notes \
    -multirow notes \
    -actions { "Add a Note" note-edit} \
    -elements {
	edit {
	    link_url_col edit_url
	    display_template {
		<img src="/resources/acs-subsite/Edit16.gif" width="16" height="16" border="0">
	    }
	    sub_class narrow
	}
	title {
	    label "Title"
	}
	delete {
	    link_url_col delete_url 
	    display_template {
		<img src="/resources/acs-subsite/Delete16.gif" width="16" height="16" border="0">
	    }
	    sub_class narrow
	}
    }

db_multirow \
    -extend {
	edit_url
	delete_url
    } notes notes_select {
	select ci.item_id,
	       n.title
        from   cr_items ci,
               mfp_notesx n
        where  n.revision_id = ci.live_revision
    } {
	set edit_url [export_vars -base "note-edit" {item_id}]
	set delete_url [export_vars -base "note-delete" {item_id}]
    }
[$OPENACS_SERVICE_NAME lib]$ emacs note-list.adp
<listtemplate name="notes"></listtemplate>

Create the add/edit page. If note_id is passed in, it display that note, and can change to edit mode if appropriate. Otherwise, it presents a form for adding notes.

[$OPENACS_SERVICE_NAME lib]$ cd /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/www
[$OPENACS_SERVICE_NAME www]$ emacs note-edit.tcl
ad_page_contract {
    This is the view-edit page for notes.

    @author Your Name (you@example.com)
    @cvs-id $Id: tutorial-pages.html,v 1.37 2006/07/17 05:38:32 torbenb Exp $
 
    @param item_id If present, assume we are editing that note.  Otherwise, we are creating a new note.
} {
    item_id:integer,optional
}

ad_form -name note -form {
    {item_id:key}
    {title:text {label Title}}
} -new_request {
    auth::require_login
    permission::require_permission -object_id [ad_conn package_id] -privilege create
    set page_title "Add a Note"
    set context [list $page_title]
} -edit_request {
    auth::require_login
    permission::require_write_permission -object_id $item_id
    mfp::note::get \
	-item_id $item_id \
	-array note_array 

    set title $note_array(title)

    set page_title "Edit a Note"
    set context [list $page_title]
} -new_data {
    mfp::note::add \
	-title $title
} -edit_data {
    mfp::note::edit \
	-item_id $item_id \
	-title $title
} -after_submit {
    ad_returnredirect "."
    ad_script_abort
}
[$OPENACS_SERVICE_NAME www]$ emacs note-edit.adp
<master>
  <property name="title">@page_title;noquote@</property>
  <property name="context">@context;noquote@</property>
  <property name="focus">note.title</property>
  
<formtemplate id="note"></formtemplate>

And the delete page. Since it has no UI, there is only a tcl page, and no adp page.

[$OPENACS_SERVICE_NAME www]$ emacs note-delete.tcl
ad_page_contract {
    This deletes a note

    @author Your Name (you@example.com)
    @cvs-id $Id: tutorial-pages.html,v 1.37 2006/07/17 05:38:32 torbenb Exp $
 
    @param item_id The item_id of the note to delete
} {
    item_id:integer
}

permission::require_write_permission -object_id $item_id
set title [item::get_title $item_id]
mfp::note::delete -item_id $item_id

ad_returnredirect "."
# stop running this code, since we're redirecting
abort

TCLWebtest

Created by Anett Szabo, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

Webtest

Created by Anett Szabo, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

API testing is only part of testing your package - it doesn't test the code in our adp/tcl pairs. For this, we can use TCLWebtest (see sourceforge).

TclWebtest is primarily for testing user interface and acceptance testing. It is a tool to write automated tests for web applications. It provides a simple API for issuing http requests, dealing with the result and assume specific response values, while taking care of the details such as redirects and cookies.
It has some basic html parsing functionality, to provide access to elements of the result html page that are needed for testing (mainly links and forms).

  • TCLWebtest provides a library of functions (see command reference) that make it easy to call a page through HTTP, examine the results, and drive forms. TCLwebtest's functions overlap slightly with acs-automated-testing; see the example provided for one approach on integrating them.
  • TCLWebtest tries to minimize the effort to write tests by implicitely assuming specific conditions whenever it makes sense. For example it always expects the server to return http codes other than 404 or 500, unless otherwise specified.
  • The assertion procedures are targeted at test writers who want to make sure the behaviour of their web applications stays the same, without caring for style or minor wording changes. In the example below, it is just assumed that there is a link with the text "login" on the first page, that clicking on it results in a page with at least one form with at least two text-entry fields on it, and that submitting the form with the specified values results in a page that contains the "logged in" text.
  • TCLWebtest should be suitable for testing larger chains of user interaction on a web application, for example a full ecommerce ordering session. tclwebtest could visit an ecommerce site as anonymous user, add some products to its shopping cart, check out the cart, register itself as user and enter a test address etc. The test script could also include the administration part of the interaction, by explicitely logging in as site admin, reviewing and processing the order, nuking the test user etc.
  • TCLWebtest must be installed for to work. Since automated testing uses it, it should be part of every OpenACS installation. Note that TCLwebtest is installed automatically by Malte's install script.

Hint:

In order to simplify the generation of tclwebtest scripts the  Webtest-Recorder extension (TwtR) for Firefox is available see http://www.km.co.at/km/twtr This module is a plugin for Firefox. It is used to generate/edit a tclwebtest script which can be used later for regression testing without the need of a browser. There is a certain overlap of the application range between selenium and TwtR. This plugin was developed by Åsmund Realfsen for regression/load testing of the assessment module.

 


A typical script for tclwebtest looks like this:

set SERVER "testserver"
do_request "http://$SERVER/sometesturl/"
assert text "some text"

link follow "login"

field fill "testuser"
field fill "testpassword"
form submit

assert text "you are logged in as testuser"
    
This script can be saved in a file, e.g. login.test, and executed with ./tclwebtest login.test. The script itself is tcl, so you can do powerful things with only a few commands.

 

 http://cvs.openacs.org/cvs/openacs-4/etc/install/tcl/twt-procs.tcl?rev=1.18

Command Reference:

 

Here are some guidelines on how to write automated tests with TCLWebtest. It is a joy to work with automated testing once you get the hang of it. We will use the "myfirstpackage" as an example.

Create the directory that will contain the test script and edit the script file. The directory location and file name are standards which are recognized by the automated testing package:

[$OPENACS_SERVICE_NAME www]$ mkdir /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/tcl/test
[$OPENACS_SERVICE_NAME www]$ cd /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/tcl/test

[$OPENACS_SERVICE_NAME test]$ emacs myfirstpackages-procs.tcl

Write the tests. This is obviously the big step :) The script should first call ad_library like any normal -procs.tcl file:

ad_library {
    ...
}

To create a test case you call aa_register_case test_case_name.. Once you've created the test case you start writing the needed logic. We'll use the tutorial package, "myfirstpackage," as an example. Let's say you just wrote an API for adding and deleting notes in the notes packages and wanted to test that. You'd probably want to write a test that first creates a note, then verifies that it was inserted, then perhaps deletes it again, and finally verifies that it is gone.

Naturally this means you'll be adding a lot of bogus data to the database, which you're not really interested in having there. To avoid this I usually do two things. I always put all my test code inside a call to aa_run_with_teardown which basically means that all the inserts, deletes, and updates will be rolled back once the test has been executed. A very useful feature. Instead of inserting bogus data like: set name "Simon", I tend to generate a random script in order avoid inserting a value that's already in the database:

set name [ad_generate_random_string]

Here's how the test case looks so far:

aa_register_case mfp_basic_test {
    My test
} {
    aa_run_with_teardown  -rollback  -test_code  {

       }
}

Now look at the actual test code. That's the code that goes inside -test_code {}. We want to implement test case API-001, "Given an object id from API-001, invoke mfp::note::get. Proc should return the specific word in the title."

      set name [ad_generate_random_string]
      set new_id [mfp::note::add -title $name]
      aa_true "Note add succeeded" [exists_and_not_null new_id]

To test our simple case, we must load the test file into the system (just as with the /tcl file in the basic tutorial, since the file didn't exist when the system started, the system doesn't know about it.) To make this file take effect, go to the APM and choose "Reload changed" for "MyFirstPackage". Since we'll be changing it frequently, select "watch this file" on the next page. This will cause the system to check this file every time any page is requested, which is bad for production systems but convenient for developing. We can also add some aa_register_case flags to make it easier to run the test. The -procs flag, which indicates which procs are tested by this test case, makes it easier to find procs in your package that aren't tested at all. The -cats flag, setting categories, makes it easier to control which tests to run. The smoke test setting means that this is a basic test case that can and should be run any time you are doing any test. (a definition of "smoke test")

Once the file is loaded, go to ACS Automated Testing and click on myfirstpackage. You should see your test case. Run it and examine the results.

 

Example

Now we can add the rest of the API tests, including a test with deliberately bad data. The complete test looks like:

ad_library {
    Test cases for my first package.
}

 


    
aa_register_case -cats {smoke api} -procs {mfp::note::add mfp::note::get mfp::note::delete} mfp_basic_test { A simple test that adds, retrieves, and deletes a record. } { aa_run_with_teardown -rollback -test_code { set name [ad_generate_random_string] set new_id [mfp::note::add -title $name] aa_true "Note add succeeded" [exists_and_not_null new_id] # Now check that the item exists mfp::note::get -item_id $new_id -array note_array aa_true "Note contains correct title" [string equal $note_array(title) $name] # Now check, if titel got the value of name    mfp::note::delete -item_id $new_id                            set get_again [catch {mfp::note::get -item_id $new_id -array note_array}]                 aa_false "After deleting a note, retrieving it fails" [expr $get_again == 0]             }     }
            
aa_register_case  -cats {api}  -procs {mfp::note::add mfp::note::get mfp::note::delete}  mfp_bad_data_test  {
        A simple test that adds, retrieves, and deletes a record, using some tricky data.
    } {
        aa_run_with_teardown  -rollback  -test_code  {
                set name {-Bad [BAD] \077 { $Bad}}    
                #Now name becomes this very unusual value: -Bad [BAD] \077 { $Bad}
                append name [ad_generate_random_string]
                set new_id [mfp::note::add -title $name]     
                #Now new_id becomes the value of the solution of proceduer add with starting argument $name as -title
                aa_true "Note add succeeded" [exists_and_not_null new_id]
                #Now test that new_id exists
                mfp::note::get -item_id $new_id -array note_array
                aa_true "Note contains correct title" [string equal $note_array(title) $name]
                aa_log "Title is $name"
                mfp::note::delete -item_id $new_id

                set get_again [catch {mfp::note::get -item_id $new_id -array note_array}]
                aa_false "After deleting a note, retrieving it fails" [expr $get_again == 0]
            }
    }  


aa_register_case
-cats {web smoke}
-libraries tclwebtest
mfp_web_basic_test
{
A simple tclwebtest test case for the tutorial demo package.

@author Peter Marklund
} {
# we need to get a user_id here so that it's available throughout
# this proc
set user_id [db_nextval acs_object_id_seq]

set note_title [ad_generate_random_string]

# NOTE: Never use the aa_run_with_teardown with the rollback switch
# when running Tclwebtest tests since this will put the test code in
# a transaction and changes won't be visible across HTTP requests.

aa_run_with_teardown -test_code {

#-------------------------------------------------------------
# Login
#-------------------------------------------------------------

# Make a site-wide admin user for this test
# We use an admin to avoid permission issues
array set user_info [twt::user::create -admin -user_id $user_id]

# Login the user
twt::user::login $user_info(email) $user_info(password)

#-------------------------------------------------------------
# New Note
#-------------------------------------------------------------

# Request note-edit page
set package_uri [apm_package_url_from_key myfirstpackage]
set edit_uri "${package_uri}note-edit"
aa_log "[twt::server_url]$edit_uri"
twt::do_request "[twt::server_url]$edit_uri"

# Submit a new note

tclwebtest::form find ~n note
tclwebtest::field find ~n title
tclwebtest::field fill $note_title
tclwebtest::form submit

#-------------------------------------------------------------
# Retrieve note
#-------------------------------------------------------------

# Request index page and verify that note is in listing
tclwebtest::do_request $package_uri
aa_true "New note with title \"$note_title\" is found in index page"
[string match "*${note_title}*" [tclwebtest::response body]]

#-------------------------------------------------------------
# Delete Note
#-------------------------------------------------------------
# Delete all notes

# Three options to delete the note
# 1) go directly to the database to get the id
# 2) require an API function that takes name and returns ID
# 3) screen-scrape for the ID
# all options are problematic. We'll do #1 in this example:

set note_id [db_string get_note_id_from_name "
select item_id
from cr_items
where name = :note_title
and content_type = 'mfp_note'
" -default 0]

aa_log "Deleting note with id $note_id"

set delete_uri "${package_uri}note-delete?item_id=${note_id}"
twt::do_request $delete_uri

# Request index page and verify that note is in listing
tclwebtest::do_request $package_uri
aa_true "Note with title \"$note_title\" is not found in index page after deletion."
![string match "*${note_title}*" [tclwebtest::response body]]

} -teardown_code {

twt::user::delete -user_id $user_id
}
}


 

API test

Created by Anett Szabo, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

Configuring an OpenACS package

Created by Malte Sussdorff, last modified by Malte Sussdorff 07 Aug 2007, at 04:33 PM

by Jade Rubick

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

After you've installed and mounted your package, you can configure each instance to act as you would like.

This is done from the Applications page. Log in, go to the Admin or Control Panel, click on the subsite the application is in, and click on Applications. If you click on the 'Parameters' link, you will see a list of parameters that you can change for this application.

Configure OpenACS look and feel with templates

Created by Malte Sussdorff, last modified by Malte Sussdorff 07 Aug 2007, at 04:28 PM

To change the look and feel of OpenACS you will need to dig into the ACS Templating system and change at least some of the given templates and CSS files to suit your needs. Reuven Lerner has written a good Introduction to OpenACS Templates for the Linux Journal.

Configuring a new OpenACS Site

Created by Malte Sussdorff, last modified by Malte Sussdorff 07 Aug 2007, at 04:26 PM

by Joel Aufrecht

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

In this chapter, Configuring refers to making changes to a new OpenACS site through the web interface. In crude terms, these changes happen in the database, and are upgrade-safe. Customizing refers to changes that touch the file system, and require some planning if easy upgradability is to be maintained.

An introductory article was written be Reuven Lerner for the Linux Journal which is a good read.  

Creating an Application Package

Created by Joel Aufrecht, last modified by Malte Sussdorff 07 Aug 2007, at 04:24 PM

To start developing new code in OpenACS, we build a new package. A package is a a discrete collection of web pages, tcl code, and database tables and procedures. A package with user interface is called an application; a package which provides functions to other packages and has no direct interface, a service. A package can be installed, upgraded, and removed. It communicates with other packages through an API. This chapter walks you through the minimum steps to create a useful package, including writing documentation, setting up database tables and procedures, writing web pages, debugging, and automatic regression testing.

Before start with this tutorial read up the introduction on OpenACS Packages and http://www.linuxjournal.com/article/6337.

This tutorial uses the content repository package. This radically simplifies the database work, but forces us to work around the content repository's limitations, including an incomplete TCL API. So the tutorial is messier than we'd like right now. Code that is temporary hackage is clearly marked.

 
In this tutorial, we will make an application package for displaying a list of text notes.

You will need:

  • A computer with a working installation of OpenACS. If you don't have this, see Chapter2, Installation Overview .

  • Example files, which are included in the standard OpenACS 5.2.3rc1 distribution.

Figure9.1.Assumptions in this section

Fully qualified domain name of your serveryourserver.test
URL of your serverhttp://yourserver.test:8000
Name of development account$OPENACS_SERVICE_NAME
New Package keymyfirstpackage

We use the ACS Package Manager (APM) to add, remove, and upgrade packages. It handles package meta-data, such as lists of files that belong in the package. Each package is uniquely identified by a package key. To start developing a new package, use the APM to create an empty package with our new package key, myfirstpackage. This will create the initial directories, meta-information files, and database entries for a new package. (More info on APM)

  1. Browse to http://yourserver:8000/acs-admin/apm .

  2. Click Create a New Package.

    Fill in the fields listed below. Ignore the rest (and leave the check boxes alone). (Some will change automatically. Don't mess with those.)

    • Package Key: myfirstpackage

    • Package Name: My First Package

    • Package Plural: My First Package

    • Package Type: Application

    • Initial Version: 0.1d

    • Summary: This is my first package.

    At the bottom, click Create Package.

This creates a package rooted at /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage . This is the "home directory" of our new package, and all files in the package will be within this directory. More on the structure of packages).

In order to see your work in progress, you must create a map between the URL space of incoming requests and the package application instance. You do this by adding the application in the main site administration). This creates a link between the incoming URL requests and an instance of the application. (More on applications and nodes)

You can have instances of a package on one site, each with a different URL and different permissions, all sharing the same code and tables. This requires that a package be developed package-aware. You'll see how to do that in this tutorial.

  1. Browse to http://yourserver.test:8000/admin/applications/application-add/ .

  2. Choose "My First Package" from the list and click OK (the other fields are optional).

By mounting the package, we've caused all requests to http://yourserver.test:8000/my-first-package to be satisfied from the files at /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/www.

The remainder of the tutorial walks you through each file one at a time as you create the package. You can skip all this, and get a working package, by doing the following:

cd /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/acs-core-docs/www/files/tutorial
psql $OPENACS_SERVICE_NAME -f myfirstpackage-create.sql
cp note-edit.* note-delete.tcl index.* ../../../../myfirstpackage/www/
mkdir ../../../../myfirstpackage/lib
cp note-list.* ../../../../myfirstpackage/lib/
cp myfirstpackage-*sql ../../../../myfirstpackage/sql/postgresql/
cp myfirstpackage-procs.tcl ../../../../myfirstpackage/tcl/test/
cp note-procs.tcl ../../../../myfirstpackage/tcl/

After restarting the server, the tutorial application will be installed and working at the url you selected in the previous step.

Overview

Created by Gustaf Neumann, last modified by Malte Sussdorff 07 Aug 2007, at 04:18 PM

A good detailed, yet somewhat outdated article is written by Reuven Lerner for the Linux Journal. It gives a good introduction, but please read on !

OpenACS (Open Architecture Community System) is an advanced toolkit for building scalable, community-oriented web applications. If you're thinking of building an enterprise-level web application, OpenACS is a solid, scalable framework for building dynamic content driven sites.

OpenACS is a collection of pre-built applications and services that you can use to build your web site/application. Through a modular architecture, OpenACS has packages for user/groups management, content management, e-commerce, news, FAQs, calendar, forums, bug tracking, full-text searching, and much more.

OpenACS relies on AOLserver, the free, multithreaded, scalable, Tcl-enabled, web/application server used by America Online for most of its web sites, and a true ACID-compliant Relational Database Management System (RDBMS). Currently OpenACS supports PostgreSQL, an open source RDBMS, and Oracle and is easily extensible to other databases which support a comparable feature set.

The OpenACS toolkit is derived from the ArsDigita Community System (ACS). ArsDigita (now part of Red Hat, Inc.) kindly made their work available under the GPL, making all of this possible.

The OpenACS project was born when Don Baccus, Ben Adida, and others decided to port ACS from Oracle to PostgreSQL, thus making it a fully open-source solution. With OpenACS 4, Oracle and PostgreSQL support were combined in one code base and with OpenACS 5, support for internationalization and localization has been added.

A vibrant and productive community has sprung up around the OpenACS software and there are many volunteer contributors as well as a commercial companies able to provide support, hosting, and custom development. Many of the production users are actively funding and contributing work back to the project. Formal, consensus driven governance has been established (with semi-annual elections) which ensures the project serves the needs of it's constituents.

The OpenACS community would like to hear your comments and can help you in your endeavors with the system. Visit our web site and feel free to ask questions or provide feedback.

Data modeling

Created by Anett Szabo, last modified by Malte Sussdorff 25 Jul 2007, at 05:57 PM

Data modeling is the hardest and most important activity in the RDBMS world. If you get the data model wrong, your application might not do what users need, it might be unreliable, it might fill up the database with garbage. Why then do we start a SQL tutorial with the most challenging part of the job? Because you can't do queries, inserts, and updates until you've defined some tables. And defining tables is data modeling.

When data modeling, you are telling the RDBMS the following:

  • what elements of the data you will store
  • how large each element can be
  • what kind of information each element can contain
  • what elements may be left blank
  • which elements are constrained to a fixed range
  • whether and how various tables are to be linked

Three-Valued Logic

Programmers in most computer languages are familiar with Boolean logic. A variable may be either true or false. Pervading SQL, however, is the alien idea of three-valued logic. A column can be true, false, or NULL. When building the data model you must affirmatively decide whether a NULL value will be permitted for a column and, if so, what it means.

For example, consider a table for recording user-submitted comments to a Web site. The publisher has made the following stipulations:

  • comments won't go live until approved by an editor
  • the admin pages will present editors with all comments that are pending approval, i.e., have been submitted but neither approved nor disapproved by an editor already
Here's the data model:

create table user_submitted_comments (
comment_id integer primary key,
user_id not null references users,
submission_time date default sysdate not null,
ip_address varchar(50) not null,
content clob,
approved_p char(1) check(approved_p in ('t','f'))
);
Implicit in this model is the assumption that approved_p can be NULL and that, if not explicitly set during the INSERT, that is what it will default to. What about the check constraint? It would seem to restrict approved_p to values of "t" or "f". NULL, however, is a special value and if we wanted to prevent approved_p from taking on NULL we'd have to add an explicit not null constraint.

How do NULLs work with queries? Let's fill user_submitted_comments with some sample data and see:


insert into user_submitted_comments
(comment_id, user_id, ip_address, content)
values
(1, 23069, '18.30.2.68', 'This article reminds me of Hemingway');

Table created.

SQL> select first_names, last_name, content, user_submitted_comments.approved_p
from user_submitted_comments, users
where user_submitted_comments.user_id = users.user_id;

FIRST_NAMES LAST_NAME CONTENT APPROVED_P
------------ --------------- ------------------------------------ ------------
Philip Greenspun This article reminds me of Hemingway
We've successfully JOINed the user_submitted_comments and users table to get both the comment content and the name of the user who submitted it. Notice that in the select list we had to explicitly request user_submitted_comments.approved_p. This is because the users table also has an approved_p column.

When we inserted the comment row we did not specify a value for the approved_p column. Thus we expect that the value would be NULL and in fact that's what it seems to be. Oracle's SQL*Plus application indicates a NULL value with white space.

For the administration page, we'll want to show only those comments where the approved_p column is NULL:


SQL> select first_names, last_name, content, user_submitted_comments.approved_p
from user_submitted_comments, users
where user_submitted_comments.user_id = users.user_id
and user_submitted_comments.approved_p = NULL;

no rows selected
"No rows selected"? That's odd. We know for a fact that we have one row in the comments table and that is approved_p column is set to NULL. How to debug the query? The first thing to do is simplify by removing the JOIN:

SQL> select * from user_submitted_comments where approved_p = NULL;

no rows selected
What is happening here is that any expression involving NULL evaluates to NULL, including one that effectively looks like "NULL = NULL". The WHERE clause is looking for expressions that evaluate to true. What you need to use is the special test IS NULL:

SQL> select * from user_submitted_comments where approved_p is NULL;

COMMENT_ID USER_ID SUBMISSION_T IP_ADDRESS
---------- ---------- ------------ ----------
CONTENT APPROVED_P
------------------------------------ ------------
1 23069 2000-05-27 18.30.2.68
This article reminds me of Hemingway
An adage among SQL programmers is that the only time you can use "= NULL" is in an UPDATE statement (to set a column's value to NULL). It never makes sense to use "= NULL" in a WHERE clause.

The bottom line is that as a data modeler you will have to decide which columns can be NULL and what that value will mean.

Back to the Mailing List

Let's return to the mailing list data model from the introduction:

create table mailing_list (
email varchar(100) not null primary key,
name varchar(100)
);

create table phone_numbers (
email varchar(100) not null references mailing_list,
number_type varchar(15) check (number_type in ('work','home','cell','beeper')),
phone_number varchar(20) not null
);
This data model locks you into some realities:
  • You will not be sending out any physical New Year's cards to folks on your mailing list; you don't have any way to store their addresses.
  • You will not be sending out any electronic mail to folks who work at companies with elaborate Lotus Notes configurations; sometimes Lotus Notes results in email addresses that are longer than 100 characters.
  • You are running the risk of filling the database with garbage since you have not constrained phone numbers in any way. American users could add or delete digits by mistake. International users could mistype country codes.
  • You are running the risk of not being able to serve rich people because the number_type column may be too constrained. Suppose William H. Gates the Third wishes to record some extra phone numbers with types of "boat", "ranch", "island", and "private_jet". The check (number_type in ('work','home','cell','beeper')) statement prevents Mr. Gates from doing this.
  • You run the risk of having records in the database for people whose name you don't know, since the name column of mailing_list is free to be NULL.
  • Changing a user's email address won't be the simplest possible operation. You're using email as a key in two tables and therefore will have to update both tables. The references mailing_list keeps you from making the mistake of only updating mailing_list and leaving orphaned rows in phone_numbers. But if users changed their email addresses frequently, you might not want to do things this way.
  • Since you've no provision for storing a password or any other means of authentication, if you allow users to update their information, you run a minor risk of allowing a malicious change. (The risk isn't as great as it seems because you probably won't be publishing the complete mailing list; an attacker would have to guess the names of people on your mailing list.)

These aren't necessarily bad realities in which to be locked. However, a good data modeler recognizes that every line of code in the .sql file has profound implications for the Web service.

To get some more information on how a simple datamodel for a Discussion Forum can evolve, read en:sql-wn-data_modeling-philip

Representing Web Site Core Content

Free-for-all Internet discussions can often be useful and occasionally are compelling, but the anchor of a good Web site is usually a set of carefully authored extended documents. Historically these have tended to be stored in the Unix file system and they don't change too often. Hence I refer to them as static pages. Examples of static pages on the photo.net server include this book chapter, the tutorial on light for photographers at http://www.photo.net/making-photographs/light.

We have some big goals to consider. We want the data in the database to

  • help community experts figure out which articles need revision and which new articles would be most valued by the community at large.
  • help contributors work together on a draft article or a new version of an old article.
  • collect and organize reader comments and discussion, both for presentation to other readers but also to assist authors in keeping content up-to-date.
  • collect and organize reader-submitted suggestions of related content out on the wider Internet (i.e., links).
  • help point readers to new or new-to-them content that might interest them, based on what they've read before or based on what kind of content they've said is interesting.
The big goals lead to some more concrete objectives:
  • We will need a table that holds the static pages themselves.
  • Since there are potentially many comments per page, we need a separate table to hold the user-submitted comments.
  • Since there are potentially many related links per page, we need a separate table to hold the user-submitted links.
  • Since there are potentially many authors for one page, we need a separate table to register the author-page many-to-one relation.
  • Considering the "help point readers to stuff that will interest them" objective, it seems that we need to store the category or categories under which a page falls. Since there are potentially many categories for one page, we need a separate table to hold the mapping between pages and categories.

create table static_pages (
page_id integer not null primary key,
url_stub varchar(400) not null unique,
original_author integer references users(user_id),
page_title varchar(4000),
page_body clob,
obsolete_p char(1) default 'f' check (obsolete_p in ('t','f')),
members_only_p char(1) default 'f' check (members_only_p in ('t','f')),
price number,
copyright_info varchar(4000),
accept_comments_p char(1) default 't' check (accept_comments_p in ('t','f')),
accept_links_p char(1) default 't' check (accept_links_p in ('t','f')),
last_updated date,
-- used to prevent minor changes from looking like new content
publish_date date
);

create table static_page_authors (
page_id integer not null references static_pages,
user_id integer not null references users,
notify_p char(1) default 't' check (notify_p in ('t','f')),
unique(page_id,user_id)
);

Note that we use a generated integer page_id key for this table. We could key the table by the url_stub (filename), but that would make it very difficult to reorganize files in the Unix file system (something that should actually happen very seldom on a Web server; it breaks links from foreign sites).

How to generate these unique integer keys when you have to insert a new row into static_pages? You could

  • lock the table
  • find the maximum page_id so far
  • add one to create a new unique page_id
  • insert the row
  • commit the transaction (releases the table lock)
Much better is to use Oracle's built-in sequence generation facility:

create sequence page_id_sequence start with 1;
Then we can get new page IDs by using page_id_sequence.nextval in INSERT statements (see the Transactions chapter for a fuller discussion of sequences).

Reference

Here is a summary of the data modeling tools available to you in Oracle, each hyperlinked to the Oracle documentation. This reference section covers the following:

---

based on  SQL for Web Nerds

Next Page