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:
Emmanuel BENOîT 2012-02-05 18:37:25 +01:00
commit 9677ad4dd3
36 changed files with 3919 additions and 0 deletions

View file

@ -0,0 +1,7 @@
<?php
$config[ 'core' ][ 'db' ][ 'name' ] = '...';
$config[ 'core' ][ 'db' ][ 'user' ] = '...';
$config[ 'core' ][ 'db' ][ 'password' ] = '...';
$config[ 'core' ][ 'pages' ][ 'baseTitle' ] = 'Tasks';

View file

@ -0,0 +1,121 @@
<?php
class Ctrl_HomePage
extends Controller
{
public final function handle( Page $page )
{
session_start( );
if ( array_key_exists( 'uid' , $_SESSION ) ) {
return 'items';
} else {
return 'login';
}
}
}
class Ctrl_Logout
extends Controller
{
public function handle( Page $page )
{
session_start( );
session_destroy( );
return 'home';
}
}
class Ctrl_CheckSession
extends Controller
{
private $loginURL;
private $sessionKey;
public function __construct( $url = 'login' , $key = 'uid' )
{
$this->loginURL = $url;
$this->sessionKey = $key;
}
public function handle( Page $page )
{
session_start( );
if ( array_key_exists( $this->sessionKey , $_SESSION ) ) {
return null;
}
return $this->loginURL;
}
}
class Ctrl_LogInForm
extends Controller
{
public function handle( Page $page )
{
return Loader::Create( 'Form' , 'Log in' , 'login' )
->addField( Loader::Create( 'Field' , 'email' , 'text' )
->setDescription( 'E-mail address:' ) )
->addField( Loader::Create( 'Field' , 'pass' , 'password' )
->setDescription( 'Password:' ) )
->setSuccessURL( 'home' )
->addController( Loader::Ctrl( 'log_in' ) )
->controller( );
}
}
class Ctrl_LogIn
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$email = $this->form->field( 'email' );
$pass = $this->form->field( 'pass' );
$user = Loader::DAO( 'users' )->checkLogin( $email->value( ) , $pass->value( ) );
if ( $user == null ) {
$email->putError( 'Invalid credentials.' );
return null;
}
$_SESSION[ 'uid' ] = $user->user_id;
return true;
}
}
class Ctrl_LoggedOut
extends Controller
{
public function handle( Page $page )
{
session_start( );
if ( array_key_exists( 'uid' , $_SESSION ) ) {
return 'home';
}
return null;
}
}

View file

@ -0,0 +1,22 @@
<?
$package[ 'requires' ][] = 'form';
$package[ 'requires' ][] = 'hub-page';
$package[ 'requires' ][] = 't-users';
$package[ 'files' ][] = 'controllers';
$package[ 'files' ][] = 'pages';
$package[ 'extras' ][] = 'AuthenticatedPage';
$package[ 'ctrls' ][] = 'check_session';
$package[ 'ctrls' ][] = 'home_page';
$package[ 'ctrls' ][] = 'log_in';
$package[ 'ctrls' ][] = 'log_in_form';
$package[ 'ctrls' ][] = 'logged_out';
$package[ 'ctrls' ][] = 'logout';
$package[ 'pages' ][] = 'tasks_home';
$package[ 'pages' ][] = 'tasks_login';
$package[ 'pages' ][] = 'tasks_logout';

View file

@ -0,0 +1,67 @@
<?php
abstract class AuthenticatedPage
extends HubPage
{
public function __construct( $pages )
{
parent::__construct( $pages );
$this->addController( Loader::Ctrl( 'check_session' ) );
}
protected function getMenu( )
{
return array(
'items' => 'Items' ,
'tasks' => 'Tasks' ,
'logout' => 'Log out'
);
}
}
class Page_TasksHome
extends HTMLPage
{
public function __construct( )
{
parent::__construct( );
$this->addController( Loader::Ctrl( 'home_page' ) );
}
protected function getMenu( )
{
return array();
}
}
class Page_TasksLogin
extends HTMLPage
{
public function __construct()
{
parent::__construct( );
$this->addController( Loader::Ctrl( 'logged_out' ) );
$this->addController( Loader::Ctrl( 'log_in_form' ) );
}
protected function getMenu( )
{
return array();
}
}
class Page_TasksLogout
extends Page_Basic
{
public function __construct( )
{
parent::__construct( );
$this->addController( Loader::Ctrl( 'logout' ) );
}
}

View 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;
}
}

View 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 );
}
}

View 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;
}
}

View 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';

View file

