diff options
49 files changed, 1678 insertions, 190 deletions
diff --git a/lib/.htaccess b/actions/.htaccess index 14249c50..14249c50 100644 --- a/lib/.htaccess +++ b/actions/.htaccess diff --git a/actions/Base.php b/actions/Base.php new file mode 100644 index 00000000..bb9b8bc1 --- /dev/null +++ b/actions/Base.php @@ -0,0 +1,55 @@ +<?php + +namespace Action; + +abstract class Base implements \Core\Listener +{ + private $project_id = 0; + private $params = array(); + + abstract public function doAction(array $data); + abstract public function getActionRequiredParameters(); + abstract public function getEventRequiredParameters(); + + public function __construct($project_id) + { + $this->project_id = $project_id; + } + + public function setParam($name, $value) + { + $this->params[$name] = $value; + } + + public function getParam($name, $default_value = null) + { + return isset($this->params[$name]) ? $this->params[$name] : $default_value; + } + + public function isExecutable(array $data) + { + if (isset($data['project_id']) && $data['project_id'] == $this->project_id && $this->hasRequiredParameters($data)) { + return true; + } + + return false; + } + + public function hasRequiredParameters(array $data) + { + foreach ($this->getEventRequiredParameters() as $parameter) { + if (! isset($data[$parameter])) return false; + } + + return true; + } + + public function execute(array $data) + { + if ($this->isExecutable($data)) { + return $this->doAction($data); + } + + return false; + } +} diff --git a/actions/task_assign_current_user.php b/actions/task_assign_current_user.php new file mode 100644 index 00000000..5a8edd01 --- /dev/null +++ b/actions/task_assign_current_user.php @@ -0,0 +1,45 @@ +<?php + +namespace Action; + +require_once __DIR__.'/base.php'; + +class TaskAssignCurrentUser extends Base +{ + public function __construct($project_id, \Model\Task $task, \Model\Acl $acl) + { + parent::__construct($project_id); + $this->task = $task; + $this->acl = $acl; + } + + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + ); + } + + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'owner_id' => $this->acl->getUserId(), + )); + + return true; + } + + return false; + } +} diff --git a/actions/task_assign_specific_user.php b/actions/task_assign_specific_user.php new file mode 100644 index 00000000..8cafde6d --- /dev/null +++ b/actions/task_assign_specific_user.php @@ -0,0 +1,45 @@ +<?php + +namespace Action; + +require_once __DIR__.'/base.php'; + +class TaskAssignSpecificUser extends Base +{ + public function __construct($project_id, \Model\Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + 'user_id' => t('Assignee'), + ); + } + + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'owner_id' => $this->getParam('user_id'), + )); + + return true; + } + + return false; + } +} diff --git a/actions/task_close.php b/actions/task_close.php new file mode 100644 index 00000000..4ac579c4 --- /dev/null +++ b/actions/task_close.php @@ -0,0 +1,39 @@ +<?php + +namespace Action; + +require_once __DIR__.'/base.php'; + +class TaskClose extends Base +{ + public function __construct($project_id, \Model\Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + ); + } + + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id')) { + $this->task->close($data['task_id']); + return true; + } + + return false; + } +} diff --git a/actions/task_duplicate_another_project.php b/actions/task_duplicate_another_project.php new file mode 100644 index 00000000..31089c67 --- /dev/null +++ b/actions/task_duplicate_another_project.php @@ -0,0 +1,43 @@ +<?php + +namespace Action; + +require_once __DIR__.'/base.php'; + +class TaskDuplicateAnotherProject extends Base +{ + public function __construct($project_id, \Model\Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + 'project_id' => t('Project'), + ); + } + + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + 'project_id', + ); + } + + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) { + + $this->task->duplicateToAnotherProject($data['task_id'], $this->getParam('project_id')); + + return true; + } + + return false; + } +} diff --git a/assets/js/board.js b/assets/js/board.js index 33e47053..501c39c3 100644 --- a/assets/js/board.js +++ b/assets/js/board.js @@ -140,6 +140,23 @@ var xhr = new XMLHttpRequest(); xhr.open("POST", "?controller=board&action=save&project_id=" + projectId, true); + + xhr.onreadystatechange = function(response) { + + if (this.readyState == this.DONE) { + try { + var response = JSON.parse(this.responseText); + + if (response.result == true) { + + // TODO: don't refresh the whole page! + window.location = "?controller=board&action=show&project_id=" + projectId; + } + } + catch (e) {} + } + }; + xhr.send(JSON.stringify(data)); } diff --git a/common.php b/common.php new file mode 100644 index 00000000..0c163b5d --- /dev/null +++ b/common.php @@ -0,0 +1,90 @@ +<?php + +require __DIR__.'/core/registry.php'; +require __DIR__.'/core/helper.php'; +require __DIR__.'/core/translator.php'; + +$registry = new Core\Registry; + +$registry->db_version = 10; + +$registry->db = function() use ($registry) { + require __DIR__.'/vendor/PicoDb/Database.php'; + require __DIR__.'/models/schema.php'; + + $db = new \PicoDb\Database(array( + 'driver' => 'sqlite', + 'filename' => DB_FILENAME + )); + + if ($db->schema()->check($registry->db_version)) { + return $db; + } + else { + die('Unable to migrate database schema!'); + } +}; + +$registry->event = function() use ($registry) { + require __DIR__.'/core/event.php'; + return new \Core\Event; +}; + +$registry->action = function() use ($registry) { + require_once __DIR__.'/models/action.php'; + return new \Model\Action($registry->shared('db'), $registry->shared('event')); +}; + +$registry->config = function() use ($registry) { + require_once __DIR__.'/models/config.php'; + return new \Model\Config($registry->shared('db'), $registry->shared('event')); +}; + +$registry->acl = function() use ($registry) { + require_once __DIR__.'/models/acl.php'; + return new \Model\Acl($registry->shared('db'), $registry->shared('event')); +}; + +$registry->user = function() use ($registry) { + require_once __DIR__.'/models/user.php'; + return new \Model\User($registry->shared('db'), $registry->shared('event')); +}; + +$registry->comment = function() use ($registry) { + require_once __DIR__.'/models/comment.php'; + return new \Model\Comment($registry->shared('db'), $registry->shared('event')); +}; + +$registry->task = function() use ($registry) { + require_once __DIR__.'/models/task.php'; + return new \Model\Task($registry->shared('db'), $registry->shared('event')); +}; + +$registry->board = function() use ($registry) { + require_once __DIR__.'/models/board.php'; + return new \Model\Board($registry->shared('db'), $registry->shared('event')); +}; + +$registry->project = function() use ($registry) { + require_once __DIR__.'/models/project.php'; + return new \Model\Project($registry->shared('db'), $registry->shared('event')); +}; + +$registry->action = function() use ($registry) { + require_once __DIR__.'/models/action.php'; + return new \Model\Action($registry->shared('db'), $registry->shared('event')); +}; + +if (file_exists('config.php')) require 'config.php'; + +// Auto-refresh frequency in seconds for the public board view +defined('AUTO_REFRESH_DURATION') or define('AUTO_REFRESH_DURATION', 60); + +// Custom session save path +defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', ''); + +// Database filename +defined('DB_FILENAME') or define('DB_FILENAME', 'data/db.sqlite'); + +// Application version +defined('APP_VERSION') or define('APP_VERSION', 'master'); diff --git a/controllers/action.php b/controllers/action.php new file mode 100644 index 00000000..3ee44364 --- /dev/null +++ b/controllers/action.php @@ -0,0 +1,140 @@ +<?php + +namespace Controller; + +require_once __DIR__.'/Base.php'; + +/** + * Automatic actions management + * + * @package controllers + * @author Frederic Guillot + */ +class Action extends Base +{ + /** + * List of automatic actions for a given project + * + * @access public + */ + public function index() + { + $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->response->html($this->template->layout('action_index', array( + 'values' => array('project_id' => $project['id']), + 'project' => $project, + 'actions' => $this->action->getAllByProject($project['id']), + 'available_actions' => $this->action->getAvailableActions(), + 'available_events' => $this->action->getAvailableEvents(), + 'available_params' => $this->action->getAllActionParameters(), + 'columns_list' => $this->board->getColumnsList($project['id']), + 'users_list' => $this->project->getUsersList($project['id'], false), + 'projects_list' => $this->project->getList(false), + 'menu' => 'projects', + 'title' => t('Automatic actions') + ))); + } + + /** + * Define action parameters (step 2) + * + * @access public + */ + public function params() + { + $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'); + } + + $values = $this->request->getValues(); + $action = $this->action->load($values['action_name'], $values['project_id']); + + $this->response->html($this->template->layout('action_params', array( + 'values' => $values, + 'action_params' => $action->getActionRequiredParameters(), + 'columns_list' => $this->board->getColumnsList($project['id']), + 'users_list' => $this->project->getUsersList($project['id'], false), + 'projects_list' => $this->project->getList(false), + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Automatic actions') + ))); + } + + /** + * Create a new action (last step) + * + * @access public + */ + public function create() + { + $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'); + } + + $values = $this->request->getValues(); + + list($valid, $errors) = $this->action->validateCreation($values); + + if ($valid) { + + if ($this->action->create($values)) { + $this->session->flash(t('Your automatic action have been created successfully.')); + } + else { + $this->session->flashError(t('Unable to create your automatic action.')); + } + } + + $this->response->redirect('?controller=action&action=index&project_id='.$project['id']); + } + + /** + * Confirmation dialog before removing an action + * + * @access public + */ + public function confirm() + { + $this->response->html($this->template->layout('action_remove', array( + 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), + 'available_events' => $this->action->getAvailableEvents(), + 'available_actions' => $this->action->getAvailableActions(), + 'menu' => 'projects', + 'title' => t('Remove an action') + ))); + } + + /** + * Remove an action + * + * @access public + */ + public function remove() + { + $action = $this->action->getById($this->request->getIntegerParam('action_id')); + + if ($action && $this->action->remove($action['id'])) { + $this->session->flash(t('Action removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this action.')); + } + + $this->response->redirect('?controller=action&action=index&project_id='.$action['project_id']); + } +} diff --git a/controllers/app.php b/controllers/app.php index 981abbbe..633433fc 100644 --- a/controllers/app.php +++ b/controllers/app.php @@ -2,6 +2,8 @@ namespace Controller; +require_once __DIR__.'/Base.php'; + class App extends Base { public function index() diff --git a/controllers/base.php b/controllers/base.php index 6dc9c0be..dd7c0642 100644 --- a/controllers/base.php +++ b/controllers/base.php @@ -2,48 +2,18 @@ namespace Controller; -require __DIR__.'/../lib/request.php'; -require __DIR__.'/../lib/response.php'; -require __DIR__.'/../lib/session.php'; -require __DIR__.'/../lib/template.php'; -require __DIR__.'/../lib/helper.php'; -require __DIR__.'/../lib/translator.php'; -require __DIR__.'/../models/base.php'; -require __DIR__.'/../models/acl.php'; -require __DIR__.'/../models/config.php'; -require __DIR__.'/../models/user.php'; -require __DIR__.'/../models/project.php'; -require __DIR__.'/../models/task.php'; -require __DIR__.'/../models/board.php'; -require __DIR__.'/../models/comment.php'; - abstract class Base { - protected $request; - protected $response; - protected $session; - protected $template; - protected $user; - protected $project; - protected $task; - protected $board; - protected $config; - protected $acl; - protected $comment; - - public function __construct() + public function __construct(\Core\Registry $registry) { - $this->request = new \Request; - $this->response = new \Response; - $this->session = new \Session; - $this->template = new \Template; - $this->config = new \Model\Config; - $this->user = new \Model\User; - $this->project = new \Model\Project; - $this->task = new \Model\Task; - $this->board = new \Model\Board; - $this->acl = new \Model\Acl; - $this->comment = new \Model\Comment; + $this->acl = $registry->acl; + $this->action = $registry->action; + $this->board = $registry->board; + $this->config = $registry->config; + $this->project = $registry->project; + $this->task = $registry->task; + $this->user = $registry->user; + $this->comment = $registry->comment; } public function beforeAction($controller, $action) @@ -74,6 +44,9 @@ abstract class Base if (! $this->acl->isPageAccessAllowed($controller, $action)) { $this->response->redirect('?controller=user&action=forbidden'); } + + // Attach events for automatic actions + $this->action->attachEvents(); } public function checkProjectPermissions($project_id) diff --git a/controllers/board.php b/controllers/board.php index 13714b3c..9cdc4386 100644 --- a/controllers/board.php +++ b/controllers/board.php @@ -2,6 +2,8 @@ namespace Controller; +require_once __DIR__.'/Base.php'; + class Board extends Base { // Change a task assignee directly from the board diff --git a/controllers/config.php b/controllers/config.php index 064fa06d..c4880b4a 100644 --- a/controllers/config.php +++ b/controllers/config.php @@ -2,6 +2,8 @@ namespace Controller; +require_once __DIR__.'/Base.php'; + class Config extends Base { // Settings page diff --git a/controllers/project.php b/controllers/project.php index 8d8584bc..8b232e94 100644 --- a/controllers/project.php +++ b/controllers/project.php @@ -2,6 +2,8 @@ namespace Controller; +require_once __DIR__.'/Base.php'; + class Project extends Base { // Display access forbidden page diff --git a/controllers/task.php b/controllers/task.php index fba4d4f5..05dd935e 100644 --- a/controllers/task.php +++ b/controllers/task.php @@ -2,6 +2,8 @@ namespace Controller; +require_once __DIR__.'/Base.php'; + class Task extends Base { // Webhook to create a task (useful for external software) diff --git a/controllers/user.php b/controllers/user.php index 10d3ad21..700e5fae 100644 --- a/controllers/user.php +++ b/controllers/user.php @@ -2,6 +2,8 @@ namespace Controller; +require_once __DIR__.'/Base.php'; + class User extends Base { // Display access forbidden page diff --git a/core/.htaccess b/core/.htaccess new file mode 100644 index 00000000..14249c50 --- /dev/null +++ b/core/.htaccess @@ -0,0 +1 @@ +Deny from all
\ No newline at end of file diff --git a/core/event.php b/core/event.php new file mode 100644 index 00000000..7addb41d --- /dev/null +++ b/core/event.php @@ -0,0 +1,124 @@ +<?php + +namespace Core; + +/** + * Event listener interface + * + * @package core + * @author Frederic Guillot + */ +interface Listener { + public function execute(array $data); +} + +/** + * Event dispatcher class + * + * @package core + * @author Frederic Guillot + */ +class Event +{ + /** + * Contains all listeners + * + * @access private + * @var array + */ + private $listeners = array(); + + /** + * The last triggered event + * + * @access private + * @var string + */ + private $lastEvent = ''; + + /** + * Triggered events list + * + * @access private + * @var array + */ + private $events = array(); + + /** + * Attach a listener object to an event + * + * @access public + * @param string $eventName Event name + * @param Listener $listener Object that implements the Listener interface + */ + public function attach($eventName, Listener $listener) + { + if (! isset($this->listeners[$eventName])) { + $this->listeners[$eventName] = array(); + } + + $this->listeners[$eventName][] = $listener; + } + + /** + * Trigger an event + * + * @access public + * @param string $eventName Event name + * @param array $data Event data + */ + public function trigger($eventName, array $data) + { + $this->lastEvent = $eventName; + $this->events[] = $eventName; + + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + $listener->execute($data); // TODO: keep an history of executed actions for unit test + } + } + } + + /** + * Get the last fired event + * + * @access public + * @return string Event name + */ + public function getLastTriggeredEvent() + { + return $this->lastEvent; + } + + /** + * Get a list of triggered events + * + * @access public + * @return array + */ + public function getTriggeredEvents() + { + return $this->events; + } + + /** + * Check if a listener bind to an event + * + * @access public + * @param string $eventName Event name + * @param mixed $instance Instance name or object itself + * @return bool Yes or no + */ + public function hasListener($eventName, $instance) + { + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + if ($listener instanceof $instance) { + return true; + } + } + } + + return false; + } +} diff --git a/lib/helper.php b/core/helper.php index 50071be1..e4ad26f1 100644 --- a/lib/helper.php +++ b/core/helper.php @@ -89,6 +89,11 @@ function summary($value, $min_length = 5, $max_length = 120, $end = '[...]') return $value; } +function contains($haystack, $needle) +{ + return strpos($haystack, $needle) !== false; +} + function in_list($id, array $listing) { if (isset($listing[$id])) { diff --git a/core/registry.php b/core/registry.php new file mode 100644 index 00000000..f11d427c --- /dev/null +++ b/core/registry.php @@ -0,0 +1,79 @@ +<?php + +namespace Core; + +/** + * The registry class is a dependency injection container + * + * @package core + * @author Frederic Guillot + */ +class Registry +{ + /** + * Contains all dependencies + * + * @access private + * @var array + */ + private $container = array(); + + /** + * Contains all instances + * + * @access private + * @var array + */ + private $instances = array(); + + /** + * Set a dependency + * + * @access public + * @param string $name Unique identifier for the service/parameter + * @param mixed $value The value of the parameter or a closure to define an object + */ + public function __set($name, $value) + { + $this->container[$name] = $value; + } + + /** + * Get a dependency + * + * @access public + * @param string $name Unique identifier for the service/parameter + * @return mixed The value of the parameter or an object + * @throws RuntimeException If the identifier is not found + */ + public function __get($name) + { + if (isset($this->container[$name])) { + + if (is_callable($this->container[$name])) { + return $this->container[$name](); + } + else { + return $this->container[$name]; + } + } + + throw new \RuntimeException('Identifier not found in the registry: '.$name); + } + + /** + * Return a shared instance of a dependency + * + * @access public + * @param string $name Unique identifier for the service/parameter + * @return mixed Same object instance of the dependency + */ + public function shared($name) + { + if (! isset($this->instances[$name])) { + $this->instances[$name] = $this->$name; + } + + return $this->instances[$name]; + } +} diff --git a/lib/request.php b/core/request.php index 8840e7a4..b2c3e12e 100644 --- a/lib/request.php +++ b/core/request.php @@ -1,5 +1,7 @@ <?php +namespace Core; + class Request { public function getStringParam($name, $default_value = '') diff --git a/lib/response.php b/core/response.php index ceaf32c5..4a00ed79 100644 --- a/lib/response.php +++ b/core/response.php @@ -1,5 +1,7 @@ <?php +namespace Core; + class Response { public function forceDownload($filename) diff --git a/lib/router.php b/core/router.php index 979968d4..5a27276c 100644 --- a/lib/router.php +++ b/core/router.php @@ -1,12 +1,21 @@ <?php +namespace Core; + +require __DIR__.'/request.php'; +require __DIR__.'/response.php'; +require __DIR__.'/session.php'; +require __DIR__.'/template.php'; + class Router { private $controller = ''; private $action = ''; + private $registry; - public function __construct($controller = '', $action = '') + public function __construct(Registry $registry, $controller = '', $action = '') { + $this->registry = $registry; $this->controller = empty($_GET['controller']) ? $controller : $_GET['controller']; $this->action = empty($_GET['action']) ? $controller : $_GET['action']; } @@ -16,7 +25,7 @@ class Router return ! ctype_alpha($value) || empty($value) ? $default_value : strtolower($value); } - public function loadController($filename, $class, $method) + public function load($filename, $class, $method) { if (file_exists($filename)) { @@ -24,7 +33,11 @@ class Router if (! method_exists($class, $method)) return false; - $instance = new $class; + $instance = new $class($this->registry); + $instance->request = new Request; + $instance->response = new Response; + $instance->session = new Session; + $instance->template = new Template; $instance->beforeAction($this->controller, $this->action); $instance->$method(); @@ -39,7 +52,7 @@ class Router $this->controller = $this->sanitize($this->controller, 'app'); $this->action = $this->sanitize($this->action, 'index'); - if (! $this->loadController('controllers/'.$this->controller.'.php', '\Controller\\'.$this->controller, $this->action)) { + if (! $this->load('controllers/'.$this->controller.'.php', '\Controller\\'.$this->controller, $this->action)) { die('Page not found!'); } } diff --git a/lib/session.php b/core/session.php index 688004b3..7fe8e0c1 100644 --- a/lib/session.php +++ b/core/session.php @@ -1,5 +1,7 @@ <?php +namespace Core; + class Session { const SESSION_LIFETIME = 2678400; // 31 days diff --git a/lib/template.php b/core/template.php index 09f9aa29..ad31ffb7 100644 --- a/lib/template.php +++ b/core/template.php @@ -1,5 +1,7 @@ <?php +namespace Core; + class Template { const PATH = 'templates/'; diff --git a/lib/translator.php b/core/translator.php index 75d40a23..75d40a23 100644 --- a/lib/translator.php +++ b/core/translator.php @@ -1,19 +1,8 @@ <?php require __DIR__.'/check_setup.php'; -require __DIR__.'/controllers/base.php'; -require __DIR__.'/lib/router.php'; +require __DIR__.'/common.php'; +require __DIR__.'/core/router.php'; -if (file_exists('config.php')) require 'config.php'; - -// Auto-refresh frequency in seconds for the public board view -defined('AUTO_REFRESH_DURATION') or define('AUTO_REFRESH_DURATION', 60); - -// Custom session save path -defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', ''); - -// Database filename -defined('DB_FILENAME') or define('DB_FILENAME', 'data/db.sqlite'); - -$router = new Router; +$router = new Core\Router($registry); $router->execute(); diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php index 29214886..b85d05d9 100644 --- a/locales/fr_FR/translations.php +++ b/locales/fr_FR/translations.php @@ -99,7 +99,7 @@ return array( 'Edit a task' => 'Modifier une tâche', 'Column' => 'Colonne', 'Color' => 'Couleur', - 'Assignee' => 'Affectation', + 'Assignee' => 'Personne assigné', 'Create another task' => 'Créer une autre tâche', 'New task' => 'Nouvelle tâche', 'Open a task' => 'Ouvrir une tâche', @@ -218,4 +218,33 @@ return array( 'Invalid date' => 'Date invalide', 'Must be done before %B %e, %G' => 'Doit être fait avant le %e %B %G', '%B %e, %G' => '%e %B %G', + 'Automatic actions' => 'Actions automatisées', + 'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.', + 'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.', + 'Remove an action' => 'Supprimer une action', + 'Unable to remove this action.' => 'Impossible de supprimer cette action', + 'Action removed successfully.' => 'Action supprimée avec succès.', + 'Automatic actions for the project "%s"' => 'Actions automatisées pour le projet « %s »', + 'Defined actions' => 'Actions définies', + 'Event name' => 'Nom de l\'événement', + 'Action name' => 'Nom de l\'action', + 'Action parameters' => 'Paramètres de l\'action', + 'Action' => 'Action', + 'Event' => 'Événement', + 'When the selected event occurs execute the corresponding action.' => 'Lorsque l\'événement sélectionné se déclenche, executer l\'action correspondante.', + 'Next step' => 'Étape suivante', + 'Define action parameters' => 'Définition des paramètres de l\'action', + 'Save this action' => 'Sauvegarder cette action', + 'Do you really want to remove this action: "%s"?' => 'Voulez-vous vraiment supprimer cette action « %s » ?', + 'Remove an automatic action' => 'Supprimer une action automatisée', + 'Close the task' => 'Fermer cette tâche', + 'Assign the task to a specific user' => 'Assigner la tâche à un utilisateur spécifique', + 'Assign the task to the person who does the action' => 'Assigner la tâche à la personne qui fait l\'action', + 'Duplicate the task to another project' => 'Dupliquer la tâche vers un autre projet', + 'Move a task to another column' => 'Déplacement d\'une tâche vers un autre colonne', + 'Move a task to another position in the same column' => 'Déplacement d\'une tâche à une autre position mais dans la même colonne', + 'Task modification' => 'Modification d\'une tâche', + 'Task creation' => 'Création d\'une tâche', + 'Open a closed task' => 'Ouverture d\'une tâche fermée', + 'Closing a task' => 'Fermeture d\'une tâche', ); diff --git a/locales/pl_PL/translations.php b/locales/pl_PL/translations.php index ae5ec4de..7da7c330 100644 --- a/locales/pl_PL/translations.php +++ b/locales/pl_PL/translations.php @@ -221,4 +221,33 @@ return array( 'Invalid date' => 'Błędna data', 'Must be done before %B %e, %G' => 'Termin do %e %B %G', '%B %e, %G' => '%e %B %G', + // 'Automatic actions' => '', + // 'Your automatic action have been created successfully.' => '', + // 'Unable to create your automatic action.' => '', + // 'Remove an action' => '', + // 'Unable to remove this action.' => '', + // 'Action removed successfully.' => '', + // 'Automatic actions for the project "%s"' => '', + // 'Defined actions' => '', + // 'Event name' => '', + // 'Action name' => '', + // 'Action parameters' => '', + // 'Action' => '', + // 'Event' => '', + // 'When the selected event occurs execute the corresponding action.' => '', + // 'Next step' => '', + // 'Define action parameters' => '', + // 'Save this action' => '', + // 'Do you really want to remove this action: "%s"?' => '', + // 'Remove an automatic action' => '', + // 'Close the task' => '', + // 'Assign the task to a specific user' => '', + // 'Assign the task to the person who does the action' => '', + // 'Duplicate the task to another project' => '', + // 'Move a task to another column' => '', + // 'Move a task to another position in the same column' => '', + // 'Task modification' => '', + // 'Task creation' => '', + // 'Open a closed task' => '', + // 'Closing a task' => '', ); diff --git a/models/acl.php b/models/acl.php index 86db3c32..25386254 100644 --- a/models/acl.php +++ b/models/acl.php @@ -2,6 +2,8 @@ namespace Model; +require_once __DIR__.'/base.php'; + class Acl extends Base { // Controllers and actions allowed from outside diff --git a/models/action.php b/models/action.php new file mode 100644 index 00000000..9b18d461 --- /dev/null +++ b/models/action.php @@ -0,0 +1,247 @@ +<?php + +namespace Model; + +require_once __DIR__.'/base.php'; +require_once __DIR__.'/task.php'; + +use \SimpleValidator\Validator; +use \SimpleValidator\Validators; + +class Action extends Base +{ + const TABLE = 'actions'; + const TABLE_PARAMS = 'action_has_params'; + + /** + * Return the name and description of available actions + * + * @access public + * @return array + */ + public function getAvailableActions() + { + return array( + 'TaskClose' => t('Close the task'), + 'TaskAssignSpecificUser' => t('Assign the task to a specific user'), + 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), + 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), + ); + } + + /** + * Return the name and description of available actions + * + * @access public + * @return array + */ + public function getAvailableEvents() + { + return array( + Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), + Task::EVENT_MOVE_POSITION => t('Move a task to another position in the same column'), + Task::EVENT_UPDATE => t('Task modification'), + Task::EVENT_CREATE => t('Task creation'), + Task::EVENT_OPEN => t('Open a closed task'), + Task::EVENT_CLOSE => t('Closing a task'), + ); + } + + /** + * Return actions and parameters for a given project + * + * @access public + * @return array + */ + public function getAllByProject($project_id) + { + $actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll(); + + foreach ($actions as &$action) { + $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); + } + + return $actions; + } + + /** + * Return all actions and parameters + * + * @access public + * @return array + */ + public function getAll() + { + $actions = $this->db->table(self::TABLE)->findAll(); + + foreach ($actions as &$action) { + $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); + } + + return $actions; + } + + /** + * Get all required action parameters for all registered actions + * + * @access public + * @return array All required parameters for all actions + */ + public function getAllActionParameters() + { + $params = array(); + + foreach ($this->getAll() as $action) { + + $action = $this->load($action['action_name'], $action['project_id']); + $params += $action->getActionRequiredParameters(); + } + + return $params; + } + + /** + * Fetch an action + * + * @access public + * @param integer $action_id Action id + * @return array Action data + */ + public function getById($action_id) + { + $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne(); + $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll(); + + return $action; + } + + /** + * Remove an action + * + * @access public + * @param integer $action_id Action id + * @return bool Success or not + */ + public function remove($action_id) + { + return $this->db->table(self::TABLE)->eq('id', $action_id)->remove(); + } + + /** + * Create an action + * + * @access public + * @param array $values Required parameters to save an action + * @return bool Success or not + */ + public function create(array $values) + { + $this->db->startTransaction(); + + $action = array( + 'project_id' => $values['project_id'], + 'event_name' => $values['event_name'], + 'action_name' => $values['action_name'], + ); + + if (! $this->db->table(self::TABLE)->save($action)) { + $this->db->cancelTransaction(); + return false; + } + + $action_id = $this->db->getConnection()->getLastId(); + + foreach ($values['params'] as $param_name => $param_value) { + + $action_param = array( + 'action_id' => $action_id, + 'name' => $param_name, + 'value' => $param_value, + ); + + if (! $this->db->table(self::TABLE_PARAMS)->save($action_param)) { + $this->db->cancelTransaction(); + return false; + } + } + + $this->db->closeTransaction(); + + return true; + } + + /** + * Load all actions and attach events + * + * @access public + */ + public function attachEvents() + { + foreach ($this->getAll() as $action) { + + $listener = $this->load($action['action_name'], $action['project_id']); + + foreach ($action['params'] as $param) { + $listener->setParam($param['name'], $param['value']); + } + + $this->event->attach($action['event_name'], $listener); + } + } + + /** + * Load an action + * + * @access public + * @param string $name Action class name + * @param integer $project_id Project id + * @return mixed Action Instance + * @throw LogicException + */ + public function load($name, $project_id) + { + switch ($name) { + case 'TaskClose': + require_once __DIR__.'/../actions/task_close.php'; + $className = '\Action\TaskClose'; + return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskAssignCurrentUser': + require_once __DIR__.'/../actions/task_assign_current_user.php'; + $className = '\Action\TaskAssignCurrentUser'; + return new $className($project_id, new Task($this->db, $this->event), new Acl($this->db, $this->event)); + case 'TaskAssignSpecificUser': + require_once __DIR__.'/../actions/task_assign_specific_user.php'; + $className = '\Action\TaskAssignSpecificUser'; + return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskDuplicateAnotherProject': + require_once __DIR__.'/../actions/task_duplicate_another_project.php'; + $className = '\Action\TaskDuplicateAnotherProject'; + return new $className($project_id, new Task($this->db, $this->event)); + default: + throw new \LogicException('Action not found: '.$name); + } + } + + /** + * Validate action creation + * + * @access public + * @param array $values Required parameters to save an action + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('event_name', t('This value is required')), + new Validators\Required('action_name', t('This value is required')), + new Validators\Required('params', t('This value is required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/models/base.php b/models/base.php index 97022576..6a1dea97 100644 --- a/models/base.php +++ b/models/base.php @@ -13,45 +13,22 @@ require __DIR__.'/../vendor/SimpleValidator/Validators/Equals.php'; require __DIR__.'/../vendor/SimpleValidator/Validators/AlphaNumeric.php'; require __DIR__.'/../vendor/SimpleValidator/Validators/GreaterThan.php'; require __DIR__.'/../vendor/SimpleValidator/Validators/Date.php'; -require __DIR__.'/../vendor/PicoDb/Database.php'; -require __DIR__.'/schema.php'; abstract class Base { - const APP_VERSION = 'master'; - const DB_VERSION = 9; - - private static $dbInstance = null; protected $db; + protected $event; - public function __construct() - { - if (self::$dbInstance === null) { - self::$dbInstance = $this->getDatabaseInstance(); - } - - $this->db = self::$dbInstance; - } - - public function getDatabaseInstance() + public function __construct(\PicoDb\Database $db, \Core\Event $event) { - $db = new \PicoDb\Database(array( - 'driver' => 'sqlite', - 'filename' => DB_FILENAME - )); - - if ($db->schema()->check(self::DB_VERSION)) { - return $db; - } - else { - die('Unable to migrate database schema!'); - } + $this->db = $db; + $this->event = $event; } // Generate a random token from /dev/urandom or with uniqid() public static function generateToken() { - if (ini_get('open_basedir') === '' and strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + if (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { $token = file_get_contents('/dev/urandom', false, null, 0, 30); } else { @@ -60,19 +37,4 @@ abstract class Base return hash('crc32b', $token); } - - public function getTimestampFromDate($value, $format) - { - $date = \DateTime::createFromFormat($format, $value); - - if ($date !== false) { - $errors = \DateTime::getLastErrors(); - if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) { - $timestamp = $date->getTimestamp(); - return $timestamp > 0 ? $timestamp : 0; - } - } - - return 0; - } } diff --git a/models/board.php b/models/board.php index 1a5b8b81..2835f02f 100644 --- a/models/board.php +++ b/models/board.php @@ -2,6 +2,9 @@ namespace Model; +require_once __DIR__.'/base.php'; +require_once __DIR__.'/task.php'; + use \SimpleValidator\Validator; use \SimpleValidator\Validators; @@ -14,8 +17,8 @@ class Board extends Base { $this->db->startTransaction(); - $taskModel = new \Model\Task; $results = array(); + $taskModel = new Task($this->db, $this->event); foreach ($values as $value) { $results[] = $taskModel->move( @@ -30,7 +33,7 @@ class Board extends Base return ! in_array(false, $results, true); } - // Create board with default columns => must executed inside a transaction + // Create board with default columns => must be executed inside a transaction public function create($project_id, array $columns) { $position = 0; @@ -75,11 +78,10 @@ class Board extends Base // Get columns and tasks for each column public function get($project_id) { - $taskModel = new \Model\Task; - $this->db->startTransaction(); $columns = $this->getColumns($project_id); + $taskModel = new Task($this->db, $this->event); foreach ($columns as &$column) { $column['tasks'] = $taskModel->getAllByColumnId($project_id, $column['id'], array(1)); diff --git a/models/comment.php b/models/comment.php index 9e5cf8fd..c476e693 100644 --- a/models/comment.php +++ b/models/comment.php @@ -2,6 +2,8 @@ namespace Model; +require_once __DIR__.'/base.php'; + use \SimpleValidator\Validator; use \SimpleValidator\Validators; @@ -17,9 +19,9 @@ class Comment extends Base self::TABLE.'.id', self::TABLE.'.date', self::TABLE.'.comment', - \Model\User::TABLE.'.username' + User::TABLE.'.username' ) - ->join(\Model\User::TABLE, 'id', 'user_id') + ->join(User::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date', 'ASC') ->eq(self::TABLE.'.task_id', $task_id) ->findAll(); diff --git a/models/config.php b/models/config.php index 8f818a3b..d2cbe785 100644 --- a/models/config.php +++ b/models/config.php @@ -2,6 +2,8 @@ namespace Model; +require_once __DIR__.'/base.php'; + use \SimpleValidator\Validator; use \SimpleValidator\Validators; diff --git a/models/project.php b/models/project.php index 238a60b4..b2a54571 100644 --- a/models/project.php +++ b/models/project.php @@ -2,6 +2,11 @@ namespace Model; +require_once __DIR__.'/base.php'; +require_once __DIR__.'/acl.php'; +require_once __DIR__.'/board.php'; +require_once __DIR__.'/task.php'; + use \SimpleValidator\Validator; use \SimpleValidator\Validators; @@ -13,16 +18,20 @@ class Project extends Base const INACTIVE = 0; // Get a list of people that can by assigned for tasks - public function getUsersList($project_id) + public function getUsersList($project_id, $prepend = true) { $allowed_users = $this->getAllowedUsers($project_id); + $userModel = new User($this->db, $this->event); if (empty($allowed_users)) { - $userModel = new User; $allowed_users = $userModel->getList(); } - return array(t('Unassigned')) + $allowed_users; + if ($prepend) { + return array(t('Unassigned')) + $allowed_users; + } + + return $allowed_users; } // Get a list of allowed people for a project @@ -30,7 +39,7 @@ class Project extends Base { return $this->db ->table(self::TABLE_USERS) - ->join(\Model\User::TABLE, 'id', 'user_id') + ->join(User::TABLE, 'id', 'user_id') ->eq('project_id', $project_id) ->asc('username') ->listing('user_id', 'username'); @@ -44,7 +53,7 @@ class Project extends Base 'not_allowed' => array(), ); - $userModel = new User; + $userModel = new User($this->db, $this->event); $all_users = $userModel->getList(); $users['allowed'] = $this->getAllowedUsers($project_id); @@ -90,7 +99,7 @@ class Project extends Base // Check if user has admin rights $nb_users = $this->db - ->table(\Model\User::TABLE) + ->table(User::TABLE) ->eq('id', $user_id) ->eq('is_admin', 1) ->count(); @@ -133,9 +142,9 @@ class Project extends Base ->asc('name') ->findAll(); - $taskModel = new \Model\Task; - $boardModel = new \Model\Board; - $aclModel = new \Model\Acl; + $boardModel = new Board($this->db, $this->event); + $taskModel = new Task($this->db, $this->event); + $aclModel = new Acl($this->db, $this->event); foreach ($projects as $pkey => &$project) { @@ -163,9 +172,13 @@ class Project extends Base return $projects; } - public function getList() + public function getList($prepend = true) { - return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + if ($prepend) { + return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + } + + return $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); } public function getAllByStatus($status) @@ -218,7 +231,7 @@ class Project extends Base $project_id = $this->db->getConnection()->getLastId(); - $boardModel = new \Model\Board; + $boardModel = new Board($this->db, $this->event); $boardModel->create($project_id, array( t('Backlog'), t('Ready'), diff --git a/models/schema.php b/models/schema.php index 14604e60..621bc981 100644 --- a/models/schema.php +++ b/models/schema.php @@ -2,6 +2,29 @@ namespace Schema; +function version_10($pdo) +{ + $pdo->exec( + 'CREATE TABLE actions ( + id INTEGER PRIMARY KEY, + project_id INTEGER, + event_name TEXT, + action_name TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )' + ); + + $pdo->exec( + 'CREATE TABLE action_has_params ( + id INTEGER PRIMARY KEY, + action_id INTEGER, + name TEXT, + value TEXT, + FOREIGN KEY(action_id) REFERENCES actions(id) ON DELETE CASCADE + )' + ); +} + function version_9($pdo) { $pdo->exec("ALTER TABLE tasks ADD COLUMN date_due INTEGER"); diff --git a/models/task.php b/models/task.php index 017c7806..fe0f6350 100644 --- a/models/task.php +++ b/models/task.php @@ -2,12 +2,21 @@ namespace Model; +require_once __DIR__.'/base.php'; +require_once __DIR__.'/comment.php'; + use \SimpleValidator\Validator; use \SimpleValidator\Validators; class Task extends Base { - const TABLE = 'tasks'; + const TABLE = 'tasks'; + const EVENT_MOVE_COLUMN = 'task.move.column'; + const EVENT_MOVE_POSITION = 'task.move.position'; + const EVENT_UPDATE = 'task.update'; + const EVENT_CREATE = 'task.create'; + const EVENT_CLOSE = 'task.close'; + const EVENT_OPEN = 'task.open'; public function getColors() { @@ -42,13 +51,13 @@ class Task extends Base self::TABLE.'.position', self::TABLE.'.is_active', self::TABLE.'.score', - \Model\Project::TABLE.'.name AS project_name', - \Model\Board::TABLE.'.title AS column_title', - \Model\User::TABLE.'.username' + Project::TABLE.'.name AS project_name', + Board::TABLE.'.title AS column_title', + User::TABLE.'.username' ) - ->join(\Model\Project::TABLE, 'id', 'project_id') - ->join(\Model\Board::TABLE, 'id', 'column_id') - ->join(\Model\User::TABLE, 'id', 'owner_id') + ->join(Project::TABLE, 'id', 'project_id') + ->join(Board::TABLE, 'id', 'column_id') + ->join(User::TABLE, 'id', 'owner_id') ->eq(self::TABLE.'.id', $task_id) ->findOne(); } @@ -75,11 +84,11 @@ class Task extends Base self::TABLE.'.position', self::TABLE.'.is_active', self::TABLE.'.score', - \Model\Board::TABLE.'.title AS column_title', - \Model\User::TABLE.'.username' + Board::TABLE.'.title AS column_title', + User::TABLE.'.username' ) - ->join(\Model\Board::TABLE, 'id', 'column_id') - ->join(\Model\User::TABLE, 'id', 'owner_id') + ->join(Board::TABLE, 'id', 'column_id') + ->join(User::TABLE, 'id', 'owner_id') ->eq(self::TABLE.'.project_id', $project_id) ->in('is_active', $status) ->desc('date_completed') @@ -107,7 +116,7 @@ class Task extends Base ->asc('position') ->findAll(); - $commentModel = new Comment; + $commentModel = new Comment($this->db, $this->event); foreach ($tasks as &$task) { $task['nb_comments'] = $commentModel->count($task['id']); @@ -126,19 +135,60 @@ class Task extends Base ->count(); } + public function duplicateToAnotherProject($task_id, $project_id) + { + $this->db->startTransaction(); + + $boardModel = new Board($this->db, $this->event); + + // Get the original task + $task = $this->getById($task_id); + + // Cleanup data + unset($task['id']); + unset($task['date_completed']); + + // Assign new values + $task['date_creation'] = time(); + $task['owner_id'] = 0; + $task['is_active'] = 1; + $task['column_id'] = $boardModel->getFirstColumn($project_id); + $task['project_id'] = $project_id; + $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']); + + // Save task + if (! $this->db->table(self::TABLE)->save($task)) { + $this->db->cancelTransaction(); + return false; + } + + $task_id = $this->db->getConnection()->getLastId(); + + $this->db->closeTransaction(); + + // Trigger events + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); + + return $task_id; + } + public function create(array $values) { $this->db->startTransaction(); - unset($values['another_task']); + // Prepare data + if (isset($values['another_task'])) { + unset($values['another_task']); + } - if (! empty($values['date_due'])) { + if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { $values['date_due'] = $this->getTimestampFromDate($values['date_due'], t('m/d/Y')) ?: null; } $values['date_creation'] = time(); $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']); + // Save task if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); return false; @@ -148,38 +198,83 @@ class Task extends Base $this->db->closeTransaction(); + // Trigger events + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values); + return $task_id; } public function update(array $values) { - if (! empty($values['date_due'])) { + // Prepare data + if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { $values['date_due'] = $this->getTimestampFromDate($values['date_due'], t('m/d/Y')) ?: null; } - return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); + $original_task = $this->getById($values['id']); + $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); + + // Trigger events + if ($result) { + + $events = array(); + + if ($this->event->getLastTriggeredEvent() !== self::EVENT_UPDATE) { + $events[] = self::EVENT_UPDATE; + } + + if (isset($values['column_id']) && $original_task['column_id'] != $values['column_id']) { + $events[] = self::EVENT_MOVE_COLUMN; + } + else if (isset($values['position']) && $original_task['position'] != $values['position']) { + $events[] = self::EVENT_MOVE_POSITION; + } + + $event_data = array_merge($original_task, $values); + $event_data['task_id'] = $original_task['id']; + + foreach ($events as $event) { + $this->event->trigger($event, $event_data); + } + } + + return $result; } // Mark a task closed public function close($task_id) { - return $this->db->table(self::TABLE) - ->eq('id', $task_id) - ->update(array( - 'is_active' => 0, - 'date_completed' => time() - )); + $result = $this->db + ->table(self::TABLE) + ->eq('id', $task_id) + ->update(array( + 'is_active' => 0, + 'date_completed' => time() + )); + + if ($result) { + $this->event->trigger(self::EVENT_CLOSE, array('task_id' => $task_id) + $this->getById($task_id)); + } + + return $result; } // Mark a task open public function open($task_id) { - return $this->db->table(self::TABLE) - ->eq('id', $task_id) - ->update(array( - 'is_active' => 1, - 'date_completed' => '' - )); + $result = $this->db + ->table(self::TABLE) + ->eq('id', $task_id) + ->update(array( + 'is_active' => 1, + 'date_completed' => '' + )); + + if ($result) { + $this->event->trigger(self::EVENT_OPEN, array('task_id' => $task_id) + $this->getById($task_id)); + } + + return $result; } // Remove a task @@ -191,10 +286,11 @@ class Task extends Base // Move a task to another column or to another position public function move($task_id, $column_id, $position) { - return (bool) $this->db - ->table(self::TABLE) - ->eq('id', $task_id) - ->update(array('column_id' => $column_id, 'position' => $position)); + return $this->update(array( + 'id' => $task_id, + 'column_id' => $column_id, + 'position' => $position, + )); } public function validateCreation(array $values) @@ -271,4 +367,19 @@ class Task extends Base $v->getErrors() ); } + + public function getTimestampFromDate($value, $format) + { + $date = \DateTime::createFromFormat($format, $value); + + if ($date !== false) { + $errors = \DateTime::getLastErrors(); + if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) { + $timestamp = $date->getTimestamp(); + return $timestamp > 0 ? $timestamp : 0; + } + } + + return 0; + } } diff --git a/models/user.php b/models/user.php index 394cf742..21c65b59 100644 --- a/models/user.php +++ b/models/user.php @@ -2,6 +2,8 @@ namespace Model; +require_once __DIR__.'/base.php'; + use \SimpleValidator\Validator; use \SimpleValidator\Validators; diff --git a/templates/action_index.php b/templates/action_index.php new file mode 100644 index 00000000..6e62949e --- /dev/null +++ b/templates/action_index.php @@ -0,0 +1,73 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <?php if (! empty($actions)): ?> + + <h3><?= t('Defined actions') ?></h3> + <table> + <tr> + <th><?= t('Event name') ?></th> + <th><?= t('Action name') ?></th> + <th><?= t('Action parameters') ?></th> + <th><?= t('Action') ?></th> + </tr> + + <?php foreach ($actions as $action): ?> + <tr> + <td><?= Helper\in_list($action['event_name'], $available_events) ?></td> + <td><?= Helper\in_list($action['action_name'], $available_actions) ?></td> + <td> + <ul> + <?php foreach ($action['params'] as $param): ?> + <li> + <?= Helper\in_list($param['name'], $available_params) ?> = + <strong> + <?php if (Helper\contains($param['name'], 'column_id')): ?> + <?= Helper\in_list($param['value'], $columns_list) ?> + <?php elseif (Helper\contains($param['name'], 'user_id')): ?> + <?= Helper\in_list($param['value'], $users_list) ?> + <?php elseif (Helper\contains($param['name'], 'project_id')): ?> + <?= Helper\in_list($param['value'], $projects_list) ?> + <?php endif ?> + </strong> + </li> + <?php endforeach ?> + </ul> + </td> + <td> + <a href="?controller=action&action=confirm&action_id=<?= $action['id'] ?>"><?= t('Remove') ?></a> + </td> + </tr> + <?php endforeach ?> + + </table> + + <?php endif ?> + + <h3><?= t('Add an action') ?></h3> + <form method="post" action="?controller=action&action=params&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Event'), 'event_name') ?> + <?= Helper\form_select('event_name', $available_events, $values) ?><br/> + + <?= Helper\form_label(t('Action'), 'action_name') ?> + <?= Helper\form_select('action_name', $available_actions, $values) ?><br/> + + <div class="form-help"> + <?= t('When the selected event occurs execute the corresponding action.') ?> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/templates/action_params.php b/templates/action_params.php new file mode 100644 index 00000000..e7efcda5 --- /dev/null +++ b/templates/action_params.php @@ -0,0 +1,38 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <h3><?= t('Define action parameters') ?></h3> + <form method="post" action="?controller=action&action=create&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', $values) ?> + <?= Helper\form_hidden('event_name', $values) ?> + <?= Helper\form_hidden('action_name', $values) ?> + + <?php foreach ($action_params as $param_name => $param_desc): ?> + + <?php if (Helper\contains($param_name, 'column_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $columns_list, $values) ?><br/> + <?php elseif (Helper\contains($param_name, 'user_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $users_list, $values) ?><br/> + <?php elseif (Helper\contains($param_name, 'project_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $projects_list, $values) ?><br/> + <?php endif ?> + + <?php endforeach ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=action&action=index&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/templates/action_remove.php b/templates/action_remove.php new file mode 100644 index 00000000..b90136e8 --- /dev/null +++ b/templates/action_remove.php @@ -0,0 +1,16 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove an automatic action') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this action: "%s"?', Helper\in_list($action['event_name'], $available_events).'/'.Helper\in_list($action['action_name'], $available_actions)) ?> + </p> + + <div class="form-actions"> + <a href="?controller=action&action=remove&action_id=<?= $action['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=action&action=index&project_id=<?= $action['project_id'] ?>"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/templates/config_index.php b/templates/config_index.php index d94ee8b9..0af2f9c2 100644 --- a/templates/config_index.php +++ b/templates/config_index.php @@ -45,7 +45,7 @@ </li> <li> <?= t('Application version:') ?> - <?= Model\Base::APP_VERSION ?> + <?= APP_VERSION ?> </li> </ul> </section> diff --git a/templates/project_index.php b/templates/project_index.php index 7d3e1844..74d807f6 100644 --- a/templates/project_index.php +++ b/templates/project_index.php @@ -69,6 +69,9 @@ <a href="?controller=board&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit board') ?></a> </li> <li> + <a href="?controller=action&action=index&project_id=<?= $project['id'] ?>"><?= t('Automatic actions') ?></a> + </li> + <li> <?php if ($project['is_active']): ?> <a href="?controller=project&action=disable&project_id=<?= $project['id'] ?>"><?= t('Disable') ?></a> <?php else: ?> diff --git a/tests/AclTest.php b/tests/AclTest.php index 0996a51f..566d7245 100644 --- a/tests/AclTest.php +++ b/tests/AclTest.php @@ -1,24 +1,18 @@ <?php -require_once __DIR__.'/../models/base.php'; -require_once __DIR__.'/../models/acl.php'; +require_once __DIR__.'/base.php'; use Model\Acl; -class AclTest extends PHPUnit_Framework_TestCase +class AclTest extends Base { - public function setUp() - { - defined('DB_FILENAME') or define('DB_FILENAME', ':memory:'); - } - public function testAllowedAction() { $acl_rules = array( 'controller1' => array('action1', 'action3'), ); - $acl = new Acl; + $acl = new Acl($this->db, $this->event); $this->assertTrue($acl->isAllowedAction($acl_rules, 'controller1', 'action1')); $this->assertTrue($acl->isAllowedAction($acl_rules, 'controller1', 'action3')); $this->assertFalse($acl->isAllowedAction($acl_rules, 'controller1', 'action2')); @@ -28,7 +22,7 @@ class AclTest extends PHPUnit_Framework_TestCase public function testIsAdmin() { - $acl = new Acl; + $acl = new Acl($this->db, $this->event); $_SESSION = array(); $this->assertFalse($acl->isAdminUser()); @@ -51,7 +45,7 @@ class AclTest extends PHPUnit_Framework_TestCase public function testIsUser() { - $acl = new Acl; + $acl = new Acl($this->db, $this->event); $_SESSION = array(); $this->assertFalse($acl->isRegularUser()); @@ -74,7 +68,7 @@ class AclTest extends PHPUnit_Framework_TestCase public function testIsPageAllowed() { - $acl = new Acl; + $acl = new Acl($this->db, $this->event); // Public access $_SESSION = array(); diff --git a/tests/ActionTest.php b/tests/ActionTest.php new file mode 100644 index 00000000..de7f2c9f --- /dev/null +++ b/tests/ActionTest.php @@ -0,0 +1,164 @@ +<?php + +require_once __DIR__.'/base.php'; + +use Model\Action; +use Model\Project; +use Model\Board; +use Model\Task; + +class ActionTest extends Base +{/* + public function testFetchActions() + { + $action = new Action($this->db, $this->event); + $board = new Board($this->db, $this->event); + $project = new Project($this->db, $this->event); + + $this->assertEquals(1, $project->create(array('name' => 'unit_test'))); + + // We should have nothing + $this->assertEmpty($action->getAll()); + $this->assertEmpty($action->getAllByProject(1)); + + // We create a new action + $this->assertTrue($action->create(array( + 'project_id' => 1, + 'event_name' => Task::EVENT_MOVE_COLUMN, + 'action_name' => 'TaskClose', + 'params' => array( + 'column_id' => 4, + ) + ))); + + // We should have our action + $this->assertNotEmpty($action->getAll()); + $this->assertEquals($action->getAll(), $action->getAllByProject(1)); + + $actions = $action->getAll(); + + $this->assertEquals(1, count($actions)); + $this->assertEquals(1, $actions[0]['project_id']); + $this->assertEquals(Task::EVENT_MOVE_COLUMN, $actions[0]['event_name']); + $this->assertEquals('TaskClose', $actions[0]['action_name']); + $this->assertEquals('column_id', $actions[0]['params'][0]['name']); + $this->assertEquals(4, $actions[0]['params'][0]['value']); + } + + public function testExecuteAction() + { + $task = new Task($this->db, $this->event); + $board = new Board($this->db, $this->event); + $project = new Project($this->db, $this->event); + $action = new Action($this->db, $this->event); + + // We create a project + $this->assertEquals(1, $project->create(array('name' => 'unit_test'))); + + // We create a task + $this->assertEquals(1, $task->create(array( + 'title' => 'unit_test', + 'project_id' => 1, + 'owner_id' => 1, + 'color_id' => 'red', + 'column_id' => 1, + ))); + + // We create a new action + $this->assertTrue($action->create(array( + 'project_id' => 1, + 'event_name' => Task::EVENT_MOVE_COLUMN, + 'action_name' => 'TaskClose', + 'params' => array( + 'column_id' => 4, + ) + ))); + + // We bind events + $action->attachEvents(); + + // Our task should be open + $t1 = $task->getById(1); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals(1, $t1['column_id']); + + // We move our task + $task->move(1, 4, 1); + + // Our task should be closed + $t1 = $task->getById(1); + $this->assertEquals(4, $t1['column_id']); + $this->assertEquals(0, $t1['is_active']); + }*/ + + public function testExecuteMultipleActions() + { + $task = new Task($this->db, $this->event); + $board = new Board($this->db, $this->event); + $project = new Project($this->db, $this->event); + $action = new Action($this->db, $this->event); + + // We create 2 projects + $this->assertEquals(1, $project->create(array('name' => 'unit_test1'))); + $this->assertEquals(2, $project->create(array('name' => 'unit_test2'))); + + // We create a task + $this->assertEquals(1, $task->create(array( + 'title' => 'unit_test', + 'project_id' => 1, + 'owner_id' => 1, + 'color_id' => 'red', + 'column_id' => 1, + ))); + + // We create 2 actions + $this->assertTrue($action->create(array( + 'project_id' => 1, + 'event_name' => Task::EVENT_CLOSE, + 'action_name' => 'TaskDuplicateAnotherProject', + 'params' => array( + 'column_id' => 4, + 'project_id' => 2, + ) + ))); + + $this->assertTrue($action->create(array( + 'project_id' => 1, + 'event_name' => Task::EVENT_MOVE_COLUMN, + 'action_name' => 'TaskClose', + 'params' => array( + 'column_id' => 4, + ) + ))); + + // We bind events + $action->attachEvents(); + + // Events should be attached + $this->assertTrue($this->event->hasListener(Task::EVENT_CLOSE, 'Action\TaskDuplicateAnotherProject')); + $this->assertTrue($this->event->hasListener(Task::EVENT_MOVE_COLUMN, 'Action\TaskClose')); + + // Our task should be open, linked to the first project and in the first column + $t1 = $task->getById(1); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals(1, $t1['column_id']); + $this->assertEquals(1, $t1['project_id']); + + // We move our task + $task->move(1, 4, 1); + $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + + // Our task should be closed + $t1 = $task->getById(1); + $this->assertEquals(4, $t1['column_id']); + $this->assertEquals(0, $t1['is_active']); + + // Our task should be duplicated to the 2nd project + $t2 = $task->getById(2); + $this->assertNotEmpty($t2); + $this->assertNotEquals(4, $t2['column_id']); + $this->assertEquals(1, $t2['is_active']); + $this->assertEquals(2, $t2['project_id']); + $this->assertEquals('unit_test', $t2['title']); + } +} diff --git a/tests/Base.php b/tests/Base.php new file mode 100644 index 00000000..6efb92e5 --- /dev/null +++ b/tests/Base.php @@ -0,0 +1,37 @@ +<?php + +require_once __DIR__.'/../vendor/PicoDb/Database.php'; +require_once __DIR__.'/../core/event.php'; +require_once __DIR__.'/../core/translator.php'; +require_once __DIR__.'/../models/schema.php'; +require_once __DIR__.'/../models/task.php'; +require_once __DIR__.'/../models/acl.php'; +require_once __DIR__.'/../models/comment.php'; +require_once __DIR__.'/../models/project.php'; +require_once __DIR__.'/../models/user.php'; +require_once __DIR__.'/../models/board.php'; +require_once __DIR__.'/../models/action.php'; + +abstract class Base extends PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->db = $this->getDbConnection(); + $this->event = new \Core\Event; + } + + public function getDbConnection() + { + $db = new \PicoDb\Database(array( + 'driver' => 'sqlite', + 'filename' => ':memory:' + )); + + if ($db->schema()->check(10)) { + return $db; + } + else { + die('Unable to migrate database schema!'); + } + } +} diff --git a/tests/ProjectTest.php b/tests/ProjectTest.php index e6725b99..c04c7ff0 100644 --- a/tests/ProjectTest.php +++ b/tests/ProjectTest.php @@ -1,24 +1,19 @@ <?php -require_once __DIR__.'/../lib/translator.php'; -require_once __DIR__.'/../models/base.php'; -require_once __DIR__.'/../models/board.php'; -require_once __DIR__.'/../models/user.php'; -require_once __DIR__.'/../models/project.php'; +require_once __DIR__.'/base.php'; use Model\Project; use Model\User; +use Model\Task; +use Model\Acl; +use Model\Board; -class ProjectTest extends PHPUnit_Framework_TestCase +class ProjectTest extends Base { - public function setUp() - { - defined('DB_FILENAME') or define('DB_FILENAME', ':memory:'); - } - public function testCreation() { - $p = new Project; + $p = new Project($this->db, $this->event); + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); $this->assertNotEmpty($p->getById(1)); } @@ -26,10 +21,10 @@ class ProjectTest extends PHPUnit_Framework_TestCase public function testAllowEverybody() { // We create a regular user - $user = new User; + $user = new User($this->db, $this->event); $user->create(array('username' => 'unittest', 'password' => 'unittest')); - $p = new Project; + $p = new Project($this->db, $this->event); $this->assertEmpty($p->getAllowedUsers(1)); // Nobody is specified for the given project $this->assertTrue($p->isUserAllowed(1, 1)); // Everybody should be allowed $this->assertTrue($p->isUserAllowed(1, 2)); // Everybody should be allowed @@ -37,7 +32,12 @@ class ProjectTest extends PHPUnit_Framework_TestCase public function testAllowUser() { - $p = new Project; + $p = new Project($this->db, $this->event); + $user = new User($this->db, $this->event); + $user->create(array('username' => 'unittest', 'password' => 'unittest')); + + // We create a project + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); // We allow the admin user $this->assertTrue($p->allowUser(1, 1)); @@ -58,7 +58,13 @@ class ProjectTest extends PHPUnit_Framework_TestCase public function testRevokeUser() { - $p = new Project; + $p = new Project($this->db, $this->event); + + $user = new User($this->db, $this->event); + $user->create(array('username' => 'unittest', 'password' => 'unittest')); + + // We create a project + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); // We revoke our admin user $this->assertTrue($p->revokeUser(1, 1)); @@ -107,7 +113,13 @@ class ProjectTest extends PHPUnit_Framework_TestCase public function testUsersList() { - $p = new Project; + $p = new Project($this->db, $this->event); + + $user = new User($this->db, $this->event); + $user->create(array('username' => 'unittest', 'password' => 'unittest')); + + // We create project + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); // No restriction, we should have everybody $this->assertEquals( diff --git a/tests/TaskTest.php b/tests/TaskTest.php index 415faede..a3417e91 100644 --- a/tests/TaskTest.php +++ b/tests/TaskTest.php @@ -1,20 +1,15 @@ <?php -require_once __DIR__.'/../models/base.php'; -require_once __DIR__.'/../models/task.php'; +require_once __DIR__.'/base.php'; use Model\Task; +use Model\Project; -class TaskTest extends PHPUnit_Framework_TestCase +class TaskTest extends Base { - public function setUp() - { - defined('DB_FILENAME') or define('DB_FILENAME', ':memory:'); - } - public function testDateFormat() { - $t = new Task; + $t = new Task($this->db, $this->event); $this->assertEquals('2014-03-05', date('Y-m-d', $t->getTimestampFromDate('05/03/2014', 'd/m/Y'))); $this->assertEquals('2014-03-05', date('Y-m-d', $t->getTimestampFromDate('03/05/2014', 'm/d/Y'))); @@ -24,4 +19,58 @@ class TaskTest extends PHPUnit_Framework_TestCase $this->assertEquals(0, $t->getTimestampFromDate('5/3/14', 'd/m/Y')); $this->assertEquals(0, $t->getTimestampFromDate('5-3-2014', 'd/m/Y')); } + + public function testDuplicateToAnotherProject() + { + $t = new Task($this->db, $this->event); + $p = new Project($this->db, $this->event); + + // We create 2 projects + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + $this->assertEquals(2, $p->create(array('name' => 'test2'))); + + // We create a task + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); + + // We duplicate our task to the 2nd project + $this->assertEquals(2, $t->duplicateToAnotherProject(1, 2)); + $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + } + + public function testEvents() + { + $t = new Task($this->db, $this->event); + $p = new Project($this->db, $this->event); + + // We create a project + $this->assertEquals(1, $p->create(array('name' => 'test'))); + + // We create task + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + + // We update a task + $this->assertTrue($t->update(array('title' => 'test2', 'id' => 1))); + $this->assertEquals(Task::EVENT_UPDATE, $this->event->getLastTriggeredEvent()); + + // We close our task + $this->assertTrue($t->close(1)); + $this->assertEquals(Task::EVENT_CLOSE, $this->event->getLastTriggeredEvent()); + + // We open our task + $this->assertTrue($t->open(1)); + $this->assertEquals(Task::EVENT_OPEN, $this->event->getLastTriggeredEvent()); + + // We change the column of our task + $this->assertTrue($t->move(1, 2, 1)); + $this->assertEquals(Task::EVENT_MOVE_COLUMN, $this->event->getLastTriggeredEvent()); + + // We change the position of our task + $this->assertTrue($t->move(1, 2, 2)); + $this->assertEquals(Task::EVENT_MOVE_POSITION, $this->event->getLastTriggeredEvent()); + + // We change the column and the position of our task + $this->assertTrue($t->move(1, 1, 3)); + $this->assertEquals(Task::EVENT_MOVE_COLUMN, $this->event->getLastTriggeredEvent()); + } } |