Task assignment

Tasks can be assigned to users. An user may decide to "claim" a task
directly, which will assign the task to him. Otherwise, it is possible
to set some arbitrary user as the assignee or remove the assignee
completely through the edition form.

Marking a task as completed will remove the assignee, while
re-activating a task will assign it to the user who re-activated it.

Also, fixed a bug which allowed a completed task to be edited.
This commit is contained in:
Emmanuel BENOîT 2012-02-06 16:38:11 +01:00
parent 850d2fa8d4
commit 56741bccaa
12 changed files with 180 additions and 80 deletions

View file

@ -77,6 +77,7 @@ CREATE TABLE tasks (
task_description TEXT NOT NULL, task_description TEXT NOT NULL,
task_added TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), task_added TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
user_id INT NOT NULL REFERENCES users(user_id) ON UPDATE NO ACTION ON DELETE CASCADE, user_id INT NOT NULL REFERENCES users(user_id) ON UPDATE NO ACTION ON DELETE CASCADE,
user_id_assigned INT REFERENCES users(user_id) ON UPDATE NO ACTION ON DELETE SET NULL ,
PRIMARY KEY(task_id) PRIMARY KEY(task_id)
); );

View file

@ -32,12 +32,17 @@ BEGIN
RETURN 1; RETURN 1;
END; END;
UPDATE tasks SET user_id_assigned = NULL WHERE task_id = t_id;
INSERT INTO notes ( task_id , user_id , note_text ) INSERT INTO notes ( task_id , user_id , note_text )
VALUES ( t_id , u_id , n_text ); VALUES ( t_id , u_id , n_text );
RETURN 0; RETURN 0;
END; END;
$finish_task$ LANGUAGE plpgsql; $finish_task$ LANGUAGE plpgsql;
REVOKE EXECUTE ON FUNCTION finish_task( INT , INT , TEXT ) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION finish_task( INT , INT , TEXT ) TO :webapp_user;
-- Restart a task -- Restart a task
CREATE OR REPLACE FUNCTION restart_task( t_id INT , u_id INT , n_text TEXT ) CREATE OR REPLACE FUNCTION restart_task( t_id INT , u_id INT , n_text TEXT )
@ -50,31 +55,62 @@ BEGIN
IF NOT FOUND THEN IF NOT FOUND THEN
RETURN 1; RETURN 1;
END IF; END IF;
UPDATE tasks SET user_id_assigned = u_id
WHERE task_id = t_id;
INSERT INTO notes ( task_id , user_id , note_text ) INSERT INTO notes ( task_id , user_id , note_text )
VALUES ( t_id , u_id , n_text ); VALUES ( t_id , u_id , n_text );
RETURN 0; RETURN 0;
END; END;
$restart_task$ LANGUAGE plpgsql; $restart_task$ LANGUAGE plpgsql;
REVOKE EXECUTE ON FUNCTION restart_task( INT , INT , TEXT ) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION restart_task( INT , INT , TEXT ) TO :webapp_user;
-- Update a task -- Update a task
CREATE OR REPLACE FUNCTION update_task( t_id INT , p_id INT , t_title TEXT , t_description TEXT , t_priority INT ) CREATE OR REPLACE FUNCTION update_task( t_id INT , p_id INT , t_title TEXT , t_description TEXT , t_priority INT , t_assignee INT )
RETURNS INT RETURNS INT
STRICT VOLATILE STRICT VOLATILE
SECURITY INVOKER SECURITY INVOKER
AS $update_task$ AS $update_task$
BEGIN BEGIN
PERFORM 1
FROM tasks
LEFT OUTER JOIN completed_tasks
USING( task_id )
WHERE task_id = t_id AND completed_task_time IS NULL
FOR UPDATE OF tasks;
IF NOT FOUND THEN
RETURN 4;
END IF;
BEGIN
IF t_assignee <= 0 THEN
t_assignee := NULL;
END IF;
BEGIN
UPDATE tasks SET user_id_assigned = t_assignee WHERE task_id = t_id;
EXCEPTION
WHEN foreign_key_violation THEN
RAISE EXCEPTION 'bad user';
END;
UPDATE tasks SET item_id = p_id , task_title = t_title , UPDATE tasks SET item_id = p_id , task_title = t_title ,
task_description = t_description , task_description = t_description ,
task_priority = t_priority task_priority = t_priority
WHERE task_id = t_id; WHERE task_id = t_id;
RETURN 0; EXCEPTION
EXCEPTION
WHEN unique_violation THEN WHEN unique_violation THEN
RETURN 1; RETURN 1;
WHEN foreign_key_violation THEN WHEN foreign_key_violation THEN
RETURN 2; RETURN 2;
WHEN raise_exception THEN
RETURN 3;
END;
RETURN 0;
END; END;
$update_task$ LANGUAGE plpgsql; $update_task$ LANGUAGE plpgsql;
REVOKE EXECUTE ON FUNCTION update_task( INT , INT , TEXT , TEXT , INT , INT ) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION update_task( INT , INT , TEXT , TEXT , INT , INT ) TO :webapp_user;

