Use XForms to create an accounting tool, Part 6: Wrapping it up

This six-part series demonstrates how to leverage the power of XForms in conjunction with MySQL and PHP to create an online accounting tool called X-Trapolate. Every good programming technology possesses a range of problems it excels at solving. This series highlights some of the problems that the XForms solves effectively, such as the need for live calculations and greater interactivity. Part 6 of this six-part series takes a final review and lessons learned approach, making sure there are no gaps in the final application and looking at future possibilities.

Nicholas Chase (ibmquestions@nicholaschase.com), Freelance writer, Backstop Media

Nicholas Chase has been involved in Web site development for companies such as Lucent Technologies, Sun Microsystems, Oracle, and the Tampa Bay Buccaneers. Nick has been a high school physics teacher, a low-level radioactive waste facility manager, an online science fiction magazine editor, a multimedia engineer, an Oracle instructor, and the Chief Technology Officer of an interactive communications company. He is the author of several books, including XML Primer Plus (Sams).



15 May 2007

Also available in

Overview

More in this series

  • Part 1 is an introduction to the entire series, summarizing the facets of the XForms specification each part covers.
  • Part 2 covers logging in and account management.
  • Part 3 covers the development of forms pertaining to asset management.
  • Part 4 continues the coverage of the development of asset management and reporting of various accounting aspects of a business.
  • Part 5 covers liability management and more enhancements.
  • Part 6 concludes the series with a summary of the developed tools, and some suggestions for improvement and further work for the tool set.

This article focuses on some of the real-world issues involved in creating an application using XForms with a PHP backend and MySQL as the database for storage and reference. You should be familiar with both XForms and PHP in order to follow along with this article. (See the Resources if you need something to get you started.)

How you got here

This series has covered a lot of ground, so let's first review. Like any real-world project, the X-Trapolate application ended up a bit different than what we mapped out in the beginning stages. Fortunately, the end result is an application that does what was originally intended, but there are still some holes, so let's examine them.

Part 1 of the series lays out the overall plan for the application, and the facets of XForms to be covered in each piece. It's a good introduction if you haven't been following the series so far.

Part 2 deals with user authentication. You create a basic login form and a registration form for new account creation. The login form was adequate to start with, but it has been completely superseded by new functionality. It doesn't take into account the user's department, which is crucial for asset management, nor does the registration process accept all of the user's data. Most importantly, the tutorial deals with an entirely isolated form, which doesn't protect any of the subsequent content. In this article (Part 6), you'll see how to correct all of these problems, including revamping the authentication system so that only users who have successfully logged in can see the forms.

In Part 3, you begin to deal with the financial aspects of running a company. You start with billing, and create a form that shows outstanding accounts. The form enables you to accept payments, send out bills, and refer accounts to collections. It also enables you to easily print out customized letters regarding these actions. One thing it doesn't do is enable you to create the invoices companies are supposed to pay. You can handle this by integrating with a separate application, or you can add this functionality to the current application. (Reviewing invoices is covered in Part 4.)

Part 3 also chronicles the creation of a budgetary form, which enables department users to see estimated profit and loss for their departments and sub-departments and easily compares them to the real numbers. This form also enables you to add new department structures to the application, which is great from a XForms perspective, but ultimately should probably be handled in a separate account management section.

Part 4 really gets into the nitty-gritty of this application. It describes a form that enables you to review existing invoices and their contents, but ultimately it must still integrate with an outside billing application, because it does not enable you to create an invoice.

This problem also plagues the asset management form created in Part 4, which enables users to report problems with equipment, and provides a way for procurement users to see what issues need to be resolved. It does not, however, provide the ability to create a new asset.

In Part 5, you also create two forms. The first, the payables form, enables you to not only see payables, or items the company needs to pay, but also to create new items. The form uses XPath manipulation to do some pretty cool things. Part 5 also shows you how you can create simple bar charts using XForms data. Although it's not required, extending this form through the use of technologies such as Scalable Vector Graphics (SVG) would improve its appearance.

The omissions that involve the ability to add new data, such as invoices and assets, can be resolved easily with techniques already covered in the series, and you will see a recap of that in a moment as you fix the omissions in the registration form. You can use this technique for virtually any "thing" that needs to be added to the database.

