diff options
author | Frédéric Guillot <fred@kanboard.net> | 2014-10-11 21:11:10 -0400 |
---|---|---|
committer | Frédéric Guillot <fred@kanboard.net> | 2014-10-11 21:11:10 -0400 |
commit | acba6839a6082e3e3800a733f8baea7c843fc02e (patch) | |
tree | e2847dcd13d9ccc3d8f0f6f936a5776df852e11b | |
parent | a8418afdebe92dde495bc5010645779c73939b7b (diff) |
Add 3 new fields for tasks: start date, time estimated and time spent
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&action=download&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a> - <span class="task-show-file-actions"> - <?php if ($file['is_image']): ?> - <a href="?controller=file&action=open&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>" class="file-popover"><?= t('open') ?></a>, - <?php endif ?> - <a href="?controller=file&action=confirm&file_id=<?= $file['id'] ?>&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&action=download&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a> + <span class="task-show-file-actions"> + <?php if ($file['is_image']): ?> + <a href="?controller=file&action=open&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>" class="file-popover"><?= t('open') ?></a>, + <?php endif ?> + <a href="?controller=file&action=confirm&file_id=<?= $file['id'] ?>&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']); + } +} |