Emmanuel BENOîT
d28f5741fe
In addition to normal dependencies, the application now supports sub-tasks. Sub-tasks can be added into any existing task (including other sub-tasks, Inception-style). Dependencies can only be added between global tasks, or between sub-tasks of the same task. It is no longer possible to mark a task as finished if it has incomplete sub-tasks, and conversedly, it is not possible to reactivate a sub-task if its parent is marked as completed. A pair of buttons allowing tasks to be moved up and down in the task hierarachy have been added.
339 lines
8.7 KiB
PHP
339 lines
8.7 KiB
PHP
<?php
|
|
|
|
class DAO_Items
|
|
extends DAO
|
|
{
|
|
private $loaded = array( );
|
|
private $tree = null;
|
|
private $treeList = null;
|
|
|
|
private $activeTasksCounted = false;
|
|
|
|
|
|
public function createBefore( $name , $before , $description = '' )
|
|
{
|
|
$query = $this->query( 'SELECT insert_item_before( $1 , $2 , $3 ) AS error' );
|
|
$result = $query->execute( $name , $before , $description );
|
|
return $result[ 0 ]->error;
|
|
}
|
|
|
|
|
|
public function createUnder( $name , $under , $description = '' )
|
|
{
|
|
$query = $this->query( 'SELECT insert_item_under( $1 , $2 , $3 ) AS error' );
|
|
$result = $query->execute( $name , $under , $description );
|
|
return $result[ 0 ]->error;
|
|
}
|
|
|
|
|
|
public function createLast( $name , $description = '' )
|
|
{
|
|
$query = $this->query( 'SELECT insert_item_last( $1 , $2 ) AS error' );
|
|
$result = $query->execute( $name , $description );
|
|
return $result[ 0 ]->error;
|
|
}
|
|
|
|
|
|
public function get( $identifier )
|
|
{
|
|
$identifier = (int)$identifier;
|
|
if ( array_key_exists( $identifier , $this->loaded ) ) {
|
|
return $this->loaded[ $identifier ];
|
|
}
|
|
|
|
$getNameQuery = $this->query( 'SELECT item_name , item_description FROM items WHERE item_id = $1' , true );
|
|
$result = $getNameQuery->execute( $identifier );
|
|
if ( empty( $result ) ) {
|
|
$rObj = null;
|
|
} else {
|
|
$rObj = new Data_Item( $identifier , $result[ 0 ]->item_name );
|
|
$rObj->description = $result[ 0 ]->item_description;
|
|
}
|
|
$this->loaded[ $identifier ] = $rObj;
|
|
|
|
return $rObj;
|
|
}
|
|
|
|
|
|
public function getLineage( Data_Item $item )
|
|
{
|
|
if ( is_array( $item->lineage ) ) {
|
|
return $item->lineage;
|
|
}
|
|
|
|
$query = $this->query(
|
|
'SELECT p.item_id , p.item_name , p.item_description FROM items_tree pt '
|
|
. 'INNER JOIN items p '
|
|
. 'ON p.item_id = pt.item_id_parent '
|
|
. 'WHERE pt.item_id_child = $1 AND pt.pt_depth > 0 '
|
|
. 'ORDER BY pt.pt_depth DESC' );
|
|
$result = $query->execute( $item->id );
|
|
|
|
$stack = array( );
|
|
foreach ( $result as $entry ) {
|
|
if ( array_key_exists( $entry->item_id , $this->loaded ) ) {
|
|
$object = $this->loaded[ $entry->item_id ];
|
|
} else {
|
|
$object = new Data_Item( $entry->item_id , $entry->item_name );
|
|
$object->description = $entry->item_description;
|
|
$this->loaded[ $entry->item_id ] = $object;
|
|
}
|
|
$object->lineage = $stack;
|
|
array_push( $stack , $entry->item_id );
|
|
}
|
|
$item->lineage = $stack;
|
|
return $item->lineage;
|
|
}
|
|
|
|
|
|
private function loadTree( )
|
|
{
|
|
$query = $this->query(
|
|
'SELECT p.item_id , p.item_name , p.item_description , MAX( t.pt_depth ) AS depth '
|
|
. 'FROM items p '
|
|
. 'INNER JOIN items_tree t ON t.item_id_child = p.item_id '
|
|
. 'GROUP BY p.item_id, p.item_name , p.item_ordering '
|
|
. 'ORDER BY p.item_ordering' );
|
|
$result = $query->execute( );
|
|
|
|
$prevEntry = null;
|
|
$stack = array( );
|
|
$stackSize = 0;
|
|
$this->tree = array( );
|
|
$this->treeList = array( );
|
|
foreach ( $result as $entry ) {
|
|
if ( $entry->depth > $stackSize ) {
|
|
array_push( $stack , $prevEntry );
|
|
$stackSize ++;
|
|
} elseif ( $entry->depth < $stackSize ) {
|
|
while ( $stackSize > $entry->depth ) {
|
|
array_pop( $stack );
|
|
$stackSize --;
|
|
}
|
|
}
|
|
|
|
if ( array_key_exists( $entry->item_id , $this->loaded ) ) {
|
|
$object = $this->loaded[ $entry->item_id ];
|
|
} else {
|
|
$object = new Data_Item( $entry->item_id , $entry->item_name );
|
|
$object->description = $entry->item_description;
|
|
$this->loaded[ $entry->item_id ] = $object;
|
|
}
|
|
$object->children = array( );
|
|
$object->lineage = $stack;
|
|
if ( $object->depth = $entry->depth ) {
|
|
$object->hasParent = true;
|
|
$object->parent = $stack[ $stackSize - 1 ];
|
|
array_push( $this->loaded[ $object->parent ]->children , $object->id );
|
|
} else {
|
|
$object->hasParent = false;
|
|
}
|
|
|
|
$this->loaded[ $object->id ] = $object;
|
|
if ( $object->depth == 0 ) {
|
|
array_push( $this->tree , $object );
|
|
}
|
|
array_push( $this->treeList , $object );
|
|
$prevEntry = $object->id;
|
|
}
|
|
}
|
|
|
|
|
|
public function getTree( )
|
|
{
|
|
if ( $this->tree !== null ) {
|
|
return $this->tree;
|
|
}
|
|
$this->loadTree( );
|
|
return $this->tree;
|
|
}
|
|
|
|
|
|
public function getTreeList( )
|
|
{
|
|
if ( $this->tree !== null ) {
|
|
return $this->tree;
|
|
}
|
|
$this->loadTree( );
|
|
return $this->treeList;
|
|
}
|
|
|
|
|
|
public function getAll( $input )
|
|
{
|
|
$output = array( );
|
|
foreach ( $input as $id ) {
|
|
array_push( $output , $this->get( $id ) );
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
public function countActiveTasks( )
|
|
{
|
|
if ( $this->activeTasksCounted ) {
|
|
return;
|
|
}
|
|
|
|
$query = $this->query(
|
|
'SELECT p.item_id , p.item_name , p.item_description , COUNT(*) AS t_count '
|
|
. 'FROM items p '
|
|
. 'INNER JOIN task_containers USING ( item_id ) '
|
|
. 'INNER JOIN tasks t USING( tc_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' );
|
|
$results = $query->execute( );
|
|
|
|
foreach ( $results as $entry ) {
|
|
if ( array_key_exists( $entry->item_id , $this->loaded ) ) {
|
|
$object = $this->loaded[ $entry->item_id ];
|
|
} else {
|
|
$object = new Data_Item( $entry->item_id , $entry->item_name );
|
|
$object->description = $entry->item_description;
|
|
$this->loaded[ $entry->item_id ] = $object;
|
|
}
|
|
$object->activeTasks = $entry->t_count;
|
|
}
|
|
|
|
$this->activeTasksCounted = true;
|
|
}
|
|
|
|
|
|
private function checkActiveTasksIn( Data_Item $item )
|
|
{
|
|
if ( (int) $item->activeTasks > 0 ) {
|
|
return true;
|
|
}
|
|
|
|
foreach ( $this->getAll( $item->children ) as $child ) {
|
|
if ( $this->checkActiveTasksIn( $child ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
public function canDelete( Data_Item $item )
|
|
{
|
|
if ( $this->tree === null ) {
|
|
$this->loadTree( );
|
|
}
|
|
$this->countActiveTasks( );
|
|
return ! $this->checkActiveTasksIn( $item );
|
|
}
|
|
|
|
|
|
public function getMoveTargetsIn( $tree , $parent , $item )
|
|
{
|
|
$positions = array( );
|
|
$count = count( $tree );
|
|
$nameProblem = false;
|
|
for ( $i = 0 ; $i <= $count ; $i ++ ) {
|
|
// Completely skip the selected item and its children
|
|
if ( $i != $count && $tree[ $i ]->id == $item->id ) {
|
|
continue;
|
|
}
|
|
|
|
// Check for invalid positions (i.e. before/after selected item)
|
|
$invalidPos = ( $i > 0 && $tree[ $i - 1 ]->id == $item->id );
|
|
|
|
// Check for duplicate name
|
|
$nameProblem = $nameProblem || ( $i != $count && $tree[ $i ]->name == $item->name );
|
|
|
|
// Get children positions
|
|
if ( $i < $count ) {
|
|
$sub = $this->getMoveTargetsIn( $this->getAll( $tree[ $i ]->children ) , $tree[ $i ] , $item );
|
|
} else {
|
|
$sub = array( );
|
|
}
|
|
|
|
array_push( $positions , array(
|
|
'item' => ( $i < $count ) ? $tree[ $i ]->id : ( is_null( $parent ) ? null : $parent->id ) ,
|
|
'end' => ( $i == $count ) ? 1 : 0 ,
|
|
'valid' => ! $invalidPos ,
|
|
'sub' => $sub
|
|
) );
|
|
}
|
|
|
|
// Add all data to output array
|
|
$realPos = array( );
|
|
foreach ( $positions as $pos ) {
|
|
if ( $pos['valid'] && ! $nameProblem ) {
|
|
array_push( $realPos , $pos['end'] . ':' . $pos[ 'item' ] );
|
|
}
|
|
$realPos = array_merge( $realPos , $pos[ 'sub' ] );
|
|
}
|
|
|
|
return $realPos;
|
|
}
|
|
|
|
|
|
public function getMoveTargets( Data_Item $item )
|
|
{
|
|
//
|
|
// A destination is a (parent,position) couple, where the
|
|
// position corresponds to the item before which the selected
|
|
// item is to be moved.
|
|
//
|
|
// A destination is valid if:
|
|
// - there is no parent or the parent does not have the
|
|
// selected item in its lineage;
|
|
// - there is no item in the parent (or at the root if
|
|
// there is no parent) that uses the same name as the selected
|
|
// item, unless that item *is* the selected item;
|
|
// - the item at the specified position is not the selected
|
|
// item, or there is no item at the specified position;
|
|
// - the item before the specified position is not the
|
|
// selected item, or the specified position is 0.
|
|
//
|
|
|
|
$result = $this->getMoveTargetsIn( $this->getTree( ) , null , $item );
|
|
return $result;
|
|
}
|
|
|
|
public function canMove( Data_Item $item )
|
|
{
|
|
$result = $this->getMoveTargets( $item );
|
|
return ! empty( $result );
|
|
}
|
|
|
|
|
|
public function moveBefore( $item , $before )
|
|
{
|
|
$result = $this->query( 'SELECT move_item_before( $1 , $2 ) AS error' )
|
|
->execute( $item , $before );
|
|
return $result[ 0 ]->error;
|
|
}
|
|
|
|
|
|
public function moveUnder( $item , $under )
|
|
{
|
|
$result = $this->query( 'SELECT move_item_under( $1 , $2 ) AS error' )
|
|
->execute( $item , $under );
|
|
return $result[ 0 ]->error;
|
|
}
|
|
|
|
|
|
public function moveLast( $item )
|
|
{
|
|
$result = $this->query( 'SELECT move_item_last( $1 ) AS error' )
|
|
->execute( $item );
|
|
return $result[ 0 ]->error;
|
|
}
|
|
|
|
|
|
public function destroy( $item )
|
|
{
|
|
$this->query( 'SELECT delete_item( $1 )' )->execute( $item );
|
|
}
|
|
|
|
|
|
public function modify( $item , $name , $description = '' )
|
|
{
|
|
$result = $this->query( 'SELECT update_item( $1 , $2 , $3 ) AS error' )
|
|
->execute( $item , $name , $description );
|
|
return $result[ 0 ]->error;
|
|
}
|
|
}
|