From 7749b8ed569f6d27b0bb2ed4c2040e8b61ed4422 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Sun, 9 Mar 2014 23:21:23 -0400 Subject: Automatic actions --- actions/.htaccess | 1 + actions/Base.php | 55 +++++++ actions/task_assign_current_user.php | 45 ++++++ actions/task_assign_specific_user.php | 45 ++++++ actions/task_close.php | 39 +++++ actions/task_duplicate_another_project.php | 43 +++++ assets/js/board.js | 17 ++ common.php | 90 +++++++++++ controllers/action.php | 140 ++++++++++++++++ controllers/app.php | 2 + controllers/base.php | 51 ++---- controllers/board.php | 2 + controllers/config.php | 2 + controllers/project.php | 2 + controllers/task.php | 2 + controllers/user.php | 2 + core/.htaccess | 1 + core/event.php | 124 +++++++++++++++ core/helper.php | 242 ++++++++++++++++++++++++++++ core/registry.php | 79 +++++++++ core/request.php | 46 ++++++ core/response.php | 139 ++++++++++++++++ core/router.php | 59 +++++++ core/session.php | 56 +++++++ core/template.php | 40 +++++ core/translator.php | 122 ++++++++++++++ index.php | 17 +- lib/.htaccess | 1 - lib/helper.php | 237 --------------------------- lib/request.php | 44 ----- lib/response.php | 137 ---------------- lib/router.php | 46 ------ lib/session.php | 54 ------- lib/template.php | 38 ----- lib/translator.php | 122 -------------- locales/fr_FR/translations.php | 31 +++- locales/pl_PL/translations.php | 29 ++++ models/acl.php | 2 + models/action.php | 247 +++++++++++++++++++++++++++++ models/base.php | 48 +----- models/board.php | 10 +- models/comment.php | 6 +- models/config.php | 2 + models/project.php | 37 +++-- models/schema.php | 23 +++ models/task.php | 175 ++++++++++++++++---- models/user.php | 2 + templates/action_index.php | 73 +++++++++ templates/action_params.php | 38 +++++ templates/action_remove.php | 16 ++ templates/config_index.php | 2 +- templates/project_index.php | 3 + tests/AclTest.php | 18 +-- tests/ActionTest.php | 164 +++++++++++++++++++ tests/Base.php | 37 +++++ tests/ProjectTest.php | 46 ++++-- tests/TaskTest.php | 67 ++++++-- 57 files changed, 2353 insertions(+), 865 deletions(-) create mode 100644 actions/.htaccess create mode 100644 actions/Base.php create mode 100644 actions/task_assign_current_user.php create mode 100644 actions/task_assign_specific_user.php create mode 100644 actions/task_close.php create mode 100644 actions/task_duplicate_another_project.php create mode 100644 common.php create mode 100644 controllers/action.php create mode 100644 core/.htaccess create mode 100644 core/event.php create mode 100644 core/helper.php create mode 100644 core/registry.php create mode 100644 core/request.php create mode 100644 core/response.php create mode 100644 core/router.php create mode 100644 core/session.php create mode 100644 core/template.php create mode 100644 core/translator.php delete mode 100644 lib/.htaccess delete mode 100644 lib/helper.php delete mode 100644 lib/request.php delete mode 100644 lib/response.php delete mode 100644 lib/router.php delete mode 100644 lib/session.php delete mode 100644 lib/template.php delete mode 100644 lib/translator.php create mode 100644 models/action.php create mode 100644 templates/action_index.php create mode 100644 templates/action_params.php create mode 100644 templates/action_remove.php create mode 100644 tests/ActionTest.php create mode 100644 tests/Base.php diff --git a/actions/.htaccess b/actions/.htaccess new file mode 100644 index 00000000..14249c50 --- /dev/null +++ b/actions/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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/core/helper.php b/core/helper.php new file mode 100644 index 00000000..e4ad26f1 --- /dev/null +++ b/core/helper.php @@ -0,0 +1,242 @@ +no_markup = true; + $parser->no_entities = true; + + return $parser->transform($text); +} + +function get_current_base_url() +{ + $url = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; + $url .= $_SERVER['SERVER_NAME']; + $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; + $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/'; + + return $url; +} + +function escape($value) +{ + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); +} + +function flash($html) +{ + $data = ''; + + if (isset($_SESSION['flash_message'])) { + $data = sprintf($html, escape($_SESSION['flash_message'])); + unset($_SESSION['flash_message']); + } + + return $data; +} + +function flash_error($html) +{ + $data = ''; + + if (isset($_SESSION['flash_error_message'])) { + $data = sprintf($html, escape($_SESSION['flash_error_message'])); + unset($_SESSION['flash_error_message']); + } + + return $data; +} + +function format_bytes($size, $precision = 2) +{ + $base = log($size) / log(1024); + $suffixes = array('', 'k', 'M', 'G', 'T'); + + return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)]; +} + +function get_host_from_url($url) +{ + return escape(parse_url($url, PHP_URL_HOST)) ?: $url; +} + +function summary($value, $min_length = 5, $max_length = 120, $end = '[...]') +{ + $length = strlen($value); + + if ($length > $max_length) { + return substr($value, 0, strpos($value, ' ', $max_length)).' '.$end; + } + else if ($length < $min_length) { + return ''; + } + + return $value; +} + +function contains($haystack, $needle) +{ + return strpos($haystack, $needle) !== false; +} + +function in_list($id, array $listing) +{ + if (isset($listing[$id])) { + return escape($listing[$id]); + } + + return '?'; +} + +function error_class(array $errors, $name) +{ + return ! isset($errors[$name]) ? '' : ' form-error'; +} + +function error_list(array $errors, $name) +{ + $html = ''; + + if (isset($errors[$name])) { + + $html .= ''; + } + + return $html; +} + +function form_value($values, $name) +{ + if (isset($values->$name)) { + return 'value="'.escape($values->$name).'"'; + } + + return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : ''; +} + +function form_hidden($name, $values = array()) +{ + return ''; +} + +function form_default_select($name, array $options, $values = array(), array $errors = array(), $class = '') +{ + $options = array('' => '?') + $options; + return form_select($name, $options, $values, $errors, $class); +} + +function form_select($name, array $options, $values = array(), array $errors = array(), $class = '') +{ + $html = ''; + $html .= error_list($errors, $name); + + return $html; +} + +function form_radios($name, array $options, array $values = array()) +{ + $html = ''; + + foreach ($options as $value => $label) { + $html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value); + } + + return $html; +} + +function form_radio($name, $label, $value, $selected = false, $class = '') +{ + return ''; +} + +function form_checkbox($name, $label, $value, $checked = false, $class = '') +{ + return ''; +} + +function form_label($label, $name, $class = '') +{ + return ''; +} + +function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + $class .= error_class($errors, $name); + + $html = ''; + $html .= error_list($errors, $name); + + return $html; +} + +function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + $class .= error_class($errors, $name); + + $html = ''; + $html .= error_list($errors, $name); + + return $html; +} + +function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('text', $name, $values, $errors, $attributes, $class); +} + +function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('password', $name, $values, $errors, $attributes, $class); +} + +function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('email', $name, $values, $errors, $attributes, $class); +} + +function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('date', $name, $values, $errors, $attributes, $class); +} + +function form_number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('number', $name, $values, $errors, $attributes, $class); +} 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 @@ +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/core/request.php b/core/request.php new file mode 100644 index 00000000..b2c3e12e --- /dev/null +++ b/core/request.php @@ -0,0 +1,46 @@ +getValues(); + return isset($values[$name]) ? $values[$name] : null; + } + + public function getValues() + { + if (! empty($_POST)) return $_POST; + + $result = json_decode($this->getBody(), true); + if ($result) return $result; + + return array(); + } + + public function getBody() + { + return file_get_contents('php://input'); + } + + public function getFileContent($name) + { + if (isset($_FILES[$name])) { + return file_get_contents($_FILES[$name]['tmp_name']); + } + + return ''; + } +} diff --git a/core/response.php b/core/response.php new file mode 100644 index 00000000..4a00ed79 --- /dev/null +++ b/core/response.php @@ -0,0 +1,139 @@ +status($status_code); + + header('Content-Type: application/json'); + echo json_encode($data); + + exit; + } + + public function text($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/plain; charset=utf-8'); + echo $data; + + exit; + } + + public function html($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/html; charset=utf-8'); + echo $data; + + exit; + } + + public function xml($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/xml; charset=utf-8'); + echo $data; + + exit; + } + + public function js($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/javascript; charset=utf-8'); + echo $data; + + exit; + } + + public function binary($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Transfer-Encoding: binary'); + header('Content-Type: application/octet-stream'); + echo $data; + + exit; + } + + public function csp(array $policies = array()) + { + $policies['default-src'] = "'self'"; + $values = ''; + + foreach ($policies as $policy => $hosts) { + + if (is_array($hosts)) { + + $acl = ''; + + foreach ($hosts as &$host) { + + if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) { + $acl .= $host.' '; + } + } + } + else { + + $acl = $hosts; + } + + $values .= $policy.' '.trim($acl).'; '; + } + + header('Content-Security-Policy: '.$values); + } + + public function nosniff() + { + header('X-Content-Type-Options: nosniff'); + } + + public function xss() + { + header('X-XSS-Protection: 1; mode=block'); + } + + public function hsts() + { + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') { + header('Strict-Transport-Security: max-age=31536000'); + } + } + + public function xframe($mode = 'DENY', array $urls = array()) + { + header('X-Frame-Options: '.$mode.' '.implode(' ', $urls)); + } +} diff --git a/core/router.php b/core/router.php new file mode 100644 index 00000000..5a27276c --- /dev/null +++ b/core/router.php @@ -0,0 +1,59 @@ +registry = $registry; + $this->controller = empty($_GET['controller']) ? $controller : $_GET['controller']; + $this->action = empty($_GET['action']) ? $controller : $_GET['action']; + } + + public function sanitize($value, $default_value) + { + return ! ctype_alpha($value) || empty($value) ? $default_value : strtolower($value); + } + + public function load($filename, $class, $method) + { + if (file_exists($filename)) { + + require $filename; + + if (! method_exists($class, $method)) return false; + + $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(); + + return true; + } + + return false; + } + + public function execute() + { + $this->controller = $this->sanitize($this->controller, 'app'); + $this->action = $this->sanitize($this->action, 'index'); + + if (! $this->load('controllers/'.$this->controller.'.php', '\Controller\\'.$this->controller, $this->action)) { + die('Page not found!'); + } + } +} diff --git a/core/session.php b/core/session.php new file mode 100644 index 00000000..7fe8e0c1 --- /dev/null +++ b/core/session.php @@ -0,0 +1,56 @@ + 'value']); + public function load() + { + if (func_num_args() < 1 || func_num_args() > 2) { + die('Invalid template arguments'); + } + + if (! file_exists(self::PATH.func_get_arg(0).'.php')) { + die('Unable to load the template: "'.func_get_arg(0).'"'); + } + + if (func_num_args() === 2) { + + if (! is_array(func_get_arg(1))) { + die('Template variables must be an array'); + } + + extract(func_get_arg(1)); + } + + ob_start(); + + include self::PATH.func_get_arg(0).'.php'; + + return ob_get_clean(); + } + + public function layout($template_name, array $template_args = array(), $layout_name = 'layout') + { + return $this->load($layout_name, $template_args + array('content_for_layout' => $this->load($template_name, $template_args))); + } +} diff --git a/core/translator.php b/core/translator.php new file mode 100644 index 00000000..75d40a23 --- /dev/null +++ b/core/translator.php @@ -0,0 +1,122 @@ +getFilename(), '.php') !== false) { + $locales = array_merge($locales, include $fileinfo->getPathname()); + } + } + } + + container($locales); + } + + function container($locales = null) + { + static $values = array(); + + if ($locales !== null) { + $values = $locales; + } + + return $values; + } +} + + +namespace { + + function t() { + return call_user_func_array('\Translator\translate', func_get_args()); + } + + function c() { + return call_user_func_array('\Translator\currency', func_get_args()); + } + + function n() { + return call_user_func_array('\Translator\number', func_get_args()); + } + + function dt() { + return call_user_func_array('\Translator\datetime', func_get_args()); + } +} diff --git a/index.php b/index.php index 8ab3dcba..bd691c10 100644 --- a/index.php +++ b/index.php @@ -1,19 +1,8 @@ execute(); diff --git a/lib/.htaccess b/lib/.htaccess deleted file mode 100644 index 14249c50..00000000 --- a/lib/.htaccess +++ /dev/null @@ -1 +0,0 @@ -Deny from all \ No newline at end of file diff --git a/lib/helper.php b/lib/helper.php deleted file mode 100644 index 50071be1..00000000 --- a/lib/helper.php +++ /dev/null @@ -1,237 +0,0 @@ -no_markup = true; - $parser->no_entities = true; - - return $parser->transform($text); -} - -function get_current_base_url() -{ - $url = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; - $url .= $_SERVER['SERVER_NAME']; - $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; - $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/'; - - return $url; -} - -function escape($value) -{ - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); -} - -function flash($html) -{ - $data = ''; - - if (isset($_SESSION['flash_message'])) { - $data = sprintf($html, escape($_SESSION['flash_message'])); - unset($_SESSION['flash_message']); - } - - return $data; -} - -function flash_error($html) -{ - $data = ''; - - if (isset($_SESSION['flash_error_message'])) { - $data = sprintf($html, escape($_SESSION['flash_error_message'])); - unset($_SESSION['flash_error_message']); - } - - return $data; -} - -function format_bytes($size, $precision = 2) -{ - $base = log($size) / log(1024); - $suffixes = array('', 'k', 'M', 'G', 'T'); - - return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)]; -} - -function get_host_from_url($url) -{ - return escape(parse_url($url, PHP_URL_HOST)) ?: $url; -} - -function summary($value, $min_length = 5, $max_length = 120, $end = '[...]') -{ - $length = strlen($value); - - if ($length > $max_length) { - return substr($value, 0, strpos($value, ' ', $max_length)).' '.$end; - } - else if ($length < $min_length) { - return ''; - } - - return $value; -} - -function in_list($id, array $listing) -{ - if (isset($listing[$id])) { - return escape($listing[$id]); - } - - return '?'; -} - -function error_class(array $errors, $name) -{ - return ! isset($errors[$name]) ? '' : ' form-error'; -} - -function error_list(array $errors, $name) -{ - $html = ''; - - if (isset($errors[$name])) { - - $html .= ''; - } - - return $html; -} - -function form_value($values, $name) -{ - if (isset($values->$name)) { - return 'value="'.escape($values->$name).'"'; - } - - return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : ''; -} - -function form_hidden($name, $values = array()) -{ - return ''; -} - -function form_default_select($name, array $options, $values = array(), array $errors = array(), $class = '') -{ - $options = array('' => '?') + $options; - return form_select($name, $options, $values, $errors, $class); -} - -function form_select($name, array $options, $values = array(), array $errors = array(), $class = '') -{ - $html = ''; - $html .= error_list($errors, $name); - - return $html; -} - -function form_radios($name, array $options, array $values = array()) -{ - $html = ''; - - foreach ($options as $value => $label) { - $html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value); - } - - return $html; -} - -function form_radio($name, $label, $value, $selected = false, $class = '') -{ - return ''; -} - -function form_checkbox($name, $label, $value, $checked = false, $class = '') -{ - return ''; -} - -function form_label($label, $name, $class = '') -{ - return ''; -} - -function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - $class .= error_class($errors, $name); - - $html = ''; - $html .= error_list($errors, $name); - - return $html; -} - -function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - $class .= error_class($errors, $name); - - $html = ''; - $html .= error_list($errors, $name); - - return $html; -} - -function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('text', $name, $values, $errors, $attributes, $class); -} - -function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('password', $name, $values, $errors, $attributes, $class); -} - -function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('email', $name, $values, $errors, $attributes, $class); -} - -function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('date', $name, $values, $errors, $attributes, $class); -} - -function form_number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('number', $name, $values, $errors, $attributes, $class); -} diff --git a/lib/request.php b/lib/request.php deleted file mode 100644 index 8840e7a4..00000000 --- a/lib/request.php +++ /dev/null @@ -1,44 +0,0 @@ -getValues(); - return isset($values[$name]) ? $values[$name] : null; - } - - public function getValues() - { - if (! empty($_POST)) return $_POST; - - $result = json_decode($this->getBody(), true); - if ($result) return $result; - - return array(); - } - - public function getBody() - { - return file_get_contents('php://input'); - } - - public function getFileContent($name) - { - if (isset($_FILES[$name])) { - return file_get_contents($_FILES[$name]['tmp_name']); - } - - return ''; - } -} diff --git a/lib/response.php b/lib/response.php deleted file mode 100644 index ceaf32c5..00000000 --- a/lib/response.php +++ /dev/null @@ -1,137 +0,0 @@ -status($status_code); - - header('Content-Type: application/json'); - echo json_encode($data); - - exit; - } - - public function text($data, $status_code = 200) - { - $this->status($status_code); - - header('Content-Type: text/plain; charset=utf-8'); - echo $data; - - exit; - } - - public function html($data, $status_code = 200) - { - $this->status($status_code); - - header('Content-Type: text/html; charset=utf-8'); - echo $data; - - exit; - } - - public function xml($data, $status_code = 200) - { - $this->status($status_code); - - header('Content-Type: text/xml; charset=utf-8'); - echo $data; - - exit; - } - - public function js($data, $status_code = 200) - { - $this->status($status_code); - - header('Content-Type: text/javascript; charset=utf-8'); - echo $data; - - exit; - } - - public function binary($data, $status_code = 200) - { - $this->status($status_code); - - header('Content-Transfer-Encoding: binary'); - header('Content-Type: application/octet-stream'); - echo $data; - - exit; - } - - public function csp(array $policies = array()) - { - $policies['default-src'] = "'self'"; - $values = ''; - - foreach ($policies as $policy => $hosts) { - - if (is_array($hosts)) { - - $acl = ''; - - foreach ($hosts as &$host) { - - if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) { - $acl .= $host.' '; - } - } - } - else { - - $acl = $hosts; - } - - $values .= $policy.' '.trim($acl).'; '; - } - - header('Content-Security-Policy: '.$values); - } - - public function nosniff() - { - header('X-Content-Type-Options: nosniff'); - } - - public function xss() - { - header('X-XSS-Protection: 1; mode=block'); - } - - public function hsts() - { - if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') { - header('Strict-Transport-Security: max-age=31536000'); - } - } - - public function xframe($mode = 'DENY', array $urls = array()) - { - header('X-Frame-Options: '.$mode.' '.implode(' ', $urls)); - } -} diff --git a/lib/router.php b/lib/router.php deleted file mode 100644 index 979968d4..00000000 --- a/lib/router.php +++ /dev/null @@ -1,46 +0,0 @@ -controller = empty($_GET['controller']) ? $controller : $_GET['controller']; - $this->action = empty($_GET['action']) ? $controller : $_GET['action']; - } - - public function sanitize($value, $default_value) - { - return ! ctype_alpha($value) || empty($value) ? $default_value : strtolower($value); - } - - public function loadController($filename, $class, $method) - { - if (file_exists($filename)) { - - require $filename; - - if (! method_exists($class, $method)) return false; - - $instance = new $class; - $instance->beforeAction($this->controller, $this->action); - $instance->$method(); - - return true; - } - - return false; - } - - public function execute() - { - $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)) { - die('Page not found!'); - } - } -} diff --git a/lib/session.php b/lib/session.php deleted file mode 100644 index 688004b3..00000000 --- a/lib/session.php +++ /dev/null @@ -1,54 +0,0 @@ - 'value']); - public function load() - { - if (func_num_args() < 1 || func_num_args() > 2) { - die('Invalid template arguments'); - } - - if (! file_exists(self::PATH.func_get_arg(0).'.php')) { - die('Unable to load the template: "'.func_get_arg(0).'"'); - } - - if (func_num_args() === 2) { - - if (! is_array(func_get_arg(1))) { - die('Template variables must be an array'); - } - - extract(func_get_arg(1)); - } - - ob_start(); - - include self::PATH.func_get_arg(0).'.php'; - - return ob_get_clean(); - } - - public function layout($template_name, array $template_args = array(), $layout_name = 'layout') - { - return $this->load($layout_name, $template_args + array('content_for_layout' => $this->load($template_name, $template_args))); - } -} diff --git a/lib/translator.php b/lib/translator.php deleted file mode 100644 index 75d40a23..00000000 --- a/lib/translator.php +++ /dev/null @@ -1,122 +0,0 @@ -getFilename(), '.php') !== false) { - $locales = array_merge($locales, include $fileinfo->getPathname()); - } - } - } - - container($locales); - } - - function container($locales = null) - { - static $values = array(); - - if ($locales !== null) { - $values = $locales; - } - - return $values; - } -} - - -namespace { - - function t() { - return call_user_func_array('\Translator\translate', func_get_args()); - } - - function c() { - return call_user_func_array('\Translator\currency', func_get_args()); - } - - function n() { - return call_user_func_array('\Translator\number', func_get_args()); - } - - function dt() { - return call_user_func_array('\Translator\datetime', func_get_args()); - } -} 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 @@ + 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 @@ +
+ +
+ + + +

+ + + + + + + + + + + + + + + + + +
+
    + +
  • + = + + + + + + + + + +
  • + +
+
+ +
+ + + +

+
+ + + + +
+ + +
+ +
+ +
+ +
+ +
+
+
+
\ 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 @@ +
+ +
+ +

+
+ + + + + + $param_desc): ?> + + + +
+ + +
+ + +
+ + + + +
+ + +
+
+
+
\ 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 @@ +
+ + +
+

+ +

+ +
+ + +
+
+
\ 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 @@
  • - +
  • 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 @@ -68,6 +68,9 @@
  • +
  • + +
  • 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 @@ 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 @@ +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 @@ +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 @@ 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 @@ 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()); + } } -- cgit v1.2.3