From a296ba5b18487d312acca2513d461a210a460fae Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 3 Jan 2016 16:43:13 -0500 Subject: Improve Automatic Actions plugin api --- ChangeLog | 27 +- app/Action/Base.php | 118 ++-- app/Action/CommentCreation.php | 17 +- app/Action/CommentCreationMoveTaskColumn.php | 94 +++ app/Action/TaskAssignCategoryColor.php | 11 + app/Action/TaskAssignCategoryLabel.php | 15 +- app/Action/TaskAssignCategoryLink.php | 13 +- app/Action/TaskAssignColorCategory.php | 11 + app/Action/TaskAssignColorColumn.php | 11 + app/Action/TaskAssignColorLink.php | 11 + app/Action/TaskAssignColorUser.php | 11 + app/Action/TaskAssignCurrentUser.php | 19 +- app/Action/TaskAssignCurrentUserColumn.php | 98 +++ app/Action/TaskAssignSpecificUser.php | 11 + app/Action/TaskAssignUser.php | 13 +- app/Action/TaskClose.php | 48 +- app/Action/TaskCloseColumn.php | 84 +++ app/Action/TaskCreation.php | 11 + app/Action/TaskDuplicateAnotherProject.php | 13 +- app/Action/TaskEmail.php | 11 + app/Action/TaskLogMoveAnotherColumn.php | 83 --- app/Action/TaskMoveAnotherProject.php | 11 + app/Action/TaskMoveColumnAssigned.php | 12 +- app/Action/TaskMoveColumnCategoryChange.php | 15 +- app/Action/TaskMoveColumnUnAssigned.php | 12 +- app/Action/TaskOpen.php | 11 + app/Action/TaskUpdateStartDate.php | 11 + app/Api/Action.php | 28 +- app/Controller/Action.php | 21 +- app/Core/Action/ActionManager.php | 141 +++++ app/Core/Base.php | 3 + app/Core/Event/EventManager.php | 83 +++ app/Core/Mail/Client.php | 20 +- app/Integration/BitbucketWebhook.php | 2 +- app/Integration/GithubWebhook.php | 2 +- app/Integration/GitlabWebhook.php | 2 +- app/Model/Action.php | 301 +-------- app/Model/ActionParameter.php | 122 ++++ app/Model/Comment.php | 1 + app/Model/ProjectUserRole.php | 6 +- app/Schema/Sqlite.php | 25 +- app/ServiceProvider/ActionProvider.php | 78 +++ app/ServiceProvider/ClassProvider.php | 4 + app/ServiceProvider/EventDispatcherProvider.php | 3 - app/Subscriber/BootstrapSubscriber.php | 13 +- app/Template/action/index.php | 30 +- app/common.php | 1 + doc/api-action-procedures.markdown | 42 +- doc/automatic-actions.markdown | 66 +- doc/plugin-registration.markdown | 37 -- tests/functionals/ApiTest.php | 6 +- tests/units/Action/BaseActionTest.php | 144 +++++ .../Action/CommentCreationMoveTaskColumnTest.php | 58 ++ tests/units/Action/CommentCreationTest.php | 165 ++--- tests/units/Action/TaskAssignCategoryColorTest.php | 60 ++ tests/units/Action/TaskAssignCategoryLabelTest.php | 85 +++ tests/units/Action/TaskAssignCategoryLinkTest.php | 32 +- tests/units/Action/TaskAssignColorCategoryTest.php | 90 +-- tests/units/Action/TaskAssignColorColumnTest.php | 55 +- tests/units/Action/TaskAssignColorLinkTest.php | 67 +- tests/units/Action/TaskAssignColorUserTest.php | 78 +-- .../Action/TaskAssignCurrentUserColumnTest.php | 75 +++ tests/units/Action/TaskAssignCurrentUserTest.php | 74 +-- tests/units/Action/TaskAssignSpecificUserTest.php | 72 +-- tests/units/Action/TaskAssignUserTest.php | 62 ++ tests/units/Action/TaskCloseColumnTest.php | 53 ++ tests/units/Action/TaskCloseTest.php | 106 +--- tests/units/Action/TaskCreationTest.php | 49 ++ .../Action/TaskDuplicateAnotherProjectTest.php | 95 +-- tests/units/Action/TaskEmailTest.php | 133 +--- tests/units/Action/TaskMoveAnotherProjectTest.php | 90 +-- tests/units/Action/TaskMoveColumnAssignedTest.php | 56 ++ .../Action/TaskMoveColumnCategoryChangeTest.php | 108 ++-- .../units/Action/TaskMoveColumnUnAssignedTest.php | 74 +++ tests/units/Action/TaskOpenTest.php | 51 ++ tests/units/Action/TaskUpdateStartDateTest.php | 54 +- tests/units/Core/Action/ActionManagerTest.php | 157 +++++ tests/units/Core/Event/EventManagerTest.php | 18 + tests/units/Integration/BitbucketWebhookTest.php | 2 +- tests/units/Integration/GithubWebhookTest.php | 2 +- tests/units/Integration/GitlabWebhookTest.php | 2 +- tests/units/Model/ActionTest.php | 698 ++++++++++++--------- tests/units/Model/ProjectDuplicationTest.php | 6 +- 83 files changed, 3028 insertions(+), 1682 deletions(-) create mode 100644 app/Action/CommentCreationMoveTaskColumn.php create mode 100644 app/Action/TaskAssignCurrentUserColumn.php create mode 100644 app/Action/TaskCloseColumn.php delete mode 100644 app/Action/TaskLogMoveAnotherColumn.php create mode 100644 app/Core/Action/ActionManager.php create mode 100644 app/Core/Event/EventManager.php create mode 100644 app/Model/ActionParameter.php create mode 100644 app/ServiceProvider/ActionProvider.php create mode 100644 tests/units/Action/BaseActionTest.php create mode 100644 tests/units/Action/CommentCreationMoveTaskColumnTest.php create mode 100644 tests/units/Action/TaskAssignCategoryColorTest.php create mode 100644 tests/units/Action/TaskAssignCategoryLabelTest.php create mode 100644 tests/units/Action/TaskAssignCurrentUserColumnTest.php create mode 100644 tests/units/Action/TaskAssignUserTest.php create mode 100644 tests/units/Action/TaskCloseColumnTest.php create mode 100644 tests/units/Action/TaskCreationTest.php create mode 100644 tests/units/Action/TaskMoveColumnAssignedTest.php create mode 100644 tests/units/Action/TaskMoveColumnUnAssignedTest.php create mode 100644 tests/units/Action/TaskOpenTest.php create mode 100644 tests/units/Core/Action/ActionManagerTest.php create mode 100644 tests/units/Core/Event/EventManagerTest.php diff --git a/ChangeLog b/ChangeLog index 3b261700..54518b34 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,24 +1,31 @@ Version 1.0.23 (unreleased) --------------------------- +Breaking changes: + +* Plugin API changes for Automatic Actions +* Automatic Action to close a task doesn't have the column parameter anymore (use the action "Close a task in a specific column") +* Action name stored in the database is now the absolute class name + New features: -- Added support of user mentions (@username) -- Added report to compare working hours between open and closed tasks -- Added the possiblity to define custom routes from plugins -- Added new method to remove metadata +* Added support of user mentions (@username) +* Added report to compare working hours between open and closed tasks +* Added the possiblity to define custom routes from plugins +* Added new method to remove metadata Improvements: -- Improving performance during task position change (SQL queries are 3 times faster than before) -- Do not show window scrollbars when individual column scrolling is enabled +* Improving performance during task position change (SQL queries are 3 times faster than before) +* Do not show window scrollbars when individual column scrolling is enabled +* Automatic Actions code improvements and unit tests Bug fixes: -- Fix compatiblity issue with FreeBSD for session.hash_function parameter -- Fix wrong constant name that cause a PHP error in project management section -- Fix pagination in group members listing -- Avoid PHP error when enabling LDAP group provider with PHP < 5.5 +* Fix compatiblity issue with FreeBSD for session.hash_function parameter +* Fix wrong constant name that cause a PHP error in project management section +* Fix pagination in group members listing +* Avoid PHP error when enabling LDAP group provider with PHP < 5.5 Version 1.0.22 -------------- diff --git a/app/Action/Base.php b/app/Action/Base.php index 81e2ccc6..febd6cfc 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -3,7 +3,6 @@ namespace Kanboard\Action; use Kanboard\Event\GenericEvent; -use Pimple\Container; /** * Base class for automatic actions @@ -13,6 +12,14 @@ use Pimple\Container; */ abstract class Base extends \Kanboard\Core\Base { + /** + * Extended events + * + * @access private + * @var array + */ + private $compatibleEvents = array(); + /** * Flag for called listener * @@ -27,7 +34,7 @@ abstract class Base extends \Kanboard\Core\Base * @access private * @var integer */ - private $project_id = 0; + private $projectId = 0; /** * User parameters @@ -38,20 +45,25 @@ abstract class Base extends \Kanboard\Core\Base private $params = array(); /** - * Attached event name + * Get automatic action name * - * @access protected - * @var string + * @final + * @access public + * @return string */ - protected $event_name = ''; + final public function getName() + { + return '\\'.get_called_class(); + } /** - * Container instance + * Get automatic action description * - * @access protected - * @var \Pimple\Container + * @abstract + * @access public + * @return string */ - protected $container; + abstract public function getDescription(); /** * Execute the action @@ -100,30 +112,26 @@ abstract class Base extends \Kanboard\Core\Base abstract public function hasRequiredCondition(array $data); /** - * Constructor + * Return class information * * @access public - * @param \Pimple\Container $container Container - * @param integer $project_id Project id - * @param string $event_name Attached event name + * @return string */ - public function __construct(Container $container, $project_id, $event_name) + public function __toString() { - $this->container = $container; - $this->project_id = $project_id; - $this->event_name = $event_name; - $this->called = false; + return $this->getName(); } /** - * Return class information + * Set project id * * @access public - * @return string + * @return Base */ - public function __toString() + public function setProjectId($project_id) { - return get_called_class(); + $this->projectId = $project_id; + return $this; } /** @@ -134,7 +142,7 @@ abstract class Base extends \Kanboard\Core\Base */ public function getProjectId() { - return $this->project_id; + return $this->projectId; } /** @@ -143,10 +151,12 @@ abstract class Base extends \Kanboard\Core\Base * @access public * @param string $name Parameter name * @param mixed $value Value + * @param Base */ public function setParam($name, $value) { $this->params[$name] = $value; + return $this; } /** @@ -154,24 +164,25 @@ abstract class Base extends \Kanboard\Core\Base * * @access public * @param string $name Parameter name - * @param mixed $default_value Default value + * @param mixed $default Default value * @return mixed */ - public function getParam($name, $default_value = null) + public function getParam($name, $default = null) { - return isset($this->params[$name]) ? $this->params[$name] : $default_value; + return isset($this->params[$name]) ? $this->params[$name] : $default; } /** * Check if an action is executable (right project and required parameters) * * @access public - * @param array $data Event data dictionary - * @return bool True if the action is executable + * @param array $data + * @param string $eventName + * @return bool */ - public function isExecutable(array $data) + public function isExecutable(array $data, $eventName) { - return $this->hasCompatibleEvent() && + return $this->hasCompatibleEvent($eventName) && $this->hasRequiredProject($data) && $this->hasRequiredParameters($data) && $this->hasRequiredCondition($data); @@ -181,11 +192,12 @@ abstract class Base extends \Kanboard\Core\Base * Check if the event is compatible with the action * * @access public + * @param string $eventName * @return bool */ - public function hasCompatibleEvent() + public function hasCompatibleEvent($eventName) { - return in_array($this->event_name, $this->getCompatibleEvents()); + return in_array($eventName, $this->getEvents()); } /** @@ -197,7 +209,7 @@ abstract class Base extends \Kanboard\Core\Base */ public function hasRequiredProject(array $data) { - return isset($data['project_id']) && $data['project_id'] == $this->project_id; + return isset($data['project_id']) && $data['project_id'] == $this->getProjectId(); } /** @@ -222,10 +234,11 @@ abstract class Base extends \Kanboard\Core\Base * Execute the action * * @access public - * @param \Event\GenericEvent $event Event data dictionary - * @return bool True if the action was executed or false when not executed + * @param \Kanboard\Event\GenericEvent $event + * @param string $eventName + * @return bool */ - public function execute(GenericEvent $event) + public function execute(GenericEvent $event, $eventName) { // Avoid infinite loop, a listener instance can be called only one time if ($this->called) { @@ -235,15 +248,38 @@ abstract class Base extends \Kanboard\Core\Base $data = $event->getAll(); $result = false; - if ($this->isExecutable($data)) { + if ($this->isExecutable($data, $eventName)) { $this->called = true; $result = $this->doAction($data); } - if (DEBUG) { - $this->logger->debug(get_called_class().' => '.($result ? 'true' : 'false')); - } + $this->logger->debug('AutomaticAction '.$this->getName().' => '.($result ? 'true' : 'false')); return $result; } + + /** + * Register a new event for the automatic action + * + * @access public + * @param string $event + * @param string $description + */ + public function addEvent($event, $description) + { + $this->eventManager->register($event, $description); + $this->compatibleEvents[] = $event; + return $this; + } + + /** + * Get all compatible events of an automatic action + * + * @access public + * @return array + */ + public function getEvents() + { + return array_unique(array_merge($this->getCompatibleEvents(), $this->compatibleEvents)); + } } diff --git a/app/Action/CommentCreation.php b/app/Action/CommentCreation.php index 73fedc3b..d6ea2074 100644 --- a/app/Action/CommentCreation.php +++ b/app/Action/CommentCreation.php @@ -14,6 +14,17 @@ use Kanboard\Integration\GitlabWebhook; */ class CommentCreation extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Create a comment from an external provider'); + } + /** * Get the list of compatible events * @@ -67,9 +78,9 @@ class CommentCreation extends Base { return (bool) $this->comment->create(array( 'reference' => isset($data['reference']) ? $data['reference'] : '', - 'comment' => empty($data['comment']) ? $data['commit_comment'] : $data['comment'], + 'comment' => $data['comment'], 'task_id' => $data['task_id'], - 'user_id' => empty($data['user_id']) ? 0 : $data['user_id'], + 'user_id' => isset($data['user_id']) && $this->projectPermission->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0, )); } @@ -82,6 +93,6 @@ class CommentCreation extends Base */ public function hasRequiredCondition(array $data) { - return ! empty($data['comment']) || ! empty($data['commit_comment']); + return ! empty($data['comment']); } } diff --git a/app/Action/CommentCreationMoveTaskColumn.php b/app/Action/CommentCreationMoveTaskColumn.php new file mode 100644 index 00000000..4473cf91 --- /dev/null +++ b/app/Action/CommentCreationMoveTaskColumn.php @@ -0,0 +1,94 @@ + t('Column')); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('task_id', 'column_id'); + } + + /** + * Execute the action (append to the task description). + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if (! $this->userSession->isLogged()) { + return false; + } + + $column = $this->board->getColumn($data['column_id']); + + return (bool) $this->comment->create(array( + 'comment' => t('Moved to column %s', $column['title']), + 'task_id' => $data['task_id'], + 'user_id' => $this->userSession->getId(), + )); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php index ffa1ac2a..f5085cb0 100644 --- a/app/Action/TaskAssignCategoryColor.php +++ b/app/Action/TaskAssignCategoryColor.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignCategoryColor extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a category based on a color'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskAssignCategoryLabel.php b/app/Action/TaskAssignCategoryLabel.php index 0ef474b6..8d291e89 100644 --- a/app/Action/TaskAssignCategoryLabel.php +++ b/app/Action/TaskAssignCategoryLabel.php @@ -12,6 +12,17 @@ use Kanboard\Integration\GithubWebhook; */ class TaskAssignCategoryLabel extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change the category based on an external label'); + } + /** * Get the list of compatible events * @@ -64,7 +75,7 @@ class TaskAssignCategoryLabel extends Base { $values = array( 'id' => $data['task_id'], - 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), + 'category_id' => $this->getParam('category_id'), ); return $this->taskModification->update($values); @@ -79,6 +90,6 @@ class TaskAssignCategoryLabel extends Base */ public function hasRequiredCondition(array $data) { - return $data['label'] == $this->getParam('label'); + return $data['label'] == $this->getParam('label') && empty($data['category_id']); } } diff --git a/app/Action/TaskAssignCategoryLink.php b/app/Action/TaskAssignCategoryLink.php index 3d00e8d3..b39e41b4 100644 --- a/app/Action/TaskAssignCategoryLink.php +++ b/app/Action/TaskAssignCategoryLink.php @@ -13,6 +13,17 @@ use Kanboard\Model\TaskLink; */ class TaskAssignCategoryLink extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a category based on a link'); + } + /** * Get the list of compatible events * @@ -65,7 +76,7 @@ class TaskAssignCategoryLink extends Base { $values = array( 'id' => $data['task_id'], - 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), + 'category_id' => $this->getParam('category_id'), ); return $this->taskModification->update($values); diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php index a2332f78..3a15b15f 100644 --- a/app/Action/TaskAssignColorCategory.php +++ b/app/Action/TaskAssignColorCategory.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorCategory extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a color based on a category'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskAssignColorColumn.php b/app/Action/TaskAssignColorColumn.php index 53140733..7474045b 100644 --- a/app/Action/TaskAssignColorColumn.php +++ b/app/Action/TaskAssignColorColumn.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorColumn extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign a color when the task is moved to a specific column'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskAssignColorLink.php b/app/Action/TaskAssignColorLink.php index 67b2ef62..f71df70e 100644 --- a/app/Action/TaskAssignColorLink.php +++ b/app/Action/TaskAssignColorLink.php @@ -12,6 +12,17 @@ use Kanboard\Model\TaskLink; */ class TaskAssignColorLink extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change task color when using a specific task link'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php index 6bf02c36..6e56bdc5 100644 --- a/app/Action/TaskAssignColorUser.php +++ b/app/Action/TaskAssignColorUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign a color to a specific user'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskAssignCurrentUser.php b/app/Action/TaskAssignCurrentUser.php index f34c4f36..192a120c 100644 --- a/app/Action/TaskAssignCurrentUser.php +++ b/app/Action/TaskAssignCurrentUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignCurrentUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign the task to the person who does the action'); + } + /** * Get the list of compatible events * @@ -22,7 +33,6 @@ class TaskAssignCurrentUser extends Base { return array( Task::EVENT_CREATE, - Task::EVENT_MOVE_COLUMN, ); } @@ -34,9 +44,7 @@ class TaskAssignCurrentUser extends Base */ public function getActionRequiredParameters() { - return array( - 'column_id' => t('Column'), - ); + return array(); } /** @@ -49,7 +57,6 @@ class TaskAssignCurrentUser extends Base { return array( 'task_id', - 'column_id', ); } @@ -83,6 +90,6 @@ class TaskAssignCurrentUser extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return true; } } diff --git a/app/Action/TaskAssignCurrentUserColumn.php b/app/Action/TaskAssignCurrentUserColumn.php new file mode 100644 index 00000000..05d08dd3 --- /dev/null +++ b/app/Action/TaskAssignCurrentUserColumn.php @@ -0,0 +1,98 @@ + t('Column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if (! $this->userSession->isLogged()) { + return false; + } + + $values = array( + 'id' => $data['task_id'], + 'owner_id' => $this->userSession->getId(), + ); + + return $this->taskModification->update($values); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php index dfcb281b..2dc3e966 100644 --- a/app/Action/TaskAssignSpecificUser.php +++ b/app/Action/TaskAssignSpecificUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignSpecificUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign the task to a specific user'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskAssignUser.php b/app/Action/TaskAssignUser.php index a5821729..bb3a83c2 100644 --- a/app/Action/TaskAssignUser.php +++ b/app/Action/TaskAssignUser.php @@ -13,6 +13,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskAssignUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change the assignee based on an external username'); + } + /** * Get the list of compatible events * @@ -78,6 +89,6 @@ class TaskAssignUser extends Base */ public function hasRequiredCondition(array $data) { - return true; + return $this->projectPermission->isAssignable($this->getProjectId(), $data['owner_id']); } } diff --git a/app/Action/TaskClose.php b/app/Action/TaskClose.php index d80bd023..a4b093a4 100644 --- a/app/Action/TaskClose.php +++ b/app/Action/TaskClose.php @@ -15,6 +15,17 @@ use Kanboard\Model\Task; */ class TaskClose extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Close a task'); + } + /** * Get the list of compatible events * @@ -24,7 +35,6 @@ class TaskClose extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, GithubWebhook::EVENT_COMMIT, GithubWebhook::EVENT_ISSUE_CLOSED, GitlabWebhook::EVENT_COMMIT, @@ -42,17 +52,7 @@ class TaskClose extends Base */ public function getActionRequiredParameters() { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return array(); - default: - return array('column_id' => t('Column')); - } + return array(); } /** @@ -63,17 +63,7 @@ class TaskClose extends Base */ public function getEventRequiredParameters() { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return array('task_id'); - default: - return array('task_id', 'column_id'); - } + return array('task_id'); } /** @@ -97,16 +87,6 @@ class TaskClose extends Base */ public function hasRequiredCondition(array $data) { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return true; - default: - return $data['column_id'] == $this->getParam('column_id'); - } + return true; } } diff --git a/app/Action/TaskCloseColumn.php b/app/Action/TaskCloseColumn.php new file mode 100644 index 00000000..09af3b96 --- /dev/null +++ b/app/Action/TaskCloseColumn.php @@ -0,0 +1,84 @@ + t('Column')); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('task_id', 'column_id'); + } + + /** + * Execute the action (close the task) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + return $this->taskStatus->close($data['task_id']); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/app/Action/TaskCreation.php b/app/Action/TaskCreation.php index af1403f0..23ff4592 100644 --- a/app/Action/TaskCreation.php +++ b/app/Action/TaskCreation.php @@ -14,6 +14,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskCreation extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Create a task from an external provider'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php index 1f6684dd..5bcdce08 100644 --- a/app/Action/TaskDuplicateAnotherProject.php +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskDuplicateAnotherProject extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Duplicate the task to another project'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskDuplicateAnotherProject extends Base return array( 'task_id', 'column_id', - 'project_id', ); } @@ -65,7 +75,6 @@ class TaskDuplicateAnotherProject extends Base public function doAction(array $data) { $destination_column_id = $this->board->getFirstColumn($this->getParam('project_id')); - return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); } diff --git a/app/Action/TaskEmail.php b/app/Action/TaskEmail.php index 7fb76c4c..4e0e06a6 100644 --- a/app/Action/TaskEmail.php +++ b/app/Action/TaskEmail.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskEmail extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Send a task by email to someone'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskLogMoveAnotherColumn.php b/app/Action/TaskLogMoveAnotherColumn.php deleted file mode 100644 index a699c4ab..00000000 --- a/app/Action/TaskLogMoveAnotherColumn.php +++ /dev/null @@ -1,83 +0,0 @@ - t('Column')); - } - - /** - * Get the required parameter for the event - * - * @access public - * @return string[] - */ - public function getEventRequiredParameters() - { - return array('task_id', 'column_id'); - } - - /** - * Execute the action (append to the task description). - * - * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed - */ - public function doAction(array $data) - { - if (! $this->userSession->isLogged()) { - return false; - } - - $column = $this->board->getColumn($data['column_id']); - - return (bool) $this->comment->create(array( - 'comment' => t('Moved to column %s', $column['title']), - 'task_id' => $data['task_id'], - 'user_id' => $this->userSession->getId(), - )); - } - - /** - * Check if the event data meet the action condition - * - * @access public - * @param array $data Event data dictionary - * @return bool - */ - public function hasRequiredCondition(array $data) - { - return $data['column_id'] == $this->getParam('column_id'); - } -} diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php index 476e2036..fdff0d8c 100644 --- a/app/Action/TaskMoveAnotherProject.php +++ b/app/Action/TaskMoveAnotherProject.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveAnotherProject extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another project'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskMoveColumnAssigned.php b/app/Action/TaskMoveColumnAssigned.php index 16622ee4..1b23a591 100644 --- a/app/Action/TaskMoveColumnAssigned.php +++ b/app/Action/TaskMoveColumnAssigned.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnAssigned extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when assigned to a user'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskMoveColumnAssigned extends Base return array( 'task_id', 'column_id', - 'project_id', 'owner_id' ); } diff --git a/app/Action/TaskMoveColumnCategoryChange.php b/app/Action/TaskMoveColumnCategoryChange.php index 1e12be4a..0f591eda 100644 --- a/app/Action/TaskMoveColumnCategoryChange.php +++ b/app/Action/TaskMoveColumnCategoryChange.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnCategoryChange extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when the category is changed'); + } + /** * Get the list of compatible events * @@ -50,7 +61,6 @@ class TaskMoveColumnCategoryChange extends Base return array( 'task_id', 'column_id', - 'project_id', 'category_id', ); } @@ -71,7 +81,8 @@ class TaskMoveColumnCategoryChange extends Base $data['task_id'], $this->getParam('dest_column_id'), $original_task['position'], - $original_task['swimlane_id'] + $original_task['swimlane_id'], + false ); } diff --git a/app/Action/TaskMoveColumnUnAssigned.php b/app/Action/TaskMoveColumnUnAssigned.php index 617c75a8..99ef9351 100644 --- a/app/Action/TaskMoveColumnUnAssigned.php +++ b/app/Action/TaskMoveColumnUnAssigned.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnUnAssigned extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when assignee is cleared'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskMoveColumnUnAssigned extends Base return array( 'task_id', 'column_id', - 'project_id', 'owner_id' ); } diff --git a/app/Action/TaskOpen.php b/app/Action/TaskOpen.php index 2e84c695..a1ab622c 100644 --- a/app/Action/TaskOpen.php +++ b/app/Action/TaskOpen.php @@ -14,6 +14,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskOpen extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Open a task'); + } + /** * Get the list of compatible events * diff --git a/app/Action/TaskUpdateStartDate.php b/app/Action/TaskUpdateStartDate.php index 4cd548af..011a5baf 100644 --- a/app/Action/TaskUpdateStartDate.php +++ b/app/Action/TaskUpdateStartDate.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskUpdateStartDate extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Automatically update the start date'); + } + /** * Get the list of compatible events * diff --git a/app/Api/Action.php b/app/Api/Action.php index 0ae91f10..bdb5b26e 100644 --- a/app/Api/Action.php +++ b/app/Api/Action.php @@ -12,17 +12,17 @@ class Action extends \Kanboard\Core\Base { public function getAvailableActions() { - return $this->action->getAvailableActions(); + return $this->actionManager->getAvailableActions(); } public function getAvailableActionEvents() { - return $this->action->getAvailableEvents(); + return $this->eventManager->getAll(); } public function getCompatibleActionEvents($action_name) { - return $this->action->getCompatibleEvents($action_name); + return $this->actionManager->getCompatibleEvents($action_name); } public function removeAction($action_id) @@ -32,22 +32,10 @@ class Action extends \Kanboard\Core\Base public function getActions($project_id) { - $actions = $this->action->getAllByProject($project_id); - - foreach ($actions as $index => $action) { - $params = array(); - - foreach ($action['params'] as $param) { - $params[$param['name']] = $param['value']; - } - - $actions[$index]['params'] = $params; - } - - return $actions; + return $this->action->getAllByProject($project_id); } - public function createAction($project_id, $event_name, $action_name, $params) + public function createAction($project_id, $event_name, $action_name, array $params) { $values = array( 'project_id' => $project_id, @@ -63,16 +51,16 @@ class Action extends \Kanboard\Core\Base } // Check if the action exists - $actions = $this->action->getAvailableActions(); + $actions = $this->actionManager->getAvailableActions(); if (! isset($actions[$action_name])) { return false; } // Check the event - $action = $this->action->load($action_name, $project_id, $event_name); + $action = $this->actionManager->getAction($action_name); - if (! in_array($event_name, $action->getCompatibleEvents())) { + if (! in_array($event_name, $action->getEvents())) { return false; } diff --git a/app/Controller/Action.php b/app/Controller/Action.php index 3caea45c..9b803893 100644 --- a/app/Controller/Action.php +++ b/app/Controller/Action.php @@ -18,17 +18,18 @@ class Action extends Base public function index() { $project = $this->getProject(); + $actions = $this->action->getAllByProject($project['id']); $this->response->html($this->projectLayout('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(), + 'actions' => $actions, + 'available_actions' => $this->actionManager->getAvailableActions(), + 'available_events' => $this->eventManager->getAll(), + 'available_params' => $this->actionManager->getAvailableParameters($actions), 'columns_list' => $this->board->getColumnsList($project['id']), 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), - 'projects_list' => $this->project->getList(false), + 'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'links_list' => $this->link->getList(0, false), @@ -53,7 +54,7 @@ class Action extends Base $this->response->html($this->projectLayout('action/event', array( 'values' => $values, 'project' => $project, - 'events' => $this->action->getCompatibleEvents($values['action_name']), + 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), 'title' => t('Automatic actions') ))); } @@ -72,14 +73,14 @@ class Action extends Base $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); } - $action = $this->action->load($values['action_name'], $values['project_id'], $values['event_name']); + $action = $this->actionManager->getAction($values['action_name']); $action_params = $action->getActionRequiredParameters(); if (empty($action_params)) { $this->doCreation($project, $values + array('params' => array())); } - $projects_list = $this->project->getList(false); + $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); unset($projects_list[$project['id']]); $this->response->html($this->projectLayout('action/params', array( @@ -139,8 +140,8 @@ class Action extends Base $this->response->html($this->projectLayout('action/remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), - 'available_events' => $this->action->getAvailableEvents(), - 'available_actions' => $this->action->getAvailableActions(), + 'available_events' => $this->eventManager->getAll(), + 'available_actions' => $this->actionManager->getAvailableActions(), 'project' => $project, 'title' => t('Remove an action') ))); diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php new file mode 100644 index 00000000..921a8b98 --- /dev/null +++ b/app/Core/Action/ActionManager.php @@ -0,0 +1,141 @@ +actions[$action->getName()] = $action; + return $this; + } + + /** + * Get automatic action instance + * + * @access public + * @param string $name Absolute class name with namespace + * @return ActionBase + */ + public function getAction($name) + { + if (isset($this->actions[$name])) { + return $this->actions[$name]; + } + + throw new RuntimeException('Automatic Action Not Found: '.$name); + } + + /** + * Get available automatic actions + * + * @access public + * @return array + */ + public function getAvailableActions() + { + $actions = array(); + + foreach ($this->actions as $action) { + if (count($action->getEvents()) > 0) { + $actions[$action->getName()] = $action->getDescription(); + } + } + + asort($actions); + + return $actions; + } + + /** + * Get all available action parameters + * + * @access public + * @param array $actions + * @return array + */ + public function getAvailableParameters(array $actions) + { + $params = array(); + + foreach ($actions as $action) { + $currentAction = $this->getAction($action['action_name']); + $params[$currentAction->getName()] = $currentAction->getActionRequiredParameters(); + } + + return $params; + } + + /** + * Get list of compatible events for a given action + * + * @access public + * @param string $name + * @return array + */ + public function getCompatibleEvents($name) + { + $events = array(); + $actionEvents = $this->getAction($name)->getEvents(); + + foreach ($this->eventManager->getAll() as $event => $description) { + if (in_array($event, $actionEvents)) { + $events[$event] = $description; + } + } + + return $events; + } + + /** + * Bind automatic actions to events + * + * @access public + * @return ActionManager + */ + public function attachEvents() + { + if ($this->userSession->isLogged()) { + $actions = $this->action->getAllByUser($this->userSession->getId()); + } else { + $actions = $this->action->getAll(); + } + + foreach ($actions as $action) { + $listener = $this->getAction($action['action_name'])->setProjectId($action['project_id']); + + foreach ($action['params'] as $param_name => $param_value) { + $listener->setParam($param_name, $param_value); + } + + $this->dispatcher->addListener($action['event_name'], array($listener, 'execute')); + } + + return $this; + } +} diff --git a/app/Core/Base.php b/app/Core/Base.php index a4cf787a..11a0be68 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -10,7 +10,9 @@ use Pimple\Container; * @package core * @author Frederic Guillot * + * @property \Kanboard\Core\Action\ActionManager $actionManager * @property \Kanboard\Core\Cache\MemoryCache $memoryCache + * @property \Kanboard\Core\Event\EventManager $eventManager * @property \Kanboard\Core\Group\GroupManager $groupManager * @property \Kanboard\Core\Http\Client $httpClient * @property \Kanboard\Core\Http\OAuth2 $oauth @@ -54,6 +56,7 @@ use Pimple\Container; * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter * @property \Kanboard\Model\Action $action + * @property \Kanboard\Model\ActionParameter $actionParameter * @property \Kanboard\Model\Authentication $authentication * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category diff --git a/app/Core/Event/EventManager.php b/app/Core/Event/EventManager.php new file mode 100644 index 00000000..8e66cc12 --- /dev/null +++ b/app/Core/Event/EventManager.php @@ -0,0 +1,83 @@ +events[$event] = $description; + return $this; + } + + /** + * Get the list of events and description that can be used from the user interface + * + * @access public + * @return array + */ + public function getAll() + { + $events = array( + TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'), + Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), + Task::EVENT_UPDATE => t('Task modification'), + Task::EVENT_CREATE => t('Task creation'), + Task::EVENT_OPEN => t('Reopen a task'), + Task::EVENT_CLOSE => t('Closing a task'), + Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), + Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), + GithubWebhook::EVENT_COMMIT => t('Github commit received'), + GithubWebhook::EVENT_ISSUE_OPENED => t('Github issue opened'), + GithubWebhook::EVENT_ISSUE_CLOSED => t('Github issue closed'), + GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'), + GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'), + GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'), + GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'), + GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'), + GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'), + GitlabWebhook::EVENT_ISSUE_REOPENED => t('Gitlab issue reopened'), + GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), + GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'), + BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'), + BitbucketWebhook::EVENT_ISSUE_OPENED => t('Bitbucket issue opened'), + BitbucketWebhook::EVENT_ISSUE_CLOSED => t('Bitbucket issue closed'), + BitbucketWebhook::EVENT_ISSUE_REOPENED => t('Bitbucket issue reopened'), + BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Bitbucket issue assignee change'), + BitbucketWebhook::EVENT_ISSUE_COMMENT => t('Bitbucket issue comment created'), + ); + + $events = array_merge($events, $this->events); + asort($events); + + return $events; + } +} diff --git a/app/Core/Mail/Client.php b/app/Core/Mail/Client.php index 7b4268bd..e1f31696 100644 --- a/app/Core/Mail/Client.php +++ b/app/Core/Mail/Client.php @@ -45,19 +45,21 @@ class Client extends Base */ public function send($email, $name, $subject, $html) { - $this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); + if (! empty($email)) { + $this->logger->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); - $start_time = microtime(true); - $author = 'Kanboard'; + $start_time = microtime(true); + $author = 'Kanboard'; - if ($this->userSession->isLogged()) { - $author = e('%s via Kanboard', $this->helper->user->getFullname()); - } + if ($this->userSession->isLogged()) { + $author = e('%s via Kanboard', $this->helper->user->getFullname()); + } - $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author); + $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author); - if (DEBUG) { - $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + if (DEBUG) { + $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + } } return $this; diff --git a/app/Integration/BitbucketWebhook.php b/app/Integration/BitbucketWebhook.php index 3814e35c..e6ba3f1a 100644 --- a/app/Integration/BitbucketWebhook.php +++ b/app/Integration/BitbucketWebhook.php @@ -303,7 +303,7 @@ class BitbucketWebhook extends \Kanboard\Core\Base 'task_id' => $task_id, 'commit_message' => $commit['message'], 'commit_url' => $commit['links']['html']['href'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Bitbucket', $actor['display_name']).']('.$commit['links']['html']['href'].')', + 'comment' => $commit['message']."\n\n[".t('Commit made by @%s on Bitbucket', $actor['display_name']).']('.$commit['links']['html']['href'].')', ) + $task) ); diff --git a/app/Integration/GithubWebhook.php b/app/Integration/GithubWebhook.php index 6dd7a8d9..cdd2fc48 100644 --- a/app/Integration/GithubWebhook.php +++ b/app/Integration/GithubWebhook.php @@ -98,7 +98,7 @@ class GithubWebhook extends \Kanboard\Core\Base 'task_id' => $task_id, 'commit_message' => $commit['message'], 'commit_url' => $commit['url'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Github', $commit['author']['username']).']('.$commit['url'].')' + 'comment' => $commit['message']."\n\n[".t('Commit made by @%s on Github', $commit['author']['username']).']('.$commit['url'].')' ) + $task) ); } diff --git a/app/Integration/GitlabWebhook.php b/app/Integration/GitlabWebhook.php index 7ab4cedf..5e0aa59d 100644 --- a/app/Integration/GitlabWebhook.php +++ b/app/Integration/GitlabWebhook.php @@ -144,7 +144,7 @@ class GitlabWebhook extends \Kanboard\Core\Base 'task_id' => $task_id, 'commit_message' => $commit['message'], 'commit_url' => $commit['url'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Gitlab', $commit['author']['name']).']('.$commit['url'].')' + 'comment' => $commit['message']."\n\n[".t('Commit made by @%s on Gitlab', $commit['author']['name']).']('.$commit['url'].')' ) + $task) ); diff --git a/app/Model/Action.php b/app/Model/Action.php index d3d18edb..c368b494 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -2,14 +2,11 @@ namespace Kanboard\Model; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; use SimpleValidator\Validator; use SimpleValidator\Validators; /** - * Action model + * Action Model * * @package model * @author Frederic Guillot @@ -24,143 +21,33 @@ class Action extends Base const TABLE = 'actions'; /** - * SQL table name for action parameters - * - * @var string - */ - const TABLE_PARAMS = 'action_has_params'; - - /** - * Extended actions - * - * @access private - * @var array - */ - private $actions = array(); - - /** - * Extend the list of default actions - * - * @access public - * @param string $className - * @param string $description - * @return Action - */ - public function extendActions($className, $description) - { - $this->actions[$className] = $description; - return $this; - } - - /** - * Return the name and description of available actions + * Return actions and parameters for a given user * * @access public + * @param integer $user_id * @return array */ - public function getAvailableActions() + public function getAllByUser($user_id) { - $values = array( - 'TaskClose' => t('Close a task'), - 'TaskOpen' => t('Open a 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'), - 'TaskMoveAnotherProject' => t('Move the task to another project'), - 'TaskMoveColumnAssigned' => t('Move the task to another column when assigned to a user'), - 'TaskMoveColumnUnAssigned' => t('Move the task to another column when assignee is cleared'), - 'TaskAssignColorColumn' => t('Assign a color when the task is moved to a specific column'), - 'TaskAssignColorUser' => t('Assign a color to a specific user'), - 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), - 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), - 'TaskAssignCategoryLink' => t('Assign automatically a category based on a link'), - 'CommentCreation' => t('Create a comment from an external provider'), - 'TaskCreation' => t('Create a task from an external provider'), - 'TaskLogMoveAnotherColumn' => t('Add a comment log when moving the task between columns'), - 'TaskAssignUser' => t('Change the assignee based on an external username'), - 'TaskAssignCategoryLabel' => t('Change the category based on an external label'), - 'TaskUpdateStartDate' => t('Automatically update the start date'), - 'TaskMoveColumnCategoryChange' => t('Move the task to another column when the category is changed'), - 'TaskEmail' => t('Send a task by email to someone'), - 'TaskAssignColorLink' => t('Change task color when using a specific task link'), - ); - - $values = array_merge($values, $this->actions); + $project_ids = $this->projectPermission->getActiveProjectIds($user_id); + $actions = array(); - asort($values); + if (! empty($project_ids)) { + $actions = $this->db->table(self::TABLE)->in('project_id', $project_ids)->findAll(); - return $values; - } - - /** - * Return the name and description of available actions - * - * @access public - * @return array - */ - public function getAvailableEvents() - { - $values = array( - TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'), - Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), - Task::EVENT_UPDATE => t('Task modification'), - Task::EVENT_CREATE => t('Task creation'), - Task::EVENT_OPEN => t('Reopen a task'), - Task::EVENT_CLOSE => t('Closing a task'), - Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), - Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), - GithubWebhook::EVENT_COMMIT => t('Github commit received'), - GithubWebhook::EVENT_ISSUE_OPENED => t('Github issue opened'), - GithubWebhook::EVENT_ISSUE_CLOSED => t('Github issue closed'), - GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'), - GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'), - GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'), - GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'), - GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'), - GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'), - GitlabWebhook::EVENT_ISSUE_REOPENED => t('Gitlab issue reopened'), - GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), - GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'), - BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'), - BitbucketWebhook::EVENT_ISSUE_OPENED => t('Bitbucket issue opened'), - BitbucketWebhook::EVENT_ISSUE_CLOSED => t('Bitbucket issue closed'), - BitbucketWebhook::EVENT_ISSUE_REOPENED => t('Bitbucket issue reopened'), - BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Bitbucket issue assignee change'), - BitbucketWebhook::EVENT_ISSUE_COMMENT => t('Bitbucket issue comment created'), - ); - - asort($values); - - return $values; - } - - /** - * Return the name and description of compatible actions - * - * @access public - * @param string $action_name Action name - * @return array - */ - public function getCompatibleEvents($action_name) - { - $action = $this->load($action_name, 0, ''); - $compatible_events = $action->getCompatibleEvents(); - $events = array(); - - foreach ($this->getAvailableEvents() as $event_name => $event_description) { - if (in_array($event_name, $compatible_events)) { - $events[$event_name] = $event_description; + foreach ($actions as &$action) { + $action['params'] = $this->actionParameter->getAll($action['id']); } } - return $events; + return $actions; } /** * Return actions and parameters for a given project * * @access public - * @param $project_id + * @param integer $project_id * @return array */ public function getAllByProject($project_id) @@ -168,7 +55,7 @@ class Action extends Base $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(); + $action['params'] = $this->actionParameter->getAll($action['id']); } return $actions; @@ -183,52 +70,27 @@ class Action extends Base public function getAll() { $actions = $this->db->table(self::TABLE)->findAll(); - $params = $this->db->table(self::TABLE_PARAMS)->findAll(); foreach ($actions as &$action) { - $action['params'] = array(); - - foreach ($params as $param) { - if ($param['action_id'] === $action['id']) { - $action['params'][] = $param; - } - } + $action['params'] = $this->actionParameter->getAll($action['id']); } 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'], $action['event_name']); - $params += $action->getActionRequiredParameters(); - } - - return $params; - } - /** * Fetch an action * * @access public - * @param integer $action_id Action id - * @return array Action data + * @param integer $action_id + * @return array */ public function getById($action_id) { $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne(); if (! empty($action)) { - $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll(); + $action['params'] = $this->actionParameter->getAll($action_id); } return $action; @@ -238,8 +100,8 @@ class Action extends Base * Remove an action * * @access public - * @param integer $action_id Action id - * @return bool Success or not + * @param integer $action_id + * @return bool */ public function remove($action_id) { @@ -263,24 +125,16 @@ class Action extends Base 'action_name' => $values['action_name'], ); - if (! $this->db->table(self::TABLE)->save($action)) { + if (! $this->db->table(self::TABLE)->insert($action)) { $this->db->cancelTransaction(); return false; } $action_id = $this->db->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; - } + if (! $this->actionParameter->create($action_id, $values)) { + $this->db->cancelTransaction(); + return false; } $this->db->closeTransaction(); @@ -288,42 +142,6 @@ class Action extends Base return $action_id; } - /** - * Load all actions and attach events - * - * @access public - */ - public function attachEvents() - { - $actions = $this->getAll(); - - foreach ($actions as $action) { - $listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']); - - foreach ($action['params'] as $param) { - $listener->setParam($param['name'], $param['value']); - } - - $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute')); - } - } - - /** - * Load an action - * - * @access public - * @param string $name Action class name - * @param integer $project_id Project id - * @param string $event Event name - * @return \Action\Base - */ - public function load($name, $project_id, $event) - { - $className = $name{0} - !== '\\' ? '\Kanboard\Action\\'.$name : $name; - return new $className($this->container, $project_id, $event); - } - /** * Copy actions from a project to another one (skip actions that cannot resolve parameters) * @@ -346,15 +164,14 @@ class Action extends Base ); if (! $this->db->table(self::TABLE)->insert($values)) { - $this->container['logger']->debug('Action::duplicate => unable to create '.$action['action_name']); $this->db->cancelTransaction(); continue; } $action_id = $this->db->getLastId(); - if (! $this->duplicateParameters($dst_project_id, $action_id, $action['params'])) { - $this->container['logger']->debug('Action::duplicate => unable to copy parameters for '.$action['action_name']); + if (! $this->actionParameter->duplicateParameters($dst_project_id, $action_id, $action['params'])) { + $this->logger->error('Action::duplicate => skip action '.$action['action_name'].' '.$action['id']); $this->db->cancelTransaction(); continue; } @@ -365,74 +182,6 @@ class Action extends Base return true; } - /** - * Duplicate action parameters - * - * @access public - * @param integer $project_id - * @param integer $action_id - * @param array $params - * @return boolean - */ - public function duplicateParameters($project_id, $action_id, array $params) - { - foreach ($params as $param) { - $value = $this->resolveParameters($param, $project_id); - - if ($value === false) { - $this->container['logger']->debug('Action::duplicateParameters => unable to resolve '.$param['name'].'='.$param['value']); - return false; - } - - $values = array( - 'action_id' => $action_id, - 'name' => $param['name'], - 'value' => $value, - ); - - if (! $this->db->table(self::TABLE_PARAMS)->insert($values)) { - return false; - } - } - - return true; - } - - /** - * Resolve action parameter values according to another project - * - * @author Antonio Rabelo - * @access public - * @param array $param Action parameter - * @param integer $project_id Project to find the corresponding values - * @return mixed - */ - public function resolveParameters(array $param, $project_id) - { - switch ($param['name']) { - case 'project_id': - return $project_id; - case 'category_id': - return $this->category->getIdByName($project_id, $this->category->getNameById($param['value'])) ?: false; - case 'src_column_id': - case 'dest_column_id': - case 'dst_column_id': - case 'column_id': - $column = $this->board->getColumn($param['value']); - - if (empty($column)) { - return false; - } - - return $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; - case 'user_id': - case 'owner_id': - return $this->projectPermission->isAssignable($project_id, $param['value']) ? $param['value'] : false; - default: - return $param['value']; - } - } - /** * Validate action creation * diff --git a/app/Model/ActionParameter.php b/app/Model/ActionParameter.php new file mode 100644 index 00000000..f170ef66 --- /dev/null +++ b/app/Model/ActionParameter.php @@ -0,0 +1,122 @@ +db->hashtable(self::TABLE)->eq('action_id', $action_id)->getAll('name', 'value'); + } + + /** + * Insert new parameters for an action + * + * @access public + * @param integer $action_id + * @param array $values + * @return boolean + */ + public function create($action_id, array $values) + { + foreach ($values['params'] as $name => $value) { + $param = array( + 'action_id' => $action_id, + 'name' => $name, + 'value' => $value, + ); + + if (! $this->db->table(self::TABLE)->save($param)) { + return false; + } + } + + return true; + } + + /** + * Duplicate action parameters + * + * @access public + * @param integer $project_id + * @param integer $action_id + * @param array $params + * @return boolean + */ + public function duplicateParameters($project_id, $action_id, array $params) + { + foreach ($params as $name => $value) { + $value = $this->resolveParameter($project_id, $name, $value); + + if ($value === false) { + $this->logger->error('ActionParameter::duplicateParameters => unable to resolve '.$name.'='.$value); + return false; + } + + $values = array( + 'action_id' => $action_id, + 'name' => $name, + 'value' => $value, + ); + + if (! $this->db->table(self::TABLE)->insert($values)) { + return false; + } + } + + return true; + } + + /** + * Resolve action parameter values according to another project + * + * @access private + * @param integer $project_id + * @param string $name + * @param string $value + * @return mixed + */ + private function resolveParameter($project_id, $name, $value) + { + switch ($name) { + case 'project_id': + return $value != $project_id ? $value : false; + case 'category_id': + return $this->category->getIdByName($project_id, $this->category->getNameById($value)) ?: false; + case 'src_column_id': + case 'dest_column_id': + case 'dst_column_id': + case 'column_id': + $column = $this->board->getColumn($value); + return empty($column) ? false : $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; + case 'user_id': + case 'owner_id': + return $this->projectPermission->isAssignable($project_id, $value) ? $value : false; + default: + return $value; + } + } +} diff --git a/app/Model/Comment.php b/app/Model/Comment.php index 71e964dc..eb83f8d6 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -75,6 +75,7 @@ class Comment extends Base self::TABLE.'.user_id', self::TABLE.'.date_creation', self::TABLE.'.comment', + self::TABLE.'.reference', User::TABLE.'.username', User::TABLE.'.name' ) diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php index b2c38622..6b9c23b0 100644 --- a/app/Model/ProjectUserRole.php +++ b/app/Model/ProjectUserRole.php @@ -52,11 +52,11 @@ class ProjectUserRole extends Base ->getAll(Project::TABLE.'.id', Project::TABLE.'.name'); $groupProjects = $this->projectGroupRole->getProjectsByUser($user_id, $status); - $groups = $userProjects + $groupProjects; + $projects = $userProjects + $groupProjects; - asort($groups); + asort($projects); - return $groups; + return $projects; } /** diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 534c3f3a..f430c00b 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,30 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 91; +const VERSION = 92; + +function version_92(PDO $pdo) +{ + $rq = $pdo->prepare('SELECT * FROM actions'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?'); + + foreach ($rows as $row) { + if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn'; + } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskCloseColumn'; + } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') { + $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn'; + } elseif ($row['action_name']{0} !== '\\') { + $row['action_name'] = '\Kanboard\Action\\'.$row['action_name']; + } + + $rq->execute(array($row['action_name'], $row['id'])); + } +} function version_91(PDO $pdo) { diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php new file mode 100644 index 00000000..0aba29f1 --- /dev/null +++ b/app/ServiceProvider/ActionProvider.php @@ -0,0 +1,78 @@ +register(new CommentCreation($container)); + $container['actionManager']->register(new CommentCreationMoveTaskColumn($container)); + $container['actionManager']->register(new TaskAssignCategoryColor($container)); + $container['actionManager']->register(new TaskAssignCategoryLabel($container)); + $container['actionManager']->register(new TaskAssignCategoryLink($container)); + $container['actionManager']->register(new TaskAssignColorCategory($container)); + $container['actionManager']->register(new TaskAssignColorColumn($container)); + $container['actionManager']->register(new TaskAssignColorLink($container)); + $container['actionManager']->register(new TaskAssignColorUser($container)); + $container['actionManager']->register(new TaskAssignCurrentUser($container)); + $container['actionManager']->register(new TaskAssignCurrentUserColumn($container)); + $container['actionManager']->register(new TaskAssignSpecificUser($container)); + $container['actionManager']->register(new TaskAssignUser($container)); + $container['actionManager']->register(new TaskClose($container)); + $container['actionManager']->register(new TaskCloseColumn($container)); + $container['actionManager']->register(new TaskCreation($container)); + $container['actionManager']->register(new TaskDuplicateAnotherProject($container)); + $container['actionManager']->register(new TaskEmail($container)); + $container['actionManager']->register(new TaskMoveAnotherProject($container)); + $container['actionManager']->register(new TaskMoveColumnAssigned($container)); + $container['actionManager']->register(new TaskMoveColumnCategoryChange($container)); + $container['actionManager']->register(new TaskMoveColumnUnAssigned($container)); + $container['actionManager']->register(new TaskOpen($container)); + $container['actionManager']->register(new TaskUpdateStartDate($container)); + + return $container; + } +} diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 559a78c1..57e63746 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -17,6 +17,7 @@ class ClassProvider implements ServiceProviderInterface private $classes = array( 'Model' => array( 'Action', + 'ActionParameter', 'Authentication', 'Board', 'Category', @@ -90,6 +91,9 @@ class ClassProvider implements ServiceProviderInterface 'Lexer', 'Template', ), + 'Core\Event' => array( + 'EventManager', + ), 'Core\Http' => array( 'Request', 'Response', diff --git a/app/ServiceProvider/EventDispatcherProvider.php b/app/ServiceProvider/EventDispatcherProvider.php index 17141fd4..6cb302e6 100644 --- a/app/ServiceProvider/EventDispatcherProvider.php +++ b/app/ServiceProvider/EventDispatcherProvider.php @@ -30,9 +30,6 @@ class EventDispatcherProvider implements ServiceProviderInterface $container['dispatcher']->addSubscriber(new TransitionSubscriber($container)); $container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container)); - // Automatic actions - $container['action']->attachEvents(); - return $container; } } diff --git a/app/Subscriber/BootstrapSubscriber.php b/app/Subscriber/BootstrapSubscriber.php index e399f688..d114bf86 100644 --- a/app/Subscriber/BootstrapSubscriber.php +++ b/app/Subscriber/BootstrapSubscriber.php @@ -3,21 +3,26 @@ namespace Kanboard\Subscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Kanboard\Core\Base; -class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class BootstrapSubscriber extends Base implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - 'app.bootstrap' => array('setup', 0), + 'app.bootstrap' => 'execute', ); } - public function setup() + public function execute() { $this->config->setupTranslations(); $this->config->setupTimezone(); - $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + $this->actionManager->attachEvents(); + + if ($this->userSession->isLogged()) { + $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + } } public function __destruct() diff --git a/app/Template/action/index.php b/app/Template/action/index.php index bf2f7475..8275f080 100644 --- a/app/Template/action/index.php +++ b/app/Template/action/index.php @@ -28,24 +28,24 @@