Create an interactive production wiki using PHP, Part 4: Task management

Customizing the controller and modifying the views

This "Create an interactive production wiki using PHP" tutorial series creates a wiki from scratch using PHP, with value-added features useful for tracking production. Wikis are widely used as tools to help speed development, increase productivity and educate others. Each part of the series develops integral parts of the wiki until it is complete and ready for prime time, with features including file uploading, a calendaring "milestone" system, and an open blog. The wiki will also contain projects whose permissions are customizable to certain users and will contain projects whose permissions are customizable to certain users. In Part 3, we added some control over who can do what. Now it's time to add some task management.

Duane O'Brien, PHP developer, 自由职业者

Duane O'Brien has been a technological Swiss Army knife since the Oregon Trail was text only. His favorite color is sushi. He has never been to the moon.



03 April 2007

Also available in

Before you start

This "Create an interactive production wiki using PHP" series is designed for PHP application developers who want to to take a run at making their own custom wikis. You'll define everything about the application, from the database all the way up to the wiki markup you want to use. In the final product, you will be able to configure much of the application at a granular level, from who can edit pages to how open the blog really is.

At the end of this tutorial, Part 4 of a five-part series, you will have basic task management functionality working in your wiki, including assigning tasks, viewing tasks, and marking task progress.

About this series

Part 1 of this series draws the big picture. You determine how you want the application to look, flow, work, and behave. You'll design the database and rough-out some scaffolding. Part 2 focuses on the primary wiki development, including defining the markup, tracking changes, and file uploads. In Part 3, you define some users and groups, as well as a way to control access to certain aspects of individual wiki pages and uploaded files. Part 4 deals with a Calendaring and Milestones feature to track tasks, to-dos, and progress against set goals. And in Part 5, you put together an open blog to allow discussion of production topics and concerns.

About this tutorial

This tutorial deals mainly with task management. Criki (your new wiki engine) has all the basic wiki features you need, but it still lacks those features that will make it useful as a tool to assist in production. When it comes to production, task management stands at the top of the needed features list.

Covered topics include:

  • Tasks workflow design
  • Building out the tasks database table
  • Basic task management features

Prerequisites

It is assumed you have completed Part 1, Part 2, and Part 3 of this "Create an interactive production wiki using PHP" series. And it is assumed that you have some experience working with the PHP programming language and MySQL. We won't be doing a lot of deep database tuning, so as long as you know the basic ins and outs, you should be fine.

System requirements

Before you begin, you need to have an environment in which you can work. The general requirements are reasonably minimal:

  • An HTTP server that supports sessions (and preferably mod_rewrite). This tutorial was written using Apache V1.3 with mod_rewrite enabled.
  • PHP V4.3.2 or later (including PHP V5). This was written using PHP V5.0.4
  • Any version of MySQL from the last few years will do. This was written using MySQL V4.1.15.

You'll also need a database and database user ready for your application to use. The tutorial will provide syntax for creating any necessary tables in MySQL.

Additionally, to save time, we will be developing Criki using a PHP framework called CakePHP. Download CakePHP by visiting CakeForge.org and downloading the latest stable version. This tutorial was written using V1.1.13. For information about installing and configuring CakePHP, check out the tutorial series titled "Cook up Web sites fast with CakePHP" (see Resources).

In addition, you may find it helpful to download and install phpMyAdmin, a browser-based administration console for your MySQL Database.


Criki so far

At the end of Part 3, you were given a few things to work on. You needed to add accesscControl to uploaded files. There was some cleanup work to be done in the controllers and the views. You should have experimented with using access checks to display or hide links and content. There were a couple problems with the access control system to be worked out. And you never defined any wiki markup for linking up uploaded files. How did you do?

Uploaded file access control

Defining permissions and access controls for file uploads should look much like it did for entries. In the uploads controller, you want to add code to the fetch action to verify the user's access level before serving the file.