@ -0,0 +1,282 @@
<?php
class Ctrl_ItemsTree
extends Controller
implements TitleProvider
{
private $useParameter;
public function __construct( $useParameter = false )
{
$this->useParameter = $useParameter;
}
public function handle( Page $page )
{
$items = Loader::DAO( 'items' );
$tree = $items->getTree( );
$items->countActiveTasks( );
if ( $this->useParameter ) {
$root = (int) $this->getParameter( $this->useParameter , 'GET' );
} else {
$root = null;
}
$buttonURL = 'items/add';
if ( $root != null ) {
$rootObj = $items->get( $root );
$tree = $items->getAll( $rootObj->children );
$boxTitle = 'Child items';
$buttonURL .= "?from=$root";
} else {
$boxTitle = null;
}
return Loader::View( 'box' , $boxTitle , Loader::View( 'items_tree' , $tree ) )
->setClass( 'list' )
->addButton( BoxButton::create( 'Add item' , $buttonURL )
->setClass( 'list-add' ) );
}
public function getTitle( )
{
return 'Items';
}
}
class Ctrl_ItemDetails
extends Controller
{
private $item;
public function __construct( Data_Item $item )
{
$this->item = $item;
}
public function handle( Page $page )
{
$items = Loader::DAO( 'items');
$items->getTree( );
$box = Loader::View( 'box' , 'Details' , Loader::View( 'item_details' , $this->item ) )
->addButton( BoxButton::create( 'Edit item' , 'items/edit?id=' . $this->item->id )
->setClass( 'icon edit' ) );
if ( $items->canMove( $this->item ) ) {
$box->addButton( BoxButton::create( 'Move item' , 'items/move?id=' . $this->item->id )
->setClass( 'icon move' ) );
}
if ( $items->canDelete( $this->item ) ) {
$box->addButton( BoxButton::create( 'Delete item' , 'items/delete?id=' . $this->item->id )
->setClass( 'icon delete' ) );
}
return $box;
}
}
class Ctrl_AddItem
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$name = $this->form->field( 'name' );
$before = $this->form->field( 'before' );
list( $after , $id ) = explode( ':' , $before->value( ) );
$items = Loader::DAO( 'items' );
if ( $id === '' ) {
$error = $items->createLast( $name->value( ) );
} elseif ( $after == 1 ) {
$error = $items->createUnder( $name->value( ) , $id );
} else {
$error = $items->createBefore( $name->value( ) , $id );
}
switch ( $error ) {
case 0:
return true;
case 1:
$name->putError( 'This name is not unique' );
break;
case 2:
$before->putError( 'The item you selected no longer exists' );
break;
default:
$name->putError( 'An unknown error occurred (' . $error . ')' );
break;
}
return null;
}
}
class Ctrl_MoveItem
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$srcId = (int) $this->form->field( 'id' )->value( );
$dest = $this->form->field( 'destination' );
list( $after , $id ) = explode( ':' , $dest->value( ) );
$items = Loader::DAO( 'items' );
if ( $id === '' ) {
$error = $items->moveLast( $srcId );
} elseif ( $after == 1 ) {
$error = $items->moveUnder( $srcId , $id );
} else {
$error = $items->moveBefore( $srcId , $id );
}
switch ( $error ) {
case 0:
return true;
case 1:
$dest->putError( 'Invalid destination' );
break;
case 2:
$before->putError( 'The place you selected no longer exists.' );
break;
default:
$name->putError( 'An unknown error occurred (' . $error . ')' );
break;
}
return null;
}
}
class Ctrl_DeleteItem
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$id = (int) $this->form->field( 'id' )->value( );
$items = Loader::DAO( 'items' );
if ( ! $items->canDelete( $items->get( $id ) ) ) {
return false;
}
$items->destroy( $id );
return true;
}
}
class Ctrl_EditItem
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$id = (int) $this->form->field( 'id' )->value( );
$items = Loader::DAO( 'items' );
$item = $items->get( $id );
$name = $this->form->field( 'name' );
if ( $name->value( ) === $item->name ) {
return true;
}
$error = $items->rename( $id , $name->value( ) );
switch ( $error ) {
case 0:
return true;
case 1:
$name->putError( 'Ce nom n\'est pas unique' );
break;
default:
$name->putError( 'Une erreur inconnue s\'est produite (' . $error . ')' );
break;
}
return null;
}
}
class Ctrl_ItemTasks
extends Controller
{
private $item;
public function __construct( Data_Item $item )
{
$this->item = $item;
}
public function handle( Page $page )
{
$tasks = Loader::DAO( 'tasks' )->getTasksAt( $this->item );
return Loader::View( 'box' , 'Tasks' , Loader::View( 'tasks' , $tasks ) )
->addButton( BoxButton::create( 'Add task' , 'tasks/add?to=' . $this->item->id )
->setClass( 'list-add' ) );
}
}

View file

@ -0,0 +1,44 @@
<?php
class Item_LocationField
implements FieldValidator , FieldModifier
{
private $okLocations;
public function __construct( $okLocations )
{
$this->okLocations = $okLocations;
}
public function replace( $value )
{
$exploded = explode( ':' , $value );
if ( count( $exploded ) != 2 ) {
$exploded = array( 0 , $value );
}
if ( $exploded[ 1 ] == '' ) {
$exploded[ 0 ] = 1;
} else {
$exploded[ 0 ] = ( $exploded[ 0 ] == '0' ) ? 0 : 1;
$exploded[ 1 ] = (int) $exploded[ 1 ];
}
return join( ':' , $exploded );
}
public function validate( $value )
{
list( $inside , $before ) = explode( ':' , $value );
if ( $before != '' && Loader::DAO( 'items' )->get( $before ) == null ) {
return array( 'This item no longer exists.' );
}
if ( ! ( empty( $this->okLocations ) || in_array( $value , $this->okLocations ) ) ) {
return array( 'Invalid destination' );
}
return null;
}
}

View file

