summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Controller/Base.php21
-rw-r--r--app/Controller/Category.php19
-rw-r--r--app/Controller/Project.php84
-rw-r--r--app/Core/Cli.php75
-rw-r--r--app/Core/Registry.php1
-rw-r--r--app/Core/Response.php21
-rw-r--r--app/Core/Tool.php34
-rw-r--r--app/Core/Translator.php26
-rw-r--r--app/Locales/de_DE/translations.php9
-rw-r--r--app/Locales/es_ES/translations.php11
-rw-r--r--app/Locales/fr_FR/translations.php11
-rw-r--r--app/Locales/pl_PL/translations.php11
-rw-r--r--app/Locales/pt_BR/translations.php11
-rw-r--r--app/Locales/sv_SE/translations.php13
-rw-r--r--app/Locales/zh_CN/translations.php11
-rw-r--r--app/Model/Task.php113
-rw-r--r--app/Templates/project_export.php33
-rw-r--r--app/Templates/project_index.php3
-rw-r--r--app/Templates/task_edit.php4
-rw-r--r--app/Templates/task_layout.php2
-rw-r--r--app/Templates/task_new.php4
-rw-r--r--assets/js/app.js39
-rwxr-xr-xkanboard51
-rw-r--r--tests/functionals/ApiTest.php1
-rw-r--r--tests/units/TaskTest.php36
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&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li>
+ <li><a href="?controller=project&amp;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&amp;action=readonly&amp;token=<?= $project['token'] ?>" target="_blank"><?= t('Public link') ?></a>
</li>
+ <li>
+ <a href="?controller=project&amp;action=export&amp;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&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;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&amp;action=show&amp;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&amp;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);