But that still leaves the authentication problems. This article also looks at the process of finishing the login and authentication system.


Fixing the registration section

Because it applies to multiple situations, let's start with fixing the registration form so that it includes all of the data necessary, not just the user's first and last name, user name, and password.

The complete code for this application can be obtained from the source download, so I'm just going to highlight changes from the original version of the form.

The first step is to add the missing data to the registration form itself (see Listing 1).

Listing 1. Additions to register.xhtml
...
    <xforms:model id="acctToolRegModel" >
      <xforms:instance id="acctToolRegInst" >
         <registerForm xmlns="">
            <password/>
            <username/>
            <unameopt1/>
            <unameopt2/>
            <unameopt3/>
            <prefix/>
            <firstName/>
            <lastName/>
            <street />
            <city />
            <state />
            <zip />
            <phone />
            <deptId />
            <company />
            <submitRegistration/>
         </registerForm>
      </xforms:instance>
...
        <xforms:secret class="required" bind="passwordbind" >
           <xforms:label>Password: </xforms:label>
        </xforms:secret><br/>

        <xforms:input ref="street" >
           <xforms:label>Street: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="city" >
           <xforms:label>City: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="state" >
           <xforms:label>State: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="zip" >
           <xforms:label>Zip: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="phone" >
           <xforms:label>Phone: </xforms:label>
        </xforms:input><br/>

        <xforms:select1 ref="deptId">
           <xforms:label>Department: </xforms:label>
            <xforms:item>
               <xforms:label>Procurement</xforms:label>
               <xforms:value>123</xforms:value>
            </xforms:item>
            <xforms:item>
               <xforms:label>Human Resources</xforms:label>
               <xforms:value>129</xforms:value>
            </xforms:item>
            <xforms:item>
               <xforms:label>Marketing</xforms:label>
               <xforms:value>333</xforms:value>
            </xforms:item>
        </xforms:select1><br/>

        <xforms:input ref="company" >
           <xforms:label>Company: </xforms:label>
        </xforms:input><br/>

        <xforms:submit submission="submit_registration" 
                   bind="submitRegistrationbind">
           <xforms:label>Register</xforms:label>
        </xforms:submit><br/>
        <hr/>
     </p>
  </body>
</html>

Most of what you see here in Listing 1 is just the straightforward addition of values to the instance and to the form. Notice that the department information has been added as a pulldown menu so that only existing departments can be added. Here they are represented as just static entries, but in the real world you would probably want to pull them from the database by populating an instance with the PHP script. (For an example of this, see Providing the appropriate menu items.)

The result looks like Figure 1.

Figure 1. The new registration form
The new registration form

The result goes to a PHP script, which also needs to have the additional data added (see Listing 2).