@ -0,0 +1,32 @@
<?php
$package[ 'requires' ][] = 'form';
$package[ 'requires' ][] = 't-basics';
$package[ 'requires' ][] = 't-data';
$package[ 'files' ][] = 'controllers';
$package[ 'files' ][] = 'fields';
$package[ 'files' ][] = 'page_controllers';
$package[ 'files' ][] = 'pages';
$package[ 'files' ][] = 'views';
$package[ 'extras' ][] = 'Item_NameField';
$package[ 'extras' ][] = 'Item_LocationField';
$package[ 'ctrls' ][] = 'add_item';
$package[ 'ctrls' ][] = 'add_item_form';
$package[ 'ctrls' ][] = 'delete_item';
$package[ 'ctrls' ][] = 'delete_item_form';
$package[ 'ctrls' ][] = 'edit_item';
$package[ 'ctrls' ][] = 'edit_item_form';
$package[ 'ctrls' ][] = 'move_item';
$package[ 'ctrls' ][] = 'move_item_form';
$package[ 'ctrls' ][] = 'item_details';
$package[ 'ctrls' ][] = 'item_tasks';
$package[ 'ctrls' ][] = 'items_tree';
$package[ 'ctrls' ][] = 'view_item';
$package[ 'views' ][] = 'item_details';
$package[ 'views' ][] = 'items_tree';
$package[ 'pages' ][] = 'tasks_items';

View file

@ -0,0 +1,278 @@
<?php
class Ctrl_ViewItem
extends Controller
{
public function handle( Page $page )
{
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return $page->getBaseURL() . '/items';
}
$item = Loader::DAO( 'items' )->get( $id );
if ( $item === null ) {
return $page->getBaseURL() . '/items';
}
$page->setTitle( $item->name . ' (item)' );
return array(
Loader::Ctrl( 'item_details' , $item ) ,
Loader::Ctrl( 'items_tree' , 'id' ) ,
Loader::Ctrl( 'item_tasks' , $item )
);
}
}
class Ctrl_AddItemForm
extends Controller
implements TitleProvider
{
private $items;
public function handle( Page $page )
{
$this->items = Loader::DAO( 'items' );
$locationField = Loader::Create( 'Item_LocationField' , array( ) );
$form = Loader::Create( 'Form' , 'Add this item' , 'create-item' , 'New item information' )
->addField( Loader::Create( 'Field' , 'name' , 'text' )
->setDescription( 'Item name:' )
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
->setValidator( Loader::Create( 'Validator_StringLength' , 'This name' , 2 , 128 ) ) )
->addField( $before = Loader::Create( 'Field' , 'before' , 'select' )
->setDescription( 'Insert before:' )
->setMandatory( false )
->setModifier( $locationField )
->setValidator( $locationField ) )
->addField( Loader::Create( 'Field' , 'from' , 'hidden' ) )
->addController( Loader::Ctrl( 'add_item' ) );
// Add options to the insert location selector
$this->addTreeOptions( $before , $this->items->getTree( ) );
$before->addOption( '1:' , 'the end of the list' );
// Try to guess return page and default insert location
try {
$from = (int) $this->getParameter( 'from' );
} catch ( ParameterException $e ) {
$from = 0;
}
if ( $from == 0 ) {
$returnURL = 'items';
$defBefore = '1:';
} else {
$returnURL = 'items/view?id=' . $from;
$defBefore = '1:' . $from;
}
$form->setURL( $returnURL );
$form->field( 'from' )->setDefaultValue( $from );
$before->setDefaultValue( $defBefore );
return $form->controller( );
}
private function addTreeOptions( $before , $tree )
{
foreach ( $tree as $item ) {
$name = '-' . str_repeat( '--' , $item->getDepth( ) ) . ' ' . $item->getName( );
$before->addOption( '0:' . $item->getIdentifier( ) , $name );
if ( !empty( $item->children ) ) {
$this->addTreeOptions( $before , $this->items->getAll( $item->children ) );
}
$name = '-' . str_repeat( '--' , $item->getDepth( ) + 1 ) . ' the end of ' . $item->getName( );
$before->addOption( '1:' . $item->getIdentifier( ) , $name );
}
}
public function getTitle( )
{
return 'Add item';
}
}
class Ctrl_MoveItemForm
extends Controller
{
private $items;
public function handle( Page $page )
{
// Check selected item
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'items';
}
$this->items = Loader::DAO( 'items' );
$item = $this->items->get( $id );
if ( $item === null ) {
return 'items';
}
$destinations = $this->items->getMoveTargets( $item );
if ( empty( $destinations ) ) {
return 'items/view?id=' . $item->id;
}
$page->setTitle( $item->name . ' (item)' );
// Field modifier / validator
$locationField = Loader::Create( 'Item_LocationField' , $destinations );
// Generate form
$form = Loader::Create( 'Form' , 'Move item' , 'move-item' )
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )->setDefaultValue( $item->id ) )
->addField( $dest = Loader::Create( 'Field' , 'destination' , 'select' )
->setDescription( 'Move before:' )
->setModifier( $locationField )
->setValidator( $locationField ) )
->setURL( 'items/view?id=' . $item->id )
->addController( Loader::Ctrl( 'move_item' ) );
$this->addDestinations( $dest , $this->items->getTree( ) , $destinations );
if ( in_array( '1:' , $destinations ) ) {
$dest->addOption( '1:' , 'the end of the list' );
}
return $form->controller( );
}
private function addDestinations( $field , $tree , $destinations )
{
foreach ( $tree as $item ) {
$id = '0:' . $item->id;
$disabled = ! in_array( $id , $destinations );
if ( $disabled && ! $this->checkChildren( $item , $destinations ) ) {
continue;
}
$name = '-' . str_repeat( '--' , $item->getDepth( ) ) . ' ' . $item->getName( );
$field->addOption( $id , $name , $disabled );
if ( !empty( $item->children ) ) {
$this->addDestinations( $field , $this->items->getAll( $item->children ) , $destinations );
}
$id = '1:' . $item->id;
if ( ! in_array( $id , $destinations ) ) {
continue;
}
$name = '-' . str_repeat( '--' , $item->getDepth( ) + 1 ) . ' the end of ' . $item->getName( );
$field->addOption( $id , $name );
}
}
private function checkChildren( $item , $destinations )
{
$children = $this->items->getAll( $item->children );
foreach ( $children as $child ) {
if ( in_array( '0:' . $child->id , $destinations ) ) {
return true;
}
}
if ( in_array( '1:' . $item->id , $destinations ) ) {
return true;
}
foreach ( $children as $child ) {
if ( $this->checkChildren( $child , $destinations ) ) {
return true;
}
}
return false;
}
}
class Ctrl_DeleteItemForm
extends Controller
{
private $items;
public function handle( Page $page )
{
// Check selected item
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'items';
}
$items = Loader::DAO( 'items' );
$item = $items->get( $id );
if ( $item === null ) {
return 'items';
}
if ( ! $items->canDelete( $item ) ) {
return 'items/view?id=' . $id;
}
$page->setTitle( $item->name . ' (item)' );
// Generate confirmation text
$confText = HTML::make( 'div' )
->appendElement( HTML::make( 'p' )
->appendText( 'You are about to delete this item.' ) )
->appendElement( HTML::make( 'p' )
->appendText( 'All child items and all tasks the item contains will be deleted permanently.' ) )
->appendElement( HTML::make( 'p' )
->appendText( 'It is impossible to undo this operation.' ) );
// Generate form
$form = Loader::Create( 'Form' , 'Delete the item' , 'delete-item' , 'Please confirm' )
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
->setDefaultValue( $item->id ) )
->addField( Loader::Create( 'Field' , 'confirm' , 'html' )->setDefaultValue( $confText ) )
->setCancelURL( 'items/view?id=' . $item->id )
->setSuccessURL( 'items' ) // XXX: use lineage
->addController( Loader::Ctrl( 'delete_item' ) );
return $form->controller( );
}
}
class Ctrl_EditItemForm
extends Controller
{
private $items;
public function handle( Page $page )
{
// Check selected item
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'items';
}
$this->items = Loader::DAO( 'items' );
$item = $this->items->get( $id );
if ( $item === null ) {
return 'items';
}
$page->setTitle( $item->name . ' (item)' );
return Loader::Create( 'Form' , 'Update item' , 'edit-item' )
->setURL( 'items/view?id=' . $item->id )
->addField( Loader::Create( 'Field' , 'name' , 'text' )
->setDescription( 'Name of the item:' )
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
->setValidator( Loader::Create( 'Validator_StringLength' , 'This name' , 2 , 128 ) )
->setDefaultValue( $item->name ) )
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
->setDefaultValue( $item->id ) )
->addController( Loader::Ctrl( 'edit_item' ) )
->controller( );
}
}

