Emmanuel BENOîT
2051303262
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.
438 lines
12 KiB
PHP
438 lines
12 KiB
PHP
<?php
|
|
|
|
class View_TasksList
|
|
extends BaseURLAwareView
|
|
{
|
|
protected $tasks;
|
|
protected $dao;
|
|
|
|
|
|
public function __construct( $tasks , $features = array( 'item' , 'assigned' , 'deps' , 'completed' ) )
|
|
{
|
|
$this->tasks = $tasks;
|
|
$this->features = array_combine( $features , array_fill( 0 , count( $features ) , 1 ) );
|
|
$this->dao = Loader::DAO( 'tasks' );
|
|
}
|
|
|
|
|
|
public final function render( )
|
|
{
|
|
if ( empty( $this->tasks ) ) {
|
|
return HTML::make( 'div' )
|
|
->setAttribute( 'class' , 'no-table' )
|
|
->appendText( 'No tasks to display.' );
|
|
}
|
|
return HTML::make( 'dl' )
|
|
->append( $this->generateList( ) )
|
|
->setAttribute( 'class' , 'tasks' );
|
|
}
|
|
|
|
|
|
private function generateList( )
|
|
{
|
|
$result = array( );
|
|
$prevPriority = 6;
|
|
foreach ( $this->tasks as $task ) {
|
|
$priority = ( $task->completed_by === null ) ? $task->priority : -1;
|
|
if ( $priority !== $prevPriority ) {
|
|
if ( $priority == -1 ) {
|
|
$text = 'Completed tasks';
|
|
$extraClass = ' completed';
|
|
} else {
|
|
$text = $this->dao->translatePriority( $priority ) . ' priority';
|
|
$extraClass = '';
|
|
}
|
|
$prevPriority = $priority;
|
|
|
|
array_push( $result , HTML::make( 'dt' )
|
|
->setAttribute( 'class' , 'sub-title' . $extraClass )
|
|
->appendText( $text ) );
|
|
}
|
|
$result = array_merge( $result , $this->generateItem( $task ) );
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
protected function generateItem( $task )
|
|
{
|
|
$cell = array( );
|
|
array_push( $cell , HTML::make( 'dt' )
|
|
->appendElement( HTML::make( 'a' )
|
|
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $task->id )
|
|
->appendText( $task->title ) ) );
|
|
$this->addParent( $cell , $task );
|
|
$classes = array( );
|
|
|
|
$addedAt = strtotime( $task->added_at );
|
|
$addedAtDate = date( 'd/m/o' , $addedAt );
|
|
$addedAtTime = date( 'H:i:s' , $addedAt );
|
|
array_push( $cell ,
|
|
HTML::make( 'dd' )->appendText( "Added $addedAtDate at $addedAtTime by {$task->added_by}" ) );
|
|
|
|
if ( $task->completed_by !== null ) {
|
|
$this->generateCompletedTask( $cell , $classes , $task );
|
|
} else {
|
|
if ( $task->unsatisfied_direct_dependencies > 0 ) {
|
|
$this->generateMissingDependencies( $cell , $classes , $task );
|
|
}
|
|
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 );
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $classes ) ) {
|
|
foreach ( $cell as $entry ) {
|
|
$entry->setAttribute( 'class' , join( ' ' , array_unique( $classes ) ) );
|
|
}
|
|
}
|
|
|
|
return $cell;
|
|
}
|
|
|
|
protected function addParent( &$cell , $task )
|
|
{
|
|
if ( ! array_key_exists( 'item' , $this->features ) ) {
|
|
return;
|
|
}
|
|
|
|
$this->addItem( $cell , $task );
|
|
if ( $task->parent_task !== null ) {
|
|
$this->addParentTask( $cell , $task );
|
|
}
|
|
}
|
|
|
|
protected function addItem( &$cell , $task )
|
|
{
|
|
$itemsDao = Loader::DAO( 'items' );
|
|
$item = $itemsDao->get( $task->item );
|
|
$lineage = $itemsDao->getLineage( $item );
|
|
array_push( $lineage , $item->id );
|
|
|
|
$contents = array( );
|
|
foreach ( Loader::DAO( 'items' )->getAll( $lineage ) as $ancestor ) {
|
|
if ( ! empty( $contents ) ) {
|
|
array_push( $contents , ' » ' );
|
|
}
|
|
array_push( $contents , HTML::make( 'a' )
|
|
->setAttribute( 'href' , $this->base . '/items/view?id=' . $ancestor->id )
|
|
->appendText( $ancestor->name ) );
|
|
}
|
|
array_unshift( $contents, 'On ' );
|
|
|
|
array_push( $cell , HTML::make( 'dd' )->append( $contents ) );
|
|
}
|
|
|
|
protected function addParentTask( &$cell , $task )
|
|
{
|
|
$parents = $this->dao->getLineage( $task );
|
|
$contents = array( );
|
|
foreach ( $parents as $parent ) {
|
|
list( $id , $title ) = $parent;
|
|
if ( ! empty( $contents ) ) {
|
|
array_push( $contents , ' » ' );
|
|
}
|
|
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 ' )
|
|
->append( $contents ) );
|
|
}
|
|
|
|
protected function generateMissingDependencies( &$cell , &$classes , $task )
|
|
{
|
|
if ( ! array_key_exists( 'deps' , $this->features ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( $task->unsatisfied_direct_dependencies > 1 ) {
|
|
$end = 'ies';
|
|
} else {
|
|
$end = 'y';
|
|
}
|
|
array_push( $cell ,
|
|
$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' );
|
|
}
|
|
|
|
protected function generateMissingSubtasks( &$cell , &$classes , $task )
|
|
{
|
|
if ( ! array_key_exists( 'deps' , $this->features ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( $task->incomplete_subtasks > 1 ) {
|
|
$end = 's';
|
|
} else {
|
|
$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' );
|
|
}
|
|
|
|
protected function generateAssignedTask( &$cell , &$classes , $task )
|
|
{
|
|
if ( ! array_key_exists( 'assigned' , $this->features ) ) {
|
|
return;
|
|
}
|
|
|
|
array_push( $cell , HTML::make( 'dd' )->appendText( 'Assigned to ' . $task->assigned_to ) );
|
|
array_push( $classes , 'assigned' );
|
|
}
|
|
|
|
protected function generateCompletedTask( &$cell , &$classes , $task )
|
|
{
|
|
if ( ! array_key_exists( 'completed' , $this->features ) ) {
|
|
return;
|
|
}
|
|
|
|
$completedAt = strtotime( $task->completed_at );
|
|
$completedAtDate = date( 'd/m/o' , $completedAt );
|
|
$completedAtTime = date( 'H:i:s' , $completedAt );
|
|
array_push( $cell , HTML::make( 'dd' )->appendText(
|
|
"Completed $completedAtDate at $completedAtTime by {$task->completed_by}" ) );
|
|
array_push( $classes , 'completed' );
|
|
}
|
|
}
|
|
|
|
|
|
class View_TaskDetails
|
|
extends BaseURLAwareView
|
|
{
|
|
private $task;
|
|
|
|
public function __construct( $task )
|
|
{
|
|
$this->task = $task;
|
|
}
|
|
|
|
public function render( )
|
|
{
|
|
$list = HTML::make( 'dl' )
|
|
->setAttribute( 'class' , 'tasks' )
|
|
->appendElement( HTML::make( 'dt' )
|
|
->appendText( 'On item:' ) )
|
|
->appendElement( HTML::make( 'dd' )
|
|
->append( $this->formatPlaceLineage( $this->task->item ) ) );
|
|
|
|
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 , ' » ' );
|
|
}
|
|
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' )
|
|
->append( $contents ) );
|
|
}
|
|
|
|
if ( $this->task->description != '' ) {
|
|
$list->appendElement( HTML::make( 'dt' )
|
|
->appendText( 'Description:' ) )
|
|
->appendElement( HTML::make( 'dd' )
|
|
->appendRaw( $this->formatDescription( ) ) );
|
|
}
|
|
|
|
$list->appendElement( HTML::make( 'dt' )
|
|
->appendText( 'Added:' ) )
|
|
->appendElement( HTML::make( 'dd' )
|
|
->appendText( $this->formatAction( $this->task->added_at , $this->task->added_by ) ) );
|
|
|
|
if ( $this->task->completed_by === null ) {
|
|
$list->appendElement( HTML::make( 'dt' )
|
|
->appendText( 'Priority:' ) )
|
|
->appendElement( HTML::make( 'dd' )
|
|
->appendText( Loader::DAO( 'tasks' )
|
|
->translatePriority( $this->task->priority ) ) );
|
|
|
|
if ( $this->task->assigned_to === null ) {
|
|
$list->appendElement( HTML::make( 'dt' )
|
|
->setAttribute( 'class' , 'unassigned-task' )
|
|
->appendText( 'Unassigned!' ) );
|
|
} else {
|
|
$list->appendElement( HTML::make( 'dt' )
|
|
->appendText( 'Assigned to:' ) )
|
|
->appendElement( HTML::make( 'dd' )
|
|
->appendText( $this->task->assigned_to ) );
|
|
}
|
|
} else {
|
|
$list->appendElement( HTML::make( 'dt' )
|
|
->appendText( 'Completed:' ) )
|
|
->appendElement( HTML::make( 'dd' )
|
|
->appendText( $this->formatAction(
|
|
$this->task->completed_at , $this->task->completed_by ) ) );
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
private function formatPlaceLineage( $item )
|
|
{
|
|
$lineage = $item->lineage;
|
|
array_push( $lineage , $item->id );
|
|
|
|
$contents = array( );
|
|
foreach ( Loader::DAO( 'items' )->getAll( $lineage ) as $ancestor ) {
|
|
if ( ! empty( $contents ) ) {
|
|
array_push( $contents , ' » ' );
|
|
}
|
|
array_push( $contents , HTML::make( 'a' )
|
|
->setAttribute( 'href' , $this->base . '/items/view?id=' . $ancestor->id )
|
|
->appendText( $ancestor->name ) );
|
|
}
|
|
|
|
return $contents;
|
|
}
|
|
|
|
|
|
private function formatDescription( )
|
|
{
|
|
$description = HTML::from( $this->task->description );
|
|
return preg_replace( '/\n/s' , '<br/>' , $description );
|
|
}
|
|
|
|
|
|
private function formatAction( $timestamp , $user )
|
|
{
|
|
$ts = strtotime( $timestamp );
|
|
$tsDate = date( 'd/m/o' , $ts );
|
|
$tsTime = date( 'H:i:s' , $ts );
|
|
return "$tsDate at $tsTime by $user";
|
|
}
|
|
}
|
|
|
|
|
|
class View_TaskNote
|
|
implements View
|
|
{
|
|
private $note;
|
|
|
|
public function __construct( $note )
|
|
{
|
|
$this->note = $note;
|
|
}
|
|
|
|
public function render( )
|
|
{
|
|
$text = HTML::make( 'p' )
|
|
->appendRaw( preg_replace( '/\n/s' , '<br/>' , HTML::from( $this->note->text ) ) );
|
|
|
|
$ts = strtotime( $this->note->added_at );
|
|
$tsDate = date( 'd/m/o' , $ts );
|
|
$tsTime = date( 'H:i:s' , $ts );
|
|
$details = HTML::make( 'div')
|
|
->setAttribute( 'style' , 'font-size: 9pt' )
|
|
->appendElement( HTML::make( 'em' )
|
|
->appendText( "Note added $tsDate at $tsTime by {$this->note->author}" ) );
|
|
|
|
return array( $text , $details );
|
|
}
|
|
}
|
|
|
|
|
|
class View_TaskDependencies
|
|
extends BaseURLAwareView
|
|
{
|
|
private $task;
|
|
private $reverse;
|
|
|
|
public function __construct( $task , $reverse )
|
|
{
|
|
$this->task = $task;
|
|
$this->reverse = $reverse;
|
|
}
|
|
|
|
public function render( )
|
|
{
|
|
$source = $this->reverse ? 'reverseDependencies' : 'dependencies';
|
|
if ( empty( $this->task->$source ) ) {
|
|
return HTML::make( 'div' )
|
|
->setAttribute( 'class' , 'no-table' )
|
|
->appendText( 'This task has no dependencies.' );
|
|
}
|
|
|
|
$list = HTML::make( 'ul' )->setAttribute( 'class' , 'dep-list' );
|
|
$prevItem = null;
|
|
$itemList = null;
|
|
foreach ( $this->task->$source as $dependency ) {
|
|
if ( $prevItem !== $dependency->item ) {
|
|
$itemList = HTML::make( 'ul' );
|
|
$list->appendElement( HTML::make( 'li' )
|
|
->appendText( 'In ' )
|
|
->appendElement( HTML::make( 'a' )
|
|
->setAttribute( 'href' , $this->base . '/items/view?id=' . $dependency->item )
|
|
->appendText( $dependency->item_name ) )
|
|
->appendElement( $itemList ) );
|
|
$prevItem = $dependency->item;
|
|
} elseif ( $itemList === null ) {
|
|
$itemList = $list;
|
|
}
|
|
|
|
$entry = HTML::make( 'li' )->appendElement(
|
|
$link = HTML::make( 'a' )
|
|
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $dependency->id )
|
|
->appendText( $dependency->title ) );
|
|
if ( ! $this->reverse ) {
|
|
$link->setAttribute( 'class' , ( $dependency->completed == 't' )
|
|
? 'satisfied' : 'missing' );
|
|
|
|
if ( $this->task->completed_at === null ) {
|
|
$entry->appendText( ' (' )
|
|
->appendElement( HTML::make( 'a' )
|
|
->setAttribute( 'href' , $this->base . '/tasks/deps/delete?from='
|
|
. $this->task->id . '&to=' . $dependency->id )
|
|
->appendText( 'remove') )
|
|
->appendText( ')' );
|
|
if ( $dependency->missing_dependencies != 0 ) {
|
|
$end = $dependency->missing_dependencies > 1 ? 'ies' : 'y';
|
|
$entry->appendElement( HTML::make( 'ul' )
|
|
->appendElement( $mdeps = HTML::make( 'li' ) ) );
|
|
$mdeps->appendText( $dependency->missing_dependencies
|
|
. " missing dependenc$end (transitively)" );
|
|
}
|
|
}
|
|
}
|
|
|
|
$itemList->appendElement( $entry );
|
|
}
|
|
return $list;
|
|
}
|
|
}
|