View file

@ -10,14 +10,16 @@ CREATE VIEW tasks_list
t.task_description AS description, t.task_added AS added_at, t.task_description AS description, t.task_added AS added_at,
u1.user_view_name AS added_by, u1.user_view_name AS added_by,
ct.completed_task_time AS completed_at, ct.completed_task_time AS completed_at,
u2.user_view_name AS completed_by , u2.user_view_name AS assigned_to ,
u3.user_view_name AS completed_by ,
t.task_priority AS priority , t.task_priority AS priority ,
bd.bad_deps AS missing_dependencies , bd.bad_deps AS missing_dependencies ,
mtd.trans_missing AS total_missing_dependencies mtd.trans_missing AS total_missing_dependencies
FROM tasks t FROM tasks t
INNER JOIN users_view u1 ON u1.user_id = t.user_id INNER JOIN users_view u1 ON u1.user_id = t.user_id
LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id
LEFT OUTER JOIN users_view u2 ON u2.user_id = ct.user_id LEFT OUTER JOIN users_view u2 ON u2.user_id = t.user_id_assigned
LEFT OUTER JOIN users_view u3 ON u3.user_id = ct.user_id
LEFT OUTER JOIN ( LEFT OUTER JOIN (
SELECT td.task_id , COUNT(*) AS bad_deps SELECT td.task_id , COUNT(*) AS bad_deps
FROM task_dependencies td FROM task_dependencies td

View file

@ -38,8 +38,8 @@ class Dao_Users
public function getUsers( ) public function getUsers( )
{ {
return $this->query( return $this->query(
'SELECT user_id , user_display_name , user_email ' 'SELECT user_id , user_display_name , user_email , user_view_name '
. 'FROM users ' . 'FROM users_view '
. 'ORDER BY LOWER( user_email )' )->execute( ); . 'ORDER BY LOWER( user_email )' )->execute( );
} }

View file

@ -76,12 +76,14 @@ class DAO_Tasks
'SELECT t.task_id AS id, t.task_title AS title, t.item_id AS item ,' '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, ' . 't.task_description AS description, t.task_added AS added_at, '
. 'u1.user_view_name AS added_by, ct.completed_task_time AS completed_at, ' . 'u1.user_view_name AS added_by, ct.completed_task_time AS completed_at, '
. 'u2.user_view_name AS completed_by, t.user_id AS uid , ' . 'u2.user_view_name AS assigned_to , u2.user_id AS assigned_id , '
. 'u3.user_view_name AS completed_by, t.user_id AS uid , '
. 't.task_priority AS priority ' . 't.task_priority AS priority '
. 'FROM tasks t ' . 'FROM tasks t '
. 'INNER JOIN users_view u1 ON u1.user_id = t.user_id ' . 'INNER JOIN users_view u1 ON u1.user_id = t.user_id '
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id ' . 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
. 'LEFT OUTER JOIN users_view u2 ON u2.user_id = ct.user_id ' . 'LEFT OUTER JOIN users_view u2 ON u2.user_id = t.user_id_assigned '
. 'LEFT OUTER JOIN users_view u3 ON u3.user_id = ct.user_id '
. 'WHERE t.task_id = $1' )->execute( $id ); . 'WHERE t.task_id = $1' )->execute( $id );
if ( empty( $result ) ) { if ( empty( $result ) ) {
return null; return null;
@ -182,10 +184,10 @@ class DAO_Tasks
->execute( $task , $_SESSION[ 'uid' ] , $noteText ); ->execute( $task , $_SESSION[ 'uid' ] , $noteText );
} }
public function updateTask( $id , $item , $title , $priority , $description ) public function updateTask( $id , $item , $title , $priority , $description , $assignee )
{ {
$result = $this->query( 'SELECT update_task( $1 , $2 , $3 , $4 , $5 ) AS error' ) $result = $this->query( 'SELECT update_task( $1 , $2 , $3 , $4 , $5 , $6 ) AS error' )
->execute( $id , $item , $title , $description , $priority ); ->execute( $id , $item , $title , $description , $priority , $assignee );
return $result[0]->error; return $result[0]->error;
} }
@ -236,4 +238,14 @@ class DAO_Tasks
->execute( $from , $to ); ->execute( $from , $to );
} }
public function assignTaskTo( $task , $user )
{
$this->query(
'UPDATE tasks _task SET user_id_assigned = $2 '
. 'FROM tasks _task2 '
. 'LEFT OUTER JOIN completed_tasks _completed '
. 'USING ( task_id ) '
. 'WHERE _task2.task_id = _task.task_id AND _completed.task_id IS NULL AND _task.task_id = $1'
)->execute( $task , $user );
}
} }