View file

@ -0,0 +1,19 @@
<?php
class Page_TasksItems
extends AuthenticatedPage
{
public function __construct()
{
parent::__construct( array(
'' => 'items_tree' ,
'view' => 'view_item' ,
'add' => 'add_item_form' ,
'move' => 'move_item_form' ,
'edit' => 'edit_item_form' ,
'delete' => 'delete_item_form' ,
) );
}
}

View file

@ -0,0 +1,96 @@
<?php
class View_ItemsTree
extends BaseURLAwareView
{
private $tree;
private $minDepth;
public function __construct( $tree )
{
$this->tree = $tree;
if ( ! empty( $tree ) ) {
$this->minDepth = $tree[ 0 ]->depth;
} else {
$this->minDepth = 0;
}
}
public function render( )
{
if ( empty( $this->tree ) ) {
return HTML::make( 'div' )
->setAttribute( 'class' , 'no-table' )
->appendText( 'No items have been defined.' );
}
$table = HTML::make( 'table' )
->appendElement( HTML::make( 'tr' )
->setAttribute( 'class' , 'header' )
->appendElement( HTML::make( 'th' )
->appendText( 'Item name' ) )
->appendElement( HTML::make( 'th' )
->setAttribute( 'class' , 'align-right' )
->appendText( 'Tasks' ) ) );
foreach ( $this->tree as $item ) {
$this->renderItem( $table , $item );
}
return $table;
}
private function renderItem( $table , $item )
{
$children = Loader::DAO( 'items' )->getAll( $item->children );
$padding = 5 + ( $item->depth - $this->minDepth ) * 16;
$table->appendElement( HTML::make( 'tr' )
->appendElement( HTML::make( 'td' )
->setAttribute( 'style' , 'padding-left:' . $padding . 'px' )
->appendElement( HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/items/view?id=' . $item->id )
->appendText( $item->name ) ) )
->appendElement( HTML::make( 'td' )
->setAttribute( 'class' , 'align-right' )
->appendRaw( (int) $item->activeTasks ) ) );
foreach ( $children as $child ) {
$this->renderItem( $table , $child );
}
}
}
class View_ItemDetails
extends BaseURLAwareView
{
private $item;
public function __construct( Data_Item $item )
{
$this->item = $item;
}
public function render( )
{
$items = Loader::DAO( 'items' );
$contents = array( );
if ( empty( $this->item->lineage ) ) {
array_push( $contents , HTML::make( 'em' ) ->appendText( 'None' ) );
} else {
foreach ( $items->getAll( $this->item->lineage ) as $ancestor ) {
if ( ! empty( $contents ) ) {
array_push( $contents , ' &raquo; ' );
}
array_push( $contents , HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/items/view?id=' . $ancestor->id )
->appendText( $ancestor->name ) );
}
}
return HTML::make( 'dl' )
->appendElement( HTML::make( 'dt' )->appendText( 'Path:' ) )
->appendElement( HTML::make( 'dd' )
->setAttribute( 'style' , 'font-size: 10pt' )
->append( $contents ) );
}
}

