summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrédéric Guillot <fred@kanboard.net>2014-10-11 21:11:10 -0400
committerFrédéric Guillot <fred@kanboard.net>2014-10-11 21:11:10 -0400
commitacba6839a6082e3e3800a733f8baea7c843fc02e (patch)
treee2847dcd13d9ccc3d8f0f6f936a5776df852e11b
parenta8418afdebe92dde495bc5010645779c73939b7b (diff)
Add 3 new fields for tasks: start date, time estimated and time spent
-rw-r--r--Vagrantfile7
-rw-r--r--app/Controller/Base.php1
-rw-r--r--app/Controller/Subtask.php3
-rw-r--r--app/Controller/Task.php51
-rw-r--r--app/Locales/de_DE/translations.php9
-rw-r--r--app/Locales/es_ES/translations.php9
-rw-r--r--app/Locales/fi_FI/translations.php9
-rw-r--r--app/Locales/fr_FR/translations.php9
-rw-r--r--app/Locales/it_IT/translations.php9
-rw-r--r--app/Locales/pl_PL/translations.php9
-rw-r--r--app/Locales/pt_BR/translations.php9
-rw-r--r--app/Locales/ru_RU/translations.php9
-rw-r--r--app/Locales/sv_SE/translations.php9
-rw-r--r--app/Locales/zh_CN/translations.php9
-rw-r--r--app/Model/Base.php1
-rw-r--r--app/Model/DateParser.php42
-rw-r--r--app/Model/SubTask.php1
-rw-r--r--app/Model/Task.php10
-rw-r--r--app/Model/TaskExport.php20
-rw-r--r--app/Model/TaskValidator.php24
-rw-r--r--app/Model/TimeTracking.php45
-rw-r--r--app/Schema/Mysql.php12
-rw-r--r--app/Schema/Postgres.php12
-rw-r--r--app/Schema/Sqlite.php13
-rw-r--r--app/Templates/file_show.php38
-rw-r--r--app/Templates/subtask_create.php2
-rw-r--r--app/Templates/subtask_edit.php4
-rw-r--r--app/Templates/subtask_show.php22
-rw-r--r--app/Templates/task_details.php15
-rw-r--r--app/Templates/task_new.php3
-rw-r--r--app/Templates/task_public.php2
-rw-r--r--app/Templates/task_show.php15
-rw-r--r--app/Templates/task_time.php15
-rw-r--r--app/Templates/task_timesheet.php13
-rw-r--r--app/helpers.php2
-rw-r--r--assets/css/app.css19
-rw-r--r--tests/functionals.mysql.xml2
-rw-r--r--tests/functionals.postgres.xml2
-rw-r--r--tests/functionals.sqlite.xml2
-rw-r--r--tests/units.postgres.xml1
-rw-r--r--tests/units/TimeTrackingTest.php44
41 files changed, 417 insertions, 116 deletions
diff --git a/Vagrantfile b/Vagrantfile
index 94eb41b8..ced296a1 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -6,7 +6,7 @@ VAGRANTFILE_API_VERSION = "2"
$script = <<SCRIPT
# install packages
apt-get update
-apt-get install -y apache2 php5 php5-sqlite php5-ldap php5-xdebug php5-curl php5-mysql php5-pgsql
+apt-get install -y apache2 php5 libapache2-mod-php5 php5-sqlite php5-ldap php5-xdebug php5-curl php5-mysql php5-pgsql
service apache2 restart
rm -f /var/www/html/index.html
date > /etc/vagrant_provisioned_at
@@ -15,6 +15,11 @@ SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
+ # Virtualbox
+ config.vm.provider "virtualbox" do |v|
+ v.memory = 1024
+ end
+
# Image
config.vm.box = "ubuntu/trusty64"
config.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box"
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index aabb1775..abe702a2 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -34,6 +34,7 @@ use Model\LastLogin;
* @property \Model\TaskValidator $taskValidator
* @property \Model\CommentHistory $commentHistory
* @property \Model\SubtaskHistory $subtaskHistory
+ * @property \Model\TimeTracking $timeTracking
* @property \Model\User $user
* @property \Model\Webhook $webhook
*/
diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php
index da9acbab..48f0d6e2 100644
--- a/app/Controller/Subtask.php
+++ b/app/Controller/Subtask.php
@@ -197,7 +197,8 @@ class Subtask extends Base
$value = array(
'id' => $subtask['id'],
- 'status' => ($subtask['status'] + 1) % 3
+ 'status' => ($subtask['status'] + 1) % 3,
+ 'task_id' => $task['id'],
);
if (! $this->subTask->update($value)) {
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index 163929d2..695e29ec 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -54,15 +54,29 @@ class Task extends Base
public function show()
{
$task = $this->getTask();
+ $subtasks = $this->subTask->getAll($task['id']);
+
+ $values = array(
+ 'id' => $task['id'],
+ 'date_started' => $task['date_started'],
+ 'time_estimated' => $task['time_estimated'] ?: '',
+ 'time_spent' => $task['time_spent'] ?: '',
+ );
+
+ $this->dateParser->format($values, array('date_started'));
$this->response->html($this->taskLayout('task_show', array(
'project' => $this->project->getById($task['project_id']),
'files' => $this->file->getAll($task['id']),
'comments' => $this->comment->getAll($task['id']),
- 'subtasks' => $this->subTask->getAll($task['id']),
+ 'subtasks' => $subtasks,
'task' => $task,
+ 'values' => $values,
+ 'timesheet' => $this->timeTracking->getTaskTimesheet($task, $subtasks),
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
+ 'date_format' => $this->config->get('application_date_format'),
+ 'date_formats' => $this->dateParser->getAvailableFormats(),
'menu' => 'tasks',
'title' => $task['title'],
)));
@@ -155,17 +169,10 @@ class Task extends Base
public function edit()
{
$task = $this->getTask();
-
- if (! empty($task['date_due'])) {
- $task['date_due'] = date($this->config->get('application_date_format'), $task['date_due']);
- }
- else {
- $task['date_due'] = '';
- }
-
- $task['score'] = $task['score'] ?: '';
$ajax = $this->request->isAjax();
+ $this->dateParser->format($task, array('date_due'));
+
$params = array(
'values' => $task,
'errors' => array(),
@@ -209,7 +216,7 @@ class Task extends Base
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
else {
- $this->response->redirect('?controller=task&action=show&task_id='.$values['id']);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
}
else {
@@ -234,6 +241,28 @@ class Task extends Base
}
/**
+ * Update time tracking information
+ *
+ * @access public
+ */
+ public function time()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->taskValidator->validateTimeModification($values);
+
+ if ($valid && $this->task->update($values)) {
+ $this->session->flash(t('Task updated successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to update your task.'));
+ }
+
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
+ }
+
+ /**
* Hide a task
*
* @access public
diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php
index 0aaf5882..fdf96638 100644
--- a/app/Locales/de_DE/translations.php
+++ b/app/Locales/de_DE/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'geschätzt',
'Sub-Tasks' => 'Unteraufgaben',
'Add a sub-task' => 'Unteraufgabe anlegen',
- 'Original Estimate' => 'Geschätzter Aufwand',
+ 'Original estimate' => 'Geschätzter Aufwand',
'Create another sub-task' => 'Weitere Unteraufgabe anlegen',
- 'Time Spent' => 'Aufgewendete Zeit',
+ 'Time spent' => 'Aufgewendete Zeit',
'Edit a sub-task' => 'Unteraufgabe bearbeiten',
'Remove a sub-task' => 'Unteraufgabe löschen',
'The time must be a numeric value' => 'Zeit nur als nummerische Angabe',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php
index 95e94075..368a11ee 100644
--- a/app/Locales/es_ES/translations.php
+++ b/app/Locales/es_ES/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'estimado',
'Sub-Tasks' => 'Sub-Tareas',
'Add a sub-task' => 'Añadir una sub-tarea',
- 'Original Estimate' => 'Estimado Original',
+ 'Original estimate' => 'Estimado Original',
'Create another sub-task' => 'Crear otra sub-tarea',
- 'Time Spent' => 'Tiempo Transcurrido',
+ 'Time spent' => 'Tiempo Transcurrido',
'Edit a sub-task' => 'Editar una sub-tarea',
'Remove a sub-task' => 'Suprimir una sub-tarea',
'The time must be a numeric value' => 'El tiempo debe de ser un valor numérico',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/fi_FI/translations.php b/app/Locales/fi_FI/translations.php
index 4364ddc5..2a2f3e96 100644
--- a/app/Locales/fi_FI/translations.php
+++ b/app/Locales/fi_FI/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'estimoitu',
'Sub-Tasks' => 'Alitehtävät',
'Add a sub-task' => 'Lisää alitehtävä',
- 'Original Estimate' => 'Alkuperäinen estimaatti',
+ 'Original estimate' => 'Alkuperäinen estimaatti',
'Create another sub-task' => 'Lisää toinen alitehtävä',
- 'Time Spent' => 'Käytetty aika',
+ 'Time spent' => 'Käytetty aika',
'Edit a sub-task' => 'Muokkaa alitehtävää',
'Remove a sub-task' => 'Poista alitehtävä',
'The time must be a numeric value' => 'Ajan pitää olla numero',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php
index 573b8e89..49e6bc2f 100644
--- a/app/Locales/fr_FR/translations.php
+++ b/app/Locales/fr_FR/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'estimé',
'Sub-Tasks' => 'Sous-Tâches',
'Add a sub-task' => 'Ajouter une sous-tâche',
- 'Original Estimate' => 'Estimation originale',
+ 'Original estimate' => 'Estimation originale',
'Create another sub-task' => 'Créer une autre sous-tâche',
- 'Time Spent' => 'Temps passé',
+ 'Time spent' => 'Temps passé',
'Edit a sub-task' => 'Modifier une sous-tâche',
'Remove a sub-task' => 'Supprimer une sous-tâche',
'The time must be a numeric value' => 'Le temps doit-être une valeur numérique',
@@ -538,4 +538,9 @@ return array(
'This project is private' => 'Ce projet est privé',
'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici',
'Add' => 'Ajouter',
+ 'Estimated time: %s hours' => 'Temps estimé: %s hours',
+ 'Time spent: %s hours' => 'Temps passé : %s heures',
+ 'Started on %B %e, %Y' => 'Commençé le %d/%m/%Y',
+ 'Start date' => 'Date de début',
+ 'Time estimated' => 'Temps estimé',
);
diff --git a/app/Locales/it_IT/translations.php b/app/Locales/it_IT/translations.php
index a7946872..9f532438 100644
--- a/app/Locales/it_IT/translations.php
+++ b/app/Locales/it_IT/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'stimate',
'Sub-Tasks' => 'Sotto-compiti',
'Add a sub-task' => 'Aggiungere un sotto-compito',
- 'Original Estimate' => 'Stima originale',
+ 'Original estimate' => 'Stima originale',
'Create another sub-task' => 'Creare un altro sotto-compito',
- 'Time Spent' => 'Tempo Trascorso',
+ 'Time spent' => 'Tempo Trascorso',
'Edit a sub-task' => 'Modificare un sotto-compito',
'Remove a sub-task' => 'Cancellare un sotto-compito',
'The time must be a numeric value' => 'Il tempo deve essere un valore numerico',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php
index d2eff65d..f2e96bb6 100644
--- a/app/Locales/pl_PL/translations.php
+++ b/app/Locales/pl_PL/translations.php
@@ -349,9 +349,9 @@ return array(
// 'estimated' => '',
// 'Sub-Tasks' => '',
// 'Add a sub-task' => '',
- // 'Original Estimate' => '',
+ // 'Original estimate' => '',
// 'Create another sub-task' => '',
- // 'Time Spent' => '',
+ // 'Time spent' => '',
// 'Edit a sub-task' => '',
// 'Remove a sub-task' => '',
// 'The time must be a numeric value' => '',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php
index 2093af95..3f208361 100644
--- a/app/Locales/pt_BR/translations.php
+++ b/app/Locales/pt_BR/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'estimada',
'Sub-Tasks' => 'Sub-tarefas',
'Add a sub-task' => 'Adicionar uma sub-tarefa',
- 'Original Estimate' => 'Estimativa original',
+ 'Original estimate' => 'Estimativa original',
'Create another sub-task' => 'Criar uma outra sub-tarefa',
- 'Time Spent' => 'Tempo gasto',
+ 'Time spent' => 'Tempo gasto',
'Edit a sub-task' => 'Editar uma sub-tarefa',
'Remove a sub-task' => 'Remover uma sub-tarefa',
'The time must be a numeric value' => 'O tempo deve ser um valor numérico',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/ru_RU/translations.php b/app/Locales/ru_RU/translations.php
index 12a5bfe6..5c634184 100644
--- a/app/Locales/ru_RU/translations.php
+++ b/app/Locales/ru_RU/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'расчетное',
'Sub-Tasks' => 'Подзадачи',
'Add a sub-task' => 'Добавить подзадачу',
- 'Original Estimate' => 'Начальная оценка',
+ 'Original estimate' => 'Начальная оценка',
'Create another sub-task' => 'Создать другую подзадачу',
- 'Time Spent' => 'Времени затрачено',
+ 'Time spent' => 'Времени затрачено',
'Edit a sub-task' => 'Изменить подзадачу',
'Remove a sub-task' => 'Удалить подзадачу',
'The time must be a numeric value' => 'Время должно быть числом!',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/sv_SE/translations.php b/app/Locales/sv_SE/translations.php
index 96e4530d..a96b6c9a 100644
--- a/app/Locales/sv_SE/translations.php
+++ b/app/Locales/sv_SE/translations.php
@@ -349,9 +349,9 @@ return array(
'estimated' => 'uppskattat',
'Sub-Tasks' => 'Deluppgifter',
'Add a sub-task' => 'Lägg till deluppgift',
- 'Original Estimate' => 'Ursprunglig uppskattning',
+ 'Original estimate' => 'Ursprunglig uppskattning',
'Create another sub-task' => 'Skapa en till deluppgift',
- 'Time Spent' => 'Nedlagd tid',
+ 'Time spent' => 'Nedlagd tid',
'Edit a sub-task' => 'Ändra en deluppgift',
'Remove a sub-task' => 'Ta bort en deluppgift',
'The time must be a numeric value' => 'Tiden måste ha ett numeriskt värde',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Locales/zh_CN/translations.php b/app/Locales/zh_CN/translations.php
index c921eb47..ea7183c7 100644
--- a/app/Locales/zh_CN/translations.php
+++ b/app/Locales/zh_CN/translations.php
@@ -349,9 +349,9 @@ return array(
// 'estimated' => '',
// 'Sub-Tasks' => '',
'Add a sub-task' => '添加一个子任务',
- 'Original Estimate' => '初步预计耗时',
+ 'Original estimate' => '初步预计耗时',
'Create another sub-task' => '创建另一个子任务',
- // 'Time Spent' => '',
+ // 'Time spent' => '',
// 'Edit a sub-task' => '',
// 'Remove a sub-task' => '',
// 'The time must be a numeric value' => '',
@@ -538,4 +538,9 @@ return array(
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // 'Start date' => '',
+ // 'Time estimated' => '',
);
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 2b13e815..ea4afc07 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -34,6 +34,7 @@ use PicoDb\Database;
* @property \Model\TaskExport $taskExport
* @property \Model\TaskHistory $taskHistory
* @property \Model\TaskValidator $taskValidator
+ * @property \Model\TimeTracking $timeTracking
* @property \Model\User $user
* @property \Model\Webhook $webhook
*/
diff --git a/app/Model/DateParser.php b/app/Model/DateParser.php
index 88e67686..38265f98 100644
--- a/app/Model/DateParser.php
+++ b/app/Model/DateParser.php
@@ -97,4 +97,46 @@ class DateParser extends Base
{
return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
}
+
+ /**
+ * Format date (form display)
+ *
+ * @access public
+ * @param array $values Database values
+ * @param array $fields Date fields
+ * @param string $format Date format
+ */
+ public function format(array &$values, array $fields, $format = '')
+ {
+ if ($format === '') {
+ $format = $this->config->get('application_date_format');
+ }
+
+ foreach ($fields as $field) {
+
+ if (! empty($values[$field])) {
+ $values[$field] = date($format, $values[$field]);
+ }
+ else {
+ $values[$field] = '';
+ }
+ }
+ }
+
+ /**
+ * Convert date (form input data)
+ *
+ * @access public
+ * @param array $values Database values
+ * @param array $fields Date fields
+ */
+ public function convert(array &$values, array $fields)
+ {
+ foreach ($fields as $field) {
+
+ if (! empty($values[$field]) && ! is_numeric($values[$field])) {
+ $values[$field] = $this->getTimestamp($values[$field]);
+ }
+ }
+ }
}
diff --git a/app/Model/SubTask.php b/app/Model/SubTask.php
index 0126030a..886ad1f3 100644
--- a/app/Model/SubTask.php
+++ b/app/Model/SubTask.php
@@ -82,6 +82,7 @@ class SubTask extends Base
->eq('task_id', $task_id)
->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name')
->join(User::TABLE, 'id', 'user_id')
+ ->asc(self::TABLE.'.id')
->findAll();
foreach ($subtasks as &$subtask) {
diff --git a/app/Model/Task.php b/app/Model/Task.php
index 54dc0a2f..fe7bbdcd 100644
--- a/app/Model/Task.php
+++ b/app/Model/Task.php
@@ -89,6 +89,9 @@ class Task extends Base
tasks.date_completed,
tasks.date_modification,
tasks.date_due,
+ tasks.date_started,
+ tasks.time_estimated,
+ tasks.time_spent,
tasks.color_id,
tasks.project_id,
tasks.column_id,
@@ -363,12 +366,9 @@ class Task extends Base
*/
public function prepare(array &$values)
{
- if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) {
- $values['date_due'] = $this->dateParser->getTimestamp($values['date_due']);
- }
-
+ $this->dateParser->convert($values, array('date_due', 'date_started'));
$this->removeFields($values, array('another_task', 'id'));
- $this->resetFields($values, array('date_due', 'score', 'category_id'));
+ $this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
$this->convertIntegerFields($values, array('is_active'));
}
diff --git a/app/Model/TaskExport.php b/app/Model/TaskExport.php
index 815f5997..b929823e 100644
--- a/app/Model/TaskExport.php
+++ b/app/Model/TaskExport.php
@@ -27,7 +27,7 @@ class TaskExport extends Base
$results = array($this->getColumns());
foreach ($tasks as &$task) {
- $results[] = array_values($this->formatOutput($task));
+ $results[] = array_values($this->format($task));
}
return $results;
@@ -60,7 +60,10 @@ class TaskExport extends Base
tasks.title,
tasks.date_creation,
tasks.date_modification,
- tasks.date_completed
+ tasks.date_completed,
+ tasks.date_started,
+ tasks.time_estimated,
+ tasks.time_spent
FROM tasks
LEFT JOIN users ON users.id = tasks.owner_id
LEFT JOIN users AS creators ON creators.id = tasks.creator_id
@@ -89,16 +92,14 @@ class TaskExport extends Base
* @param array $task Task properties
* @return array
*/
- public function formatOutput(array &$task)
+ public function format(array &$task)
{
$colors = $this->color->getList();
- $task['score'] = $task['score'] ?: '';
+
$task['is_active'] = $task['is_active'] == Task::STATUS_OPEN ? e('Open') : e('Closed');
$task['color_id'] = $colors[$task['color_id']];
- $task['date_creation'] = date('Y-m-d', $task['date_creation']);
- $task['date_due'] = $task['date_due'] ? date('Y-m-d', $task['date_due']) : '';
- $task['date_modification'] = $task['date_modification'] ? date('Y-m-d', $task['date_modification']) : '';
- $task['date_completed'] = $task['date_completed'] ? date('Y-m-d', $task['date_completed']) : '';
+
+ $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d');
return $task;
}
@@ -127,6 +128,9 @@ class TaskExport extends Base
e('Creation date'),
e('Modification date'),
e('Completion date'),
+ e('Start date'),
+ e('Time estimated'),
+ e('Time spent'),
);
}
}
diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php
index 1c7b0b14..816008cf 100644
--- a/app/Model/TaskValidator.php
+++ b/app/Model/TaskValidator.php
@@ -31,6 +31,9 @@ class TaskValidator extends Base
new Validators\Integer('category_id', t('This value must be an integer')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()),
+ new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateFormats()),
+ new Validators\Numeric('time_spent', t('This value must be numeric')),
+ new Validators\Numeric('time_estimated', t('This value must be numeric')),
);
}
@@ -189,4 +192,25 @@ class TaskValidator extends Base
$v->getErrors()
);
}
+
+ /**
+ * Validate time tracking modification (form)
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateTimeModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The id is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
}
diff --git a/app/Model/TimeTracking.php b/app/Model/TimeTracking.php
new file mode 100644
index 00000000..4ddddf12
--- /dev/null
+++ b/app/Model/TimeTracking.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Model;
+
+/**
+ * Time tracking model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TimeTracking extends Base
+{
+ /**
+ * Calculate time metrics for a task
+ *
+ * Use subtasks time metrics if not empty otherwise return task time metrics
+ *
+ * @access public
+ * @param array $task Task properties
+ * @param array $subtasks Subtasks list
+ * @return array
+ */
+ public function getTaskTimesheet(array $task, array $subtasks)
+ {
+ $timesheet = array(
+ 'time_spent' => 0,
+ 'time_estimated' => 0,
+ 'time_remaining' => 0,
+ );
+
+ foreach ($subtasks as &$subtask) {
+ $timesheet['time_estimated'] += $subtask['time_estimated'];
+ $timesheet['time_spent'] += $subtask['time_spent'];
+ }
+
+ if ($timesheet['time_estimated'] == 0 && $timesheet['time_spent'] == 0) {
+ $timesheet['time_estimated'] = $task['time_estimated'];
+ $timesheet['time_spent'] = $task['time_spent'];
+ }
+
+ $timesheet['time_remaining'] = $timesheet['time_estimated'] - $timesheet['time_spent'];
+
+ return $timesheet;
+ }
+}
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index c7293a43..7a0b2cd1 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -5,7 +5,17 @@ namespace Schema;
use PDO;
use Core\Security;
-const VERSION = 31;
+const VERSION = 32;
+
+function version_32($pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN date_started INTEGER");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN time_spent FLOAT DEFAULT 0");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN time_estimated FLOAT DEFAULT 0");
+
+ $pdo->exec("ALTER TABLE task_has_subtasks MODIFY time_estimated FLOAT");
+ $pdo->exec("ALTER TABLE task_has_subtasks MODIFY time_spent FLOAT");
+}
function version_31($pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index fe2cce54..e42e8e72 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -5,7 +5,17 @@ namespace Schema;
use PDO;
use Core\Security;
-const VERSION = 12;
+const VERSION = 13;
+
+function version_13($pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN date_started INTEGER");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN time_spent FLOAT DEFAULT 0");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN time_estimated FLOAT DEFAULT 0");
+
+ $pdo->exec("ALTER TABLE task_has_subtasks ALTER COLUMN time_estimated TYPE FLOAT");
+ $pdo->exec("ALTER TABLE task_has_subtasks ALTER COLUMN time_spent TYPE FLOAT");
+}
function version_12($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index cefe8ab5..45a6fb22 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -5,7 +5,14 @@ namespace Schema;
use Core\Security;
use PDO;
-const VERSION = 31;
+const VERSION = 32;
+
+function version_32($pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN date_started INTEGER");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN time_spent FLOAT DEFAULT 0");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN time_estimated FLOAT DEFAULT 0");
+}
function version_31($pdo)
{
@@ -169,8 +176,8 @@ function version_18($pdo)
id INTEGER PRIMARY KEY,
title TEXT COLLATE NOCASE,
status INTEGER DEFAULT 0,
- time_estimated INTEGER DEFAULT 0,
- time_spent INTEGER DEFAULT 0,
+ time_estimated NUMERIC DEFAULT 0,
+ time_spent NUMERIC DEFAULT 0,
task_id INTEGER NOT NULL,
user_id INTEGER,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
diff --git a/app/Templates/file_show.php b/app/Templates/file_show.php
index b570de2d..3832a0f5 100644
--- a/app/Templates/file_show.php
+++ b/app/Templates/file_show.php
@@ -1,17 +1,23 @@
-<div class="page-header">
- <h2><?= t('Attachments') ?></h2>
-</div>
+<?php if (! empty($files)): ?>
+<div id="attachments" class="task-show-section">
+
+ <div class="page-header">
+ <h2><?= t('Attachments') ?></h2>
+ </div>
-<ul class="task-show-files">
-<?php foreach ($files as $file): ?>
- <li>
- <a href="?controller=file&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
- <span class="task-show-file-actions">
- <?php if ($file['is_image']): ?>
- <a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="file-popover"><?= t('open') ?></a>,
- <?php endif ?>
- <a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('remove') ?></a>
- </span>
- </li>
-<?php endforeach ?>
-</ul> \ No newline at end of file
+ <ul class="task-show-files">
+ <?php foreach ($files as $file): ?>
+ <li>
+ <a href="?controller=file&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
+ <span class="task-show-file-actions">
+ <?php if ($file['is_image']): ?>
+ <a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="file-popover"><?= t('open') ?></a>,
+ <?php endif ?>
+ <a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('remove') ?></a>
+ </span>
+ </li>
+ <?php endforeach ?>
+ </ul>
+
+</div>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Templates/subtask_create.php b/app/Templates/subtask_create.php
index f1b27ab9..c8ee556b 100644
--- a/app/Templates/subtask_create.php
+++ b/app/Templates/subtask_create.php
@@ -14,7 +14,7 @@
<?= Helper\form_label(t('Assignee'), 'user_id') ?>
<?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/>
- <?= Helper\form_label(t('Original Estimate'), 'time_estimated') ?>
+ <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?>
<?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
<?= Helper\form_checkbox('another_subtask', t('Create another sub-task'), 1, isset($values['another_subtask']) && $values['another_subtask'] == 1) ?>
diff --git a/app/Templates/subtask_edit.php b/app/Templates/subtask_edit.php
index fc65d3b3..91690d0a 100644
--- a/app/Templates/subtask_edit.php
+++ b/app/Templates/subtask_edit.php
@@ -18,10 +18,10 @@
<?= Helper\form_label(t('Assignee'), 'user_id') ?>
<?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/>
- <?= Helper\form_label(t('Original Estimate'), 'time_estimated') ?>
+ <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?>
<?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
- <?= Helper\form_label(t('Time Spent'), 'time_spent') ?>
+ <?= Helper\form_label(t('Time spent'), 'time_spent') ?>
<?= Helper\form_numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/>
<div class="form-actions">
diff --git a/app/Templates/subtask_show.php b/app/Templates/subtask_show.php
index 112262bd..f1b0466f 100644
--- a/app/Templates/subtask_show.php
+++ b/app/Templates/subtask_show.php
@@ -5,14 +5,6 @@
<h2><?= t('Sub-Tasks') ?></h2>
</div>
- <?php
-
- $total_spent = 0;
- $total_estimated = 0;
- $total_remaining = 0;
-
- ?>
-
<table class="subtasks-table">
<tr>
<th width="40%"><?= t('Title') ?></th>
@@ -64,11 +56,6 @@
</td>
<?php endif ?>
</tr>
- <?php
- $total_estimated += $subtask['time_estimated'];
- $total_spent += $subtask['time_spent'];
- $total_remaining = $total_estimated - $total_spent;
- ?>
<?php endforeach ?>
</table>
@@ -81,14 +68,5 @@
</form>
<?php endif ?>
- <div class="subtasks-time-tracking">
- <h4><?= t('Time tracking') ?></h4>
- <ul>
- <li><?= t('Estimate:') ?> <strong><?= Helper\escape($total_estimated) ?></strong> <?= t('hours') ?></li>
- <li><?= t('Spent:') ?> <strong><?= Helper\escape($total_spent) ?></strong> <?= t('hours') ?></li>
- <li><?= t('Remaining:') ?> <strong><?= Helper\escape($total_remaining > 0 ? $total_remaining : 0) ?></strong> <?= t('hours') ?></li>
- </ul>
- </div>
-
</div>
<?php endif ?>
diff --git a/app/Templates/task_details.php b/app/Templates/task_details.php
index 8766beac..a4fdf6ce 100644
--- a/app/Templates/task_details.php
+++ b/app/Templates/task_details.php
@@ -22,11 +22,26 @@
<?= dt('Completed on %B %e, %Y at %k:%M %p', $task['date_completed']) ?>
</li>
<?php endif ?>
+ <?php if ($task['date_started']): ?>
+ <li>
+ <?= dt('Started on %B %e, %Y', $task['date_started']) ?>
+ </li>
+ <?php endif ?>
<?php if ($task['date_due']): ?>
<li>
<strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong>
</li>
<?php endif ?>
+ <?php if ($task['time_estimated']): ?>
+ <li>
+ <?= t('Estimated time: %s hours', $task['time_estimated']) ?>
+ </li>
+ <?php endif ?>
+ <?php if ($task['time_spent']): ?>
+ <li>
+ <?= t('Time spent: %s hours', $task['time_spent']) ?>
+ </li>
+ <?php endif ?>
<?php if ($task['creator_username']): ?>
<li>
<?= t('Created by %s', $task['creator_name'] ?: $task['creator_username']) ?>
diff --git a/app/Templates/task_new.php b/app/Templates/task_new.php
index 867bcbc9..51142165 100644
--- a/app/Templates/task_new.php
+++ b/app/Templates/task_new.php
@@ -38,6 +38,9 @@
<?= Helper\form_label(t('Complexity'), 'score') ?>
<?= Helper\form_number('score', $values, $errors) ?><br/>
+ <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?>
+ <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
+
<?= Helper\form_label(t('Due Date'), 'date_due') ?>
<?= Helper\form_text('date_due', $values, $errors, array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?><br/>
<div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
diff --git a/app/Templates/task_public.php b/app/Templates/task_public.php
index 4578b720..bc4608d1 100644
--- a/app/Templates/task_public.php
+++ b/app/Templates/task_public.php
@@ -2,6 +2,8 @@
<?= Helper\template('task_details', array('task' => $task, 'project' => $project)) ?>
+ <p class="align-right"><?= Helper\a(t('Back to the board'), 'board', 'readonly', array('token' => $project['token'])) ?></p>
+
<?= Helper\template('task_show_description', array('task' => $task)) ?>
<?= Helper\template('subtask_show', array('task' => $task, 'subtasks' => $subtasks, 'not_editable' => true)) ?>
diff --git a/app/Templates/task_show.php b/app/Templates/task_show.php
index ece4c57c..0964a8f0 100644
--- a/app/Templates/task_show.php
+++ b/app/Templates/task_show.php
@@ -1,14 +1,7 @@
-
<?= Helper\template('task_details', array('task' => $task, 'project' => $project)) ?>
-
+<?= Helper\template('task_time', array('values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?>
<?= Helper\template('task_show_description', array('task' => $task)) ?>
-
<?= Helper\template('subtask_show', array('task' => $task, 'subtasks' => $subtasks)) ?>
-
-<?php if (! empty($files)): ?>
-<div id="attachments" class="task-show-section">
- <?= Helper\template('file_show', array('task' => $task, 'files' => $files)) ?>
-</div>
-<?php endif ?>
-
-<?= Helper\template('task_comments', array('task' => $task, 'comments' => $comments)) ?>
+<?= Helper\template('task_timesheet', array('timesheet' => $timesheet)) ?>
+<?= Helper\template('file_show', array('task' => $task, 'files' => $files)) ?>
+<?= Helper\template('task_comments', array('task' => $task, 'comments' => $comments)) ?> \ No newline at end of file
diff --git a/app/Templates/task_time.php b/app/Templates/task_time.php
new file mode 100644
index 00000000..11a76303
--- /dev/null
+++ b/app/Templates/task_time.php
@@ -0,0 +1,15 @@
+<form method="post" action="<?= Helper\u('task', 'time', array('task_id' => $values['id'])) ?>" class="form-inline task-time-form" autocomplete="off">
+ <?= Helper\form_csrf() ?>
+ <?= Helper\form_hidden('id', $values) ?>
+
+ <?= Helper\form_label(t('Start date'), 'date_started') ?>
+ <?= Helper\form_text('date_started', $values, array(), array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?>
+
+ <?= Helper\form_label(t('Time estimated'), 'time_estimated') ?>
+ <?= Helper\form_numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?>
+
+ <?= Helper\form_label(t('Time spent'), 'time_spent') ?>
+ <?= Helper\form_numeric('time_spent', $values, array(), array('placeholder="'.t('hours').'"')) ?>
+
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+</form> \ No newline at end of file
diff --git a/app/Templates/task_timesheet.php b/app/Templates/task_timesheet.php
new file mode 100644
index 00000000..cd093657
--- /dev/null
+++ b/app/Templates/task_timesheet.php
@@ -0,0 +1,13 @@
+<?php if ($timesheet['time_estimated'] > 0 || $timesheet['time_spent'] > 0): ?>
+
+<div class="page-header">
+ <h2><?= t('Time tracking') ?></h2>
+</div>
+
+<ul class="listing">
+ <li><?= t('Estimate:') ?> <strong><?= Helper\escape($timesheet['time_estimated']) ?></strong> <?= t('hours') ?></li>
+ <li><?= t('Spent:') ?> <strong><?= Helper\escape($timesheet['time_spent']) ?></strong> <?= t('hours') ?></li>
+ <li><?= t('Remaining:') ?> <strong><?= Helper\escape($timesheet['time_remaining']) ?></strong> <?= t('hours') ?></li>
+</ul>
+
+<?php endif ?> \ No newline at end of file
diff --git a/app/helpers.php b/app/helpers.php
index 1ab8586a..622d0ce3 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -589,4 +589,4 @@ function u($controller, $action, array $params = array(), $csrf = false)
}
return $html;
-} \ No newline at end of file
+}
diff --git a/assets/css/app.css b/assets/css/app.css
index b4f8f6a6..45b550a6 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -26,6 +26,10 @@ body {
text-rendering: optimizeLegibility;
}
+.align-right {
+ text-align: right;
+}
+
/* links */
a {
color: #3366CC;
@@ -841,6 +845,12 @@ a.task-board-nobody {
margin-top: 10px;
}
+.task-time-form {
+ margin-top: 10px;
+ margin-bottom: 25px;
+ padding: 3px;
+}
+
/* comments */
.comment {
margin-bottom: 20px;
@@ -918,15 +928,6 @@ a.task-board-nobody {
vertical-align: middle;
}
-.subtasks-time-tracking h4 {
- margin-bottom: 5px;
-}
-
-.subtasks-time-tracking li {
- list-style-type: square;
- margin-left: 30px;
-}
-
/* markdown content */
.markdown {
line-height: 1.4em;
diff --git a/tests/functionals.mysql.xml b/tests/functionals.mysql.xml
index aa5b6ec1..f667cafa 100644
--- a/tests/functionals.mysql.xml
+++ b/tests/functionals.mysql.xml
@@ -5,7 +5,7 @@
</testsuite>
</testsuites>
<php>
- <const name="API_URL" value="http://localhost:8080/jsonrpc.php" />
+ <const name="API_URL" value="http://localhost:8000/jsonrpc.php" />
<const name="API_KEY" value="19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929" />
<const name="DB_DRIVER" value="mysql" />
<const name="DB_NAME" value="kanboard" />
diff --git a/tests/functionals.postgres.xml b/tests/functionals.postgres.xml
index ecfe72ef..38904d1a 100644
--- a/tests/functionals.postgres.xml
+++ b/tests/functionals.postgres.xml
@@ -5,7 +5,7 @@
</testsuite>
</testsuites>
<php>
- <const name="API_URL" value="http://localhost:8080/jsonrpc.php" />
+ <const name="API_URL" value="http://localhost:8000/jsonrpc.php" />
<const name="API_KEY" value="19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929" />
<const name="DB_DRIVER" value="postgres" />
<const name="DB_NAME" value="kanboard" />
diff --git a/tests/functionals.sqlite.xml b/tests/functionals.sqlite.xml
index 62aa94c3..bf5d4117 100644
--- a/tests/functionals.sqlite.xml
+++ b/tests/functionals.sqlite.xml
@@ -5,7 +5,7 @@
</testsuite>
</testsuites>
<php>
- <const name="API_URL" value="http://localhost:8080/jsonrpc.php" />
+ <const name="API_URL" value="http://localhost:8000/jsonrpc.php" />
<const name="API_KEY" value="19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929" />
<const name="DB_DRIVER" value="sqlite" />
<const name="DB_FILENAME" value="data/db.sqlite" />
diff --git a/tests/units.postgres.xml b/tests/units.postgres.xml
index 0583c7f8..22e09c03 100644
--- a/tests/units.postgres.xml
+++ b/tests/units.postgres.xml
@@ -7,6 +7,7 @@
<php>
<const name="DB_DRIVER" value="postgres" />
<const name="DB_USERNAME" value="postgres" />
+ <const name="DB_PASSWORD" value="postgres" />
<const name="DB_NAME" value="kanboard_unit_test" />
</php>
</phpunit> \ No newline at end of file
diff --git a/tests/units/TimeTrackingTest.php b/tests/units/TimeTrackingTest.php
new file mode 100644
index 00000000..50d5cb53
--- /dev/null
+++ b/tests/units/TimeTrackingTest.php
@@ -0,0 +1,44 @@
+<?php
+
+require_once __DIR__.'/Base.php';
+
+use Model\SubTask;
+use Model\Task;
+use Model\Project;
+use Model\TimeTracking;
+
+class TimeTrackingTest extends Base
+{
+ public function testCalculateTime()
+ {
+ $t = new Task($this->registry);
+ $p = new Project($this->registry);
+ $s = new SubTask($this->registry);
+ $ts = new TimeTracking($this->registry);
+
+ $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'time_estimated' => 4.5)));
+ $this->assertTrue($t->update(array('id' => 1, 'time_spent' => 3.5)));
+
+ $task = $t->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(4.5, $task['time_estimated']);
+ $this->assertEquals(3.5, $task['time_spent']);
+
+ $timesheet = $ts->getTaskTimesheet($task, array());
+ $this->assertNotEmpty($timesheet);
+ $this->assertEquals(4.5, $timesheet['time_estimated']);
+ $this->assertEquals(3.5, $timesheet['time_spent']);
+ $this->assertEquals(1, $timesheet['time_remaining']);
+
+ // Subtasks calculation
+ $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'time_estimated' => 5.5, 'time_spent' => 3)));
+ $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_estimated' => '', 'time_spent' => 4)));
+
+ $timesheet = $ts->getTaskTimesheet($task, $s->getAll(1));
+ $this->assertNotEmpty($timesheet);
+ $this->assertEquals(5.5, $timesheet['time_estimated']);
+ $this->assertEquals(7, $timesheet['time_spent']);
+ $this->assertEquals(-1.5, $timesheet['time_remaining']);
+ }
+}