diff options
-rw-r--r-- | app/Controller/Base.php | 21 | ||||
-rw-r--r-- | app/Controller/Category.php | 19 | ||||
-rw-r--r-- | app/Controller/Project.php | 84 | ||||
-rw-r--r-- | app/Core/Cli.php | 75 | ||||
-rw-r--r-- | app/Core/Registry.php | 1 | ||||
-rw-r--r-- | app/Core/Response.php | 21 | ||||
-rw-r--r-- | app/Core/Tool.php | 34 | ||||
-rw-r--r-- | app/Core/Translator.php | 26 | ||||
-rw-r--r-- | app/Locales/de_DE/translations.php | 9 | ||||
-rw-r--r-- | app/Locales/es_ES/translations.php | 11 | ||||
-rw-r--r-- | app/Locales/fr_FR/translations.php | 11 | ||||
-rw-r--r-- | app/Locales/pl_PL/translations.php | 11 | ||||
-rw-r--r-- | app/Locales/pt_BR/translations.php | 11 | ||||
-rw-r--r-- | app/Locales/sv_SE/translations.php | 13 | ||||
-rw-r--r-- | app/Locales/zh_CN/translations.php | 11 | ||||
-rw-r--r-- | app/Model/Task.php | 113 | ||||
-rw-r--r-- | app/Templates/project_export.php | 33 | ||||
-rw-r--r-- | app/Templates/project_index.php | 3 | ||||
-rw-r--r-- | app/Templates/task_edit.php | 4 | ||||
-rw-r--r-- | app/Templates/task_layout.php | 2 | ||||
-rw-r--r-- | app/Templates/task_new.php | 4 | ||||
-rw-r--r-- | assets/js/app.js | 39 | ||||
-rwxr-xr-x | kanboard | 51 | ||||
-rw-r--r-- | tests/functionals/ApiTest.php | 1 | ||||
-rw-r--r-- | tests/units/TaskTest.php | 36 |
25 files changed, 555 insertions, 89 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 8890db4c..462529b1 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -246,4 +246,25 @@ abstract class Base return $task; } + + /** + * Common method to get a project + * + * @access protected + * @return array + */ + protected function getProject() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->checkProjectPermissions($project['id']); + + return $project; + } } diff --git a/app/Controller/Category.php b/app/Controller/Category.php index 9e2bcdbb..5fd59c0a 100644 --- a/app/Controller/Category.php +++ b/app/Controller/Category.php @@ -11,25 +11,6 @@ namespace Controller; class Category extends Base { /** - * Get the current project (common method between actions) - * - * @access private - * @return array - */ - private function getProject() - { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - return $project; - } - - /** * Get the category (common method between actions) * * @access private diff --git a/app/Controller/Project.php b/app/Controller/Project.php index 0de67691..8c21801b 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -3,6 +3,7 @@ namespace Controller; use Model\Task as TaskModel; +use Core\Translator; /** * Project controller @@ -13,30 +14,54 @@ use Model\Task as TaskModel; class Project extends Base { /** + * Task export + * + * @access public + */ + public function export() + { + $project = $this->getProject(); + $from = $this->request->getStringParam('from'); + $to = $this->request->getStringParam('to'); + + if ($from && $to) { + Translator::disableEscaping(); + $data = $this->task->export($project['id'], $from, $to); + $this->response->forceDownload('Export_'.date('Y_m_d_H_i_S').'.csv'); + $this->response->csv($data); + } + + $this->response->html($this->template->layout('project_export', array( + 'values' => array( + 'controller' => 'project', + 'action' => 'export', + 'project_id' => $project['id'], + 'from' => $from, + 'to' => $to, + ), + 'errors' => array(), + 'menu' => 'projects', + 'project' => $project, + 'title' => t('Tasks Export') + ))); + } + + /** * Task search for a given project * * @access public */ public function search() { - $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->getProject(); $search = $this->request->getStringParam('search'); - - $project = $this->project->getById($project_id); $tasks = array(); $nb_tasks = 0; - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->checkProjectPermissions($project['id']); - if ($search !== '') { $filters = array( - array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id), + array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']), 'or' => array( array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'), //array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'), @@ -58,7 +83,7 @@ class Project extends Base ), 'menu' => 'projects', 'project' => $project, - 'columns' => $this->board->getColumnsList($project_id), + 'columns' => $this->board->getColumnsList($project['id']), 'categories' => $this->category->getList($project['id'], false), 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') ))); @@ -71,18 +96,10 @@ class Project extends Base */ public function tasks() { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->checkProjectPermissions($project['id']); + $project = $this->getProject(); $filters = array( - array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id), + array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']), array('column' => 'is_active', 'operator' => 'eq', 'value' => TaskModel::STATUS_CLOSED), ); @@ -92,7 +109,7 @@ class Project extends Base $this->response->html($this->template->layout('project_tasks', array( 'menu' => 'projects', 'project' => $project, - 'columns' => $this->board->getColumnsList($project_id), + 'columns' => $this->board->getColumnsList($project['id']), 'categories' => $this->category->getList($project['id'], false), 'tasks' => $tasks, 'nb_tasks' => $nb_tasks, @@ -169,12 +186,7 @@ class Project extends Base */ public function edit() { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } + $project = $this->getProject(); $this->response->html($this->template->layout('project_edit', array( 'errors' => array(), @@ -220,12 +232,7 @@ class Project extends Base */ public function confirm() { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } + $project = $this->getProject(); $this->response->html($this->template->layout('project_remove', array( 'project' => $project, @@ -298,12 +305,7 @@ class Project extends Base */ public function users() { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } + $project = $this->getProject(); $this->response->html($this->template->layout('project_users', array( 'project' => $project, diff --git a/app/Core/Cli.php b/app/Core/Cli.php new file mode 100644 index 00000000..13533b9a --- /dev/null +++ b/app/Core/Cli.php @@ -0,0 +1,75 @@ +<?php + +namespace Core; + +use Closure; + +/** + * CLI class + * + * @package core + * @author Frederic Guillot + */ +class Cli +{ + /** + * Default command name + * + * @access public + * @var string + */ + public $default_command = 'help'; + + /** + * List of registered commands + * + * @access private + * @var array + */ + private $commands = array(); + + /** + * + * + * @access public + * @param string $command Command name + * @param Closure $callback Command callback + */ + public function register($command, Closure $callback) + { + $this->commands[$command] = $callback; + } + + /** + * Execute a command + * + * @access public + * @param string $command Command name + */ + public function call($command) + { + if (isset($this->commands[$command])) { + $this->commands[$command](); + exit; + } + } + + /** + * Determine which command to execute + * + * @access public + */ + public function execute() + { + if (php_sapi_name() !== 'cli') { + die('This script work only from the command line.'); + } + + if ($GLOBALS['argc'] === 1) { + $this->call($this->default_command); + } + + $this->call($GLOBALS['argv'][1]); + $this->call($this->default_command); + } +} diff --git a/app/Core/Registry.php b/app/Core/Registry.php index 0311dc62..d8b9063e 100644 --- a/app/Core/Registry.php +++ b/app/Core/Registry.php @@ -1,6 +1,7 @@ <?php namespace Core; + use RuntimeException; /** diff --git a/app/Core/Response.php b/app/Core/Response.php index aee029af..1ccf9f5e 100644 --- a/app/Core/Response.php +++ b/app/Core/Response.php @@ -71,6 +71,22 @@ class Response } /** + * Send a CSV response + * + * @access public + * @param array $data Data to serialize in csv + * @param integer $status_code HTTP status code + */ + public function csv(array $data, $status_code = 200) + { + $this->status($status_code); + $this->nocache(); + header('Content-Type: text/csv'); + Tool::csv($data); + exit; + } + + /** * Send a Json response * * @access public @@ -83,7 +99,6 @@ class Response $this->nocache(); header('Content-Type: application/json'); echo json_encode($data); - exit; } @@ -100,7 +115,6 @@ class Response $this->nocache(); header('Content-Type: text/plain; charset=utf-8'); echo $data; - exit; } @@ -117,7 +131,6 @@ class Response $this->nocache(); header('Content-Type: text/html; charset=utf-8'); echo $data; - exit; } @@ -134,7 +147,6 @@ class Response $this->nocache(); header('Content-Type: text/xml; charset=utf-8'); echo $data; - exit; } @@ -169,7 +181,6 @@ class Response header('Content-Transfer-Encoding: binary'); header('Content-Type: application/octet-stream'); echo $data; - exit; } diff --git a/app/Core/Tool.php b/app/Core/Tool.php new file mode 100644 index 00000000..ade99cad --- /dev/null +++ b/app/Core/Tool.php @@ -0,0 +1,34 @@ +<?php + +namespace Core; + +/** + * Tool class + * + * @package core + * @author Frederic Guillot + */ +class Tool +{ + /** + * Write a CSV file + * + * @static + * @access public + * @param array $rows Array of rows + * @param string $filename Output filename + */ + public static function csv(array $rows, $filename = 'php://output') + { + $fp = fopen($filename, 'w'); + + if (is_resource($fp)) { + + foreach ($rows as $fields) { + fputcsv($fp, $fields); + } + + fclose($fp); + } + } +} diff --git a/app/Core/Translator.php b/app/Core/Translator.php index 7cd3cc4f..43015e48 100644 --- a/app/Core/Translator.php +++ b/app/Core/Translator.php @@ -27,6 +27,26 @@ class Translator private static $locales = array(); /** + * Flag to enable HTML escaping + * + * @static + * @access private + * @var boolean + */ + private static $enable_escaping = true; + + /** + * Disable HTML escaping for translations + * + * @static + * @access public + */ + public static function disableEscaping() + { + self::$enable_escaping = false; + } + + /** * Get a translation * * $translator->translate('I have %d kids', 5); @@ -42,8 +62,10 @@ class Translator array_shift($args); array_unshift($args, $this->get($identifier, $identifier)); - foreach ($args as &$arg) { - $arg = htmlspecialchars($arg, ENT_QUOTES, 'UTF-8', false); + if (self::$enable_escaping) { + foreach ($args as &$arg) { + $arg = htmlspecialchars($arg, ENT_QUOTES, 'UTF-8', false); + } } return call_user_func_array( diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php index b4dd8a38..01be45c7 100644 --- a/app/Locales/de_DE/translations.php +++ b/app/Locales/de_DE/translations.php @@ -387,4 +387,13 @@ return array( 'Unlink my GitHub Account' => 'Verbindung mit meinem GitHub Account trennen', 'Created by %s' => 'Erstellt durch %s', 'Last modified on %B %e, %G at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', ); diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php index 2dd3765f..2b7420d9 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locales/es_ES/translations.php @@ -190,7 +190,7 @@ return array( 'Timezone' => 'Zona horaria', 'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', 'Page not found' => 'Página no encontrada', - 'Story Points' => 'Complejidad', + 'Complexity' => 'Complejidad', 'limit' => 'límite', 'Task limit' => 'Número máximo de tareas', 'This value must be greater than %d' => 'Este valor no debe de ser más grande que %d', @@ -386,4 +386,13 @@ return array( // 'Unlink my GitHub Account' => '', // 'Created by %s' => 'Créé par %s', // 'Last modified on %B %e, %G at %k:%M %p' => '', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', ); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php index 5067ea61..3d1d313b 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locales/fr_FR/translations.php @@ -190,7 +190,7 @@ return array( 'Timezone' => 'Fuseau horaire', 'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', 'Page not found' => 'Page introuvable', - 'Story Points' => 'Complexité', + 'Complexity' => 'Complexité', 'limit' => 'limite', 'Task limit' => 'Nombre maximum de tâches', 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d', @@ -384,4 +384,13 @@ return array( 'Unlink my GitHub Account' => 'Ne plus utiliser mon compte Github', 'Created by %s' => 'Créé par %s', 'Last modified on %B %e, %G at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M', + 'Tasks Export' => 'Exportation des tâches', + 'Tasks exportation for "%s"' => 'Exportation des tâches pour « %s »', + 'Start Date' => 'Date de début', + 'End Date' => 'Date de fin', + 'Execute' => 'Exécuter', + 'Task Id' => 'Identifiant de la tâche', + 'Creator' => 'Créateur', + 'Modification date' => 'Date de modification', + 'Completion date' => 'Date de complétion', ); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php index a96d5672..eaafe7c5 100644 --- a/app/Locales/pl_PL/translations.php +++ b/app/Locales/pl_PL/translations.php @@ -192,7 +192,7 @@ return array( 'Description' => 'Opis', 'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', 'Page not found' => 'Strona nie istnieje', - 'Story Points' => 'Poziom trudności', + 'Complexity' => 'Poziom trudności', 'limit' => 'limit', 'Task limit' => 'Limit zadań', 'This value must be greater than %d' => 'Wartość musi być większa niż %d', @@ -387,4 +387,13 @@ return array( // 'Unlink my GitHub Account' => '', // 'Created by %s' => 'Créé par %s', // 'Last modified on %B %e, %G at %k:%M %p' => '', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', ); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php index 8ba9b64a..a422a660 100644 --- a/app/Locales/pt_BR/translations.php +++ b/app/Locales/pt_BR/translations.php @@ -189,7 +189,7 @@ return array( 'Timezone' => 'Fuso horário', 'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', 'Page not found' => 'Página não encontrada', - 'Story Points' => 'Complexidade', + 'Complexity' => 'Complexidade', 'limit' => 'limite', 'Task limit' => 'Limite da tarefa', 'This value must be greater than %d' => 'Este valor deve ser maior que %d', @@ -384,4 +384,13 @@ return array( // 'Unlink my GitHub Account' => '', // 'Created by %s' => 'Créé par %s', // 'Last modified on %B %e, %G at %k:%M %p' => '', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', ); diff --git a/app/Locales/sv_SE/translations.php b/app/Locales/sv_SE/translations.php index cae457b1..d69f6604 100644 --- a/app/Locales/sv_SE/translations.php +++ b/app/Locales/sv_SE/translations.php @@ -190,7 +190,7 @@ return array( 'Timezone' => 'Tidszon', 'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.', 'Page not found' => 'Sidan hittas inte', - 'Story Points' => 'Ungefärligt antal timmar', + 'Complexity' => 'Ungefärligt antal timmar', 'limit' => 'max', 'Task limit' => 'Uppgiftsbegränsning', 'This value must be greater than %d' => 'Värdet måste vara större än %d', @@ -385,5 +385,14 @@ return array( 'Link my GitHub Account' => 'Anslut mitt GitHub-konto', 'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto', 'Created by %s' => 'Skapad av %s', - 'Last modified on %B %e, %G at %k:%M %p' => 'Senaste ändring %B %e, %G kl %k:%M %p'', + 'Last modified on %B %e, %G at %k:%M %p' => 'Senaste ändring %B %e, %G kl %k:%M %p', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', ); diff --git a/app/Locales/zh_CN/translations.php b/app/Locales/zh_CN/translations.php index 9ed0c8a3..de12c424 100644 --- a/app/Locales/zh_CN/translations.php +++ b/app/Locales/zh_CN/translations.php @@ -193,7 +193,7 @@ return array( 'Timezone' => '时区', 'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!', 'Page not found' => '页面未找到', - 'Story Points' => '评估分值', + 'Complexity' => '评估分值', 'limit' => '限制', 'Task limit' => '任务限制', 'This value must be greater than %d' => '该数值必须大于%d', @@ -392,4 +392,13 @@ return array( // 'Unlink my GitHub Account' => '', // 'Created by %s' => 'Créé par %s', // 'Last modified on %B %e, %G at %k:%M %p' => '', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', ); diff --git a/app/Model/Task.php b/app/Model/Task.php index 8933cb14..0b2b9cf9 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -5,6 +5,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; use DateTime; +use PDO; /** * Task model @@ -106,7 +107,7 @@ class Task extends Base '; $rq = $this->db->execute($sql, array($task_id)); - return $rq->fetch(\PDO::FETCH_ASSOC); + return $rq->fetch(PDO::FETCH_ASSOC); } else { @@ -167,12 +168,14 @@ class Task extends Base 'tasks.title', 'tasks.description', 'tasks.date_creation', + 'tasks.date_modification', 'tasks.date_completed', 'tasks.date_due', 'tasks.color_id', 'tasks.project_id', 'tasks.column_id', 'tasks.owner_id', + 'tasks.creator_id', 'tasks.position', 'tasks.is_active', 'tasks.score', @@ -667,4 +670,112 @@ class Task extends Base 'Y_m_d', ); } + + /** + * For a given timestamp, reset the date to midnight + * + * @access public + * @param integer $timestamp Timestamp + * @return integer + */ + public function resetDateToMidnight($timestamp) + { + return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp)); + } + + /** + * Export a list of tasks for a given project and date range + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function export($project_id, $from, $to) + { + $sql = ' + SELECT + tasks.id, + projects.name AS project_name, + tasks.is_active, + project_has_categories.name AS category_name, + columns.title AS column_title, + tasks.position, + tasks.color_id, + tasks.date_due, + creators.username AS creator_username, + users.username AS assignee_username, + tasks.score, + tasks.title, + tasks.date_creation, + tasks.date_modification, + tasks.date_completed + FROM tasks + LEFT JOIN users ON users.id = tasks.owner_id + LEFT JOIN users AS creators ON creators.id = tasks.creator_id + LEFT JOIN project_has_categories ON project_has_categories.id = tasks.category_id + LEFT JOIN columns ON columns.id = tasks.column_id + LEFT JOIN projects ON projects.id = tasks.project_id + WHERE tasks.date_creation >= ? AND tasks.date_creation <= ? AND tasks.project_id = ? + '; + + if (! is_numeric($from)) { + $from = $this->resetDateToMidnight($this->parseDate($from)); + } + + if (! is_numeric($to)) { + $to = $this->resetDateToMidnight(strtotime('+1 day', $this->parseDate($to))); + } + + $rq = $this->db->execute($sql, array($from, $to, $project_id)); + $tasks = $rq->fetchAll(PDO::FETCH_ASSOC); + + $columns = array( + t('Task Id'), + t('Project'), + t('Status'), + t('Category'), + t('Column'), + t('Position'), + t('Color'), + t('Due date'), + t('Creator'), + t('Assignee'), + t('Complexity'), + t('Title'), + t('Creation date'), + t('Modification date'), + t('Completion date'), + ); + + $results = array($columns); + + foreach ($tasks as &$task) { + $results[] = array_values($this->formatOutput($task)); + } + + return $results; + } + + /** + * Format the output of a task array + * + * @access public + * @param array $task Task properties + * @return array + */ + public function formatOutput(array &$task) + { + $colors = $this->getColors(); + $task['score'] = $task['score'] ?: ''; + $task['is_active'] = $task['is_active'] == self::STATUS_OPEN ? t('Open') : t('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']) : ''; + + return $task; + } } diff --git a/app/Templates/project_export.php b/app/Templates/project_export.php new file mode 100644 index 00000000..946a68a8 --- /dev/null +++ b/app/Templates/project_export.php @@ -0,0 +1,33 @@ +<section id="main"> + <div class="page-header"> + <h2> + <?= t('Tasks exportation for "%s"', $project['name']) ?> + </h2> + <ul> + <li><a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li> + <li><a href="?controller=project&action=index"><?= t('List of projects') ?></a></li> + </ul> + </div> + <section id="project-section"> + + <form method="get" action="?" autocomplete="off"> + + <?= Helper\form_hidden('controller', $values) ?> + <?= Helper\form_hidden('action', $values) ?> + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Start Date'), 'from') ?> + <?= Helper\form_text('from', $values, $errors, array('required', 'placeholder="'.t('month/day/year').'"'), 'form-date') ?><br/> + + <?= Helper\form_label(t('End Date'), 'to') ?> + <?= Helper\form_text('to', $values, $errors, array('required', 'placeholder="'.t('month/day/year').'"'), 'form-date') ?> + + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> + </form> + + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/project_index.php b/app/Templates/project_index.php index 927924a5..097ebd1f 100644 --- a/app/Templates/project_index.php +++ b/app/Templates/project_index.php @@ -89,6 +89,9 @@ <li> <a href="?controller=board&action=readonly&token=<?= $project['token'] ?>" target="_blank"><?= t('Public link') ?></a> </li> + <li> + <a href="?controller=project&action=export&project_id=<?= $project['id'] ?>"><?= t('Tasks Export') ?></a> + </li> </ul> </td> <?php endif ?> diff --git a/app/Templates/task_edit.php b/app/Templates/task_edit.php index 0f1ec6f7..a3a0eddd 100644 --- a/app/Templates/task_edit.php +++ b/app/Templates/task_edit.php @@ -7,7 +7,7 @@ </ul> <?php endif ?> </div> - <section> + <section id="task-section"> <form method="post" action="?controller=task&action=update&task_id=<?= $task['id'] ?>&ajax=<?= $ajax ?>" autocomplete="off"> <?= Helper\form_csrf() ?> @@ -39,7 +39,7 @@ <?= Helper\form_label(t('Color'), 'color_id') ?> <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/> - <?= Helper\form_label(t('Story Points'), 'score') ?> + <?= Helper\form_label(t('Complexity'), 'score') ?> <?= Helper\form_number('score', $values, $errors) ?><br/> <?= Helper\form_label(t('Due Date'), 'date_due') ?> diff --git a/app/Templates/task_layout.php b/app/Templates/task_layout.php index 9a6bbd00..96c45608 100644 --- a/app/Templates/task_layout.php +++ b/app/Templates/task_layout.php @@ -5,7 +5,7 @@ <li><a href="?controller=board&action=show&project_id=<?= $task['project_id'] ?>"><?= t('Back to the board') ?></a></li> </ul> </div> - <section class="task-show"> + <section class="task-show" id="task-section"> <?= Helper\template('task_sidebar', array('task' => $task)) ?> diff --git a/app/Templates/task_new.php b/app/Templates/task_new.php index 5e4e3ee6..e07d436c 100644 --- a/app/Templates/task_new.php +++ b/app/Templates/task_new.php @@ -2,7 +2,7 @@ <div class="page-header"> <h2><?= t('New task') ?></h2> </div> - <section> + <section id="task-section"> <form method="post" action="?controller=task&action=save" autocomplete="off"> <?= Helper\form_csrf() ?> @@ -35,7 +35,7 @@ <?= Helper\form_label(t('Color'), 'color_id') ?> <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/> - <?= Helper\form_label(t('Story Points'), 'score') ?> + <?= Helper\form_label(t('Complexity'), 'score') ?> <?= Helper\form_number('score', $values, $errors) ?><br/> <?= Helper\form_label(t('Due Date'), 'date_due') ?> diff --git a/assets/js/app.js b/assets/js/app.js index 2b65da99..bf98d689 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -28,8 +28,8 @@ var Kanboard = (function() { }, // Return true if the page is visible - IsVisible: function() - { + IsVisible: function() { + var property = ""; if (typeof document.hidden !== "undefined") { @@ -47,6 +47,17 @@ var Kanboard = (function() { } return true; + }, + + // Common init + Before: function() { + + // Datepicker + $(".form-date").datepicker({ + showOtherMonths: true, + selectOtherMonths: true, + dateFormat: 'yy-mm-dd' + }); } }; @@ -228,12 +239,7 @@ Kanboard.Task = (function() { return { Init: function() { - // Datepicker for the due date - $("#form-date_due").datepicker({ - showOtherMonths: true, - selectOtherMonths: true, - dateFormat: 'yy-mm-dd' - }); + Kanboard.Before(); // Image preview for attachments $(".file-popover").click(Kanboard.Popover); @@ -243,13 +249,28 @@ Kanboard.Task = (function() { })(); +// Project related functions +Kanboard.Project = (function() { + + return { + Init: function() { + Kanboard.Before(); + } + }; + +})(); + + // Initialization $(function() { if ($("#board").length) { Kanboard.Board.Init(); } - else { + else if ($("#task-section").length) { Kanboard.Task.Init(); } + else if ($("#project-section").length) { + Kanboard.Project.Init(); + } }); diff --git a/kanboard b/kanboard new file mode 100755 index 00000000..ec1a9258 --- /dev/null +++ b/kanboard @@ -0,0 +1,51 @@ +#!/usr/bin/env php +<?php + +require __DIR__.'/app/common.php'; + +use Core\Cli; +use Core\Tool; +use Core\Translator; +use Model\Config; +use Model\Task; + +$config = new Config($registry->shared('db'), $registry->shared('event')); + +// Load translations +$language = $config->get('language', 'en_US'); +if ($language !== 'en_US') Translator::load($language); + +// Set timezone +date_default_timezone_set($config->get('timezone', 'UTC')); + +// Setup CLI +$cli = new Cli; + +// Usage +$cli->register('help', function() { + echo 'Kanboard command line interface'.PHP_EOL.'==============================='.PHP_EOL; + echo '- Task export to stdout (CSV format): '.$GLOBALS['argv'][0].' export-csv <project_id> <start_date> <end_date>'.PHP_EOL; +}); + +// CSV Export +$cli->register('export-csv', function() use ($cli, $registry) { + + if ($GLOBALS['argc'] !== 5) { + $cli->call($cli->default_command); + } + + $project_id = $GLOBALS['argv'][2]; + $start_date = $GLOBALS['argv'][3]; + $end_date = $GLOBALS['argv'][4]; + + Translator::disableEscaping(); + + $task = new Task($registry->shared('db'), $registry->shared('event')); + $data = $task->export($project_id, $start_date, $end_date); + + if (is_array($data)) { + Tool::csv($data); + } +}); + +$cli->execute(); diff --git a/tests/functionals/ApiTest.php b/tests/functionals/ApiTest.php index 89d525a2..8396be4f 100644 --- a/tests/functionals/ApiTest.php +++ b/tests/functionals/ApiTest.php @@ -173,6 +173,7 @@ class Api extends PHPUnit_Framework_TestCase $task['color_id'] = 'green'; $task['column_id'] = 1; $task['description'] = 'test'; + $task['date_due'] = ''; $this->assertTrue($this->client->updateTask($task)); } diff --git a/tests/units/TaskTest.php b/tests/units/TaskTest.php index da7e6a70..b8b3cf46 100644 --- a/tests/units/TaskTest.php +++ b/tests/units/TaskTest.php @@ -4,9 +4,45 @@ require_once __DIR__.'/Base.php'; use Model\Task; use Model\Project; +use Model\Category; class TaskTest extends Base { + public function testExport() + { + $t = new Task($this->db, $this->event); + $p = new Project($this->db, $this->event); + $c = new Category($this->db, $this->event); + + $this->assertEquals(1, $p->create(array('name' => 'Export Project'))); + $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertNotFalse($c->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertNotFalse($c->create(array('name' => 'Category #3', 'project_id' => 1))); + + for ($i = 1; $i <= 100; $i++) { + + $task = array( + 'title' => 'Task #'.$i, + 'project_id' => 1, + 'column_id' => rand(1, 3), + 'creator_id' => rand(0, 1), + 'owner_id' => rand(0, 1), + 'color_id' => rand(0, 1) === 0 ? 'green' : 'purple', + 'category_id' => rand(0, 3), + 'date_due' => array_rand(array(0, date('Y-m-d'), date('Y-m-d', strtotime('+'.$i.'day')))), + 'score' => rand(0, 21) + ); + + $this->assertEquals($i, $t->create($task)); + } + + $rows = $t->export(1, strtotime('-1 day'), strtotime('+1 day')); + $this->assertEquals($i, count($rows)); + $this->assertEquals('Task Id', $rows[0][0]); + $this->assertEquals(1, $rows[1][0]); + $this->assertEquals('Task #'.($i - 1), $rows[$i - 1][11]); + } + public function testFilter() { $t = new Task($this->db, $this->event); |