View file

@ -0,0 +1,309 @@
<?php
class Ctrl_AddTask
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$item = $this->form->field( 'item' );
$name = $this->form->field( 'title' );
$priority = $this->form->field( 'priority' );
$description = $this->form->field( 'description' );
$error = Loader::DAO( 'tasks' )->addTask( (int) $item->value( ) , $name->value( ) ,
(int) $priority->value( ) , $description->value( ) );
switch ( $error ) {
case 0:
return true;
case 1:
$name->putError( 'Duplicate task name for this item.' );
break;
case 2:
$item->putError( 'This item has been deleted' );
break;
default:
$name->putError( "An unknown error occurred ($error)" );
break;
}
return null;
}
}
class Ctrl_TaskDetails
extends Controller
{
private $task;
public function __construct( $task )
{
$this->task = $task;
}
public function handle( Page $page )
{
if ( $this->task->completed_at !== null ) {
$bTitle = "Completed task";
} else {
$bTitle = "Active task";
}
$items = Loader::DAO( 'items' );
$items->getLineage( $this->task->item = $items->get( $this->task->item ) );
$box = Loader::View( 'box' , $bTitle , Loader::View( 'task_details' , $this->task ) );
if ( $this->task->completed_by === null ) {
$box->addButton( BoxButton::create( 'Edit task' , 'tasks/edit?id=' . $this->task->id )
->setClass( 'icon edit' ) )
->addButton( BoxButton::create( 'Mark as completed' ,
'tasks/finish?id=' . $this->task->id )
->setClass( 'icon stop' ) );
} else {
$box->addButton( BoxButton::create( 'Re-activate' , 'tasks/restart?id=' . $this->task->id )
->setClass( 'icon start' ) );
$timestamp = strtotime( $this->task->completed_at );
}
if ( Loader::DAO( 'tasks' )->canDelete( $this->task ) ) {
$box->addButton( BoxButton::create( 'Delete' , 'tasks/delete?id=' . $this->task->id )
->setClass( 'icon delete' ) );
}
return $box;
}
}
class Ctrl_TaskNotes
extends Controller
{
private $task;
public function __construct( $task )
{
$this->task = $task;
}
public function handle( Page $page )
{
$result = array( );
foreach ( $this->task->notes as $note ) {
$box = Loader::View( 'box' , null , Loader::View( 'task_note' , $note ) );
if ( $this->task->completed_at === null && $note->uid == $_SESSION[ 'uid' ] ) {
$box->addButton( BoxButton::create( 'Edit comment' , 'tasks/notes/edit?id=' . $note->id )
->setClass( 'icon edit' ) )
->addButton( BoxButton::create( 'Delete comment' , 'tasks/notes/delete?id=' . $note->id )
->setClass( 'icon delete' ) );
}
array_push( $result , $box );
}
return $result;
}
}
class Ctrl_DeleteTask
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
Loader::DAO( 'tasks' )->delete( (int) $this->form->field( 'id' )->value( ) );
return true;
}
}
class Ctrl_ToggleTask
extends Controller
{
private $restart;
public function __construct( $restart )
{
$this->isRestart = $restart;
}
public function handle( Page $page )
{
// Check selected task
try {
$id = (int) $this->getParameter( 'id' , 'GET' );
} catch ( ParameterException $e ) {
return 'tasks';
}
$tasks = Loader::DAO( 'tasks' );
$task = $tasks->get( $id );
if ( $task === null ) {
return 'tasks';
}
if ( $this->isRestart ) {
$tasks->restart( $id , '[AUTO] Task re-activated.' );
} else {
$tasks->finish( $id , '[AUTO] Task completed.' );
}
return 'tasks/view?id=' . $id;
}
}
class Ctrl_EditTask
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$id = $this->form->field( 'id' );
$item = $this->form->field( 'item' );
$name = $this->form->field( 'title' );
$priority = $this->form->field( 'priority' );
$description = $this->form->field( 'description' );
$error = Loader::DAO( 'tasks' )->updateTask( (int) $id->value( ) ,
(int) $item->value( ) , $name->value( ) ,
(int) $priority->value( ) , $description->value( ) );
switch ( $error ) {
case 0:
return true;
case 1:
$name->putError( "Une tâche porte déjà ce nom à cet endroit" );
break;
case 2:
$item->putError( "Cet endroit a été supprimé" );
break;
default:
$name->putError( "Une erreur inconnue s'est produite ($error)" );
break;
}
return null;
}
}
class Ctrl_AddTaskNoteForm
extends Controller
{
private $task;
public function __construct( $task )
{
$this->task = $task;
}
public function handle( Page $page )
{
return Loader::Create( 'Form' , 'Add' , 'add-note' , 'Add a comment' )
->setSuccessURL( 'tasks/view?id=' . $this->task->id )
->setAction( '?id=' . $this->task->id . '#add-note-form' )
->addField( Loader::Create( 'Field' , 'text' , 'textarea' )
->setDescription( 'Comment:' )
->setValidator( Loader::Create( 'Validator_StringLength' , 'This comment' , 5 ) ) )
->addController( Loader::Ctrl( 'add_task_note' , $this->task ) )
->controller( );
}
}
class Ctrl_AddTaskNote
extends Controller
implements FormAware
{
private $task;
private $form;
public function __construct( $task )
{
$this->task = $task;
}
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
Loader::DAO( 'tasks' )->addNote( $this->task->id , $this->form->field( 'text' )->value( ) );
return true;
}
}
class Ctrl_DeleteNote
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
Loader::DAO( 'tasks' )->deleteNote( (int) $this->form->field( 'id' )->value( ) );
return true;
}
}
class Ctrl_EditNote
extends Controller
implements FormAware
{
private $form;
public function setForm( Form $form )
{
$this->form = $form;
}
public function handle( Page $page )
{
$id = (int) $this->form->field( 'id' )->value( );
$text = $this->form->field( 'text' )->value( );
Loader::DAO( 'tasks' )->updateNote( $id , $text );
return true;
}
}