View file

@ -71,10 +71,16 @@ class Ctrl_TaskDetails
if ( $this->task->completed_by === null ) { if ( $this->task->completed_by === null ) {
$box->addButton( BoxButton::create( 'Edit task' , 'tasks/edit?id=' . $this->task->id ) $box->addButton( BoxButton::create( 'Edit task' , 'tasks/edit?id=' . $this->task->id )
->setClass( 'icon edit' ) ); ->setClass( 'icon edit' ) );
if ( $tasks->canFinish( $this->task ) ) { if ( $tasks->canFinish( $this->task ) ) {
$box->addButton( BoxButton::create( 'Mark as completed' , 'tasks/finish?id=' . $this->task->id ) $box->addButton( BoxButton::create( 'Mark as completed' , 'tasks/finish?id=' . $this->task->id )
->setClass( 'icon stop' ) ); ->setClass( 'icon stop' ) );
}; }
if ( $this->task->assigned_id !== $_SESSION[ 'uid' ] ) {
$box->addButton( BoxButton::create( 'Claim task' , 'tasks/claim?id=' . $this->task->id )
->setClass( 'icon claim' ) );
}
} else { } else {
if ( $tasks->canRestart( $this->task ) ) { if ( $tasks->canRestart( $this->task ) ) {
$box->addButton( BoxButton::create( 'Re-activate' , 'tasks/restart?id=' . $this->task->id ) $box->addButton( BoxButton::create( 'Re-activate' , 'tasks/restart?id=' . $this->task->id )
@ -228,10 +234,12 @@ class Ctrl_EditTask
$name = $this->form->field( 'title' ); $name = $this->form->field( 'title' );
$priority = $this->form->field( 'priority' ); $priority = $this->form->field( 'priority' );
$description = $this->form->field( 'description' ); $description = $this->form->field( 'description' );
$assignee = $this->form->field( 'assigned-to' );
$error = Loader::DAO( 'tasks' )->updateTask( (int) $id->value( ) , $error = Loader::DAO( 'tasks' )->updateTask( (int) $id->value( ) ,
(int) $item->value( ) , $name->value( ) , (int) $item->value( ) , $name->value( ) ,
(int) $priority->value( ) , $description->value( ) ); (int) $priority->value( ) , $description->value( ) ,
(int) $assignee->value( ) );
switch ( $error ) { switch ( $error ) {
@ -239,11 +247,15 @@ class Ctrl_EditTask
return true; return true;
case 1: case 1:
$name->putError( "A task already uses this title for this item." ); $name->putError( 'A task already uses this title for this item.' );
break; break;
case 2: case 2:
$item->putError( "This item has been deleted." ); $item->putError( 'This item has been deleted.' );
break;
case 3:
$assignee->putError( 'This user has been deleted.' );
break; break;
default: default:
@ -406,3 +418,23 @@ class Ctrl_DependencyDelete
return true; return true;
} }
} }
class Ctrl_TaskClaim
extends Controller
{
public function handle( Page $page )
{
try {
$id = (int) $this->getParameter( 'id' );
} catch ( ParameterException $e ) {
return 'tasks';
}
$dao = Loader::DAO( 'tasks' );
$dao->assignTaskTo( $id , $_SESSION[ 'uid' ] );
return 'tasks/view?id=' . $id;
}
}

View file

@ -28,6 +28,7 @@ $package[ 'ctrls' ][] = 'edit_task_form';
$package[ 'ctrls' ][] = 'edit_task'; $package[ 'ctrls' ][] = 'edit_task';
$package[ 'ctrls' ][] = 'task_dependencies'; $package[ 'ctrls' ][] = 'task_dependencies';
$package[ 'ctrls' ][] = 'task_details'; $package[ 'ctrls' ][] = 'task_details';
$package[ 'ctrls' ][] = 'task_claim';
$package[ 'ctrls' ][] = 'task_notes'; $package[ 'ctrls' ][] = 'task_notes';
$package[ 'ctrls' ][] = 'toggle_task'; $package[ 'ctrls' ][] = 'toggle_task';
$package[ 'ctrls' ][] = 'view_task'; $package[ 'ctrls' ][] = 'view_task';

View file

@ -233,6 +233,9 @@ class Ctrl_EditTaskForm
if ( $task === null ) { if ( $task === null ) {
return 'tasks'; return 'tasks';
} }
if ( $task->completed_at !== null ) {
return 'tasks/view?id=' . $id;
}
$page->setTitle( $task->title . ' (task)' ); $page->setTitle( $task->title . ' (task)' );
@ -253,6 +256,8 @@ class Ctrl_EditTaskForm
->setDescription( 'Description:' ) ->setDescription( 'Description:' )
->setMandatory( false ) ->setMandatory( false )
->setDefaultValue( $task->description ) ) ->setDefaultValue( $task->description ) )
->addField( $this->createAssigneeSelector( )
->setDefaultValue( $task->assigned_id ) )
->addController( Loader::Ctrl( 'edit_task' ) ) ->addController( Loader::Ctrl( 'edit_task' ) )
->controller( ); ->controller( );
} }
@ -271,6 +276,22 @@ class Ctrl_EditTaskForm
return $select; return $select;
} }
private function createAssigneeSelector( )
{
$select = Loader::Create( 'Field' , 'assigned-to' , 'select' )
->setDescription( 'Assigned to:' )
->setMandatory( false );
$select->addOption( '' , '(unassigned task)' );
$users = Loader::DAO( 'users' )->getUsers( );
foreach ( $users as $user ) {
$select->addOption( $user->user_id , $user->user_view_name );
}
return $select;
}
} }

