Fixed sub-task handling

The previous implementation of sub-tasks did not work as expected: it
was possible to mark sub-tasks as completed before the parent task's
dependencies were satisfied. In addition, it was impossible to retrieve
a task's path from the database without running a recursive query.

Full paths to sub-tasks added to views, since it is now possible to
obtain them.
This commit is contained in:
Emmanuel BENOîT 2012-02-15 10:04:11 +01:00
parent 91ae4f81fd
commit 2051303262
13 changed files with 1023 additions and 224 deletions

View file

@ -175,10 +175,10 @@ class DAO_Items
}
$query = $this->query(
'SELECT p.item_id , p.item_name , p.item_description , COUNT(*) AS t_count '
'SELECT p.item_id , p.item_name , p.item_description , COUNT(*) AS t_count_all , '
. 'COUNT( NULLIF( t.task_id_parent IS NULL , FALSE ) ) AS t_count '
. 'FROM items p '
. 'INNER JOIN task_containers USING ( item_id ) '
. 'INNER JOIN tasks t USING( tc_id ) '
. 'INNER JOIN tasks t USING( item_id ) '
. 'LEFT OUTER JOIN completed_tasks c ON t.task_id = c.task_id '
. 'WHERE c.task_id IS NULL '
. 'GROUP BY item_id, p.item_name' );
@ -193,6 +193,7 @@ class DAO_Items
$this->loaded[ $entry->item_id ] = $object;
}
$object->activeTasks = $entry->t_count;
$object->activeTasksTotal = $entry->t_count_all;
}
$this->activeTasksCounted = true;

View file

