Starting the Work

Starting the Work

Now you and your clients understand exactly what will be implemented in the first iteration, or the Customer Contact Report:

  • Story 9: Authentication and Authorization, 4 points

  • Story 1: Customer Contact Report, 3 points

  • Story 6: Sales Manager Notification, 3 points

  • Story 8: Mail Room Notification, 2 points

Outlining Details of Story 9

"Each sales person needs to have his or her own private login."

This presumes, for one thing, that "salesperson'' exists as a logical entity (not necessarily a PHP class, yet) so let's outline its needs.

There exists the need for basic human-readable user information:

   First Name    Last Name 

Yes, you are a number as well as a name:

   Employee ID 

Because the person needs to log in to the system, we'll allow him or her to choose both a login name and password:

   Login Name    Login Password 

You also need to know the role in which the person accesses the system. Although a person's role may be related to a job title, titles tend to be ephemeral. In addition, you need to determine only one of three roles: salesperson, accountant, sales manager.

   Company Role 

This brings up a good question: Can someone be in more than one role at a time? Can someone both be an accountant and a sales manager? What about a sales person and a sales manager? Wendy answers your question with a "Probably not; Wade and Edwina's responsibilities don't overlap." Popping your head into Wade's tidy cubicle confirms your hypothesis, as does a phone call to Edwina. Roles, therefore, are exclusive.

Because you are taking care of authentication, let's decide how we'd like to use the system.

Writing Tests

Ask yourself a few questions:

You know what the user consists of:

   Employee ID    First Name    Last Name    Company Role 

And what is required for authentication:

   Login Name    Login Password 

Recall from Chapter 15 the UserSession class? This is a plausible situation to use with two yet-to-be-defined classes, WidgetSession and WidgetUser:

   <?php    $session = new WidgetSession(); // inherited from UserSession    $session->impress();    // authentication    $session->login("ed","12345");    if ($session->isLoggedIn() == false) exit;    $user = $session->getUser(); // returns WidgetUser    print $user->first_name;    print $user->last_name;    print $user->email;    // authorization    print $user->role;    print $user->isSalesPerson();    print $user->isSalesManager();    print $user->isAccountant();    ?> 

Keep in mind that the preceding code snippet is only "a plausible scenario.'' It's not yet a formalized test or running code; it is what exists solely in your mind regarding the use of new classes. When you are happy with this "what if'' scenario, you will formalize its use in a test. Let's continue and write some tests!

PhpUnit

The idea here is that you will write the test to simulate the way in which you plan on using the software associated with your current story.

It's called test-driven development, and in addition to its development strengths, you end up with a great suite of tests.

Important 

Testing the application in the same manner in which you will be using it forces you to look at the function or object and nail down the arguments and other elements. This avoids creating functionality that isn't used or is used in the wrong places.

Far from "tying you down," having a testing suite allows you a great degree of freedom because you know instantly whether you happened to break distant parts of the system by adding new features or modifying existing code.