Listing 2. Additions to register.php
<?php

   if (!isset($HTTP_RAW_POST_DATA)) $HTTP_RAW_POST_DATA = 
                                file_get_contents("php://input");
   $xml = $HTTP_RAW_POST_DATA;
   $doc = new DomDocument('1.0');
   $doc->loadXML($xml);

   $sqldb = mysql_connect('localhost', 'root');
   if (!$sqldb) 
       die('Could not connect to MySQL server at localhost because: '
                                                    . mysql_error());

   $sqlseldb = mysql_select_db('acct1', $sqldb);
   if (!$sqlseldb) 
      die ('Can\'t select the acct1 database on MySQL server @ ' . 
                         'localhost because: ' . mysql_error());

   // first check to see if that username is already taken
   $uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;
   $sqlQuery = sprintf('SELECT * FROM accounts WHERE username=\'%s\'', 
                                 mysql_real_escape_string($uname));

   $queryData = mysql_query($sqlQuery, $sqldb);
   if (!$queryData)
      die ('Could not query the accounts table in the acct db because:'   
                                            . mysql_error());

   if(mysql_num_rows($queryData) > 0)
   {
      mysql_close($sqldb);
      $doc->getElementsByTagName("message")->item(0)->nodeValue = 
         'The username ' . $uname . 
                           ' is already taken.  Choose another.';
      echo $doc->saveXML();
      exit;
   }
   else
   {
      $pass =  mysql_real_escape_string(
         $doc->getElementsByTagName("password")->item(0)->nodeValue);
      $fname =  mysql_real_escape_string(
        $doc->getElementsByTagName("firstName")->item(0)->nodeValue);
      $prefix =  mysql_real_escape_string(
        $doc->getElementsByTagName("prefix")->item(0)->nodeValue);
      $lname =  mysql_real_escape_string(
        $doc->getElementsByTagName("lastName")->item(0)->nodeValue);
      $street =  mysql_real_escape_string(
        $doc->getElementsByTagName("street")->item(0)->nodeValue);
      $city =  mysql_real_escape_string(
        $doc->getElementsByTagName("city")->item(0)->nodeValue);
      $state =  mysql_real_escape_string(
        $doc->getElementsByTagName("state")->item(0)->nodeValue);
      $zip =  mysql_real_escape_string(
        $doc->getElementsByTagName("zip")->item(0)->nodeValue);
      $phone =  mysql_real_escape_string(
        $doc->getElementsByTagName("phone")->item(0)->nodeValue);
      $deptId =  mysql_real_escape_string(
        $doc->getElementsByTagName("deptId")->item(0)->nodeValue);
      $company =  mysql_real_escape_string(
        $doc->getElementsByTagName("company")->item(0)->nodeValue);

      $sqlQuery = "INSERT INTO accounts ( username, password, " . 
                   "firstName, lastName, prefix, street, city, ".
                   "state, zip, phone, deptId, company) VALUES ( ".
        sprintf("'%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', ".
                           "'%s', '%s', '%s', '%s');", 
                $uname, $pass, $fname, $lname, $prefix, $street, $city, 
                $state, $zip, $phone, $deptId, $company);
      $queryData = mysql_query($sqlQuery, $sqldb);
      if (!$queryData) 
        die ('Could not insert token into accounts table on MySQL ".
                             "server because:  ' . mysql_error());
      mysql_close($sqldb);
      $host= $_SERVER['HTTP_HOST'];
      $reldir= rtrim(dirname($_SERVER['PHP_SELF']), '/\\');
      header("Location: http://$host$reldir/login.xhtml");
      exit;
   }
?>

I'm including the whole script here because it contains changes throughout, and because it serves as an example for other additions you should make to the application. Start by loading the submitted instance, and by connecting to the database. From there, you'll first need to check to see whether the proposed username is already taken. Note the use of the mysql_real_escape_string() function, which helps to prevent SQL injection attacks by escaping single quotes, semicolons, and so on. ALWAYS santitize user-submitted data before using it to build an SQL statement.

If the proposed username is already taken, you will need to send a message back to the XForms form. You will need to do that by adding the message to the XML instance to be returned; a regular text error message won't show up in the form.

If the username is available, extract the remaining data, again remembering to sanitize it. You can then use it directly in an insert statement to add the data to the database. Once that's complete, close the database and use HTTP headers to force the browser to go to the login page.

You can use this technique for adding new assets, invoices, and so on. Now let's move on to the login process itself.


Upgrading the login process

In a typical application, you have a single login page from which the user authenticates him or herself, and any additional pages he or she accesses are displayed based on whether or not the user has been authenticated. When pages are served by PHP, this process is easy; you can check a session variable, or other persistent indicator. When the pages are static forms such as XHTML, however, the process is a little more complex.

One option would be to dynamically load the initial data instance, and base the contents of the page on the structure of data contained in that instance. For example, your data might all be relevant only if the instance contains a loggedIn element.(You'll see an example of this in a moment.) At this point, however, this would mean significant revisions to all of the work you've done.

A second option is to create a PHP "wrapper," which displays the static XHTML only if the user is logged in. The advantage here is that it involves little or no changes to existing pages, so that's the route you're going to take.

The first step is to make sure that the login page itself does everything it needs to to facilitate this process. Originally, it simply checked the username and password and created a loginToken if they were correct. Now you will need it to do a bit more than that.

Fortunately, most of the functionality you need, such as adding the user's department ID to this session, has already been created in the login_logout.php script. So the first step is to point the login form to that script (see Listing 3).

Listing 3. Altering login.xhtml
...

      <xforms:submission id="submit_login" 
                         action="login_logout.php" method="post"  
                         instance="acctToolLoginInst" 
                         replace="instance" 
                         ref="instance('acctToolLoginInst')" >
      </xforms:submission>
    </xforms:model>
  </head>
  <body>
     <p>
       
...
            <xforms:output class="errormessage" 
                  ref="instance('acctToolLoginInst')/message" />

            <xforms:trigger
                     model="acctToolLoginModel" 
                     ref="instance('acctToolLoginInst')/loggedIn">
               <xforms:label>Menu</xforms:label>
               <xforms:load ev:event="DOMActivate" 
                        resource="appManager.php?module=appMenu"/>
            </xforms:trigger><br/>

     </p>
  </body>
</html>

The first change is pretty obvious; this form points to the enhanced PHP login script. But what happens if the user has successfully logged in? This functionality was left open in Part 2. Now you're going to fill it in.

If the user successfully logs in, the response will include a new loggedIn element -- you'll see that in a moment -- and that makes the Menu trigger appear. That trigger loads a new page, which includes only the appropriate choices for that user. You'll create that functionality in Protecting pages and Providing the appropriate menu, but before you move on, the login_logout.php script has undergone some important changes.

The login_logout.php script was created as a "general purpose" script, designed to enable users to both log in and log out from individual pages. Because it will now be used to sign users in from just one place, you can simplify it significantly (see Listing 4).

Listing 4. login_logout.php
<?php

session_start();

unset($_SESSION['loginToken']);
unset($_SESSION['username']);
unset($_SESSION['at_deptId']);
unset($_SESSION['acctId']);

if (!isset($HTTP_RAW_POST_DATA)) 
    $HTTP_RAW_POST_DATA = file_get_contents("php://input");
$xml = $HTTP_RAW_POST_DATA;
$doc = new DomDocument('1.0');

if ($xml != "") {               
   $doc->loadXML($xml);
} else {
   header("Location: login.xhtml");
}

if ($doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue 
                                                      == 'Logout'){

   $doc->GetElementsByTagName('deptId')->item(0)->nodeValue = "";
   $doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue = 
                                                           "Login";
   $doc->GetElementsByTagName("username")->item(0)->nodeValue = "";
   $doc->GetElementsByTagName("loginToken")->item(0)->nodeValue = "";
   $doc->GetElementsByTagName('login')->item(0)->nodeValue=1;

   echo $doc->saveXML();
   exit;
}

$pass = $doc->getElementsByTagName("password")->item(0)->nodeValue;
$uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;

if ($uname == '' && $pass == ''){
   echo $doc->saveXML();
   exit;
}

$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb) 
   die('Could not connect to MySQL server at localhost because: ' . 
                                                     mysql_error());

$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb) 
      die ('Can\'t select the acct database on MySQL server @ ".
                            "localhost because: ' . mysql_error());

$sqlQuery = sprintf(
  "SELECT * FROM accounts WHERE username='%s' and password='%s'", 
  mysql_real_escape_string($uname), mysql_real_escape_string($pass));
$queryData = mysql_query($sqlQuery, $sqldb);

if (!$queryData) die ('Could not insert token into tokentable on ".
                     "MySQL server because:  ' . mysql_error());

$doc->getElementsByTagName("password")->item(0)->nodeValue = '';
$numRows = mysql_num_rows($queryData);
if($numRows <= 0)
{
   mysql_close($sqldb);
   $doc->getElementsByTagName("message")->item(0)->nodeValue = 
                                    'Invalid login credentials!';
   echo $doc->saveXML();
   exit;
}
else
{
   $row = mysql_fetch_assoc($queryData);
   $_SESSION['at_deptId'] = $row['deptId'];
   $_SESSION['acctId'] = $row['acctId'];
   $_SESSION['username'] = $uname;
   $loginToken = rand();
   $_SESSION['loginToken'] = $loginToken;
   $now = localtime();
   $datetime = sprintf("%04d-%02d-%02d",$now[5]+1900,$now[4],$now[3]);
   $sqlQuery = sprintf(
            "INSERT INTO logintokens (logintoken, username, creation)". 
                            "VALUES ( '%d', '%s', '%s');", 
            $_SESSION['loginToken'],
            mysql_real_escape_string($username),
            $datetime);
   $queryData = mysql_query($sqlQuery, $sqldb);
   if (!$queryData) 
        die ('Could not insert token into tokentable on MySQL server". 
                                     " because:  ' . mysql_error());
   mysql_close($sqldb);

   $doc->getElementsByTagName("password")->item(0)->nodeValue = '';
   $doc->GetElementsByTagName("errorMessage")->item(0)->nodeValue = 
                      sprintf('%s Logged In',$_SESSION['username']);
   $doc->getElementsByTagName('loginLogout')->item(0)->nodeValue = 
                      "Logout";
   $doc->GetElementsByTagName("loginToken")->item(0)->nodeValue = 
                      $_SESSION['loginToken'];
   $doc->getElementsByTagName('deptId')->item(0)->nodeValue = 
                      $_SESSION['at_deptId'];
   $doc->GetElementsByTagName('login')->item(0)->nodeValue=0;

   $notLoggedElement = 
            $doc->getElementsByTagname('notLoggedIn')->item(0);
   $notLoggedElement->parentNode
            ->appendChild($doc->createElement('loggedIn'));
   $notLoggedElement->parentNode->removeChild($notLoggedElement);

   echo $doc->saveXML();
   exit;
}
?>

The first step is to make sure that the user is logged out by unsetting the relevant session variables. If the user successfully logs in, he or she will get another loginToken. If not, they will remain logged out.

Next, check to make sure that the login information has actually been submitted. If not, simply go back to the login page.

The script can still handle explicit logout requests, and building and returning the appropriate instance information, so links on the individual pages will still work.

If the user hasn't submitted a username or password, the script simply returns the original instance to the login page, but if the information is there, check it against the database. If the login credentials are wrong, the script inserts a message to that effect into the message element (as opposed to the errormessage element), where it gets displayed on the page, as in Figure 2.

Figure 2. Invalid credentials
invalid credentials

If, on the other hand, the credentials are correct, populate the session, the logintokens table, and the return as before. One change here, however, is that you also need to add the loggedIn element to the returned instance, and remove the notLoggedIn element.

Making these changes to the instance makes the login form irrelevant, and the menu button relevant, as you can see in Figure 3.

Figure 3. A successful login
a successful login

Clicking the Menu button takes the user to the list of pages, but what's to stop the user from going there directly, without successfully logging in?


Protecting pages

The Menu button takes the user to the URL: http://<myhost>/appManager.php?module=appMenu.

That page is a PHP script that checks to make sure that the user is logged in before providing sensitive information. It works as follows (see Listing 5):

Listing 5. The appManager.php
<?php

session_start();

$loggedIn = false;

if (isset($_SESSION['loginToken'])){

   $sqldb = mysql_connect('localhost', 'root');
   if (!$sqldb) 
        die('Could not connect to MySQL server at localhost: ' . 
                                               mysql_error());

   $sqlseldb = mysql_select_db('acct1', $sqldb);
   if (!$sqlseldb) 
        die ('Can\'t select the acct database on MySQL server '.
                         "@ localhost because: ' . mysql_error());

   $sqlQuery = 
      sprintf("SELECT * FROM logintokens WHERE logintoken='%s'", 
              mysql_real_escape_string($_SESSION['loginToken']));

   $queryData = mysql_query($sqlQuery, $sqldb);

   if ( mysql_fetch_assoc($queryData)){
       $loggedIn = true;
   }

} 

$module = $_GET['module'];

if ($loggedIn){
    $filename = $module.".xhtml";
    header("Content-type: text/xml");
    echo file_get_contents($filename);
} else {
    header("Location: login.xhtml");
}
?>

First, initialize the session and set the loggedIn variable to false. It'll stay that way unless you detect the appropriate loginToken in the session, as verified in the database. (This also enables you to do things like expiring tokens in the database and so on.)

Then, retrieve the page, or "module," from the querystring. If the user's logged in, you are going to send the browser the contents of the appropriate file, making sure to specify that it's XML first. (The browser thinks this is a PHP script, so it won't automatically make that leap.) If the user's not logged in, simply send him or her back to the login page.

Now, in this example, all of the pages are in the same directory, which doesn't seem very secure, and it's not. But there's nothing stopping you from moving the pages to a directory that's not accessible using the Web server. If you do that, then appManager.php becomes the only way to access them, and if the user's not logged in, he or she won't get them.

You can also extend this file so that it checks a user's permissions, serving only files for which that user has access. For example, you might limit the budgetary pages only to management.

Now the application is almost complete, but you still need to deal with the menu.


Providing the appropriate menu

At this point, the menu is only accessible to users who are logged in, but how can you make sure that users only get the options to which they're entitled? The menu page itself, after all, is a static XHTML page (see Listing 6).

Listing 6. Menu page
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
"http://www.w3.org/TR/xhtml1/D/tdxhtml1-strict.dtd">
<html
   xmlns="http://www.w3.org/1999/xhtml"
   xmlns:xforms="http://www.w3.org/2002/xforms" 
   xmlns:ev="http://www.w3.org/2001/xml-events"
>
  <head>
    <title>XForms Accounting Menu Page</title>
    <link rel="stylesheet" href="style.css" type="text/css"/>
    <xforms:model id="menuPage" >

        <xforms:instance xmlns="" src="appMenu.php" />

    </xforms:model>
  </head>
  <body>
     <p>
       
        <xforms:repeat nodeset="//link">

            <xforms:trigger>
               <xforms:label ref="." />
               <xforms:action ev:event="DOMActivate">
                    <xforms:load ref="./@href" show="replace" />
               </xforms:action>
            </xforms:trigger>

        </xforms:repeat>

     </p>
  </body>
</html>

The key here is in the instance. Rather than having a static list of options, it receives its data from a PHP script, appMenu.php. Each of those elements can then be used to create a trigger and a load element that uses the data in the href attribute of each link element to determine where to send the browser.

The appMenu.php script is straightforward (see Listing 7).

Listing 7. appMenu.php
<?php
    header("Content-type: text/xml");

    $deptId = $_SESSION['deptId'];

    if ($deptId = 123){

?>
     <links>
         <link href="appManager.php?module=billing">Billing</link>
         <link href="appManager.php?module=budget">Budget</link>
         <link href="appManager.php?module=invoices">Invoices</link>
         <link href=
      "appManager.php?module=assetManagement">Asset Management</link>
         <link href="appManager.php?module=payables">Payables</link>
         <link href="appManager.php?module=analyze">Analyze</link>
     </links>

<?php

} else {

?>

     <links>
         <link href="appManager.php?module=billing">Billing</link>
         <link href="appManager.php?module=budget">Budget</link>
         <link href="appManager.php?module=invoices">Invoices</link>
         <link href="appManager.php?module=payables">Payables</link>
         <link href="appManager.php?module=analyze">Analyze</link>
     </links>


<?php

}

?>

Here you're relying on the user already being logged in, since the XForms form can only be accessed through appManager.php. That said, you can use the department ID to dynamically determine which pages to serve. Notice that each one of these links serves the pages through the appManager.php script. The result looks something like Figure 4.

Figure 4. The menu page
menu page

You can, of course, customize the page as you like.


Summary

Over the course of the last six articles and tutorials, you saw the creation of an accounting tool using XForms, PHP, and MySQL. This tool performs actions such as reviewing invoices and helping to plan budgets using the strengths of XForms, while leaving actions such as determining whether or not a user has logged in to PHP. By designing the application in this way, you enable each part of your architecture to play to its strengths and mitigate its weaknesses.

And that's what designing for the real world is all about.


Download

DescriptionNameSize
Part 6 sample codextrapolate.zip42KB

Resources

Learn

Get products and technologies

  • The XForms Recommendation is maintained by the W3C.
  • Download WAMP, which includes PHP and MySQL preconfigured and ready to go.
  • Get MozzIE, an open-source control that allows you to render XForms in Internet Explorer.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into XML on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML
ArticleID=219810
ArticleTitle=Use XForms to create an accounting tool, Part 6: Wrapping it up
publish-date=05152007