@ -27,15 +27,14 @@ class DAO_Tasks
. 'priority '
. 'ELSE '
. '-1 '
. 'END ) DESC , total_missing_dependencies ASC NULLS FIRST , added_at DESC' )->execute( );
. 'END ) DESC , badness , added_at DESC' )->execute( );
}
public function getAllActiveTasks( )
{
return $this->query(
'SELECT * FROM tasks_list '
. 'WHERE completed_at IS NULL AND missing_dependencies IS NULL '
. 'AND missing_subtasks IS NULL '
. 'WHERE completed_at IS NULL AND badness = 0 '
. 'ORDER BY priority DESC , added_at DESC' )->execute( );
}
@ -43,9 +42,8 @@ class DAO_Tasks
{
return $this->query(
'SELECT * FROM tasks_list '
. 'WHERE completed_at IS NULL '
. 'AND ( missing_dependencies IS NOT NULL OR missing_subtasks IS NOT NULL ) '
. 'ORDER BY priority DESC , total_missing_dependencies ASC , added_at DESC' )->execute( );
. 'WHERE badness <> 0 '
. 'ORDER BY priority DESC , badness , added_at DESC' )->execute( );
}
@ -53,13 +51,13 @@ class DAO_Tasks
{
return $this->query(
'SELECT * FROM tasks_list '
. 'WHERE item = $1 '
. 'WHERE item = $1 AND parent_task IS NULL '
. 'ORDER BY ( CASE '
. 'WHEN completed_at IS NULL THEN '
. 'priority '
. 'ELSE '
. '-1 '
. 'END ) DESC , total_missing_dependencies ASC NULLS FIRST , added_at DESC'
. 'END ) DESC , badness , added_at DESC'
)->execute( $item->id );
}
@ -69,7 +67,7 @@ class DAO_Tasks
return $this->query(
'SELECT * FROM tasks_list '
. 'WHERE assigned_to_id = $1 '
. 'ORDER BY priority DESC , missing_dependencies ASC NULLS FIRST , added_at DESC'
. 'ORDER BY priority DESC , badness , added_at DESC'
)->execute( $user->user_id );
}
@ -113,48 +111,67 @@ class DAO_Tasks
. 'priority '
. 'ELSE '
. '-1 '
. 'END ) DESC , missing_dependencies ASC NULLS FIRST , added_at DESC'
. 'END ) DESC , badness , added_at DESC'
)->execute( $id );
$task->moveDownTargets = $this->query(
'SELECT * FROM tasks_move_down_targets '
. 'WHERE task_id = $1 '
. 'ORDER BY target_title' )->execute( $id );
$task->dependencies = $this->query(
'SELECT t.task_id AS id , t.task_title AS title , tc.item_id AS item , '
'SELECT t.task_id AS id , t.task_title AS title , t.item_id AS item , '
. 'i.item_name AS item_name , '
. '( ct.completed_task_time IS NOT NULL ) AS completed , '
. 'tl.total_missing_dependencies AS missing_dependencies '
. 'tl.badness AS missing_dependencies '
. 'FROM task_dependencies td '
. 'INNER JOIN tasks t ON t.task_id = td.task_id_depends '
. 'INNER JOIN task_containers tc USING ( tc_id ) '
. 'INNER JOIN tasks_list tl ON tl.id = t.task_id '
. 'LEFT OUTER JOIN items i USING ( item_id ) '
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
. 'WHERE td.task_id = $1 '
. 'ORDER BY i.item_name , t.task_priority DESC , t.task_title' )->execute( $id );
$task->reverseDependencies = $this->query(
'SELECT t.task_id AS id , t.task_title AS title , tc.item_id AS item , '
'SELECT t.task_id AS id , t.task_title AS title , t.item_id AS item , '
. 'i.item_name AS item_name , '
. '( ct.completed_task_time IS NOT NULL ) AS completed '
. 'FROM task_dependencies td '
. 'INNER JOIN tasks t USING( task_id ) '
. 'INNER JOIN task_containers tc USING ( tc_id ) '
. 'LEFT OUTER JOIN items i USING ( item_id ) '
. 'LEFT OUTER JOIN completed_tasks ct ON t.task_id = ct.task_id '
. 'WHERE td.task_id_depends = $1 '
. 'ORDER BY i.item_name , t.task_priority DESC , t.task_title' )->execute( $id );
$task->possibleDependencies = $this->query(
'SELECT t.task_id AS id , t.task_title AS title , tc.item_id AS item , '
'SELECT t.task_id AS id , t.task_title AS title , t.item_id AS item , '
. 'i.item_name AS item_name '
. 'FROM tasks_possible_dependencies( $1 ) t '
. 'INNER JOIN task_containers tc USING ( tc_id ) '
. 'LEFT OUTER JOIN items i USING ( item_id ) '
. 'ORDER BY i.item_name , t.task_priority , t.task_title' )->execute( $id );
$task->lineage = null;
return $task;
}
public function getLineage( $task )
{
if ( ! in_array( 'lineage' , get_object_vars( $task ) ) || $task->lineage === null ) {
$result = $this->query(
'SELECT task_id , task_title '
. 'FROM tasks_tree tt '
. 'INNER JOIN tasks '
. 'ON task_id = tt.task_id_parent '
. 'WHERE task_id_child = $1 AND tt_depth > 0 '
. 'ORDER BY tt_depth DESC'
)->execute( $task->id );
$task->lineage = array( );
foreach ( $result as $row ) {
array_push( $task->lineage , array( $row->task_id , $row->task_title ) );
}
}
return $task->lineage;
}
public function canDelete( $task )
{
if ( $task->completed_by !== null ) {
@ -169,17 +186,7 @@ class DAO_Tasks
public function canFinish( $task )
{
assert( $task->completed_at == null );
foreach ( $task->dependencies as $dependency ) {
if ( $dependency->completed != 't' ) {
return false;
}
}
foreach ( $task->subtasks as $subtask ) {
if ( $subtask->completed_at === null ) {
return false;
}
}
return true;
return ( $task->badness == 0 );
}

View file

@ -11,7 +11,8 @@ class Data_Item
public $depth;
public $lineage;
public $activeTasks;
public $activeTasks = 0;
public $activeTasksTotal = 0;
public $inactiveTasks;
public function __construct( $id , $name )

View file

@ -48,9 +48,12 @@ class View_ItemsTree
->appendElement( HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/items/view?id=' . $item->id )
->appendText( $item->name ) ) )
->appendElement( HTML::make( 'td' )
->appendElement( $tasks = HTML::make( 'td' )
->setAttribute( 'class' , 'align-right' )
->appendRaw( (int) $item->activeTasks ) ) );
->appendRaw( $item->activeTasks ) ) );
if ( $item->activeTasksTotal != $item->activeTasks ) {
$tasks->appendText( " ({$item->activeTasksTotal})" );
}
foreach ( $children as $child ) {
$this->renderItem( $table , $child );

View file

@ -98,16 +98,16 @@ class Ctrl_TaskDetails
$bTitle = "Active task";
}
if ( $this->task->item !== null ) {
$items = Loader::DAO( 'items' );
$items->getLineage( $this->task->item = $items->get( $this->task->item ) );
} else {
$this->task->parent_task = Loader::DAO( 'tasks' )->get( $this->task->parent_task );
$items = Loader::DAO( 'items' );
$tasks = Loader::DAO( 'tasks' );
$items->getLineage( $this->task->item = $items->get( $this->task->item ) );
if ( $this->task->parent_task !== null ) {
$this->task->parent_task = $tasks->get( $this->task->parent_task );
}
$box = Loader::View( 'box' , $bTitle , Loader::View( 'task_details' , $this->task ) );
$tasks = Loader::DAO( 'tasks' );
if ( $this->task->completed_by === null ) {
$box->addButton( BoxButton::create( 'Edit task' , 'tasks/edit?id=' . $this->task->id )
->setClass( 'icon edit' ) );
@ -138,7 +138,7 @@ class Ctrl_TaskDetails
$timestamp = strtotime( $this->task->completed_at );
}
if ( Loader::DAO( 'tasks' )->canDelete( $this->task ) ) {
if ( $tasks->canDelete( $this->task ) ) {
$box->addButton( BoxButton::create( 'Delete' , 'tasks/delete?id=' . $this->task->id )
->setClass( 'icon delete' ) );
}

View file

@ -287,9 +287,9 @@ class Ctrl_EditTaskForm
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
->setDefaultValue( $task->id ) )
->addField( Loader::Create( 'Field' , 'nested' , 'hidden' )
->setDefaultValue( $task->item === null ? 1 : 0 ) );
->setDefaultValue( $task->parent_task === null ? 0 : 1 ) );
if ( $task->item !== null ) {
if ( $task->parent_task === null ) {
$form->addField( $this->createItemSelector( )
->setDefaultValue( $task->item ) );
}
@ -457,7 +457,7 @@ class Ctrl_DependencyAddForm
$form = Loader::Create( 'Form' , 'Add dependency' , 'add-dep' )
->addField( Loader::Create( 'Field' , 'to' , 'hidden' )
->setDefaultValue( $id ) );
$this->addDependencySelector( $form , $task->possibleDependencies , $task->item !== null );
$this->addDependencySelector( $form , $task->possibleDependencies , $task->parent_task === null );
return $form->setURL( 'tasks/view?id=' . $id )
->addController( Loader::Ctrl( 'dependency_add' ) )
->controller( );

View file

@ -72,12 +72,15 @@ class View_TasksList
if ( $task->completed_by !== null ) {
$this->generateCompletedTask( $cell , $classes , $task );
} else {
if ( $task->missing_dependencies !== null ) {
if ( $task->unsatisfied_direct_dependencies > 0 ) {
$this->generateMissingDependencies( $cell , $classes , $task );
}
if ( $task->missing_subtasks !== null ) {
if ( $task->incomplete_subtasks > 0 ) {
$this->generateMissingSubtasks( $cell , $classes , $task );
}
if ( $task->unsatisfied_inherited_dependencies > 0 ) {
$this->generateMissingInherited( $cell , $classes , $task );
}
if ( $task->assigned_to !== null ) {
$this->generateAssignedTask( $cell , $classes , $task );
}
@ -98,9 +101,8 @@ class View_TasksList
return;
}
if ( $task->item !== null ) {
$this->addItem( $cell , $task );
} else {
$this->addItem( $cell , $task );
if ( $task->parent_task !== null ) {
$this->addParentTask( $cell , $task );
}
}
@ -128,13 +130,21 @@ class View_TasksList
protected function addParentTask( &$cell , $task )
{
$parent = $this->dao->get( $task->parent_task );
$parents = $this->dao->getLineage( $task );
$contents = array( );
foreach ( $parents as $parent ) {
list( $id , $title ) = $parent;
if ( ! empty( $contents ) ) {
array_push( $contents , ' &raquo; ' );
}
array_push( $contents , HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $id )
->appendText( $title ) );
}
array_push( $cell , HTML::make( 'dd' )
->appendText( 'Sub-task of ' )
->appendElement( HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $parent->id )
->appendText( $parent->title ) ) );
->append( $contents ) );
}
protected function generateMissingDependencies( &$cell , &$classes , $task )
@ -143,15 +153,15 @@ class View_TasksList
return;
}
if ( $task->missing_dependencies > 1 ) {
if ( $task->unsatisfied_direct_dependencies > 1 ) {
$end = 'ies';
} else {
$end = 'y';
}
array_push( $cell ,
$md = HTML::make( 'dd' )->appendText( "{$task->missing_dependencies} missing dependenc$end" ) );
if ( $task->total_missing_dependencies != $task->missing_dependencies ) {
$md->appendText( " ({$task->total_missing_dependencies} when counting transitive dependencies)" );
$md = HTML::make( 'dd' )->appendText( "{$task->unsatisfied_direct_dependencies} missing dependenc$end" ) );
if ( $task->unsatisfied_direct_dependencies != $task->unsatisfied_transitive_dependencies ) {
$md->appendText( " ({$task->unsatisfied_transitive_dependencies} when counting transitive dependencies)" );
}
array_push( $classes , 'missing-deps' );
@ -163,13 +173,30 @@ class View_TasksList
return;
}
if ( $task->missing_subtasks > 1 ) {
if ( $task->incomplete_subtasks > 1 ) {
$end = 's';
} else {
$end = '';
}
array_push( $cell ,
$md = HTML::make( 'dd' )->appendText( "{$task->missing_subtasks} incomplete sub-task$end" ) );
array_push( $cell , HTML::make( 'dd' )->appendText(
"{$task->incomplete_subtasks} incomplete sub-task$end (out of {$task->total_subtasks})" ) );
array_push( $classes , 'missing-deps' );
}
protected function generateMissingInherited( &$cell , &$classes , $task )
{
if ( ! array_key_exists( 'deps' , $this->features ) ) {
return;
}
if ( $task->unsatisfied_inherited_dependencies > 1 ) {
$end = 'ies';
} else {
$end = 'y';
}
array_push( $cell , HTML::make( 'dd' )->appendText(
"{$task->unsatisfied_inherited_dependencies} unsatisfied dependenc$end in parent task(s)" ) );
array_push( $classes , 'missing-deps' );
}
@ -213,21 +240,28 @@ class View_TaskDetails
public function render( )
{
$list = HTML::make( 'dl' )
->setAttribute( 'class' , 'tasks' );
->setAttribute( 'class' , 'tasks' )
->appendElement( HTML::make( 'dt' )
->appendText( 'On item:' ) )
->appendElement( HTML::make( 'dd' )
->append( $this->formatPlaceLineage( $this->task->item ) ) );
if ( $this->task->item !== null ) {
$list->appendElement( HTML::make( 'dt' )
->appendText( 'On item:' ) )
->appendElement( HTML::make( 'dd' )
->append( $this->formatPlaceLineage( $this->task->item ) ) );
} else {
if ( $this->task->parent_task !== null ) {
$parents = Loader::DAO( 'tasks' )->getLineage( $this->task );
$contents = array( );
foreach ( $parents as $parent ) {
list( $id , $title ) = $parent;
if ( ! empty( $contents ) ) {
array_push( $contents , ' &raquo; ' );
}
array_push( $contents , HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $id )
->appendText( $title ) );
}
$list->appendElement( HTML::make( 'dt' )
->appendText( 'Sub-task of:' ) )
->appendElement( HTML::make( 'dd' )
->appendElement( HTML::make( 'a' )
->setAttribute( 'href' , $this->base .
'/tasks/view?id=' . $this->task->parent_task->id )
->appendText( $this->task->parent_task->title ) ) );
->append( $contents ) );
}
if ( $this->task->description != '' ) {