Listing 1. Access control in the uploads fetch action
...
$upload = $this->Upload->read(null, $id);
if ($upload) {
  $user = $this->Session->read('User');
  $user = $this->Upload->User->read(null, $user['id']);
  if ($user['User']['access'] < $upload['Upload']['access']) {
    $this->Session->setFlash('Access Denied.');
    $this->redirect('/uploads/index');
    exit;
  }
  header('Content-Type: application/octet-stream');

...

You also need some promote/demote code added to the uploads controller to allow the user to protect the files. The promote and demote actions look almost identical to those you created for the entries controller. Rather than reproduce both here, just the promote action has been shown. Both actions are included in the code archive below.

Listing 2. Uploads controller promote action
function promote($id = null) {
  if ($this->Session->check('User')) {
    $user = $this->Session->read('User');
    if(!$id) {
      $this->Session->setFlash('Invalid id for Upload');
      $this->redirect('/uploads/index');
      exit;
    }
    $user = $this->Upload->User->read(null, $user['id']);
    if ($user['User']['access'] == 0) {
      $this->Session->setFlash('Contributors cannot promote.');
      $this->redirect('/uploads/view/'.$id);
      exit;
    }
    $subject = $this->Upload->findById($id);
    if ($user['User']['access'] > $subject['Upload']['access']) {
      $subject['Upload']['access'] += 4;
      $this->Upload->save($subject);
      $this->Session->setFlash('The Upload has been promoted');
      $this->redirect('/uploads/view/'.$id);
    } else {
$this->Session->setFlash('You cannot promote an Upload of equal or higher clearance');
      $this->redirect('/uploads/view/'.$id);
    }
  } else {
    $this->Session->setFlash('You must be logged in to perform this action');
    $this->redirect('/users/login');
  }
}

Finally, you'll need to modify the uploads views (index and view to show the appropriate promote/demote links. This will look much like it did for entries, with the exception that you are passing the upload id, not the title. For example, in the index view, the code to show the promote/demote links might look like Listing 3.

Listing 3. Promote/demote links in uploads index view
  $user_data = $session->read('User');
  if ($user_data['access'] > $upload['Upload']['access']) {
    echo $html->link('Promote','/uploads/promote/' . $upload['Upload']['id']);
    echo " ";
  }
  if ($user_data['access'] >= $upload['Upload']['access'] && $upload['Upload']['access'] 
!= 0) {
    echo $html->link('Demote','/uploads/demote/' . $upload['Upload']['id']);
  }

Remember: There's an outstanding task related to access control on revisions. We will address that shortly.

Controller and view cleanup (including enhanced access checks)

There were many unused actions and links across the controllers and views as a result of the initial baking you did and the shape Criki took. The changes are too numerous to spell out here. The updated code in the source code for this tutorial contains cleaned-up versions of all the controllers and views. If you have deviated heavily from the code as provided, you should make sure to go through the code archive to see what changes have been made.

Mainly, you just want to be sure that unused actions (like the delete action for the uploads controller) and links to the unused actions (like the link to the delete action in the uploads view view) have been removed. See the source code for details.

Included in the view updates are the enhanced access checks to show promote/demote links for entries and uploads in useful places, such as the related view views. The code for showing these links looks like it did when you wrote it for the index views. See the source code for details.

Access control issues

There were two main access control issues you needed to address: user access-level changes requiring a user login/logout to take effect (for proper view rendering), and access control across revisions.

Updating user access levels without login/logout

The solution to this particular problem is simple. Recall that when a user logs in, the user data is set into the session.

$this->Session->write('User', 
$this->User->findByUsername($this->data['User']['username']));

The simplest way to avoid forcing a login/logout for updated user permissions is to take advantage of the various access checks you perform in the controllers. Anytime you retrieve the user's information to verify access, write the information back into to session. For example, in the view action of the entries controller, the user information is retrieved to verify access levels.

Listing 4. Retrieving user data in the entries controller
...
if ($entry) {
  $user = $this->Session->read('User');
  $user = $this->Entry->User->read(null, $user['id']);
  if ($user['User']['access'] < $entry['Entry']['access']) {
    $this->Session->setFlash('Access Denied.');
    $this->redirect('/entries/index');
  }
...

This is a good opportunity to write the information back into session.

Listing 5. Amended entries user retrieval code
...
if ($entry) {
  $user = $this->Session->read('User');
  $user = $this->Entry->User->read(null, $user['id']);
  $this->Session->write('User', $user);
  if ($user['User']['access'] < $entry['Entry']['access']) {
    $this->Session->setFlash('Access Denied.');
    $this->redirect('/entries/index');
  }
...

If you really wanted to, you could just pull the user's information on every page load, but that adds an additional query to each page load, which isn't efficient.

Access control across revisions

When you promote or demote an entry or an upload, any associated revisions are not promoted or demoted. It may not be necessary to promote or demote revisions. But if you find it necessary to do so, you can do it in one of two ways: by taking advantage of the relationship to the revisions table and updating the revisions with the new access level (which is heavy-handed, but would probably be a little tighter in terms of security), or you can check the access level of the master record when viewing a revision and allow or deny access based on the access check. Neither method seems especially useful to Criki, as the revisions will retain the access level they held before revision. Editing an entry with an access level of 4 will create a revision with an access level of 4. This should be sufficient, provided you add basic access controls as outlined in the source code.

Wiki markup for uploaded files

There are many ways to deal with wiki markup for uploaded files. The cheapest, laziest, and easiest way is to make your users just provide links to uploaded files, like any other link.

[[[http://localhost/uploads/view/1|Uploaded File]]]

For that matter, if the user pasted the URL to the uploaded file without using wiki markup, it would still be rendered as a link.

That's a pretty cheap way to go about it, but it demonstrates an important concept: It's not always necessary to reinvent the wheel.

A couple other ways to address the problem would be to either use new markup to indicate that a file is being referenced (wrapping the filename in +++, for example) or adding handling to the link-managing markup allowing the user to specify that the link is to a file, not an entry title -- such as [[[file:FileName.txt]]]. Neither concept is written into Criki at this time. If you've got a great solution to the problem, feel free to incorporate it into the code from the source code.

You've filled in the gaps. It's time to sink your teeth into the next feature: task management.


Defining user tasks

Since Criki is intended to be a wiki used to track production, the next logical step in developing Criki would be to add the ability to assign tasks to users and for users to be able to keep an eye on their tasks. This will represent a whole new workflow that hasn't yet been thought through. Before you jump into getting a table together writing some code, spend some time thinking about how the tasks workflow will look.

Thinking through the tasks workflow

Before you can write a line of code, you need to get a clear image in your head for how the tasks workflow will look. Unless you know where you're going, you'll find it hard to get there, or even know when you've gotten there.

There are several questions that need to be addressed when thinking through the tasks workflow. What information will need to be tracked for a task? Who can assign tasks to whom? How will a user mark a task completed? How will a user see what tasks are assigned to him? Considering these questions will help the tasks workflow to take on a definite shape.

What information will need to be tracked for a task?

Before you can start thinking about how to assign or view tasks, you have a more important question to answer: What is a task? You might say, "A thing that needs to be done." And that's a good place to start. But you need to know what the thing looks like. You can do this by defining what information you're tracking.

For each individual task, there will be specific pieces of information that need to be tracked. You should strive to keep this information minimal, but make sure to cover the basics. Tasks should be somewhat loose and conceptual, not tight and restrictive. For example, while you might track such things as last modified, last viewed, number of views, etc., most of that information isn't especially useful to the user. The following basic pieces of information represent the basic necessary information for a task:

  1. Who has the task been assigned to?
  2. Who assigned the task?
  3. When is the task to be completed?
  4. What is the task?
  5. How much of the task has been completed?

It can also be helpful to include a title for the task, so that the task can be briefly described in a list. You might be able to argue that other information should be tracked, but this basic list should do for now.

Who can assign tasks to whom?

Now you know what a task is, and you probably are starting to think about what the a record in the tasks table will look like. You can start thinking about tasks and who can assign them.

In keeping with the open spirit of Criki, the tasks workflow will not be tightly controlled. The rules about who can assign tasks to whom are very simple:

  • A user must be logged in to assign a task.
  • A user cannot assign a task to someone with an access level higher than his own. For example, an editor cannot assign a task to an administrator, but an editor can assign a task to another editor.

The idea here, once again, is to leverage the basic trust at work within the structure of the wiki. Derive strength from the openness of the wiki. In some cases, this structure might not work for you. You may want tighter control over how tasks are assigned. After you get the basic tasks structure built, you can tweak it to suit your own needs.

How will a user's tasks be viewed?

You know what a task looks like, and you know who can assign them. As a user, now you need to be able to see your tasks.

When it comes to viewing tasks, there are several ways to display task information to the user. A list of tasks across users will be helpful when trying to determine which users have less to do. Viewing a list of tasks for a specific user will be helpful for the user and for anyone interested in the user's current workload. Of course, individual tasks should be viewable. And displaying the information in a calendar-style format will make it easy for a user to visualize what tasks are coming up when.

The task structure as it is being defined doesn't include things like setting task permissions or controlling access to individual tasks. It's not necessary to include it at this time, but it would be a good exercise for you to try adding permissions and access controls to tasks, since you learned all about that in Part 3.

How will a user mark a task completed?

You can now see your task. You've finished whatever the task called for. Now you need to be able to mark your task complete. A user should be able to update tasks, indicating how far along he has gotten in completing a task.

A user shouldn't be able to change anything else about a task -- not the description, the title, or the due date. As for completion, this will be tracked as a basic percentage, broken into quarters: 0, 25, 50, 75, 100 percent. Getting more granular than this in tracking the percentage of task completion probably isn't necessary. The more choices you give the user, the more time he will spend trying to determine which is most accurate. In most cases, it's not really important if a task is 20 or 25 percent done because both numbers translate into "Not yet half."

It might be easier to think of the percentage breakdowns as phrases: "Not started," "Just started," "Half done," "Mostly done," and "Completed." Breaking the percentages into coarse pieces will also pay off when it comes time to display the tasks in a calendar.

You have a pretty good idea at this point for how the tasks workflow will be put together. Now you can jump in and start putting it together.

Defining the tasks database

At this point, you should have a pretty good idea for what your tasks database is going to look like. You'll want an ID field set to auto_increment, as with other tables. You'll need a field to hold the ID of the user to whom the task has been assigned, as well as a field to hold the ID of the user who assigned the task. You also need something to hold the due date, a title, description and percentage complete. The SQL to create the table is shown below.

Listing 6. Tasks table SQL
CREATE TABLE 'tasks' (
 'id' int(10) NOT NULL auto_increment,
 'user_id' int(10) NOT NULL default '0',
 'assigned_id' int(10) NOT NULL default '0',
 'duedate' datetime NOT NULL default '0000-00-00 00:00:00',
 'title' varchar(255) NOT NULL default ,
 'description' text NOT NULL,
 'percent' enum('0','25','50','75','100') NOT NULL default '0',
 PRIMARY KEY  ('id')
) ENGINE=MyISAM DEFAULT CHARSET=latin1 ;

It's all pretty straightforward, with the possible exception of the user_id and assigned_id fields, which represent the user to whom the task has been assigned and the assigning user, respectively. You may have questions as to how to establish the necessary model relationships to get the data back the way you want it. That's good. It means you're paying attention.

Creating the task model

Now that you have the tasks table created, you need to write the model to get data from the table. But the task model won't look like your other models. All the models you have defined so far have either had no model associations or very simple ones. For example, consider the entry model below.

Listing 7. Entry model
<?php
class Entry extends AppModel {

        var $name = 'Entry'; 

        var $belongsTo = array('User' => array (
                'className' => 'User',
                'conditions' => ,
                'order' => ,
                'foreignKey' => 'user_id'
                )
        );
}
?>

This model has a simple belongsTo relationship defined to indicate that the user_id field in the entries table references a row in the users table. If you have debugging turned on in CakePHP, you might see a SQL statement like this when viewing an entry (formatted to easier reading), as shown below.

Listing 8. SQL statement to retrieve an entry
SELECT 
  'Entry'.'id', 
  'Entry'.'title', 
  'Entry'.'content', 
  'Entry'.'access', 
  'Entry'.'modified', 
  'Entry'.'user_id', 
  'Entry'.'ip', 
  'Entry'.'revision', 
  'Entry'.'accessed', 
  'User'.'id', 
  'User'.'username',
  'User'.'email', 
  'User'.'password', 
  'User'.'access', 
  'User'.'created', 
  'User'.'login' 
FROM 
   'entries' AS 'Entry' 
LEFT JOIN 
   'users' AS 'User' 
ON 
   'Entry'.'user_id' = 'User'.'id' 
WHERE 
   ('Entry'.'title' = 'foo') 
LIMIT 1

If you've worked with SQL much at all, you should be able to parse this statement fairly easily. It's a query on the entries table, which is joined to users by the user_id field.

But what you want for a task is to join tasks to the users table on two different fields: user_id (the user to whom the task has been assigned) and assigned_id (the user who assigned the task). You can probably write out the SQL statement just fine.

Listing 9. SQL statement to retrieve a task
SELECT 
  'Task'.'id', 
  'Task'.'user_id', 
  'Task'.'assigned_id', 
  'Task'.'title', 
  'Task'.'description', 
  'Task'.'duedate', 
  'Task'.'percent', 
  'User'.'id', 
  'User'.'username',
  'User'.'email', 
  'User'.'password', 
  'User'.'access', 
  'User'.'created', 
  'User'.'login', 
  'Assigned'.'id', 
  'Assigned'.'username',
  'Assigned'.'email', 
  'Assigned'.'password', 
  'Assigned'.'access', 
  'Assigned'.'created', 
  'Assigned'.'login'
FROM 
   'tasks' AS 'Task' 
LEFT JOIN 
   'users' AS 'User' 
ON 
   'Task'.'user_id' = 'User'.'id' 
LEFT JOIN
   'users' AS 'Assigned'     
ON 
   'Task'.'assigned_id' = 'Assigned'.'id' 
WHERE 
   ('Task'.'id' = 1) 
LIMIT 1

It's a long query, but it's not terribly complicated. And it may seem difficult to get the model to retrieve the data in this way. But it's actually very simple. When you define the belongsTo for the task model, you will define two tables, both of them users, but with different aliases. It should look like Listing 10.

Listing 10. Task model with belongsTo aliases
<?php
class Task extends AppModel {

       var $name = 'Task';

       var $belongsTo = array(
               'User' => array (
                       'className' => 'User',
                       'conditions' => ,
                       'order' => ,
                       'foreignKey' => 'user_id'
               ),
               'Assigned'=>array(
                       'className'=>'User',
                       'conditions' => ,
                       'order' => ,
                       'foreignKey' => 'assigned_id'

               )
       );

}
?>

You are basically just aliasing the user as assigned, and specifying the foreign key to be used. Easy as pie.

But you won't be able to see this in action until you get your controllers and views into place. You can short-circuit the process by using Bake, as you did before.

Baking the tasks controller and views

I hope you are somewhat accustomed to using Bake at this point. You used it in Part 1, and you've probably played with it on your own or followed the "Cook Up Web sites Fast with CakePHP tutorial series" (see Resources), which also walks you through using Bake. However, as a reminder, you need to make sure the PHP executable is in your PATH and that you have changed into the directory where you installed Cake.

To run bake, use php cake\scripts\bake.php. Walk through the menus specifying that you want to bake first a controller for the tasks table. When you have baked a controller, bake the views for tasks. If you need a refresher on the bake menus, consult Part 1 of this series.

Once you have the controller and views baked, turn the debugging up to at least 2 if you haven't already. This will show you the SQL involved in page rendering. In app/config/core.php, use define('DEBUG', 2);.

That should be it. Create a couple basic tasks and check out the SQL statement that's generated when you view them. It should look exactly like the one you wrote out above to join tasks to users and assigned (the users alias).

That gives you the basic controller and views. Now it's time to tweak them to better suit your needs.


Customizing the controller

Hang onto your hats. It's going to get bumpy.

The controller you baked should have had some basic actions in it to index, add, view, edit, and delete tasks. None of the actions are suitable for Criki as written, and some will be deleted altogether. Looking back at the workflow you've designed for tasks, you should be able to make a list of things to be done with the tasks controller:

  • The index action needs to accept filtering parameters. In this case, user_id and duedate.
  • The view action needs to set a variable to show or hide the edit link.
  • The add action needs heavy reworking to perform access checks and tweak or verify data before insertion.
  • The edit action needs to be changed to set the values of the percent-select list.
  • The delete action will go away completely.
  • A new action will be added called buildCalendar and will be used to generate an array with dates and associated tasks, for use in displaying a Calendar element.

Rather than go over this line by line, the highlights will be covered, leaving you on your own to dig into the source code if you want to get a closer look at all the changes. Even taking this approach of focusing on the highlights, there's a lot to cover.

Modifying the index action

The index action is baked to pull all tasks and format them for display. You need to change this action so you can use it to pull all the tasks for a specific user, or all the tasks for a specific user on a specific day.

For starters, you'll want to specify that the index action receives two parameters: $user_id and $duedate.

function index($user_id = null, $duedate = null) {

Now when you access http://localhost/tasks/USERID/DUEDATE, you pass in the value of USERID as $user_id and the value of DUEDATE as $duedate.

But passing the DUEDATE will be much easier to pass as a UNIX® timestamp rather than a date-formatted string. This will mean additional work, as the database will be expecting a date-formatted string. You will translate back and forth between the two.

The rest of the action is pretty straightforward. Since you want the index action to pull all tasks, all tasks for a user, or all tasks for a user on a date, you need to check to see what parameters have been passed, if any, and tailor your data retrieval accordingly. For details, consult the source code.

Modifying the view action

The view action doesn't need much modification. You just need to set a variable the view can use to show or hide an edit link for the task. The rule here is that a task can only be edited by the user to whom it has been assigned.

Listing 11. Setting the showedit variable
$showedit = false;
if ($this->Session->check('User')) {
  $user = $this->Session->read('User');
  if ($task['Task']['user_id'] == $user['id']) {
    $showedit = true;
  }
}
$this->set('showedit', $showedit);

The rest of the action is pretty much the same, though the view will be heavily edited.

Modifying the add action

The add action needs the most modification. You have more checking to do at this step, and some additional data massaging is also involved.

For starters, you'll need to make sure that the task is not being assigned in the past.

Listing 12. Deny adding past tasks
if ($this->data['Task']['duedate'] < strtotime('today') 
		&& $duedate < strtotime('today')) {
  $this->Session->setFlash('you cannot assign tasks in the past');
  $this->redirect('/tasks/index');
  exit;
}

It's important to call exit after the redirect, as redirect does not imply exit and the action could continue to execute.

Next, you need to make sure the target user exists and that the target user's access level is not higher than the assigning user.

Listing 13. Verify target user exists and access levels
if (!$target) {
  $this->Session->setFlash('User not found');
  $this->redirect('/tasks/index');
  exit;
} else if ($target['User']['access'] > $user['access']) {
  $this->Session->
  	setFlash('You cannot assign tasks to users with higher access than your own.');
  $this->redirect('/users/login');
  exit;
}

The last main point is to make sure you remember to set the 'assigned_id' value to be the ID of the user performing the action. By the time you get to that stage of the action, the logged-in user's information is in the variable $user. While you are setting that value, you should set the default percentage of completion to 0, and format the duedate.

$this->data['Task']['percent'] = '0';
$this->data['Task']['duedate'] = date('Y-m-d H:i:s', $this->data['Task']['duedate']);
$this->data['Task']['assigned_id'] = $user['id'];

The rest of the action should be fairly straightforward. Consult the source code for details.

Modifying the edit action

The edit action doesn't need much work (or does it?). Mainly, you need to set the $percents variable for the view to use when rendering the percent-select list.

$this->set('percents', array ('0' => '0%', '25' => '25%', '50' => '50%', 
	'75' => '75%', '100' => '100%'));

You'll be modifying the view later to only display the percent field for modification.

Adding the buildCalendar action

If you've been looking at the actions as defined in the tasks controller in the source code, you'll have seen references to a new action: buildCalendar. They look something like this:

$this->set('month', $this->buildCalendar($task['Task']['user_id'], 
	$task['Task']['duedate']));

Setting the output of the buildCalendar action to the month variable allows you to create something new: a Calendar element. This will allow you to put the Calendar wherever you like, without having to reproduce lots of code. It's a great time-saver.

This new action will be used to generate an array of dates to be displayed in a standard Calendar format. The buildCalendar action will need to accept two parameters: $user_id (the user for whom the Calendar is being displayed) and $date (the base date to be used when building the Calendar data).

This action begins by looking for tasks for the provided $user_id, building out an array of dates if there are tasks, and including the status (percentage complete) of the task.

Listing 14. Building a dates/tasks array
$dates = array();
if ($user_id) {
  $tasks = $this->Task->findAllByUserId($user_id);
  foreach ($tasks as $task) {
    $dates[strtotime($task['Task']['duedate'])][] = $task['Task']['percent'];
  }
}

Once this array is built, the base date needs to be evaluated. Since you are building a list of date information for use in displaying a Calendar, you want the first date to be a Sunday. Therefore, check to see what day the base date is and set base_date to the previous Sunday if the date is not a Sunday.

Listing 15. Tweaking the base date
$base_date = date('Y-m-d', strtotime($date));
$dow = date('w', strtotime($date));
if ($dow != 0) {
  $base_date = date('Y-m-d', strtotime($base_date . '- ' . $dow . ' days'));
}

The number of weeks to be shown has been hardcoded to three. This will allow you to include the Calendar on any page without taking up too much real estate, while still allowing a multiweek look at what needs to be done.

Listing 16. Initialize the $month variable
$month = array(
  '1' => array(),
  '2' => array(),
  '3' => array(),
);

Finally, you'll need to iterate through the days following the base_date, checking to see if there are tasks for that day. If there are tasks, update the status of that day to the least-completed task. You will use this information to color-code the Calendar.

Listing 17. Walking the dates
$n=0;
for ($i = 0; $i < 21; $i++) {
  $status = null;
  if ($i % 7 == 0) {
    $n++;
  }
  $date = strtotime($base_date . ' +' . $i . ' days');
  if ($dates && array_key_exists($date, $dates)) {
    foreach ($dates[$date] as $task) {
      if ($status == null) {
        $status = $task;
      } else {
        $status = $task < $status ? $task : $status;
      }
    }
  }
  $month[$n][] = array('date' => $date, 'status' => $status);
}

Last but not least, slide in the user data for the Calendar (so you don't have to call it up later) and return the data.

$month['user_data'] = $this->Task->User->findById($user_id);
return $month;

Phew! That's a lot of groundwork. But now that you have built the buildCalendar action and set its output to the month variable, you can create an element to take advantage of it.

Creating the Calendar element

In CakePHP, an element is simply a small block of presentation code you want to display in multiple places. A good example would be a list of menu items. Like other views, an element is just a PHP file that specifically outputs formatted data in HTML.

In Criki, you've laid the groundwork to use a Calendar element. In the controller, you made an action to create an array of data that the Calendar element can use. You've called the action a few times and assigned the results to the month variable. Any subsequent view or action should be able to then access the information via the $month variable.

The Calendar element template

Take a look at app/views/elements/calendar.thtml in the source code. The full text is too long to reproduce here, but most of it is HTML anyway. The important parts will be covered. For starters, look at the top of the element below.

Listing 18. Calendar element variable inits
<?php
  $caluser = $month['user_data'];
  unset($month['user_data']);
  $caluser_id = $caluser['User']['id'];
  $calusername = $caluser['User']['username'];
  $begin = current(current($month));
  $end = end(end($month));
  $base_month = date('M', $begin['date']);
  $base_day = date('d', $begin['date']);
  $base_year = date('o', $begin['date']);
  $last_month = date('M', $end['date']);
  $last_day = date('d', $end['date']);
  $last_year = date('o', $end['date']);
  $base_color = '#eeeeee';
  $colors = array (
    '0' => '#dd0000',
    '25' => '#ff9966',
    '50' => '#ffff00',
    '75' => '#ccff33',
    '100' => '#00cc66',
  );
?>

The file starts by pulling the user data out of the $month variable, setting some variables for use later. Then, the beginning and ending dates from the $month array are identified, so that the Calendar element can be labeled intelligently. Finally, some colors are defined, using the same coarse percentages you defined earlier as array keys. This will allow you to set color-coded backgrounds in the Calendar element, ranging from Red (0-percent complete) to Yellow (50-percent complete) to Green (100-percent complete).

The only other potentially sticky bit is setting the background color for a specific day. It's helpful to change the background colors slightly if the Calendar element spans multiple months, but mainly you want to show the background color that corresponds to the completion status of the tasks for the given day.

Listing 19. Setting Calendar day background colors
<?php
  foreach ($week as $day):
    if ($base_month != date('F', $day['date'])) :
      $base_month = date('F', $day['date']);
      $base_year = date('o', $day['date']);
      $base_color = $base_color == '#eeeeee' ? '#dddddd' : '#eeeeee';
    endif;
    if ($day['status'] != null) {
      $color = $colors[$day['status']];
    } else {
      $color = $base_color;
    }
    $bg_color = $color;
?>

The rest of the Calendar element should look like a regular view. The days themselves link to the tasks view for the user in question.

Now that you've got the Calendar element defined, you need to put it someplace where it can be used.

Modifying the default layout

The easiest way to include the Calendar element would be to conditionally include it in the default layout. You will recall that you modified app/views/layouts/default.thtml to customize the look and feel of Criki. By modifying this file further, you can include the Calendar element. You simply check to see if the $month variable has been defined. If it has, include the element as shown below.

Listing 20. Including the Calendar element
<?php if (isset($month)) : ?>
<div id="calendar">
<?php echo $this->renderElement('calendar', array("month" => $month)); ?>
</div><?php endif; ?>

If you have some tasks defined, and if you are using the code from the source code for this tutorial, you should be able to view the tasks for a user and see the Calendar element in action.

Figure 1. Calendar element in action
Calendar element in action

In order to get the element to fit into the layout, some modification was done to the default CSS file (app/webroot/css/cake.generic.css). If your layout doesn't look much like the screenshot, make sure you replaced your version of the default CSS file with the one from the source code.

Setting the Calendar data from the users controller

It would be especially helpful to see the Calendar element for a given user when viewing that user's profile. To do this, you will need to use modify the view action in the users controller to set the month variable using the requestAction method.

Listing 21. Modifying the view action in the users controller
function view($id = null) {
  if(!$id) {
    $this->Session->setFlash('Invalid id for User.');
    $this->redirect('/user/index');
  }
  $this->set('user', $this->User->read(null, $id));
  $this->set('month', 
  	$this->requestAction('/tasks/buildCalendar/'.$id.'/'.date('Y-m-d')));
  $this->set('user_id', $id);
}

The requestAction method is used by a controller to call an action on another controller. You'll remember that the buildCalendar action took in two parameters: the $user_id and the $date to be used as the base_date. Calling the action with requestAction should look familiar to you -- it's exactly like calling it via a URL.

Once you've modified the view action in the users controller, view any user, preferably one with tasks assigned to him. It should look like Figure 2.

Figure 2. User view with Calendar element
User view with Calendar element

You've got the Calendar element working, and you're putting it to good use. Now it's time to clean up those task views you baked earlier.


Customizing the views

You're almost there. Your controller should be in good shape. You've got a fancy Calendar element to show what tasks are coming up at a glance. Now you need to make some modifications to those views you baked earlier. Each view needs modified in some way. As with the controllers, rather than fully reproduce the code for each view, the primary changes will be highlighted.

Modifying the index view

The index view modifications are very straightforward. You'll want to show the user name for the user to whom the task has been assigned rather than the user's ID. The same goes for the user who assigned the task. You can omit displaying the full description of the task -- that's why you included the shorter title field.

Of particular interest may be the following:

Listing 22. Showing the edit link
<?php 
  if ($task['Task']['user_id'] == $user['id']) : 
    echo $html->link('Edit','/tasks/edit/' . $task['Task']['id']); 
  endif;
?>

Recall that the standing rule for editing a task is that only the user to whom the task has been assigned can edit the task, and even then, he can only change the percent-complete field. By checking in the view to see if the user_id for the task is the same is the ID of the logged-in user, you can determine if the edit link should be shown.

We see the whole view in the source code at app/views/tasks/index.thtml.

Modifying the add view

The add view doesn't need much modification at all. We want to trim back the fields the user can fill in, as fields like assigned_id should be set by the controller. As for the duedate field, the approach that has been taken is that it is passed to the initial request to load the add view, and the value is passed as a hidden field. In the view, the net result is a need to format the duedate.

<?php echo $form->labelTag('Task/duedate', 'Duedate');?>
<b><?php echo date('F d, Y', $task['Task']['duedate']) ?></b>
<?php echo $html->hidden('Task/duedate');?>

The idea behind this approach is to simplify the user experience by reducing the number of fields to be filled out. If you looked at the way the duedate field was rendered when baking the views, you'd have seen something like Figure 3.

Figure 3. Many fields in the add view
Many fields in the add view

Rather than having to modify all those fields, it should be much simpler to click on a date in a user's calendar and create a task for him. If your users dislike the approach, you can experiment with different approaches and find what works.

Modifying the edit view

The most important thing about modifying the edit view is to remove all editable fields except for the percent field. You shouldn't remove all the information. You can display the user name and the due date. Just don't provide the ability for the user to modify the fields. The only field a user should need to modify is percent.

Listing 23. Edit view percent field
<?php echo $form->labelTag('Task/percent', 'Percent');?>
<?php echo $html->selectTag('Task/percent', $percents, 
	$html->tagValue('Task/percent'), array(), array(), true);?>
<?php echo $html->tagErrorMsg('Task/percent', 'Please select the Percent.') ?>

Also of note in the edit view is some modification to the list of actions a user can perform. You have access to a lot of information in the edit and view views. You should use the information to provide convenient links (see Figure 4).

Figure 4. Edit view actions
Edit view actions

These same actions will be of particular use in the view view.

Modifying the view view

The view view shouldn't need much modification. Again, you'll want to show users' names, rather than their IDs. This is the only place that the full task description can be seen. Remember to leave it in. Mainly, you just want to modify the actions links to point to actions the user can perform. The links will look much like they did on the edit page.

Figure 5. View view actions
View view actions

That should wrap it up! The controller is doing its job. Your Calendar element is working overtime. You have the views looking like they should. Spend some time playing with task management. See if you can find ways to improve it.

Filling in the gaps

As usual, there are many ways in which Criki can still be improved. See what you can do with these tasks:

  • There's at least one glaring security hole in the task management edit workflow. Find it and fix it, as well as any others you recognize.
  • Wouldn't it be great to be able to use the wiki markup when writing task descriptions? Come up with a way to render wiki markup in task descriptions without rewriting the wiki markup code yet again. (Hint: Take a look at how the buildCalendar action is put together.)
  • In the tasks views from the source code, where user names are displayed as text, they could be displayed as links to the users controller to allow for easy viewing of a user's profile. What would that look like?

You are encouraged to get your fingers deep into the code. Find something you don't like? Think it should work differently? Think it's broken? Awesome. Fix it. It's the best way to learn.


Summary

Good task management should now be easy for everyone. Assigning a task to a user should be intuitive. As a user, task management should help the user keep track of priorities, rather than hindering by burdening with overly complicated workflows. The task management system you have put in place for Criki should meet these basic standards. Be sure to read our final tutorial in this "Create an interactive production wiki using PHP" series (Part 5), where we add an open blog to Criki to allow discussion of production topics and concerns.


Download

DescriptionNameSize
Part 4 source codeos-php-wiki4.source.zip26KB

Resources

Learn

Get products and technologies

  • Innovate your next open source development project with IBM trial software, available for download or on DVD.

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 Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=205131
ArticleTitle=Create an interactive production wiki using PHP, Part 4: Task management
publish-date=04032007