Here are some new tests that are designed to reflect the common usage scenario, test.widgetsession.php:

   <?php    require_once ("widgetsession.phpm");    require_once ("lib/phpunit/phpunit.php");    class TestWidgetSession extends TestCase    {        private $_session;        function setUp() {            $dsn = array ('phptype' => "pgsql",                          'hostspec' => "localhost",                          'database' => "widgetworld",                          'username' => "wuser",                          'password' => "foobar");            $this->_session = new WidgetSession($dsn, true);        }        function testValidLogin() {            $this->_session->login("ed","12345");            $this->assertEquals(true, $this->_session->isLoggedIn());        }        function testInvalidLogin() {            $this->_session->login("ed","54321"); // fail            $this->assertEquals(false, $this->_session->isLoggedIn());        }        function testUser() {            $user = $this->_session->getUser();            $this->assertEquals("Lecky-Thompson", $user->last_name);            $this->assertEquals("Ed", $user->first_name);            $this->assertEquals("ed@lecky-thompson.com", $user->email);        }        function testAuthorization () {            $user = $this->_session->getUser();            $this->assertEquals("Sales Person", $user->role);            $this->assertEquals(true, $user->isSalesPerson());            $this->assertEquals(false, $user->isSalesManager());            $this->assertEquals(false, $user->isAccountant());        }    }    $suite = new TestSuite;    $suite->addTest(new TestWidgetSession("testValidLogin"));    $suite->addTest(new TestWidgetSession("testInvalidLogin"));    $suite->addTest(new TestWidgetSession("testUser"));    $suite->addTest(new TestWidgetSession("testAuthorization"));    $testRunner = new TestRunner();    $testRunner->run($suite);    ?> 

However, when it is run you get the following error:

   Class 'WidgetSession' not found in test.widgetsession.php on line 16 

For now, let's mock up appropriate dummy WidgetSession and WidgetUser classes so that our tests can at least run. Incorporate the following code at the top of the previous listing:

   class WidgetSession {       public function __construct ($one, $two) {}       public function login() {}       public function isLoggedIn()     { return null; }       public function getUser() {           return new WidgetUser();       }    }    class WidgetUser {       public $first_name = "";       public $last_name = "";       public $email = "";       public function isSalesPerson()  { return null; }       public function isSalesManager() { return null; }       public function isAccountant()   { return null; }    } 

Now run the tests again in test.widgetsession.php. Here is the corresponding output from PhpUnit:

   TestWidgetSession - testValidLogin FAIL    TestWidgetSession - testInvalidLogin FAIL    TestWidgetSession - testUser FAIL    TestWidgetSession - testAuthorization FAIL    4 tests run.    9 failures.    0 errors.    Failures       1. testValidLogin             true   type:boolean             null   type:NULL       2. testInvalidLogin             false   type:boolean             null   type:NULL       3. testUser             Lecky-Thompson  type:string             type:string       4. testUser             Ed   type:string             type:string       5. testUser             ed@lecky-thompson.com  type:string             type:string       6. testAuthorization             Sales Person  type:string             type:string       7. testAuthorization             true   type:boolean             null   type:NULL       8. testAuthorization             false  type:boolean             null  type:NULL       9. testAuthorization             false   type:boolean             null   type:NULL 

At this point of the development process, every one of your tests should run without syntax error, but should be failing. This is because you haven't yet implemented any of the functionality for which the tests are designed! So yes, the first time you run the tests you should be happy that they all fail. Remember that the tests are based on the common usage scenario and that you should be testing all the features required by the scenario.

It is now your responsibility to implement the actual code that is required to pass the tests. The login and session process can be fixed quite easily because that behavior is found in UserSession.

Take care of the low-hanging fruit in regard to the WidgetSession. Because WidgetSession is adding functionality to UserSession, it makes sense to extend UserSession. So start with your spoofed WidgetSession class and extend UserSession:

   class WidgetSession extends UserSession {       public function getUser() {           return new WidgetUser();       }    } 

Now rerun the test.widgetsession.php tests:

   TestWidgetSession - testValidLogin ok    TestWidgetSession - testInvalidLogin ok    TestWidgetSession - testUser FAIL    TestWidgetSession - testAuthorization FAIL    4 tests run.    7 failures.    0 errors. 

By utilizing the functionality inherent in UserSession, you've already passed half the tests.

Next, start work on the WidgetUser objects by first overriding UserSession's getUserObject() function to return a WidgetUser class:

   class WidgetSession extends UserSession {       public function getUserObject() {           $uid = $this->GetUserID(); // calling up from UserSession           if ($uid == false) return null;           // pull ourselves out of the database           $stmt = "select * FROM \"user\" WHERE id = ".$uid;           $result = $this->getDatabaseHandle()->query($stmt);           return new WidgetUser($result->fetchRow());       }    } 

Your WidgetUser class needs to hold its state. However, the WidgetUser class is getting pumped out from a database which can return data in an associative array, so just go ahead and store WidgetUser's state in an associative array.

Of course, __set and __get both need to be redefined.

   class WidgetUser {       protected $contentBase = array();       function __construct($initdict) {           $this->contentBase = $initdict; // copy       }       function __get ($key) {           if (array_key_exists ($key, $this->contentBase)) {               return $this->contentBase[$key];           }           return null;       }       function __set ($key, $value) {           if (array_key_exists ($key, $this->contentBase)) {               $this->contentBase[$key]=$value;           }       }       public function isSalesPerson()  { return null; }       public function isSalesManager() { return null; }       public function isAccountant()   { return null; }    } 

Once again, rerunning test.widgetsession.php results in most of the user tests passing, with one notable exception:

   1. testUser       ed@lecky-thompson.com   type:string       null   type:NULL 

Hmmm, looking closer at the MySQL version of the user table

   CREATE TABLE "user" (      id serial PRIMARY KEY,       username varchar(32) default NULL,       md5_pw varchar(32) default NULL,       first_name varchar(64) default NULL,       last_name varchar(64) default NULL    ); 

reveals that the user table is missing e-mail, so you need to add it to make the table look like this:

   CREATE TABLE "user" (      id serial PRIMARY KEY,      username varchar(32) default NULL,      md5_pw varchar(32) default NULL,      first_name varchar(64) default NULL,      last_name varchar(64) default NULL,      email varchar(255) default NULL    ); 

Run the test.widgetsession.php tests:

   TestWidgetSession - testValidLogin ok    TestWidgetSession - testInvalidLogin ok    TestWidgetSession - testUser ok    TestWidgetSession - testAuthorization FAIL    4 tests run.    4 failures. 

Zero errors.

Now that the testUser passes, pay attention to the testAuthorization. Because "role'' is not included in the database yet, add that as well. The default value of 's' is short for "salesperson'':

   CREATE TABLE "user" (      id serial PRIMARY KEY,      username varchar(32) default NULL,      md5_pw varchar(32) default NULL,      first_name varchar(64) default NULL,      last_name varchar(64) default NULL,      email varchar(255) default NULL,      role char(1) NOT NULL default 's'    ); 

Putting some state-retrieval accessor methods into WidgetUser solves most of the remaining issues:

   public function isSalesPerson() {        if ($this->role == "s") return true;        return false;    }    public function isSalesManager() {        if ($this->role == "m") return true;        return false;    }    public function isAccountant() {        if ($this->role == "a") return true;        return false;    } 

However, the test still fails on "role'' because it expects a human-readable string and instead is limited to the single digits of s, m, and a:

   1. testAuthorization       Sales Person   type:string       s   type:string 

Recall from earlier what is being tested for:

   function testAuthorization () {        $user = $this->_session->getUser();        $this->assertEquals("Sales Person", $user->role);        $this->assertEquals(true, $user->isSalesPerson());        $this->assertEquals(false, $user->isSalesManager());        $this->assertEquals(false, $user->isAccountant());    } 

Note that because of the nature of the next problem, when $user->role is called the expectation is to get "Sales Person." Keep in mind that that role is directly from the database, so either change the database or change your software instead.

How about this idea: Because you'd like to continue to directly access $user->role in the __get function, you can determine which role is being queried for and dispatch it to a getRole() function.

Here it is:

   class WidgetUser {       protected $contentBase = array();       protected $dispatchFunctions = array ("role" => "getrole");       function __construct($initdict) {           $this->contentBase = $initdict; // copy       }       function __get ($key) {           // dispatch by function first           if (array_key_exists ($key, $this->dispatchFunctions)) {               $funcname = $this->dispatchFunctions[$key];               return $this->$funcname();           }           // otherwise return based on state           if (array_key_exists ($key, $this->contentBase)) {               return $this->contentBase[$key];           }           return null;       }       function __set ($key, $value) {           if (array_key_exists ($key, $this->contentBase)) {               $this->contentBase[$key]=$value;           }       }       public function getRole() {           switch ($this->contentBase["role"]) {               case "s": return ("Sales Person");               case "m": return ("Sales Manager");               case "a": return ("Accountant");               default: return ("");           }       }       public function isSalesPerson() {           if ($this->contentBase["role"] == "s") return true;           return false;       }       public function isSalesManager() {           if ($this->contentBase["role"] == "m") return true;           return false;       }       public function isAccountant() {           if ($this->contentBase["role"] == "a") return true;           return false;       }    } 

Note that isSalesPerson(), isSalesManager(), and isAccountant() all access contentBase rather than call $this->role. Otherwise it would get dispatched through the getRole() function, which returns human-readable results such as "Sales Manager.'' It is not a good idea to try to return Boolean values based on human-readable results as they could change at whim.

Creating the Login Screen

Now that your WidgetUser and WidgetSession objects are working, it's time to use them with a real login.

Although the login screen is not terribly complicated, you'll continue to use Smarty as the templating system simply for its convenience.

Here's your initial index.php file:

   <?php    require_once ("Smarty.class.php");    require_once ("widgetsession.phpm");    $session = new WidgetSession(array ('phptype'  => "pgsql",                                        'hostspec' => "localhost",                                        'database' => "widgetworld",                                        'username' => "wuser",                                        'password' => "foobar"));    $session->Impress();    $smarty = new Smarty;    if ($_REQUEST["action"] == "login") {        $session->login($_REQUEST["login_name"],$_REQUEST["login_pass"]);        if ($session->isLoggedIn()) {            $smarty->assign_by_ref("user", $session->getUserObject());            $smarty->display ("main.tpl");            exit;        } else {            $smarty->assign('error', "Invalid login, try again.");            $smarty->display ("login.tpl");            exit;        }    } else {        if ($session->isLoggedIn() == true) {            $smarty->assign_by_ref("user", $session->getUserObject());            $smarty->display ("main.tpl");            exit;        }    }    $smarty->display ("login.tpl");    ?> 

The logic for the screen is as follows: If the action variable of the form is login, it indicates that a login attempt is trying to be made, so an attempt to log in is made by calling $session->login(). A positive result is dealt with by rendering the main menu. A negative result causes an error variable to be set, and the login screen again presented to the user.

Of course, if you're not trying to log in, the $session->isLoggedIn() is checked to confirm the login status with the session. If the user is logged in, the main menu screen is displayed. Otherwise, the login screen is again redisplayed.

The two screens, login and main, along with the header and footer, are all simple:

login.tpl:

   {include file="header.tpl" title="Widget World Login"}    <h3>Please Login:</h3>    <p>    {section name=one loop=$error}{sectionelse}      <font color="#FF0000">{$error}</font><p>    {/section}    <form action="index.php" method="post">    <table border="0">    <tr><td width="20"></td><td>User:</td>        <td><input name="login_name" type="text" size="20" maxsize="50"></td></tr>    <tr><td width="20"></td><td>Password:</td>        <td><input name="login_pass" type="password" size="20"    maxsize="50"></td></tr>    <tr><td width="20"></td><td></td>        <td><input type="submit" value=" Login "></td></tr>    </table>    <input type="hidden" name="action" value="login">    </form>    {include file="footer.tpl"} 

header.tpl:

   <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">    <html>    <head>    <meta HTTP-EQUIV="content-type" CONTENT="text/html; charset=ISO-8859-1">    <title>{$title|default:"no title"}</title>    </head>    <h1>Widget World</h1>    <hr><p> 

footer.tpl:

   <br/><br/>    <hr/>    For Widget World use only - testing environment.    </body>    </html> 

main.tpl:

   {include file="header.tpl" title="Widget World Menu"}    Welcome {$user->first_name} the {$user->role}!    <table border="0" cellspacing="8" cellpadding="8">    {strip}    {section name=security show=$user->isAccountant()}      <tr><td><h3>Accountant functionality goes here.</h3></td></tr>    {/section}      <tr><td valign="top"><h3><a href="travel-expenses.php">New Travel    Expenses</a></h3></td></tr>      <tr><td valign="top"><h3><a href="customer-contacts.php">New Customer    Contacts</a></h3></td></tr>    {/strip}    </table>    {include file="footer.tpl"} 

Figure 22-3 shows what this looks like.


Figure 22-3

After you are logged in, you get the menu shown in Figure 22-4.


Figure 22-4

If you change ed to be an accountant by changing his role to be a, then ed would have a slightly different view, as displayed in Figure 22-5.


Figure 22-5

Congratulations your first story is now complete. You have done a lot of work with PHP sessions, database lookups, PEAR::DB, and PhpUnit to accomplish the functionality required to determine who is logging in and what they should be able to do, which is not at all a trivial feat.

Take a nice little break and enjoy munching on a biscuit; there is a lot more work coming up.

The Next Story

Your previous story (Story 9: "Authentication and Authorization'') was worth 4 points. Write down how long in days or half days it took you to accomplish this 4-point task.

Recall that this iteration of the Customer Contact Report comprises:

Time to get back to work.

Customer Contact Requirements

Recall from earlier (refer to Figure 22-1) what the Weekly Contact Report requires. The data requirements for this look something like this:

   CREATE TABLE contact_visits (      emp_id integer NOT NULL,      week_start date NOT NULL,      seq integer NOT NULL,      company_name varchar(40) default NULL,      contact_name varchar(40) default NULL,      city varchar(40) default NULL,      state varchar(40) default NULL,      accomplishments text,      followup text,      literature_request text    );    CREATE UNIQUE INDEX cv_pk on contact_visits (emp_id,week_start,seq);    CREATE INDEX cv_emp_id ON contact_visits (emp_id);    CREATE INDEX cv_week_start ON contact_visits (week_start);    CREATE INDEX cv_seq ON contact_visits (seq); 

The data is per week, is associated with the employee, and is unique when completed with the SEQuence column; there may be a lot.

Note that state is 40 chars long in order to accommodate non-U.S. states and provinces in Canada and Mexico.

Also note that the customer contact report has the employee's department that currently doesn't exist in the USER table. Go ahead and add it:

   CREATE TABLE "user" (      id serial PRIMARY KEY,      username varchar(32) default NULL,      md5_pw varchar(32) default NULL,      first_name varchar(64) default NULL,      last_name varchar(64) default NULL,      email varchar(255) default NULL,      role char(1) NOT NULL default 's',      department varchar(40) NOT NULL default ''    ); 

Customer Contact Tests

Think about how you intend to use the customer contacts. From this input screen, nearly all the form data is provided to you via the Web server, which means that when you prepare to persist it you'll need the employee ID available from WidgetSession.

Also consider what minimum information you'll require before dumping into the table: id, week_start, and sequence with this PhpUnit test.

   function testValidContactVisit() {        $cv = new ContactVisit (            array ('emp_id'               => "1",                   'seq'                  => "1",                   'week_start'           => "1980-01-01",                   'company_name'         => "test one",                   'contact_name'         => "Big One",                   'city'                 => "Columbus",                   'state'                => "OH",                   'accomplishments'      => "phone call",                   'followup'             => "",                   'literature_request'   => ""));        $this->assertEquals(true, $cv->isValid(), "valid log");    } 

This testing is the minimum information required by ContactVisit, thus if emp_id, seq, and week_start contain any values then isValid() will return true. Conversely, the opposite should also be tested:

   function testInvalidContactVisit() {        $cv = new ContactVisit (            array ('emp_id'             => "1",                   'week_start'         => "", // date required                   'company_name'       => "test one",                   'contact_name'       => "Big One",                   'city'               => "Columbus",                   'state'              => "OH",                   'accomplishments'    => "phone call",                   'followup'           => "",                   'literature_request' => ""));        $this->assertEquals(false, $cv->isValid(), "invalid visit");    } 

Because the visits each require a unique sequence value, they might as well determine their order themselves:

   function testSequence() {        $cv1 = new ContactVisit(array());        $this->assertEquals(1, $cv1->seq);        $cv2 = new ContactVisit(array());        $this->assertEquals(2, $cv2->seq);    } 

Note that by creating ContactVisits, the sequence (seq) of each new ContactVisit is automatically incremented, which eliminates the need to require the container to assign each a unique sequence number.

Persistence is still in a state of flux, but this will certainly work:

       function testPersistence() {            $this->_session->getDatabaseHandle()->query("delete FROM contact_visits    WHERE emp_id = 1 and week_start = '1980-01-01'"); // remove multiples            $cv = new ContactVisit (                array ('emp_id'             => "1",                       'week_start'         => "1980-01-01",                       'seq'                => 1,                       'company_name'       => "test one",                       'contact_name'       => "Big One",                       'city'               => "Columbus",                       'state'              => "OH",                       'accomplishments'    => "phone call",                       'followup'           => "",                       'literature_request' => ""));            $result = $this->_session->getDatabaseHandle()->query("select * FROM    contact_visits WHERE emp_id = 1 and week_start = '1980-01-01'");            $this->assertEquals(0, $result->numRows());            $cv->persist();             $result = $this->_session->getDatabaseHandle()->query("select * FROM    contact_visits WHERE emp_id = 1 and week_start = '1980-01-01'");            $this->assertEquals(1, $result->numRows());        } 

Also note that week_start's value of New Year's day, 1980, was chosen for a reason:

Before running the tests, don't forget to stub out ContactVisit to make sure that the tests successfully FAIL (yet syntactically execute) before correctly satisfying the tests. Here are the contents of contact. phpm:

   class ContactVisit {        function __construct ($results) { }        public function isValid() { return null; }        public function persist() { }        public function getSequence() { return null; } } 

Satisifying the Tests

Because the visits are going to be in sequence, utilize a static variable so that the sequence count is shared across all instances of the class. Incrementing it in the constructor also ensures that all your bases are covered without requiring any more plumbing. Go ahead and implement the constructor of ContactVisit in contact.phpm:

   class ContactVisit {        function __construct ($results, $dbh = null) {            static $sequence = 0;            $this->dbh = $dbh;            $this->contentBase = $results; // copy            $sequence = $sequence + 1; // increment across class            $this->contentBase["seq"] = $sequence;        }       public function isValid() { return null; }       public function persist() { }       public function getSequence() { return null; }    } 

Also note that ContactVisit needs to store a reference to the database; this is in addition to the normal data (company name, city, state, and so on) so return ContactVisit's state in the last return of ContactVisit's __get() function. Here it is in contact.phpm:

   function __get ($key) {        if (array_key_exists ($key, $this->contentBase)) {            return $this->contentBase[$key];        }        return $this->$key;    } 

Put it all together, add some functions for generating its required SQL, and you're set. The file contact.phpm follows:

   class ContactVisit {        protected $contentBase = array();        protected $dbh = null; // database handle        function __get ($key) {            if (array_key_exists ($key, $this->contentBase)) {                return $this->contentBase[$key];            }            return $this->$key;       }        function __construct ($results, $dbh = null) {            static $sequence = 0;            $this->dbh = $dbh;            $this->contentBase = $results; // copy            $sequence = $sequence + 1; // increment across class            $this->contentBase["seq"] = $sequence;       }        private function isEmpty($key) {            if (array_key_exists($key, $this->contentBase) == false) return true;            if ($this->contentBase[$key] == null) return true;            if ($this->contentBase[$key] == "") return true;           return false;        }        public function isValid() {            if ($this->isEmpty("emp_id") == true) return false;            if ($this->isEmpty("week_start") == true) return false;            if ($this->isEmpty("company_name") == true) return false;            return true;        }    private function implodeQuoted (&$values, $delimiter) {        $sql = "";        $flagIsFirst = true;        foreach ($values as $value) {            if ($flagIsFirst) {                $flagIsFirst = false;            } else {                $sql .= $delimiter;            }            if (gettype ($value) == "string") {                $sql .= "'".$value."'";            } else {                $sql .= $value;            }        }        return $sql;    }    private function generateSqlInsert ($tableName, &$metas, &$values) {        return "insert into ".$tableName.            "        ( ".implode              ($metas,  ", ")." ) ".            " values ( ".$this->implodeQuoted ($values, ", ")." ) ";    }    public function persist() {        if ($this->isValid() == false) return false;        $sql = $this->generateSqlInsert ("contact_visits",                                         array ( "emp_id",                                                 "week_start",                                                 "seq",                                                 "company_name",                                                 "contact_name",                                                 "city",                                                 "state",                                                 "accomplishments",                                                 "followup",                                                 "literature_request" ),                                         array ( $this->emp_id,                                                 $this->week_start,                                                 $this->seq,                                                 $this->company_name,                                                 $this->contact_name,                                                 $this->city,                                                 $this->state,                                                 $this->accomplishments,                                                 $this->followup,                                                 $this->literature_request ));        if (DB::isError ($this->dbh->query($sql))) return false;        return true;    } } 

Briefly, the methods implemented are as follows:

It may be noted that implodeQuoted() and generateSqlInsert() both insist that the arrays are passed by reference rather than by value. Of course, the default behavior of PHP5 is to pass objects by reference, but arrays are passed by value in the default case.

Creating the Screen

Consider this form, called customer-contacts.tpl:

   {include file="header.tpl" title="Widget World - Customer Contact"}    <h3>Customer Contact Report</h3>    <form action="customer-contacts.php" method="post">    <table border="0" width="100%">    <tr><td><b>Employee Name:</b></td><td>{$user->first_name}    {$user->last_name}</td>    <td><b>Department:</b></td><td>{$user->department}</td></tr>    <tr><td><b>Number:</b></td><td>{$user->id}</td><td><b>Start Week:</b></td>    <td><SELECT NAME="week_start">{html_options values=$start_weeks    output=$start_weeks selected=$current_start_week}</SELECT></td></tr>    </table>    <br><br><hr>    <p><font size="+1"><b>Significant Distributors and Customers    Visited:</b></font><br>    (also distributors/OEM/prospects)<p>    <table border="0">    {section name=idx loop=$max_weekly_contacts}{strip}    <tr><td    width="20"></td><td><b>Company</b></td><td><b>Contact</b></td><td><b>City</b>    </td><td><b>State</b></td><td><b>FollowUp</b></td><td><b>Literature    Request</b></td></tr>    <tr>    <td width="20"></td>    <td><input name="company_name_{$smarty.section.idx.index}"       size="20"    maxlength="50"></td>    <td><input name="contact_name_{$smarty.section.idx.index}"       size="20"    maxlength="50"></td>    <td><input name="city_{$smarty.section.idx.index}"               size="20"    maxlength="50"></td>    <td><input name="state_{$smarty.section.idx.index}"              size="10"    maxlength="50"></td>    <td><input name="followup_{$smarty.section.idx.index}"           size="20"    maxlength="2000"></td>    <td><input name="literature_request_{$smarty.section.idx.index}" size="20"    maxlength="2000"></td>    </tr>    <tr>    <td width="20"></td>    <td colspan="7"><b>Accomplishments:</b></td>    </tr>    <tr>    <td width="20"></td>    <td colspan="7"><TEXTAREA NAME="accomplishments_{$smarty.section.idx.index}"    ROWS=4 COLS=95></TEXTAREA><br><br>    </td>    </tr>    {/strip}{/section}    </table>    <br><hr>    <input type="hidden" name="action" value="persist_contact">    <br><br>    <center>    <input type="submit" name="submit" value=" Save " onclick="return    checkInputs(this.form);">    </center>    </form>    {include file="footer.tpl"} 

Note that the main loop, {section name=idx loop=$max_weekly_contacts}, iterates and creates unique names of inputs: company_name_{$smarty.section.idx.index}, which are then operated on.

Smarty offers the handy feature of populating drop-down boxes based on an array of dates for the current week:

   <SELECT NAME="week_start">{html_options values=$start_weeks output=$start_weeks    selected=$current_start_week}</SELECT> 

You may have noticed that the input screen is rather Spartan in appearance because it uses regular HTML rather than something stricter with better cross-platform browser compatibility such as XHTML and CSS. This is because at this point in the process you're more interested in getting the system to work, because the definition may change slightly in the near future. Later in the process, the display can be spruced up.

Feeding the Beast

Even though ContactVisits know how to persist themselves, it takes a surprising amount of work to gather together several weeks' worth of drop-down information and then save all contacts to the database. Here is the required functionality, simply placed in functions in customer-contacts .php:

   <?php    require_once ("Smarty.class.php");    require_once ("widgetsession.phpm");    $session = new WidgetSession(array ('phptype'  => "pgsql",                                        'hostspec' => "localhost",                                        'database' => "widgetworld",                                        'username' => "uwuser",                                        'password' => "foobar"));    $session->Impress();    $smarty = new Smarty;    $GLOBALS["max-weekly-contacts"] = 5;    function getStartDateOffset ($i) {        if($i<0)$i=5;        $dates = array("Sunday" => 0, "Monday" => -1, "Tuesday" => -2,    "Wednesday" => -3, "Thursday" => -4, "Friday" => -5, "Saturday" => -6);        return date("Y-m-d", mktime (0,0,0,date("m"), date("d")+$dates[date("l")]-    (($i-5)*7),date("Y")));    }    function getCurrentStartWeek () {        if (strlen($_REQUEST["week_start"]) >= 8) return $_REQUEST["week_start"];        return getStartDateOffset(-1); // this sunday    }    function getStartWeeks () {        $sudayArray = array();        for ($i=20; $i > 0; $i--) {            array_push($sudayArray, getStartDateOffset($i));        }        return ($sudayArray);    }    function persistContactVisits (&$dbh, $emp_id) {        $dbh->query("delete from contact_visits where emp_id = ".$emp_id." and    week_start = '".getCurrentStartWeek()."'");        $seq = 0;        for ($i = 0; $i < $GLOBALS["max-weekly-contacts"]; $i++) {             $cv = new ContactVisit (                 array ("emp_id"            => $emp_id,                        "week_start"        => getCurrentStartWeek(),                        "company_name"      => $_REQUEST["company_name_".$i],                        "contact_name"      => $_REQUEST["contact_name_".$i],                        "city"              => $_REQUEST["city_".$i],                        "state"             => $_REQUEST["state_".$i],                        "accomplishments"   => $_REQUEST["accomplishments_".$i],                        "followup"          => $_REQUEST["followup_".$i],                        "literature_request"=> $_REQUEST["literature_request_".$i]),                  $dbh);              $cv->persist();        }    }    $user = $session->getUserObject();    // display    if ($_REQUEST["action"] != "persist_contact") {        $smarty->assign_by_ref ("user", $user);        $smarty->assign('start_weeks', getStartWeeks());        $smarty->assign('current_start_week', getCurrentStartWeek());        $smarty->assign("max_weekly_contacts", $GLOBALS["max-weekly-contacts"]);        $smarty->display('customer-contacts.tpl');        exit();    }    // persist contact visits    require_once ("contact.phpm");    persistContactVisits ($session->getDatabaseHandle(), $user->id);    $smarty->display('thankyou.tpl');    ?> 

The interesting features of this begin with getStartDateOffset(), which populates the date drop-down by returning the day of the start week based on an integer offset passed in.

   function getStartDateOffset ($i) {        if($i<0)$i=5;        $dates = array("Sunday" => 0, "Monday" => -1, "Tuesday" => -2,    "Wednesday" => -3, "Thursday" => -4, "Friday" => -5, "Saturday" => -6);        return date("Y-m-d", mktime (0,0,0,date("m"), date("d")+$dates[date("l")]-    (($i-5)*7),date("Y")));    } 

It works by associating the weekday (say, "Tuesday''), retrieved by the use of date("l") to access the $dates array, which gets you an integer representing the number of days away from the target day of Sunday. When you know the date of the closest Sunday, the Sunday + week offset is calculated by multiplying the closest Sunday date by 7 and then adding those days to the current Sunday. Thus, regardless of the current day of the week, a Sunday +/- $i weeks is returned.

But because getStartWeeks() returns an array of 20 Sundays by counting from 20 to 0, getStarteDateOffset() also automatically adds 5 weeks to the default. This results in the week drop-down selection box's having 15 past weeks, the current Sunday and 4 weeks in the future.

The heavy lifting is done by persistContactVisits(), which removes the old contacts, creates new ContactVisits, and calls persist() on each one in order to save the results in the database.

Figure 22-6 shows the features in action.


Figure 22-6

So now, with two stories under your belt, you can take a decadent 10-minute break before heading off to get Wendy, Wade, Edwina, and Harold using the application. You'll need the rest.

Re-Estimation

The good news is that Wendy, Wade, Harold, and Edwina love the application. But keep in mind that they are human and have brought up some quite reasonable requests:

Doing some quick estimations, you figure the new tasks as:

The original plan:

Considering that the half-pointers don't really count but the new ones do, you discuss it with your clients and determine that they'd like prefer it in this order:

They'd also prefer to get the spreadsheet in the next iteration before requiring the notifications, so these can be dropped for now:

Now back to work.



Professional PHP5 (Programmer to Programmer Series)
Professional PHP5 (Programmer to Programmer Series)
ISBN: N/A
EAN: N/A
Year: 2003
Pages: 182
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net