View file

@ -9,6 +9,7 @@ class Page_TasksTasks
parent::__construct( array( parent::__construct( array(
'' => 'all_tasks' , '' => 'all_tasks' ,
'add' => 'add_task_form' , 'add' => 'add_task_form' ,
'claim' => 'task_claim' ,
'delete' => 'delete_task_form' , 'delete' => 'delete_task_form' ,
'edit' => 'edit_task_form' , 'edit' => 'edit_task_form' ,
'finish' => array( 'toggle_task' , false ) , 'finish' => array( 'toggle_task' , false ) ,

View file

@ -51,20 +51,6 @@ abstract class View_TasksBase
return $result; 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 ) protected function generateItem( $task )
{ {
$cell = array( ); $cell = array( );
@ -72,8 +58,7 @@ class View_AllTasks
->appendElement( HTML::make( 'a' ) ->appendElement( HTML::make( 'a' )
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $task->id ) ->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $task->id )
->appendText( $task->title ) ) ); ->appendText( $task->title ) ) );
$cell = array_merge( $cell , $this->generateSpecificLines( $task ) );
array_push( $cell , HTML::make( 'dd' )->append( $this->formatPlaceLineage( $task->item ) ) );
$addedAt = strtotime( $task->added_at ); $addedAt = strtotime( $task->added_at );
$addedAtDate = date( 'd/m/o' , $addedAt ); $addedAtDate = date( 'd/m/o' , $addedAt );
@ -95,6 +80,11 @@ class View_AllTasks
foreach ( $cell as $entry ) { foreach ( $cell as $entry ) {
$entry->setAttribute( 'class' , 'missing-deps' ); $entry->setAttribute( 'class' , 'missing-deps' );
} }
} elseif ( $task->assigned_to !== null ) {
array_push( $cell , HTML::make( 'dd' )->appendText( 'Assigned to ' . $task->assigned_to ) );
foreach ( $cell as $entry ) {
$entry->setAttribute( 'class' , 'assigned' );
}
} elseif ( $task->completed_by !== null ) { } elseif ( $task->completed_by !== null ) {
$completedAt = strtotime( $task->completed_at ); $completedAt = strtotime( $task->completed_at );
$completedAtDate = date( 'd/m/o' , $completedAt ); $completedAtDate = date( 'd/m/o' , $completedAt );
@ -110,6 +100,25 @@ class View_AllTasks
return $cell; return $cell;
} }
protected abstract function generateSpecificLines( $task );
}
class View_AllTasks
extends View_TasksBase
{
public function __construct( $tasks )
{
parent::__construct( );
$this->tasks = $tasks;
}
protected function generateSpecificLines( $task )
{
return array( HTML::make( 'dd' )->append( $this->formatPlaceLineage( $task->item ) ) );
}
private function formatPlaceLineage( $item ) private function formatPlaceLineage( $item )
{ {
$item = Loader::DAO( 'items' )->get( $item ); $item = Loader::DAO( 'items' )->get( $item );
@ -142,47 +151,9 @@ class View_Tasks
} }
protected function generateItem( $task ) protected function generateSpecificLines( $task )
{ {
$cell = array( ); return 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->missing_dependencies !== null ) {
if ( $task->missing_dependencies > 1 ) {
$end = 'ies';
} else {
$end = 'y';
}
array_push( $cell ,
$md = HTML::make( 'dd' )->appendText( "{$task->missing_dependencies} missing dependenc$end" ) );
if ( $task->total_missing_dependencies != $task->missing_dependencies ) {
$md->appendText( " ({$task->total_missing_dependencies} when counting transitive dependencies)" );
}
foreach ( $cell as $entry ) {
$entry->setAttribute( 'class' , 'missing-deps' );
}
} elseif ( $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;
} }
} }
@ -225,6 +196,17 @@ class View_TaskDetails
->appendElement( HTML::make( 'dd' ) ->appendElement( HTML::make( 'dd' )
->appendText( Loader::DAO( 'tasks' ) ->appendText( Loader::DAO( 'tasks' )
->translatePriority( $this->task->priority ) ) ); ->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 { } else {
$list->appendElement( HTML::make( 'dt' ) $list->appendElement( HTML::make( 'dt' )
->appendText( 'Completed:' ) ) ->appendText( 'Completed:' ) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -357,6 +357,9 @@ div.box-buttons a.stop:hover { background-position: -18px -72px }
div.box-buttons a.start { background-position: 0px -90px } div.box-buttons a.start { background-position: 0px -90px }
div.box-buttons a.start:hover { background-position: -18px -90px } div.box-buttons a.start:hover { background-position: -18px -90px }
div.box-buttons a.claim { background-position: 0px -108px }
div.box-buttons a.claim:hover { background-position: -18px -108px }
div.box-buttons a.list-add::after { div.box-buttons a.list-add::after {
content: '+'; content: '+';
font-weight: bold; font-weight: bold;
@ -393,6 +396,10 @@ dl.tasks .missing-deps, dl.tasks .missing-deps * {
color: #7f3f3f color: #7f3f3f
} }
dl.tasks .assigned, dl.tasks .assigned * {
color: #1f7f1f
}
dl.tasks dt.sub-title.completed { dl.tasks dt.sub-title.completed {
color: #bfbfbf; color: #bfbfbf;
text-shadow: 1px 1px 2px #3f3f3f; text-shadow: 1px 1px 2px #3f3f3f;
@ -405,3 +412,8 @@ ul.dep-list a.missing {
ul.dep-list a.satisfied { ul.dep-list a.satisfied {
color: green; color: green;
} }
dt.unassigned-task {
color: #7f3f3f;
font-style: italic;
}