View file

@ -0,0 +1,35 @@
<?php
$package[ 'requires' ][] = 'form';
$package[ 'requires' ][] = 't-data';
$package[ 'requires' ][] = 't-basics';
$package[ 'files' ][] = 'controllers';
$package[ 'files' ][] = 'page_controllers';
$package[ 'files' ][] = 'pages';
$package[ 'files' ][] = 'views';
$package[ 'ctrls' ][] = 'all_tasks';
$package[ 'ctrls' ][] = 'add_task_form';
$package[ 'ctrls' ][] = 'add_task';
$package[ 'ctrls' ][] = 'add_task_note_form';
$package[ 'ctrls' ][] = 'add_task_note';
$package[ 'ctrls' ][] = 'delete_note_form';
$package[ 'ctrls' ][] = 'delete_note';
$package[ 'ctrls' ][] = 'delete_task_form';
$package[ 'ctrls' ][] = 'delete_task';
$package[ 'ctrls' ][] = 'edit_note_form';
$package[ 'ctrls' ][] = 'edit_note';
$package[ 'ctrls' ][] = 'edit_task_form';
$package[ 'ctrls' ][] = 'edit_task';
$package[ 'ctrls' ][] = 'task_details';
$package[ 'ctrls' ][] = 'task_notes';
$package[ 'ctrls' ][] = 'toggle_task';
$package[ 'ctrls' ][] = 'view_task';
$package[ 'views' ][] = 'all_tasks';
$package[ 'views' ][] = 'tasks';
$package[ 'views' ][] = 'task_details';
$package[ 'views' ][] = 'task_note';
$package[ 'pages' ][] = 'tasks_tasks';

View file

