Initial import of tasks application
This initial import is a heavily modified version of the code I had here, as Arse was modified for other purposes in the meantime and the application no longer worked with it. In addition: * I did not import the user management part yet, * task dependencies are supported in-base, but there is no interface for that yet.
This commit is contained in:
commit
9677ad4dd3
36 changed files with 3919 additions and 0 deletions
333
includes/t-data/dao_items.inc.php
Normal file
333
includes/t-data/dao_items.inc.php
Normal file
|
@ -0,0 +1,333 @@
|
|||
<?php
|
||||
|
||||
class DAO_Items
|
||||
extends DAO
|
||||
{
|
||||
private $loaded = array( );
|
||||
private $tree = null;
|
||||
private $treeList = null;
|
||||
|
||||
private $activeTasksCounted = false;
|
||||
|
||||
|
||||
public function createBefore( $name , $before )
|
||||
{
|
||||
$query = $this->query( 'SELECT insert_item_before( $1 , $2 ) AS error' );
|
||||
$result = $query->execute( $name , $before );
|
||||
return $result[ 0 ]->error;
|
||||
}
|
||||
|
||||
|
||||
public function createUnder( $name , $under )
|
||||
{
|
||||
$query = $this->query( 'SELECT insert_item_under( $1 , $2 ) AS error' );
|
||||
$result = $query->execute( $name , $under );
|
||||
return $result[ 0 ]->error;
|
||||
}
|
||||
|
||||
|
||||
public function createLast( $name )
|
||||
{
|
||||
$query = $this->query( 'SELECT insert_item_last( $1 ) AS error' );
|
||||
$result = $query->execute( $name );
|
||||
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 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 );
|
||||
}
|
||||
$this->loaded[ $identifier ] = $rObj;
|
||||
|
||||
return $rObj;
|
||||
}
|
||||
|
||||
|
||||
public function getLineage( Data_Item $item )
|
||||
{
|
||||
if ( is_array( $item->lineage ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $this->query(
|
||||
'SELECT p.item_id , p.item_name 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 );
|
||||
$this->loaded[ $entry->item_id ] = $object;
|
||||
}
|
||||
$object->lineage = $stack;
|
||||
array_push( $stack , $entry->item_id );
|
||||
}
|
||||
$item->lineage = $stack;
|
||||
}
|
||||
|
||||
|
||||
private function loadTree( )
|
||||
{
|
||||
$query = $this->query(
|
||||
'SELECT p.item_id , p.item_name , 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 );
|
||||
$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 , COUNT(*) AS t_count '
|
||||
. 'FROM items p '
|
||||
. 'INNER JOIN tasks t USING( item_id ) '
|
||||
. 'LEFT OUTER JOIN completed_tasks c USING( 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 );
|
||||
$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 rename( $item , $name )
|
||||
{
|
||||
$result = $this->query( 'SELECT rename_item( $1 , $2 ) AS error' )
|
||||
->execute( $item , $name );
|
||||
return $result[ 0 ]->error;
|
||||
}
|
||||
}
|
176
includes/t-data/dao_tasks.inc.php
Normal file
176
includes/t-data/dao_tasks.inc.php
Normal file
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
class DAO_Tasks
|
||||
extends DAO
|
||||
{
|
||||
private static $priorities = array(
|
||||
'1' => 'Lowest' ,
|
||||
'2' => 'Low' ,
|
||||
'3' => 'Normal' ,
|
||||
'4' => 'High' ,
|
||||
'5' => 'Very high' ,
|
||||
);
|
||||
|
||||
|
||||
public function translatePriority( $value )
|
||||
{
|
||||
return DAO_Tasks::$priorities[ "$value" ];
|
||||
}
|
||||
|
||||
|
||||
public function getAllTasks( )
|
||||
{
|
||||
return $this->query(
|
||||
'SELECT t.task_id AS id, t.item_id AS item, t.task_title AS title, '
|
||||
. 't.task_description AS description, t.task_added AS added_at, '
|
||||
. 'u1.user_email AS added_by, ct.completed_task_time AS completed_at, '
|
||||
. 'u2.user_email AS completed_by , t.task_priority AS priority '
|
||||
. 'FROM tasks t '
|
||||
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||
. 'LEFT OUTER JOIN users u2 ON u2.user_id = ct.user_id '
|
||||
. 'ORDER BY ( CASE WHEN ct.task_id IS NULL THEN t.task_priority ELSE -1 END ) DESC , '
|
||||
. 't.task_added DESC' )->execute( );
|
||||
}
|
||||
|
||||
public function getAllActiveTasks( )
|
||||
{
|
||||
return $this->query(
|
||||
'SELECT t.task_id AS id, t.item_id AS item, t.task_title AS title, '
|
||||
. 't.task_description AS description, t.task_added AS added_at, '
|
||||
. 'u1.user_email AS added_by, NULL AS completed_at, NULL AS completed_by , '
|
||||
. 't.task_priority AS priority '
|
||||
. 'FROM tasks t '
|
||||
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||
. 'WHERE ct.task_id IS NULL '
|
||||
. 'ORDER BY t.task_priority DESC , t.task_added DESC' )->execute( );
|
||||
}
|
||||
|
||||
|
||||
public function getTasksAt( Data_Item $item )
|
||||
{
|
||||
return $this->query(
|
||||
'SELECT t.task_id AS id, t.task_title AS title, '
|
||||
. 't.task_description AS description, t.task_added AS added_at, '
|
||||
. 'u1.user_email AS added_by, ct.completed_task_time AS completed_at, '
|
||||
. 'u2.user_email AS completed_by , t.task_priority AS priority '
|
||||
. 'FROM tasks t '
|
||||
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||
. 'LEFT OUTER JOIN users u2 ON u2.user_id = ct.user_id '
|
||||
. 'WHERE t.item_id = $1'
|
||||
. 'ORDER BY ( CASE WHEN ct.task_id IS NULL THEN t.task_priority ELSE -1 END ) DESC , '
|
||||
. 't.task_added DESC' )->execute( $item->id );
|
||||
}
|
||||
|
||||
|
||||
public function addTask( $item , $title , $priority , $description )
|
||||
{
|
||||
$result = $this->query( 'SELECT add_task( $1 , $2 , $3 , $4 , $5 ) AS error' )
|
||||
->execute( $item , $title , $description , $priority , $_SESSION[ 'uid' ] );
|
||||
return $result[0]->error;
|
||||
}
|
||||
|
||||
|
||||
public function get( $id )
|
||||
{
|
||||
$result = $this->query(
|
||||
'SELECT t.task_id AS id, t.task_title AS title, t.item_id AS item ,'
|
||||
. 't.task_description AS description, t.task_added AS added_at, '
|
||||
. 'u1.user_email AS added_by, ct.completed_task_time AS completed_at, '
|
||||
. 'u2.user_email AS completed_by, t.user_id AS uid , '
|
||||
. 't.task_priority AS priority '
|
||||
. 'FROM tasks t '
|
||||
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||
. 'LEFT OUTER JOIN users u2 ON u2.user_id = ct.user_id '
|
||||
. 'WHERE t.task_id = $1' )->execute( $id );
|
||||
if ( empty( $result ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$task = $result[ 0 ];
|
||||
$task->notes = $this->query(
|
||||
'SELECT n.note_id AS id , n.user_id AS uid , u.user_email AS author , '
|
||||
. 'n.note_added AS added_at , n.note_text AS "text" '
|
||||
. 'FROM notes n '
|
||||
. 'INNER JOIN users u USING (user_id) '
|
||||
. 'WHERE n.task_id = $1 '
|
||||
. 'ORDER BY n.note_added DESC' )->execute( $id );
|
||||
return $task;
|
||||
}
|
||||
|
||||
|
||||
public function canDelete( $task )
|
||||
{
|
||||
if ( $task->completed_by !== null ) {
|
||||
$ts = strtotime( $task->completed_at );
|
||||
return ( time() - $ts > 7 * 3600 * 24 );
|
||||
}
|
||||
$ts = strtotime( $task->added_at );
|
||||
return ( time() - $ts < 600 ) && ( $task->uid == $_SESSION[ 'uid' ] );
|
||||
}
|
||||
|
||||
|
||||
public function delete( $task )
|
||||
{
|
||||
$this->query( 'DELETE FROM tasks WHERE task_id = $1' )->execute( $task );
|
||||
}
|
||||
|
||||
|
||||
public function finish( $task , $noteText )
|
||||
{
|
||||
$this->query( 'SELECT finish_task( $1 , $2 , $3 )' )
|
||||
->execute( $task , $_SESSION[ 'uid' ] , $noteText );
|
||||
}
|
||||
|
||||
|
||||
public function restart( $task , $noteText )
|
||||
{
|
||||
$this->query( 'SELECT restart_task( $1 , $2 , $3 )' )
|
||||
->execute( $task , $_SESSION[ 'uid' ] , $noteText );
|
||||
}
|
||||
|
||||
public function updateTask( $id , $item , $title , $priority , $description )
|
||||
{
|
||||
$result = $this->query( 'SELECT update_task( $1 , $2 , $3 , $4 , $5 ) AS error' )
|
||||
->execute( $id , $item , $title , $description , $priority );
|
||||
return $result[0]->error;
|
||||
}
|
||||
|
||||
public function addNote( $task , $note )
|
||||
{
|
||||
$this->query( 'INSERT INTO notes ( task_id , user_id , note_text ) VALUES ( $1 , $2 , $3 )' )
|
||||
->execute( $task , $_SESSION[ 'uid' ] , $note );
|
||||
}
|
||||
|
||||
public function getNote( $id )
|
||||
{
|
||||
$query = $this->query(
|
||||
'SELECT n.note_id AS id , n.note_text AS text , n.note_added AS added_at , '
|
||||
. 'n.task_id AS task , '
|
||||
. '( n.user_id = $2 AND t.task_id IS NULL ) AS editable '
|
||||
. 'FROM notes n '
|
||||
. 'LEFT OUTER JOIN completed_tasks t USING (task_id) '
|
||||
. 'WHERE n.note_id = $1' );
|
||||
$result = $query->execute( $id , $_SESSION[ 'uid' ] );
|
||||
if ( empty( $result ) ) {
|
||||
return null;
|
||||
}
|
||||
$result[ 0 ]->editable = ( $result[ 0 ]->editable == 't' );
|
||||
return $result[ 0 ];
|
||||
}
|
||||
|
||||
public function deleteNote( $id )
|
||||
{
|
||||
$this->query( 'DELETE FROM notes WHERE note_id = $1' )->execute( $id );
|
||||
}
|
||||
|
||||
public function updateNote( $id , $text )
|
||||
{
|
||||
$this->query( 'UPDATE notes SET note_text = $2 , note_added = now( ) WHERE note_id = $1' )
|
||||
->execute( $id , $text );
|
||||
}
|
||||
|
||||
}
|
42
includes/t-data/item.inc.php
Normal file
42
includes/t-data/item.inc.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
class Data_Item
|
||||
{
|
||||
public $id;
|
||||
public $name;
|
||||
public $hasParent;
|
||||
public $parent;
|
||||
public $children;
|
||||
public $depth;
|
||||
public $lineage;
|
||||
|
||||
public $activeTasks;
|
||||
public $inactiveTasks;
|
||||
|
||||
public function __construct( $id , $name )
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
|
||||
public function getIdentifier( )
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
|
||||
public function getName( )
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
|
||||
public function getDepth( )
|
||||
{
|
||||
if ( $this->depth === null ) {
|
||||
throw new Exception( "Method not implemented" );
|
||||
}
|
||||
return $this->depth;
|
||||
}
|
||||
}
|
10
includes/t-data/package.inc.php
Normal file
10
includes/t-data/package.inc.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
$package[ 'requires' ][] = 'core';
|
||||
|
||||
$package[ 'files' ][] = 'item';
|
||||
$package[ 'files' ][] = 'dao_items';
|
||||
$package[ 'files' ][] = 'dao_tasks';
|
||||
|
||||
$package[ 'daos' ][] = 'items';
|
||||
$package[ 'daos' ][] = 'tasks';
|
Loading…
Add table
Add a link
Reference in a new issue