@ -0,0 +1,352 @@
<?php
class Ctrl_AllTasks
extends Controller
{
public function handle( Page $page )
{
try {
$mode = $this->getParameter( 'mode' , 'GET' );
} catch ( ParameterException $e ) {
$mode = 'active';
}
if ( $mode == 'active' ) {
$tasks = Loader::DAO( 'tasks' )->getAllActiveTasks( );
$title = 'Active tasks';
$bTitle = 'Display all tasks';
$bMode = 'all';
} else {
$mode = 'all';
$tasks = Loader::DAO( 'tasks' )->getAllTasks( );
$title = 'All tasks';
$bTitle = 'Display active tasks only';
$bMode = 'active';
}
$tree = Loader::DAO( 'items' )->getTree( );
$box = Loader::View( 'box' , $title , Loader::View( 'all_tasks' , $tasks , $mode ) )
->addButton( BoxButton::create( $bTitle , 'tasks?mode=' . $bMode )
->setClass( 'icon refresh' ) );
if ( !empty( $tree ) ) {
$box ->addButton( BoxButton::create( 'New task' , 'tasks/add' )
->setClass( 'list-add' ) );
}
return $box;
}
}
abstract class Ctrl_TaskFormBase
extends Controller
{
protected final function createPrioritySelector( )
{
$select = Loader::Create( 'Field' , 'priority' , 'select' )
->setDescription( 'Priority:' )
->setValidator( Loader::Create( 'Validator_IntValue' , 'Priorité invalide' )
->setMinValue( 1 )->setMaxValue( 5 ) );
$tasks = Loader::DAO( 'tasks' );
for ( $i = 5 ; $i >= 1 ; $i -- ) {
$select->addOption( $i , $tasks->translatePriority( $i ) );
}
return $select;
}
}
class Ctrl_AddTaskForm
extends Ctrl_TaskFormBase
{
public function handle( Page $page )
{
try {
$target = (int) $this->getParameter( 'to' );
} catch ( ParameterException $e ) {
$target = null;
}
$form = Loader::Create( 'Form' , 'Add this task' , 'create-task' );
if ( $target === null ) {
$returnURL = 'tasks';
if ( ! $this->addItemSelector( $form ) ) {
return 'items';
}
} else {
$item = Loader::DAO( 'items' )->get( $target );
if ( $item === null ) {
return 'items';
}
$returnURL = 'items/view?id=' . $target;
$form->addField( Loader::Create( 'Field' , 'to' , 'hidden' )
->setDefaultValue( $target ) )
->addField( Loader::Create( 'Field' , 'item' , 'hidden' )
->setDefaultValue( $target ) );
}
$page->setTitle( 'New task' );
return $form->addField( Loader::Create( 'Field' , 'title' , 'text' )
->setDescription( 'Task title:' )
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
->setValidator( Loader::Create( 'Validator_StringLength' , 'This title' , 5 , 256 ) ) )
->addField( $this->createPrioritySelector( )
->setDefaultValue( 3 ) )
->addField( Loader::Create( 'Field' , 'description' , 'textarea' )
->setDescription( 'Description:' )
->setMandatory( false ) )
->setURL( $returnURL )
->addController( Loader::Ctrl( 'add_task' ) )
->controller( );
}
private function addItemSelector( $form )
{
$form->addField( $select = Loader::Create( 'Field' , 'item' , 'select' )
->setDescription( 'Item:' )
->addOption( '' , '(please select an item)' ) );
$items = Loader::DAO( 'items' )->getTreeList( );
if ( empty( $items ) ) {
return false;
}
foreach ( $items as $item ) {
$name = '-' . str_repeat( '--' , $item->depth ) . ' ' . $item->name;
$select->addOption( $item->id , $name );
}
return true;
}
}
class Ctrl_ViewTask
extends Controller
{
public function handle( Page $page )
{
try {
$id = (int) $this->getParameter( 'id' , 'GET' );
} catch ( ParameterException $e ) {
return 'tasks';
}
$task = Loader::DAO( 'tasks' )->get( $id );
if ( $task === null ) {
return 'tasks';
}
$page->setTitle( $task->title . ' (task)' );
$result = array( Loader::Ctrl( 'task_details' , $task ) );
if ( $task->completed_by === null ) {
array_push( $result , Loader::Ctrl( 'add_task_note_form' , $task ) );
}
array_push( $result , Loader::Ctrl( 'task_notes' , $task ) );
return $result;
}
}
class Ctrl_DeleteTaskForm
extends Controller
{
public function handle( Page $page )
{
// Check selected task
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'tasks';
}
$tasks = Loader::DAO( 'tasks' );
$task = $tasks->get( $id );
if ( $task === null ) {
return 'tasks';
}
if ( ! $tasks->canDelete( $task ) ) {
return 'tasks/view?id=' . $id;
}
$page->setTitle( $task->title . ' (task)' );
// Generate confirmation text
$confText = HTML::make( 'div' )
->appendElement( HTML::make( 'p' )
->appendText( "You are about to delete this task, and any comment attached to it." ) )
->appendElement( HTML::make( 'p' )
->appendText( "This operation cannot be undone." ) );
// Generate form
return Loader::Create( 'Form' , 'Delete the task' , 'delete-task' , 'Task deletion' )
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
->setDefaultValue( $task->id ) )
->addField( Loader::Create( 'Field' , 'confirm' , 'html' )->setDefaultValue( $confText ) )
->setCancelURL( 'tasks/view?id=' . $task->id )
->setSuccessURL( 'items/view?id=' . $task->item )
->addController( Loader::Ctrl( 'delete_task' ) )
->controller( );
}
}
class Ctrl_EditTaskForm
extends Ctrl_TaskFormBase
{
public function handle( Page $page )
{
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'tasks';
}
$task = Loader::DAO( 'tasks' )->get( $id );
if ( $task === null ) {
return 'tasks';
}
$page->setTitle( $task->title . ' (task)' );
return Loader::Create( 'Form' , 'Update task' , 'edit-task' , 'Editing task' )
->setURL( 'tasks/view?id=' . $task->id )
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
->setDefaultValue( $task->id ) )
->addField( $this->createItemSelector( )
->setDefaultValue( $task->item ) )
->addField( Loader::Create( 'Field' , 'title' , 'text' )
->setDescription( 'Title:' )
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
->setValidator( Loader::Create( 'Validator_StringLength' , 'This title' , 5 , 256 ) )
->setDefaultValue( $task->title ) )
->addField( $this->createPrioritySelector( )
->setDefaultValue( $task->priority ) )
->addField( Loader::Create( 'Field' , 'description' , 'textarea' )
->setDescription( 'Description:' )
->setMandatory( false )
->setDefaultValue( $task->description ) )
->addController( Loader::Ctrl( 'edit_task' ) )
->controller( );
}
private function createItemSelector( )
{
$select = Loader::Create( 'Field' , 'item' , 'select' )
->setDescription( 'On item:' );
$items = Loader::DAO( 'items' )->getTreeList( );
foreach ( $items as $item ) {
$name = '-' . str_repeat( '--' , $item->depth ) . ' ' . $item->name;
$select->addOption( $item->id , $name );
}
return $select;
}
}
class Ctrl_DeleteNoteForm
extends Controller
{
public function handle( Page $page )
{
// Check selected note
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'tasks';
}
$tasks = Loader::DAO( 'tasks' );
$note = $tasks->getNote( $id );
if ( $note === null ) {
return 'tasks';
}
if ( !$note->editable ) {
return 'tasks/view?id=' . $note->task;
}
$task = $tasks->get( $note->task );
$page->setTitle( $task->title . ' (task)' );
// Generate confirmation text
$confText = HTML::make( 'div' )
->appendElement( HTML::make( 'p' )
->appendText( 'You are about to delete a comment attached to this task.' ) )
->appendElement( HTML::make( 'p' )
->appendText( 'This operation cannot be undone.' ) );
// Generate form
return Loader::Create( 'Form' , 'Delete this comment' , 'delete-note' , 'Comment deletion' )
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
->setDefaultValue( $note->id ) )
->addField( Loader::Create( 'Field' , 'confirm' , 'html' )->setDefaultValue( $confText ) )
->setURL( 'tasks/view?id=' . $note->task )
->addController( Loader::Ctrl( 'delete_note' ) )
->controller( );
}
}
class Ctrl_EditNoteForm
extends Controller
{
public function handle( Page $page )
{
// Check selected note
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'tasks';
}
$tasks = Loader::DAO( 'tasks' );
$note = $tasks->getNote( $id );
if ( $note === null ) {
return 'tasks';
}
if ( !$note->editable ) {
return 'tasks/view?id=' . $note->task;
}
$task = $tasks->get( $note->task );
$page->setTitle( $task->title . ' (task)' );
// Generate form
return Loader::Create( 'Form' , 'Update comment' , 'edit-note' )
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
->setDefaultValue( $note->id ) )
->addField( Loader::Create( 'Field' , 'text' , 'textarea' )
->setDescription( 'Comment:' )
->setValidator( Loader::Create( 'Validator_StringLength' , 'Le texte' , 5 ) )
->setDefaultValue( $note->text ) )
->setURL( 'tasks/view?id=' . $note->task )
->addController( Loader::Ctrl( 'edit_note' ) )
->controller( );
}
}

View file

@ -0,0 +1,22 @@
<?php
class Page_TasksTasks
extends AuthenticatedPage
{
public function __construct()
{
parent::__construct( array(
'' => 'all_tasks' ,
'add' => 'add_task_form' ,
'delete' => 'delete_task_form' ,
'edit' => 'edit_task_form' ,
'finish' => array( 'toggle_task' , false ) ,
'restart' => array( 'toggle_task' , true ) ,
'view' => 'view_task' ,
'notes/edit' => 'edit_note_form' ,
'notes/delete' => 'delete_note_form' ,
));
}
}

View file

@ -0,0 +1,301 @@
<?php
abstract class View_TasksBase
extends BaseURLAwareView
{
protected $tasks;
protected $dao;
protected function __construct( )
{
$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 abstract function generateItem( $task );
}
class View_AllTasks
extends View_TasksBase
{
public function __construct( $tasks )
{
parent::__construct( );
$this->tasks = $tasks;
}
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 ) ) );
array_push( $cell , HTML::make( 'dd' )->append( $this->formatPlaceLineage( $task->item ) ) );
$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 ) {
$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}" ) );
foreach ( $cell as $entry ) {
$entry->setAttribute( 'class' , 'completed' );
}
}
return $cell;
}
private function formatPlaceLineage( $item )
{
$item = Loader::DAO( 'items' )->get( $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 , ' &raquo; ' );
}
array_push( $contents , HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/items/view?id=' . $ancestor->id )
->appendText( $ancestor->name ) );
}
array_unshift( $contents, 'On ' );
return $contents;
}
}
class View_Tasks
extends View_TasksBase
{
public function __construct( $tasks )
{
parent::__construct( );
$this->tasks = $tasks;
}
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 ) ) );
$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 ) {
$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}" ) );
foreach ( $cell as $entry ) {
$entry->setAttribute( 'class' , 'completed' );
}
}
return $cell;
}
}
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->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 ) ) );
} 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 , ' &raquo; ' );
}
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
implements View
{
private $task;
private $reverse;
public function __construct( $task , $reverse )
{
$this->task = $task;
$this->reverse = $reverse;
}
public function render( )
{
$source = $this->reverse ? 'reverseDependencies' : 'dependencies';
$list = HTML::make( 'ul' )->setAttribute( 'class' , 'dep-list' );
foreach ( $this->task->$source as $dependency ) {
$link = HTML::make( 'a' )
->setAttribute( 'href' , 'tasks/view?id=' . $dependency->id )
->appendText( $dependency->title );
if ( ! $this->reverse ) {
$link->setAttribute( 'class' , ( $dependency->completed == 't' )
? 'satisfied' : 'missing' );
}
$list->appendElement( HTML::make( 'li' )->appendElement( $link ) );
}
}
}

View file

@ -0,0 +1,5 @@
<?php
$package[ 'requires' ][] = 'core';
$package[ 'files' ][] = 'users';
$package[ 'daos' ][] = 'users';

View file

@ -0,0 +1,43 @@
<?php
class Dao_Users
extends DAO
{
private function hashPassword( $password , $salt , $iterations )
{
$hash = $password;
$salt = trim( $salt );
do {
$hash = sha1( "$salt$hash$salt" );
$iterations --;
} while ( $iterations > 0 );
return $hash;
}
public function getUser( $email )
{
$query = $this->query( 'SELECT * FROM users WHERE user_email = LOWER( $1 )' );
$results = $query->execute( $email );
if ( empty( $results ) ) {
return null;
}
return array_shift( $results );
}
public function checkLogin( $email , $password )
{
$userData = $this->getUser( $email );
if ( $userData != null ) {
$hashed = $this->hashPassword( $password ,
$userData->user_salt ,
$userData->user_iterations );
if ( $hashed === $userData->user_hash ) {
return $userData;
}
}
return null;
}
}