diff options
Diffstat (limited to 'app')
108 files changed, 1597 insertions, 427 deletions
diff --git a/app/Action/Base.php b/app/Action/Base.php index e5c65a17..e0ed8bde 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -216,7 +216,8 @@ abstract class Base extends \Kanboard\Core\Base */ public function hasRequiredProject(array $data) { - return isset($data['project_id']) && $data['project_id'] == $this->getProjectId(); + return (isset($data['project_id']) && $data['project_id'] == $this->getProjectId()) || + (isset($data['task']['project_id']) && $data['task']['project_id'] == $this->getProjectId()); } /** @@ -226,10 +227,14 @@ abstract class Base extends \Kanboard\Core\Base * @param array $data Event data dictionary * @return bool True if all keys are there */ - public function hasRequiredParameters(array $data) + public function hasRequiredParameters(array $data, array $parameters = array()) { - foreach ($this->getEventRequiredParameters() as $parameter) { - if (! isset($data[$parameter])) { + $parameters = $parameters ?: $this->getEventRequiredParameters(); + + foreach ($parameters as $key => $value) { + if (is_array($value)) { + return isset($data[$key]) && $this->hasRequiredParameters($data[$key], $value); + } else if (! isset($data[$value])) { return false; } } diff --git a/app/Action/CommentCreationMoveTaskColumn.php b/app/Action/CommentCreationMoveTaskColumn.php index 1b16f481..8ab792ad 100644 --- a/app/Action/CommentCreationMoveTaskColumn.php +++ b/app/Action/CommentCreationMoveTaskColumn.php @@ -55,7 +55,13 @@ class CommentCreationMoveTaskColumn extends Base */ public function getEventRequiredParameters() { - return array('task_id', 'column_id'); + return array( + 'task_id', + 'task' => array( + 'column_id', + 'project_id', + ), + ); } /** @@ -71,7 +77,7 @@ class CommentCreationMoveTaskColumn extends Base return false; } - $column = $this->columnModel->getById($data['column_id']); + $column = $this->columnModel->getById($data['task']['column_id']); return (bool) $this->commentModel->create(array( 'comment' => t('Moved to column %s', $column['title']), @@ -89,6 +95,6 @@ class CommentCreationMoveTaskColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php index fc486870..2df90b2c 100644 --- a/app/Action/TaskAssignCategoryColor.php +++ b/app/Action/TaskAssignCategoryColor.php @@ -60,7 +60,10 @@ class TaskAssignCategoryColor extends Base { return array( 'task_id', - 'color_id', + 'task' => array( + 'project_id', + 'color_id', + ), ); } @@ -90,6 +93,6 @@ class TaskAssignCategoryColor extends Base */ public function hasRequiredCondition(array $data) { - return $data['color_id'] == $this->getParam('color_id'); + return $data['task']['color_id'] == $this->getParam('color_id'); } } diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php index 284b8f40..91860be4 100644 --- a/app/Action/TaskAssignColorCategory.php +++ b/app/Action/TaskAssignColorCategory.php @@ -60,7 +60,10 @@ class TaskAssignColorCategory extends Base { return array( 'task_id', - 'category_id', + 'task' => array( + 'project_id', + 'category_id', + ), ); } @@ -90,6 +93,6 @@ class TaskAssignColorCategory extends Base */ public function hasRequiredCondition(array $data) { - return $data['category_id'] == $this->getParam('category_id'); + return $data['task']['category_id'] == $this->getParam('category_id'); } } diff --git a/app/Action/TaskAssignColorColumn.php b/app/Action/TaskAssignColorColumn.php index 57fd6f44..6c674b1f 100644 --- a/app/Action/TaskAssignColorColumn.php +++ b/app/Action/TaskAssignColorColumn.php @@ -61,7 +61,10 @@ class TaskAssignColorColumn extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -91,6 +94,6 @@ class TaskAssignColorColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskAssignColorPriority.php b/app/Action/TaskAssignColorPriority.php index eae1b771..57000ba8 100644 --- a/app/Action/TaskAssignColorPriority.php +++ b/app/Action/TaskAssignColorPriority.php @@ -60,7 +60,10 @@ class TaskAssignColorPriority extends Base { return array( 'task_id', - 'priority', + 'task' => array( + 'project_id', + 'priority', + ), ); } @@ -90,6 +93,6 @@ class TaskAssignColorPriority extends Base */ public function hasRequiredCondition(array $data) { - return $data['priority'] == $this->getParam('priority'); + return $data['task']['priority'] == $this->getParam('priority'); } } diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php index 4bcf7a5c..385db793 100644 --- a/app/Action/TaskAssignColorUser.php +++ b/app/Action/TaskAssignColorUser.php @@ -61,7 +61,10 @@ class TaskAssignColorUser extends Base { return array( 'task_id', - 'owner_id', + 'task' => array( + 'project_id', + 'owner_id', + ), ); } @@ -91,6 +94,6 @@ class TaskAssignColorUser extends Base */ public function hasRequiredCondition(array $data) { - return $data['owner_id'] == $this->getParam('user_id'); + return $data['task']['owner_id'] == $this->getParam('user_id'); } } diff --git a/app/Action/TaskAssignCurrentUserColumn.php b/app/Action/TaskAssignCurrentUserColumn.php index bc28a90b..e4eade33 100644 --- a/app/Action/TaskAssignCurrentUserColumn.php +++ b/app/Action/TaskAssignCurrentUserColumn.php @@ -59,7 +59,10 @@ class TaskAssignCurrentUserColumn extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -93,6 +96,6 @@ class TaskAssignCurrentUserColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php index 50a2b2ae..2c7dcacd 100644 --- a/app/Action/TaskAssignSpecificUser.php +++ b/app/Action/TaskAssignSpecificUser.php @@ -61,7 +61,10 @@ class TaskAssignSpecificUser extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -91,6 +94,6 @@ class TaskAssignSpecificUser extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskCloseColumn.php b/app/Action/TaskCloseColumn.php index 1edce8fa..4f1ffc92 100644 --- a/app/Action/TaskCloseColumn.php +++ b/app/Action/TaskCloseColumn.php @@ -55,7 +55,13 @@ class TaskCloseColumn extends Base */ public function getEventRequiredParameters() { - return array('task_id', 'column_id'); + return array( + 'task_id', + 'task' => array( + 'project_id', + 'column_id', + ) + ); } /** @@ -79,6 +85,6 @@ class TaskCloseColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskCloseNoActivityColumn.php b/app/Action/TaskCloseNoActivityColumn.php new file mode 100644 index 00000000..7af0b7fc --- /dev/null +++ b/app/Action/TaskCloseNoActivityColumn.php @@ -0,0 +1,96 @@ +<?php + +namespace Kanboard\Action; + +use Kanboard\Model\TaskModel; + +/** + * Close automatically a task after inactive and in an defined column + * + * @package action + * @author Frederic Guillot + */ +class TaskCloseNoActivityColumn extends Base +{ + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Close a task when there is no activity in an specific column'); + } + + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array(TaskModel::EVENT_DAILY_CRONJOB); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'duration' => t('Duration in days'), + 'column_id' => t('Column') + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('tasks'); + } + + /** + * 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) + { + $results = array(); + $max = $this->getParam('duration') * 86400; + + foreach ($data['tasks'] as $task) { + $duration = time() - $task['date_modification']; + + if ($duration > $max && $task['column_id'] == $this->getParam('column_id')) { + $results[] = $this->taskStatusModel->close($task['id']); + } + } + + return in_array(true, $results, true); + } + + /** + * 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 count($data['tasks']) > 0; + } +} diff --git a/app/Action/TaskCreation.php b/app/Action/TaskCreation.php index e9e5c5f3..0620afd3 100644 --- a/app/Action/TaskCreation.php +++ b/app/Action/TaskCreation.php @@ -52,6 +52,7 @@ class TaskCreation extends Base public function getEventRequiredParameters() { return array( + 'project_id', 'reference', 'title', ); diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php index d70d2ee8..d6d8d51f 100644 --- a/app/Action/TaskDuplicateAnotherProject.php +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -62,7 +62,10 @@ class TaskDuplicateAnotherProject extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ) ); } @@ -76,7 +79,12 @@ class TaskDuplicateAnotherProject extends Base public function doAction(array $data) { $destination_column_id = $this->columnModel->getFirstColumnId($this->getParam('project_id')); - return (bool) $this->taskProjectDuplicationModel->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); + return (bool) $this->taskProjectDuplicationModel->duplicateToProject( + $data['task_id'], + $this->getParam('project_id'), + null, + $destination_column_id + ); } /** @@ -88,6 +96,6 @@ class TaskDuplicateAnotherProject extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id'); + return $data['task']['column_id'] == $this->getParam('column_id') && $data['task']['project_id'] != $this->getParam('project_id'); } } diff --git a/app/Action/TaskEmail.php b/app/Action/TaskEmail.php index 7f9ba416..526e9aa8 100644 --- a/app/Action/TaskEmail.php +++ b/app/Action/TaskEmail.php @@ -62,7 +62,10 @@ class TaskEmail extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -78,13 +81,14 @@ class TaskEmail extends Base $user = $this->userModel->getById($this->getParam('user_id')); if (! empty($user['email'])) { - $task = $this->taskFinderModel->getDetails($data['task_id']); - $this->emailClient->send( $user['email'], $user['name'] ?: $user['username'], $this->getParam('subject'), - $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->configModel->get('application_url'))) + $this->template->render('notification/task_create', array( + 'task' => $data['task'], + 'application_url' => $this->configModel->get('application_url'), + )) ); return true; @@ -102,6 +106,6 @@ class TaskEmail extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php index 66635a63..148b6b0c 100644 --- a/app/Action/TaskMoveAnotherProject.php +++ b/app/Action/TaskMoveAnotherProject.php @@ -61,8 +61,10 @@ class TaskMoveAnotherProject extends Base { return array( 'task_id', - 'column_id', - 'project_id', + 'task' => array( + 'project_id', + 'column_id', + ) ); } @@ -87,6 +89,6 @@ class TaskMoveAnotherProject extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id'); + return $data['task']['column_id'] == $this->getParam('column_id') && $data['task']['project_id'] != $this->getParam('project_id'); } } diff --git a/app/Action/TaskMoveColumnAssigned.php b/app/Action/TaskMoveColumnAssigned.php index 7e3db9c5..1c1f657a 100644 --- a/app/Action/TaskMoveColumnAssigned.php +++ b/app/Action/TaskMoveColumnAssigned.php @@ -61,8 +61,13 @@ class TaskMoveColumnAssigned extends Base { return array( 'task_id', - 'column_id', - 'owner_id' + 'task' => array( + 'project_id', + 'column_id', + 'owner_id', + 'position', + 'swimlane_id', + ) ); } @@ -75,14 +80,12 @@ class TaskMoveColumnAssigned extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPositionModel->movePosition( - $data['project_id'], + $data['task']['project_id'], $data['task_id'], $this->getParam('dest_column_id'), - $original_task['position'], - $original_task['swimlane_id'], + $data['task']['position'], + $data['task']['swimlane_id'], false ); } @@ -96,6 +99,6 @@ class TaskMoveColumnAssigned extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('src_column_id') && $data['owner_id'] > 0; + return $data['task']['column_id'] == $this->getParam('src_column_id') && $data['task']['owner_id'] > 0; } } diff --git a/app/Action/TaskMoveColumnCategoryChange.php b/app/Action/TaskMoveColumnCategoryChange.php index e4f88760..4c2b289a 100644 --- a/app/Action/TaskMoveColumnCategoryChange.php +++ b/app/Action/TaskMoveColumnCategoryChange.php @@ -60,8 +60,13 @@ class TaskMoveColumnCategoryChange extends Base { return array( 'task_id', - 'column_id', - 'category_id', + 'task' => array( + 'project_id', + 'column_id', + 'category_id', + 'position', + 'swimlane_id', + ) ); } @@ -74,14 +79,12 @@ class TaskMoveColumnCategoryChange extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPositionModel->movePosition( - $data['project_id'], + $data['task']['project_id'], $data['task_id'], $this->getParam('dest_column_id'), - $original_task['position'], - $original_task['swimlane_id'], + $data['task']['position'], + $data['task']['swimlane_id'], false ); } @@ -95,6 +98,6 @@ class TaskMoveColumnCategoryChange extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] != $this->getParam('dest_column_id') && $data['category_id'] == $this->getParam('category_id'); + return $data['task']['column_id'] != $this->getParam('dest_column_id') && $data['task']['category_id'] == $this->getParam('category_id'); } } diff --git a/app/Action/TaskMoveColumnUnAssigned.php b/app/Action/TaskMoveColumnUnAssigned.php index c3ae9e1d..0e9a8a16 100644 --- a/app/Action/TaskMoveColumnUnAssigned.php +++ b/app/Action/TaskMoveColumnUnAssigned.php @@ -61,8 +61,13 @@ class TaskMoveColumnUnAssigned extends Base { return array( 'task_id', - 'column_id', - 'owner_id' + 'task' => array( + 'project_id', + 'column_id', + 'owner_id', + 'position', + 'swimlane_id', + ) ); } @@ -75,14 +80,12 @@ class TaskMoveColumnUnAssigned extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPositionModel->movePosition( - $data['project_id'], + $data['task']['project_id'], $data['task_id'], $this->getParam('dest_column_id'), - $original_task['position'], - $original_task['swimlane_id'], + $data['task']['position'], + $data['task']['swimlane_id'], false ); } @@ -96,6 +99,6 @@ class TaskMoveColumnUnAssigned extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('src_column_id') && $data['owner_id'] == 0; + return $data['task']['column_id'] == $this->getParam('src_column_id') && $data['task']['owner_id'] == 0; } } diff --git a/app/Action/TaskUpdateStartDate.php b/app/Action/TaskUpdateStartDate.php index e5410a87..cc016da1 100644 --- a/app/Action/TaskUpdateStartDate.php +++ b/app/Action/TaskUpdateStartDate.php @@ -59,7 +59,10 @@ class TaskUpdateStartDate extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -89,6 +92,6 @@ class TaskUpdateStartDate extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Auth/TotpAuth.php b/app/Auth/TotpAuth.php index f4304930..8e1ebe35 100644 --- a/app/Auth/TotpAuth.php +++ b/app/Auth/TotpAuth.php @@ -123,7 +123,8 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface return ''; } - return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret); + $options = array('issuer' => TOTP_ISSUER); + return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret, null, $options); } /** @@ -139,6 +140,7 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface return ''; } - return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret); + $options = array('issuer' => TOTP_ISSUER); + return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret, null, $options); } } diff --git a/app/Controller/ColumnController.php b/app/Controller/ColumnController.php index e3f9bfff..d3f0e36e 100644 --- a/app/Controller/ColumnController.php +++ b/app/Controller/ColumnController.php @@ -66,7 +66,15 @@ class ColumnController extends BaseController list($valid, $errors) = $this->columnValidator->validateCreation($values); if ($valid) { - if ($this->columnModel->create($project['id'], $values['title'], $values['task_limit'], $values['description'], $values['hide_in_dashboard']) !== false) { + $result = $this->columnModel->create( + $project['id'], + $values['title'], + $values['task_limit'], + $values['description'], + isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0 + ); + + if ($result !== false) { $this->flash->success(t('Column created successfully.')); return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id'])), true); } else { @@ -111,7 +119,15 @@ class ColumnController extends BaseController list($valid, $errors) = $this->columnValidator->validateModification($values); if ($valid) { - if ($this->columnModel->update($values['id'], $values['title'], $values['task_limit'], $values['description'], $values['hide_in_dashboard']) !== false) { + $result = $this->columnModel->update( + $values['id'], + $values['title'], + $values['task_limit'], + $values['description'], + isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0 + ); + + if ($result) { $this->flash->success(t('Board updated successfully.')); return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id']))); } else { diff --git a/app/Controller/ProjectPermissionController.php b/app/Controller/ProjectPermissionController.php index f3ca6ed9..99f556e8 100644 --- a/app/Controller/ProjectPermissionController.php +++ b/app/Controller/ProjectPermissionController.php @@ -147,7 +147,7 @@ class ProjectPermissionController extends BaseController $values = $this->request->getValues(); if (empty($values['group_id']) && ! empty($values['external_id'])) { - $values['group_id'] = $this->groupModel->create($values['name'], $values['external_id']); + $values['group_id'] = $this->groupModel->getOrCreateExternalGroupId($values['name'], $values['external_id']); } if ($this->projectGroupRoleModel->addGroup($project['id'], $values['group_id'], $values['role'])) { diff --git a/app/Core/Base.php b/app/Core/Base.php index 8103ec14..098bd880 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -150,6 +150,12 @@ use Pimple\Container; * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer + * @property \Kanboard\Job\CommentEventJob $commentEventJob + * @property \Kanboard\Job\SubtaskEventJob $subtaskEventJob + * @property \Kanboard\Job\TaskEventJob $taskEventJob + * @property \Kanboard\Job\TaskFileEventJob $taskFileEventJob + * @property \Kanboard\Job\ProjectFileEventJob $projectFileEventJob + * @property \Kanboard\Job\NotificationJob $notificationJob * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php index 7a9a714f..626d7614 100644 --- a/app/Core/Filter/LexerBuilder.php +++ b/app/Core/Filter/LexerBuilder.php @@ -69,7 +69,7 @@ class LexerBuilder foreach ($attributes as $attribute) { $this->filters[$attribute] = $filter; - $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute); + $this->lexer->addToken(sprintf("/^(%s:)/i", $attribute), $attribute); if ($default) { $this->lexer->setDefaultToken($attribute); diff --git a/app/Core/Http/Request.php b/app/Core/Http/Request.php index e0df2d3c..2e84958d 100644 --- a/app/Core/Http/Request.php +++ b/app/Core/Http/Request.php @@ -301,6 +301,7 @@ class Request extends Base public function getIpAddress() { $keys = array( + 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', diff --git a/app/Core/Queue/JobHandler.php b/app/Core/Queue/JobHandler.php index 7ca36328..326f3cef 100644 --- a/app/Core/Queue/JobHandler.php +++ b/app/Core/Queue/JobHandler.php @@ -2,6 +2,7 @@ namespace Kanboard\Core\Queue; +use Exception; use Kanboard\Core\Base; use Kanboard\Job\BaseJob; use SimpleQueue\Job; @@ -39,16 +40,23 @@ class JobHandler extends Base public function executeJob(Job $job) { $payload = $job->getBody(); - $className = $payload['class']; - $this->memoryCache->flush(); - $this->prepareJobSession($payload['user_id']); - if (DEBUG) { - $this->logger->debug(__METHOD__.' Received job => '.$className.' ('.getmypid().')'); - } + try { + $className = $payload['class']; + $this->memoryCache->flush(); + $this->prepareJobSession($payload['user_id']); + + if (DEBUG) { + $this->logger->debug(__METHOD__.' Received job => '.$className.' ('.getmypid().')'); + $this->logger->debug(__METHOD__.' => '.json_encode($payload)); + } - $worker = new $className($this->container); - call_user_func_array(array($worker, 'execute'), $payload['params']); + $worker = new $className($this->container); + call_user_func_array(array($worker, 'execute'), $payload['params']); + } catch (Exception $e) { + $this->logger->error(__METHOD__.': Error during job execution: '.$e->getMessage()); + $this->logger->error(__METHOD__ .' => '.json_encode($payload)); + } } /** diff --git a/app/Core/Queue/QueueManager.php b/app/Core/Queue/QueueManager.php index f34cb220..dcf0ebf5 100644 --- a/app/Core/Queue/QueueManager.php +++ b/app/Core/Queue/QueueManager.php @@ -42,9 +42,13 @@ class QueueManager extends Base */ public function push(BaseJob $job) { + $jobClassName = get_class($job); + if ($this->queue !== null) { + $this->logger->debug(__METHOD__.': Job pushed in queue: '.$jobClassName); $this->queue->push(JobHandler::getInstance($this->container)->serializeJob($job)); } else { + $this->logger->debug(__METHOD__.': Job executed synchronously: '.$jobClassName); call_user_func_array(array($job, 'execute'), $job->getJobParams()); } @@ -60,7 +64,7 @@ class QueueManager extends Base public function listen() { if ($this->queue === null) { - throw new LogicException('No Queue Driver defined!'); + throw new LogicException('No queue driver defined!'); } while ($job = $this->queue->pull()) { diff --git a/app/Event/FileEvent.php b/app/Event/FileEvent.php deleted file mode 100644 index 482a4eab..00000000 --- a/app/Event/FileEvent.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php - -namespace Kanboard\Event; - -class FileEvent extends GenericEvent -{ -} diff --git a/app/Event/ProjectFileEvent.php b/app/Event/ProjectFileEvent.php new file mode 100644 index 00000000..5d57e463 --- /dev/null +++ b/app/Event/ProjectFileEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Kanboard\Event; + +class ProjectFileEvent extends GenericEvent +{ +} diff --git a/app/Event/TaskFileEvent.php b/app/Event/TaskFileEvent.php new file mode 100644 index 00000000..fa3bdde9 --- /dev/null +++ b/app/Event/TaskFileEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Kanboard\Event; + +class TaskFileEvent extends GenericEvent +{ +} diff --git a/app/EventBuilder/BaseEventBuilder.php b/app/EventBuilder/BaseEventBuilder.php new file mode 100644 index 00000000..c677563e --- /dev/null +++ b/app/EventBuilder/BaseEventBuilder.php @@ -0,0 +1,23 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Core\Base; +use Kanboard\Event\GenericEvent; + +/** + * Class BaseEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +abstract class BaseEventBuilder extends Base +{ + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + abstract public function build(); +} diff --git a/app/EventBuilder/CommentEventBuilder.php b/app/EventBuilder/CommentEventBuilder.php new file mode 100644 index 00000000..7b4060e4 --- /dev/null +++ b/app/EventBuilder/CommentEventBuilder.php @@ -0,0 +1,48 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\CommentEvent; + +/** + * Class CommentEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class CommentEventBuilder extends BaseEventBuilder +{ + protected $commentId = 0; + + /** + * Set commentId + * + * @param int $commentId + * @return $this + */ + public function withCommentId($commentId) + { + $this->commentId = $commentId; + return $this; + } + + /** + * Build event data + * + * @access public + * @return CommentEvent|null + */ + public function build() + { + $comment = $this->commentModel->getById($this->commentId); + + if (empty($comment)) { + return null; + } + + return new CommentEvent(array( + 'comment' => $comment, + 'task' => $this->taskFinderModel->getDetails($comment['task_id']), + )); + } +} diff --git a/app/EventBuilder/ProjectFileEventBuilder.php b/app/EventBuilder/ProjectFileEventBuilder.php new file mode 100644 index 00000000..70514a99 --- /dev/null +++ b/app/EventBuilder/ProjectFileEventBuilder.php @@ -0,0 +1,50 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\ProjectFileEvent; +use Kanboard\Event\GenericEvent; + +/** + * Class ProjectFileEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class ProjectFileEventBuilder extends BaseEventBuilder +{ + protected $fileId = 0; + + /** + * Set fileId + * + * @param int $fileId + * @return $this + */ + public function withFileId($fileId) + { + $this->fileId = $fileId; + return $this; + } + + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + public function build() + { + $file = $this->projectFileModel->getById($this->fileId); + + if (empty($file)) { + $this->logger->debug(__METHOD__.': File not found'); + return null; + } + + return new ProjectFileEvent(array( + 'file' => $file, + 'project' => $this->projectModel->getById($file['project_id']), + )); + } +} diff --git a/app/EventBuilder/SubtaskEventBuilder.php b/app/EventBuilder/SubtaskEventBuilder.php new file mode 100644 index 00000000..f0271257 --- /dev/null +++ b/app/EventBuilder/SubtaskEventBuilder.php @@ -0,0 +1,79 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\SubtaskEvent; +use Kanboard\Event\GenericEvent; + +/** + * Class SubtaskEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class SubtaskEventBuilder extends BaseEventBuilder +{ + /** + * SubtaskId + * + * @access protected + * @var int + */ + protected $subtaskId = 0; + + /** + * Changed values + * + * @access protected + * @var array + */ + protected $values = array(); + + /** + * Set SubtaskId + * + * @param int $subtaskId + * @return $this + */ + public function withSubtaskId($subtaskId) + { + $this->subtaskId = $subtaskId; + return $this; + } + + /** + * Set values + * + * @param array $values + * @return $this + */ + public function withValues(array $values) + { + $this->values = $values; + return $this; + } + + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + public function build() + { + $eventData = array(); + $eventData['subtask'] = $this->subtaskModel->getById($this->subtaskId, true); + + if (empty($eventData['subtask'])) { + $this->logger->debug(__METHOD__.': Subtask not found'); + return null; + } + + if (! empty($this->values)) { + $eventData['changes'] = array_diff_assoc($this->values, $eventData['subtask']); + } + + $eventData['task'] = $this->taskFinderModel->getDetails($eventData['subtask']['task_id']); + return new SubtaskEvent($eventData); + } +} diff --git a/app/EventBuilder/TaskEventBuilder.php b/app/EventBuilder/TaskEventBuilder.php new file mode 100644 index 00000000..e7a5653d --- /dev/null +++ b/app/EventBuilder/TaskEventBuilder.php @@ -0,0 +1,123 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\TaskEvent; + +/** + * Class TaskEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class TaskEventBuilder extends BaseEventBuilder +{ + /** + * TaskId + * + * @access protected + * @var int + */ + protected $taskId = 0; + + /** + * Task + * + * @access protected + * @var array + */ + protected $task = array(); + + /** + * Extra values + * + * @access protected + * @var array + */ + protected $values = array(); + + /** + * Changed values + * + * @access protected + * @var array + */ + protected $changes = array(); + + /** + * Set TaskId + * + * @param int $taskId + * @return $this + */ + public function withTaskId($taskId) + { + $this->taskId = $taskId; + return $this; + } + + /** + * Set task + * + * @param array $task + * @return $this + */ + public function withTask(array $task) + { + $this->task = $task; + return $this; + } + + /** + * Set values + * + * @param array $values + * @return $this + */ + public function withValues(array $values) + { + $this->values = $values; + return $this; + } + + /** + * Set changes + * + * @param array $changes + * @return $this + */ + public function withChanges(array $changes) + { + $this->changes = $changes; + return $this; + } + + /** + * Build event data + * + * @access public + * @return TaskEvent|null + */ + public function build() + { + $eventData = array(); + $eventData['task_id'] = $this->taskId; + $eventData['task'] = $this->taskFinderModel->getDetails($this->taskId); + + if (empty($eventData['task'])) { + $this->logger->debug(__METHOD__.': Task not found'); + return null; + } + + if (! empty($this->changes)) { + if (empty($this->task)) { + $this->task = $eventData['task']; + } + + $eventData['changes'] = array_diff_assoc($this->changes, $this->task); + unset($eventData['changes']['date_modification']); + } + + return new TaskEvent(array_merge($eventData, $this->values)); + } +} diff --git a/app/EventBuilder/TaskFileEventBuilder.php b/app/EventBuilder/TaskFileEventBuilder.php new file mode 100644 index 00000000..7f1ce3b3 --- /dev/null +++ b/app/EventBuilder/TaskFileEventBuilder.php @@ -0,0 +1,50 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\TaskFileEvent; +use Kanboard\Event\GenericEvent; + +/** + * Class TaskFileEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class TaskFileEventBuilder extends BaseEventBuilder +{ + protected $fileId = 0; + + /** + * Set fileId + * + * @param int $fileId + * @return $this + */ + public function withFileId($fileId) + { + $this->fileId = $fileId; + return $this; + } + + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + public function build() + { + $file = $this->taskFileModel->getById($this->fileId); + + if (empty($file)) { + $this->logger->debug(__METHOD__.': File not found'); + return null; + } + + return new TaskFileEvent(array( + 'file' => $file, + 'task' => $this->taskFinderModel->getDetails($file['task_id']), + )); + } +} diff --git a/app/Filter/TaskPriorityFilter.php b/app/Filter/TaskPriorityFilter.php new file mode 100644 index 00000000..75f6ae3d --- /dev/null +++ b/app/Filter/TaskPriorityFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\TaskModel; + +/** + * Class TaskPriorityFilter + * + * @package Kanboard\Filter + * @author Frederic Guillot + */ +class TaskPriorityFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('priority'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(TaskModel::TABLE.'.priority', $this->value); + return $this; + } +} diff --git a/app/Formatter/TaskAutoCompleteFormatter.php b/app/Formatter/TaskAutoCompleteFormatter.php index 4f1c4c69..2d9f7341 100644 --- a/app/Formatter/TaskAutoCompleteFormatter.php +++ b/app/Formatter/TaskAutoCompleteFormatter.php @@ -3,6 +3,7 @@ namespace Kanboard\Formatter; use Kanboard\Core\Filter\FormatterInterface; +use Kanboard\Model\ProjectModel; use Kanboard\Model\TaskModel; /** @@ -21,11 +22,15 @@ class TaskAutoCompleteFormatter extends BaseFormatter implements FormatterInterf */ public function format() { - $tasks = $this->query->columns(TaskModel::TABLE.'.id', TaskModel::TABLE.'.title')->findAll(); + $tasks = $this->query->columns( + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.title', + ProjectModel::TABLE.'.name AS project_name' + )->asc(TaskModel::TABLE.'.id')->findAll(); foreach ($tasks as &$task) { $task['value'] = $task['title']; - $task['label'] = '#'.$task['id'].' - '.$task['title']; + $task['label'] = $task['project_name'].' > #'.$task['id'].' '.$task['title']; } return $tasks; diff --git a/app/Helper/TaskHelper.php b/app/Helper/TaskHelper.php index e1d65cca..481a5efb 100644 --- a/app/Helper/TaskHelper.php +++ b/app/Helper/TaskHelper.php @@ -50,6 +50,7 @@ class TaskHelper extends Base public function selectDescription(array $values, array $errors) { $html = $this->helper->form->label(t('Description'), 'description'); + $html .= '<div class="markdown-editor-container">'; $html .= $this->helper->form->textarea( 'description', $values, @@ -62,6 +63,7 @@ class TaskHelper extends Base 'markdown-editor' ); + $html .= '</div>'; return $html; } diff --git a/app/Job/CommentEventJob.php b/app/Job/CommentEventJob.php new file mode 100644 index 00000000..c89350ed --- /dev/null +++ b/app/Job/CommentEventJob.php @@ -0,0 +1,50 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\CommentEventBuilder; +use Kanboard\Model\CommentModel; + +/** + * Class CommentEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class CommentEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $commentId + * @param string $eventName + * @return $this + */ + public function withParams($commentId, $eventName) + { + $this->jobParams = array($commentId, $eventName); + return $this; + } + + /** + * Execute job + * + * @param int $commentId + * @param string $eventName + * @return $this + */ + public function execute($commentId, $eventName) + { + $event = CommentEventBuilder::getInstance($this->container) + ->withCommentId($commentId) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + + if ($eventName === CommentModel::EVENT_CREATE) { + $this->userMentionModel->fireEvents($event['comment']['comment'], CommentModel::EVENT_USER_MENTION, $event); + } + } + } +} diff --git a/app/Job/NotificationJob.php b/app/Job/NotificationJob.php index 904a9273..8fb260e8 100644 --- a/app/Job/NotificationJob.php +++ b/app/Job/NotificationJob.php @@ -17,69 +17,27 @@ class NotificationJob extends BaseJob * * @param GenericEvent $event * @param string $eventName - * @param string $eventObjectName * @return $this */ - public function withParams(GenericEvent $event, $eventName, $eventObjectName) + public function withParams(GenericEvent $event, $eventName) { - $this->jobParams = array($event->getAll(), $eventName, $eventObjectName); + $this->jobParams = array($event->getAll(), $eventName); return $this; } /** * Execute job * - * @param array $event + * @param array $eventData * @param string $eventName - * @param string $eventObjectName */ - public function execute(array $event, $eventName, $eventObjectName) + public function execute(array $eventData, $eventName) { - $eventData = $this->getEventData($event, $eventObjectName); - - if (! empty($eventData)) { - if (! empty($event['mention'])) { - $this->userNotificationModel->sendUserNotification($event['mention'], $eventName, $eventData); - } else { - $this->userNotificationModel->sendNotifications($eventName, $eventData); - $this->projectNotificationModel->sendNotifications($eventData['task']['project_id'], $eventName, $eventData); - } - } - } - - /** - * Get event data - * - * @param array $event - * @param string $eventObjectName - * @return array - */ - public function getEventData(array $event, $eventObjectName) - { - $values = array(); - - if (! empty($event['changes'])) { - $values['changes'] = $event['changes']; + if (! empty($eventData['mention'])) { + $this->userNotificationModel->sendUserNotification($eventData['mention'], $eventName, $eventData); + } else { + $this->userNotificationModel->sendNotifications($eventName, $eventData); + $this->projectNotificationModel->sendNotifications($eventData['task']['project_id'], $eventName, $eventData); } - - switch ($eventObjectName) { - case 'Kanboard\Event\TaskEvent': - $values['task'] = $this->taskFinderModel->getDetails($event['task_id']); - break; - case 'Kanboard\Event\SubtaskEvent': - $values['subtask'] = $this->subtaskModel->getById($event['id'], true); - $values['task'] = $this->taskFinderModel->getDetails($values['subtask']['task_id']); - break; - case 'Kanboard\Event\FileEvent': - $values['file'] = $event; - $values['task'] = $this->taskFinderModel->getDetails($values['file']['task_id']); - break; - case 'Kanboard\Event\CommentEvent': - $values['comment'] = $this->commentModel->getById($event['id']); - $values['task'] = $this->taskFinderModel->getDetails($values['comment']['task_id']); - break; - } - - return $values; } } diff --git a/app/Job/ProjectFileEventJob.php b/app/Job/ProjectFileEventJob.php new file mode 100644 index 00000000..d68949c5 --- /dev/null +++ b/app/Job/ProjectFileEventJob.php @@ -0,0 +1,45 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\ProjectFileEventBuilder; + +/** + * Class ProjectFileEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class ProjectFileEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function withParams($fileId, $eventName) + { + $this->jobParams = array($fileId, $eventName); + return $this; + } + + /** + * Execute job + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function execute($fileId, $eventName) + { + $event = ProjectFileEventBuilder::getInstance($this->container) + ->withFileId($fileId) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + } + } +} diff --git a/app/Job/SubtaskEventJob.php b/app/Job/SubtaskEventJob.php new file mode 100644 index 00000000..1dc243ef --- /dev/null +++ b/app/Job/SubtaskEventJob.php @@ -0,0 +1,48 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\SubtaskEventBuilder; + +/** + * Class SubtaskEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class SubtaskEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $subtaskId + * @param string $eventName + * @param array $values + * @return $this + */ + public function withParams($subtaskId, $eventName, array $values = array()) + { + $this->jobParams = array($subtaskId, $eventName, $values); + return $this; + } + + /** + * Execute job + * + * @param int $subtaskId + * @param string $eventName + * @param array $values + * @return $this + */ + public function execute($subtaskId, $eventName, array $values = array()) + { + $event = SubtaskEventBuilder::getInstance($this->container) + ->withSubtaskId($subtaskId) + ->withValues($values) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + } + } +} diff --git a/app/Job/TaskEventJob.php b/app/Job/TaskEventJob.php new file mode 100644 index 00000000..46f7a16c --- /dev/null +++ b/app/Job/TaskEventJob.php @@ -0,0 +1,75 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\Event\TaskEvent; +use Kanboard\EventBuilder\TaskEventBuilder; +use Kanboard\Model\TaskModel; + +/** + * Class TaskEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class TaskEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $taskId + * @param array $eventNames + * @param array $changes + * @param array $values + * @param array $task + * @return $this + */ + public function withParams($taskId, array $eventNames, array $changes = array(), array $values = array(), array $task = array()) + { + $this->jobParams = array($taskId, $eventNames, $changes, $values, $task); + return $this; + } + + /** + * Execute job + * + * @param int $taskId + * @param array $eventNames + * @param array $changes + * @param array $values + * @param array $task + * @return $this + */ + public function execute($taskId, array $eventNames, array $changes = array(), array $values = array(), array $task = array()) + { + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId($taskId) + ->withChanges($changes) + ->withValues($values) + ->withTask($task) + ->build(); + + if ($event !== null) { + foreach ($eventNames as $eventName) { + $this->fireEvent($eventName, $event); + } + } + } + + /** + * Trigger event + * + * @access protected + * @param string $eventName + * @param TaskEvent $event + */ + protected function fireEvent($eventName, TaskEvent $event) + { + $this->logger->debug(__METHOD__.' Event fired: '.$eventName); + $this->dispatcher->dispatch($eventName, $event); + + if ($eventName === TaskModel::EVENT_CREATE) { + $this->userMentionModel->fireEvents($event['task']['description'], TaskModel::EVENT_USER_MENTION, $event); + } + } +} diff --git a/app/Job/TaskFileEventJob.php b/app/Job/TaskFileEventJob.php new file mode 100644 index 00000000..de2c40db --- /dev/null +++ b/app/Job/TaskFileEventJob.php @@ -0,0 +1,45 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\TaskFileEventBuilder; + +/** + * Class TaskFileEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class TaskFileEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function withParams($fileId, $eventName) + { + $this->jobParams = array($fileId, $eventName); + return $this; + } + + /** + * Execute job + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function execute($fileId, $eventName) + { + $event = TaskFileEventBuilder::getInstance($this->container) + ->withFileId($fileId) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + } + } +} diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index e3c29261..6a062068 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index ce66eee5..b9a4de6e 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index f9bc0031..050a37d9 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index d3192f46..d6c8bf60 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1216,5 +1216,14 @@ return array( 'Global tags' => 'Globale Schlagwörter', 'There is no global tag at the moment.' => 'Es gibt zur Zeit kein globales Schlagwort', 'This field cannot be empty' => 'Dieses Feld kann nicht leer sein', - 'Hide tasks in this column in the Dashboard' => 'Aufgaben in dieser Spalte im Dashboard ausblenden', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + 'Hide tasks in this column in the dashboard' => 'Aufgaben in dieser Spalte im Dashboard ausblenden', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index 745acaea..87ea68b0 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 604a5e3e..1a4bae82 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 73b65463..5d37cb82 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index e3ee8a4b..c8f7d343 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1217,5 +1217,14 @@ return array( 'Global tags' => 'Libellés globaux', 'There is no global tag at the moment.' => 'Il n\'y a aucun libellé global pour le moment.', 'This field cannot be empty' => 'Ce champ ne peut être vide', - // 'Hide tasks in this column in the Dashboard' => '', + 'Close a task when there is no activity in an specific column' => 'Fermer une tâche lorsqu\'il n\'y a aucune activité dans une colonne spécifique', + '%s removed a subtask for the task #%d' => '%s a supprimé une sous-tâche de la tâche n°%d', + '%s removed a comment on the task #%d' => '%s a supprimé un commentaire de la tâche n°%d', + 'Comment removed on task #%d' => 'Commentaire supprimé sur la tâche n°%d', + 'Subtask removed on task #%d' => 'Sous-tâche supprimée sur la tâche n°%d', + 'Hide tasks in this column in the dashboard' => 'Cacher les tâches de cette colonne dans le tableau de bord', + '%s removed a comment on the task %s' => '%s a supprimé un commentaire de la tâche %s', + '%s removed a subtask for the task %s' => '%s a supprimé une sous-tâche de la tâche %s', + 'Comment removed' => 'Commentaire supprimé', + 'Subtask removed' => 'Sous-tâche supprimée', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 9acdbd1a..febf8bc0 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index d8c42cf4..18a7a72d 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 9d2af814..f6c63076 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -1216,5 +1216,14 @@ return array( 'Global tags' => 'Tag globali', 'There is no global tag at the moment.' => 'Non sono definiti tag globali al momento.', 'This field cannot be empty' => 'Questo campo non può essere vuoto', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 9f6ab88f..dab731d2 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php index f48b7486..0b6007b1 100644 --- a/app/Locale/ko_KR/translations.php +++ b/app/Locale/ko_KR/translations.php @@ -100,9 +100,9 @@ return array( 'There is nobody assigned' => '담당자가 없습니다', 'Column on the board:' => '칼럼:', 'Close this task' => '할일 마치기', - 'Open this task' => '할일 열기', - 'There is no description.' => '설명이 없습니다', - 'Add a new task' => '할일 추가 ', + 'Open this task' => '할일을 열다', + 'There is no description.' => '설명이 없다', + 'Add a new task' => '할일을 추가하는 ', 'The username is required' => '사용자 이름이 필요합니다', 'The maximum length is %d characters' => '최대 길이는 "%d" 글자 입니다', 'The minimum length is %d characters' => '최소 길이는 "%d" 글자 입니다', @@ -145,7 +145,7 @@ return array( 'User removed successfully.' => '사용자를 삭제했습니다.', 'Unable to remove this user.' => '사용자 삭제에 실패했습니다.', 'Board updated successfully.' => '보드를 갱신했습니다.', - 'Ready' => '준비완료', + 'Ready' => '준비중', 'Backlog' => '요구사항', 'Work in progress' => '진행중', 'Done' => '완료', @@ -189,15 +189,15 @@ return array( 'Remove an automatic action' => '자동 액션의 삭제', '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' => '할일을 다른 칼럼으로 이동', + 'Duplicate the task to another project' => ' 다른 프로젝트에 할일을 복제하는 ', + 'Move a task to another column' => '할일을 다른 칼럼에 이동하는 ', 'Task modification' => '할일 변경', - 'Task creation' => '할일 만들기', - 'Closing a task' => '할일 종료', - 'Assign a color to a specific user' => '사용자 색 할당', + 'Task creation' => '할일을 만들', + 'Closing a task' => '할일을 닫혔다', + 'Assign a color to a specific user' => '색을 사용자에 할당', 'Column title' => '칼럼의 제목', 'Position' => '위치', - 'Duplicate to another project' => '다른 프로젝트로 복사', + 'Duplicate to another project' => '다른 프로젝트에 복사', 'Duplicate' => '복사', 'link' => '링크', 'Comment updated successfully.' => '댓글을 갱신했습니다.', @@ -327,7 +327,7 @@ return array( 'Comment updated' => '댓글가 갱신되었습니다', 'New subtask' => ' 새로운 서브 할일', 'Subtask updated' => '서브 할일 갱신', - 'Task updated' => '할일 업데이트', + 'Task updated' => '할일 갱신', 'Task closed' => '할일 마침', 'Task opened' => '할일 시작', 'I want to receive notifications only for those projects:' => '다음 프로젝트의 알림만 받겠습니다:', @@ -365,7 +365,7 @@ return array( 'Password modified successfully.' => '패스워드를 변경했습니다.', 'Unable to change the password.' => '비밀 번호가 변경할 수 없었습니다.', 'Change category' => '카테고리 수정', - '%s updated the task %s' => '%s이 할일 %s을 업데이트했습니다', + '%s updated the task %s' => '%s이 할일 %s을 갱신 하였습니다', '%s opened the task %s' => '%s이 할일 %s을 시작시켰습니다', '%s moved the task %s to the position #%d in the column "%s"' => '%s이 할일%s을 위치#%d컬럼%s로 옮겼습니다', '%s moved the task %s to the column "%s"' => '%s이 할일 %s을 칼럼 "%s" 로 옮겼습니다', @@ -665,13 +665,13 @@ return array( 'Show tasks based on the start date' => '시작 날짜로 할일 보기', 'Subtasks time tracking' => '서브 할일 시간 트래킹', 'User calendar view' => '담당자 달력 보기', - 'Automatically update the start date' => '시작일에 자동 업데이트', + 'Automatically update the start date' => '시작일에 자동 갱신', // 'iCal feed' => '', 'Preferences' => '우선권', 'Security' => '보안', 'Two factor authentication disabled' => '이중 인증이 비활성화 되었습니다', 'Two factor authentication enabled' => '이중 인증이 활성화 되었습니다', - 'Unable to update this user.' => '담당자의 업데이트가 가능합니다', + 'Unable to update this user.' => '담당자 갱신이 가능합니다', 'There is no user management for private projects.' => '비밀 프로젝트의 관리 담당자가 없습니다', 'User that will receive the email' => '그 담당자가 이메일을 수신할 것입니다', 'Email subject' => '이메일 제목', @@ -710,7 +710,7 @@ return array( 'Recurrence settings have been modified' => '반복할일 설정 수정', 'Time spent changed: %sh' => '경과시간 변경: %s시간', 'Time estimated changed: %sh' => '%s시간으로 예상시간 변경', - 'The field "%s" have been updated' => '%s 필드가 업데이트 되어있습니다', + 'The field "%s" have been updated' => '%s 필드가 갱신되어 있습니다', 'The description has been modified:' => '설명이 수정되어 있습니다: ', 'Do you really want to close the task "%s" as well as all subtasks?' => '할일 "%s"과 서브 할일을 모두 마치시겠습니까?', 'I want to receive notifications for:' => '다음의 알림을 받기를 원합니다:', @@ -855,11 +855,11 @@ return array( 'Web' => '웹', // 'New attachment on task #%d: %s' => '', 'New comment on task #%d' => '할일 #%d에 새로운 댓글이 달렸습니다', - 'Comment updated on task #%d' => '할일 #%d에 댓글이 업데이트되었습니다', - 'New subtask on task #%d' => '서브 할일 #%d이 업데이트되었습니다', - 'Subtask updated on task #%d' => '서브 할일 #%d가 업데이트되었습니다', + 'Comment updated on task #%d' => '할일 #%d에 댓글이 갱신 되었습니다', + 'New subtask on task #%d' => '서브 할일 #%d이 갱신 되었습니다', + 'Subtask updated on task #%d' => '서브 할일 #%d가 갱신 되었습니다', 'New task #%d: %s' => '할일 #%d: %s이 추가되었습니다', - 'Task updated #%d' => '할일 #%d이 업데이트되었습니다', + 'Task updated #%d' => '할일 #%d이 갱신 되었습니다', 'Task #%d closed' => '할일 #%d를 마쳤습니다', 'Task #%d opened' => '할일 #%d가 시작되었습니다', 'Column changed for task #%d' => '할일 #%d의 칼럼이 변경되었습니다', @@ -1159,40 +1159,40 @@ return array( 'Upload files' => '파일 올리기', 'Installed Plugins' => '설치된 플러그인', 'Plugin Directory' => '플러그인 폴더', - 'Plugin installed successfully.' => '플러그인이 성공적으로 설치 되었습니다', - 'Plugin updated successfully.' => '플러그인이 성공적으로 업데이트 되었습니다', - 'Plugin removed successfully.' => '플러그인이 성공적으로 삭제 되었습니다', - 'Subtask converted to task successfully.' => '서브 할일이 성공적으로 할일로 변경 되었습니다', - 'Unable to convert the subtask.' => '서브할일 변경 비활성화', - 'Unable to extract plugin archive.' => '플러그인 보관소 비활성화', - 'Plugin not found.' => '플러그인을 찾을 수 없습니다', - 'You don\'t have the permission to remove this plugin.' => '플러그인 삭제 권한이 없습니다', - 'Unable to download plugin archive.' => '플러그인 보관소 다운로드 비활성화', - 'Unable to write temporary file for plugin.' => '플러그인의 임시 파일 기록 비활성화', - 'Unable to open plugin archive.' => '플러그인 보관소 열기 비활성화', - 'There is no file in the plugin archive.' => '플러그인 보관소에 파일이 없습니다', - 'Create tasks in bulk' => '벌크에 할일 생성', + 'Plugin installed successfully.' => '플러그인이 성공적으로 설치 되었습니다.', + 'Plugin updated successfully.' => '플러그인이 성공적으로 갱신 되었습니다.', + 'Plugin removed successfully.' => '플러그인이 성공적으로 삭제 되었습니다.', + 'Subtask converted to task successfully.' => '서브 할일이 할일로 성공적으로 변경 되었습니다.', + 'Unable to convert the subtask.' => '서브 할일로 변경할 수 없습니다.', + 'Unable to extract plugin archive.' => '플러그인 아카이브를 추출할 수 없습니다.', + 'Plugin not found.' => '플러그인을 찾을 수 없습니다.', + 'You don\'t have the permission to remove this plugin.' => '이 플러그인의 삭제 권한이 없습니다.', + 'Unable to download plugin archive.' => '플러그인 아카이브를 다운로드할 수 없습니다.', + 'Unable to write temporary file for plugin.' => '플러그인의 임시 파일을 기록할 수 없습니다.', + 'Unable to open plugin archive.' => '플러그인 아카이브를 열수 없습니다.', + 'There is no file in the plugin archive.' => '플러그인 아카이브에 파일이 존재하지 않습니다.', + 'Create tasks in bulk' => '대량의 할일 만들기', // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', - 'There is no plugin available.' => '사용할 수 있는 플러그인이 없습니다', + 'There is no plugin available.' => '사용 가능한 플러그인이 없습니다.', 'Install' => '설치', - 'Update' => '업데이트', - 'Up to date' => '최신의', + 'Update' => '갱신', + 'Up to date' => '날짜 갱신', 'Not available' => '사용 불가능', 'Remove plugin' => '플러그인 삭제', - 'Do you really want to remove this plugin: "%s"?' => '플러그인을 삭제 하시겠습니까: "%s"?', - 'Uninstall' => '제거', + 'Do you really want to remove this plugin: "%s"?' => '정말로 플러그인을 삭제하시겠습니까: "%s"?', + 'Uninstall' => '삭제', 'Listing' => '목록', - 'Metadata' => '메타 데이터', + 'Metadata' => '메타데이터', 'Manage projects' => '프로젝트 관리', - 'Convert to task' => '할일로 변경', - 'Convert sub-task to task' => '서브 할일을 할일로 변경', - 'Do you really want to convert this sub-task to a task?' => '서브 할일을 일로 변경하시겠습니까?', - 'My task title' => '할일 제목', - // 'Enter one task by line.' => '', - 'Number of failed login:' => '로그인 실패 횟수', - 'Account locked until:' => '계정이 잠겼습니다:', + 'Convert to task' => '할일로 변경하기', + 'Convert sub-task to task' => '서브 할일을 할일로 변경하기', + 'Do you really want to convert this sub-task to a task?' => '정말로 서브 할일을 할일로 변경하시겠습니까?', + 'My task title' => '나의 할일 제목', + 'Enter one task by line.' => '정확히 하나의 할일로 진입', + 'Number of failed login:' => '로그인 실패 횟수:', + 'Account locked until:' => '다음동안 계정이 잠겼습니다:', 'Email settings' => '이메일 설정', - 'Email sender address' => '이메일 송신자 주소', + 'Email sender address' => '이메일 보낸이 주소', 'Email transport' => '이메일 전송', // 'Webhook token' => '', 'Imports' => '가져오기', @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index bad6d919..3d66b0bb 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 19372419..14e260cb 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 8ba0d394..8b47d514 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - //' Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 09c247d8..e72649e6 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 7659ba2b..7b64f0e7 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 1f9a7030..5267b03b 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -1215,6 +1215,15 @@ return array( 'Do you really want to remove this tag: "%s"?' => 'Tem a certeza que pretende remover esta etiqueta: "%s"?', 'Global tags' => 'Etiquetas globais', 'There is no global tag at the moment.' => 'De momento não existe nenhuma etiqueta global.', - // 'This field cannot be empty' => '', - //'Hide tasks in this column in the Dashboard' => '', + 'This field cannot be empty' => 'Este campo não pode ficar vazio', + 'Close a task when there is no activity in an specific column' => 'Fechar tarefa quando não houver actividade numa coluna especifica', + '%s removed a subtask for the task #%d' => '%s removeu uma sub-tarefa da tarefa #%d', + '%s removed a comment on the task #%d' => '%s removeu um comentário da tarefa #%d ', + 'Comment removed on task #%d' => 'Comentário removido da tarefa #%d', + 'Subtask removed on task #%d' => 'Sub-tarefa removida da tarefa #%d', + 'Hide tasks in this column in the dashboard' => 'Esconder do meu painel tarefas nesta coluna', + '%s removed a comment on the task %s' => '%s removeu um comentário da tarefa %s', + '%s removed a subtask for the task %s' => '%s removeu uma sub-tarefa da tarefa %s', + 'Comment removed' => 'Comentário removido', + 'Subtask removed' => 'Sub-tarefa removida', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index fe950172..b3682f03 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -1216,5 +1216,14 @@ return array( 'Global tags' => 'Глобальные метка', 'There is no global tag at the moment.' => 'Нет глобальных меток.', 'This field cannot be empty' => 'Это поле не может быть пустым', - 'Hide tasks in this column in the Dashboard' => 'Не показывать задачи из этой колонки в кабинете', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + 'Hide tasks in this column in the dashboard' => 'Не показывать задачи из этой колонки в кабинете', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 7aec7142..157d9e2d 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index bce1ccfb..e42a801d 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 48a0b3de..56adbdb8 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index b25ef122..4f4c84cd 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index d000b706..01eaff17 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Model/ColumnModel.php b/app/Model/ColumnModel.php index 0a9c55a8..5498ef54 100644 --- a/app/Model/ColumnModel.php +++ b/app/Model/ColumnModel.php @@ -138,11 +138,12 @@ class ColumnModel extends Base * Add a new column to the board * * @access public - * @param integer $project_id Project id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Column description - * @return boolean|integer + * @param integer $project_id Project id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Column description + * @param integer $hide_in_dashboard + * @return bool|int */ public function create($project_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) { @@ -166,6 +167,7 @@ class ColumnModel extends Base * @param string $title Column title * @param integer $task_limit Task limit * @param string $description Optional description + * @param integer $hide_in_dashboard * @return boolean */ public function update($column_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) diff --git a/app/Model/CommentModel.php b/app/Model/CommentModel.php index 4231f29d..a9e48bd3 100644 --- a/app/Model/CommentModel.php +++ b/app/Model/CommentModel.php @@ -2,7 +2,6 @@ namespace Kanboard\Model; -use Kanboard\Event\CommentEvent; use Kanboard\Core\Base; /** @@ -27,6 +26,7 @@ class CommentModel extends Base */ const EVENT_UPDATE = 'comment.update'; const EVENT_CREATE = 'comment.create'; + const EVENT_DELETE = 'comment.delete'; const EVENT_USER_MENTION = 'comment.user.mention'; /** @@ -130,9 +130,7 @@ class CommentModel extends Base $comment_id = $this->db->table(self::TABLE)->persist($values); if ($comment_id !== false) { - $event = new CommentEvent(array('id' => $comment_id) + $values); - $this->dispatcher->dispatch(self::EVENT_CREATE, $event); - $this->userMentionModel->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); + $this->queueManager->push($this->commentEventJob->withParams($comment_id, self::EVENT_CREATE)); } return $comment_id; @@ -153,7 +151,7 @@ class CommentModel extends Base ->update(array('comment' => $values['comment'])); if ($result) { - $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values)); + $this->queueManager->push($this->commentEventJob->withParams($values['id'], self::EVENT_UPDATE)); } return $result; @@ -168,6 +166,7 @@ class CommentModel extends Base */ public function remove($comment_id) { + $this->commentEventJob->execute($comment_id, self::EVENT_DELETE); return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove(); } } diff --git a/app/Model/FileModel.php b/app/Model/FileModel.php index 8cdea9a0..98032f9d 100644 --- a/app/Model/FileModel.php +++ b/app/Model/FileModel.php @@ -5,7 +5,6 @@ namespace Kanboard\Model; use Exception; use Kanboard\Core\Base; use Kanboard\Core\Thumbnail; -use Kanboard\Event\FileEvent; use Kanboard\Core\ObjectStorage\ObjectStorageException; /** @@ -44,13 +43,13 @@ abstract class FileModel extends Base abstract protected function getPathPrefix(); /** - * Get event name + * Fire file creation event * * @abstract * @access protected - * @return string + * @param integer $file_id */ - abstract protected function getEventName(); + abstract protected function fireCreationEvent($file_id); /** * Get PicoDb query to get all files @@ -130,16 +129,16 @@ abstract class FileModel extends Base * Create a file entry in the database * * @access public - * @param integer $id Foreign key - * @param string $name Filename - * @param string $path Path on the disk - * @param integer $size File size + * @param integer $foreign_key_id Foreign key + * @param string $name Filename + * @param string $path Path on the disk + * @param integer $size File size * @return bool|integer */ - public function create($id, $name, $path, $size) + public function create($foreign_key_id, $name, $path, $size) { $values = array( - $this->getForeignKey() => $id, + $this->getForeignKey() => $foreign_key_id, 'name' => substr($name, 0, 255), 'path' => $path, 'is_image' => $this->isImage($name) ? 1 : 0, @@ -152,8 +151,7 @@ abstract class FileModel extends Base if ($result) { $file_id = (int) $this->db->getLastId(); - $event = new FileEvent($values + array('file_id' => $file_id)); - $this->dispatcher->dispatch($this->getEventName(), $event); + $this->fireCreationEvent($file_id); return $file_id; } diff --git a/app/Model/GroupModel.php b/app/Model/GroupModel.php index 0a975570..b43423b3 100644 --- a/app/Model/GroupModel.php +++ b/app/Model/GroupModel.php @@ -116,4 +116,23 @@ class GroupModel extends Base { return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); } + + /** + * Get groupId from externalGroupId and create the group if not found + * + * @access public + * @param string $name + * @param string $external_id + * @return bool|integer + */ + public function getOrCreateExternalGroupId($name, $external_id) + { + $group_id = $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOneColumn('id'); + + if (empty($group_id)) { + $group_id = $this->create($name, $external_id); + } + + return $group_id; + } } diff --git a/app/Model/NotificationModel.php b/app/Model/NotificationModel.php index 4d697b5e..925d646e 100644 --- a/app/Model/NotificationModel.php +++ b/app/Model/NotificationModel.php @@ -70,10 +70,14 @@ class NotificationModel extends Base return e('%s updated a subtask for the task #%d', $event_author, $event_data['task']['id']); case SubtaskModel::EVENT_CREATE: return e('%s created a subtask for the task #%d', $event_author, $event_data['task']['id']); + case SubtaskModel::EVENT_DELETE: + return e('%s removed a subtask for the task #%d', $event_author, $event_data['task']['id']); case CommentModel::EVENT_UPDATE: return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']); case CommentModel::EVENT_CREATE: return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); + case CommentModel::EVENT_DELETE: + return e('%s removed a comment on the task #%d', $event_author, $event_data['task']['id']); case TaskFileModel::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); case TaskModel::EVENT_USER_MENTION: @@ -102,10 +106,14 @@ class NotificationModel extends Base return e('New comment on task #%d', $event_data['comment']['task_id']); case CommentModel::EVENT_UPDATE: return e('Comment updated on task #%d', $event_data['comment']['task_id']); + case CommentModel::EVENT_DELETE: + return e('Comment removed on task #%d', $event_data['comment']['task_id']); case SubtaskModel::EVENT_CREATE: return e('New subtask on task #%d', $event_data['subtask']['task_id']); case SubtaskModel::EVENT_UPDATE: return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); + case SubtaskModel::EVENT_DELETE: + return e('Subtask removed on task #%d', $event_data['subtask']['task_id']); case TaskModel::EVENT_CREATE: return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); case TaskModel::EVENT_UPDATE: @@ -149,9 +157,11 @@ class NotificationModel extends Base return $event_data['file']['task_id']; case CommentModel::EVENT_CREATE: case CommentModel::EVENT_UPDATE: + case CommentModel::EVENT_DELETE: return $event_data['comment']['task_id']; case SubtaskModel::EVENT_CREATE: case SubtaskModel::EVENT_UPDATE: + case SubtaskModel::EVENT_DELETE: return $event_data['subtask']['task_id']; case TaskModel::EVENT_CREATE: case TaskModel::EVENT_UPDATE: diff --git a/app/Model/ProjectFileModel.php b/app/Model/ProjectFileModel.php index b464bb2a..4de4d66d 100644 --- a/app/Model/ProjectFileModel.php +++ b/app/Model/ProjectFileModel.php @@ -61,14 +61,13 @@ class ProjectFileModel extends FileModel } /** - * Get event name + * Fire file creation event * - * @abstract * @access protected - * @return string + * @param integer $file_id */ - protected function getEventName() + protected function fireCreationEvent($file_id) { - return self::EVENT_CREATE; + $this->queueManager->push($this->projectFileEventJob->withParams($file_id, self::EVENT_CREATE)); } } diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index a97bddbf..f3fc72ba 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -4,7 +4,6 @@ namespace Kanboard\Model; use PicoDb\Database; use Kanboard\Core\Base; -use Kanboard\Event\SubtaskEvent; /** * Subtask Model @@ -66,7 +65,7 @@ class SubtaskModel extends Base ->join(TaskModel::TABLE, 'id', 'task_id') ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; } - + /** * Get available status * @@ -235,10 +234,7 @@ class SubtaskModel extends Base $subtask_id = $this->db->table(self::TABLE)->persist($values); if ($subtask_id !== false) { - $this->container['dispatcher']->dispatch( - self::EVENT_CREATE, - new SubtaskEvent(array('id' => $subtask_id) + $values) - ); + $this->queueManager->push($this->subtaskEventJob->withParams($subtask_id, self::EVENT_CREATE)); } return $subtask_id; @@ -255,13 +251,10 @@ class SubtaskModel extends Base public function update(array $values, $fire_events = true) { $this->prepare($values); - $subtask = $this->getById($values['id']); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); if ($result && $fire_events) { - $event = $subtask; - $event['changes'] = array_diff_assoc($values, $subtask); - $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new SubtaskEvent($event)); + $this->queueManager->push($this->subtaskEventJob->withParams($values['id'], self::EVENT_UPDATE, $values)); } return $result; @@ -377,14 +370,8 @@ class SubtaskModel extends Base */ public function remove($subtask_id) { - $subtask = $this->getById($subtask_id); - $result = $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); - - if ($result) { - $this->container['dispatcher']->dispatch(self::EVENT_DELETE, new SubtaskEvent($subtask)); - } - - return $result; + $this->subtaskEventJob->execute($subtask_id, self::EVENT_DELETE); + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); } /** diff --git a/app/Model/TaskCreationModel.php b/app/Model/TaskCreationModel.php index cd70a028..1c0fd7d9 100644 --- a/app/Model/TaskCreationModel.php +++ b/app/Model/TaskCreationModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Creation @@ -42,7 +41,10 @@ class TaskCreationModel extends Base $this->taskTagModel->save($values['project_id'], $task_id, $tags); } - $this->fireEvents($task_id, $values); + $this->queueManager->push($this->taskEventJob->withParams( + $task_id, + array(TaskModel::EVENT_CREATE_UPDATE, TaskModel::EVENT_CREATE) + )); } return (int) $task_id; @@ -51,10 +53,10 @@ class TaskCreationModel extends Base /** * Prepare data * - * @access public + * @access protected * @param array $values Form values */ - public function prepare(array &$values) + protected function prepare(array &$values) { $values = $this->dateParser->convert($values, array('date_due')); $values = $this->dateParser->convert($values, array('date_started'), true); @@ -84,26 +86,4 @@ class TaskCreationModel extends Base $values['date_moved'] = $values['date_creation']; $values['position'] = $this->taskFinderModel->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; } - - /** - * Fire events - * - * @access private - * @param integer $task_id Task id - * @param array $values Form values - */ - private function fireEvents($task_id, array $values) - { - $event = new TaskEvent(array('task_id' => $task_id) + $values); - - $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE_UPDATE); - $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE); - - $this->dispatcher->dispatch(TaskModel::EVENT_CREATE_UPDATE, $event); - $this->dispatcher->dispatch(TaskModel::EVENT_CREATE, $event); - - if (! empty($values['description'])) { - $this->userMentionModel->fireEvents($values['description'], TaskModel::EVENT_USER_MENTION, $event); - } - } } diff --git a/app/Model/TaskFileModel.php b/app/Model/TaskFileModel.php index 7603019a..0163da28 100644 --- a/app/Model/TaskFileModel.php +++ b/app/Model/TaskFileModel.php @@ -61,18 +61,6 @@ class TaskFileModel extends FileModel } /** - * Get event name - * - * @abstract - * @access protected - * @return string - */ - protected function getEventName() - { - return self::EVENT_CREATE; - } - - /** * Get projectId from fileId * * @access public @@ -101,4 +89,15 @@ class TaskFileModel extends FileModel $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png'; return $this->uploadContent($task_id, $original_filename, $blob); } + + /** + * Fire file creation event + * + * @access protected + * @param integer $file_id + */ + protected function fireCreationEvent($file_id) + { + $this->queueManager->push($this->taskFileEventJob->withParams($file_id, self::EVENT_CREATE)); + } } diff --git a/app/Model/TaskModificationModel.php b/app/Model/TaskModificationModel.php index be5f53c8..16b48f3d 100644 --- a/app/Model/TaskModificationModel.php +++ b/app/Model/TaskModificationModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Modification @@ -23,14 +22,14 @@ class TaskModificationModel extends Base */ public function update(array $values, $fire_events = true) { - $original_task = $this->taskFinderModel->getById($values['id']); + $task = $this->taskFinderModel->getById($values['id']); - $this->updateTags($values, $original_task); + $this->updateTags($values, $task); $this->prepare($values); - $result = $this->db->table(TaskModel::TABLE)->eq('id', $original_task['id'])->update($values); + $result = $this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values); if ($fire_events && $result) { - $this->fireEvents($original_task, $values); + $this->fireEvents($task, $values); } return $result; @@ -39,43 +38,56 @@ class TaskModificationModel extends Base /** * Fire events * - * @access public - * @param array $task - * @param array $new_values + * @access protected + * @param array $task + * @param array $changes */ - public function fireEvents(array $task, array $new_values) + protected function fireEvents(array $task, array $changes) { $events = array(); - $event_data = array_merge($task, $new_values, array('task_id' => $task['id'])); - // Values changed - $event_data['changes'] = array_diff_assoc($new_values, $task); - unset($event_data['changes']['date_modification']); - - if ($this->isFieldModified('owner_id', $event_data['changes'])) { + if ($this->isAssigneeChanged($task, $changes)) { $events[] = TaskModel::EVENT_ASSIGNEE_CHANGE; - } elseif (! empty($event_data['changes'])) { + } elseif ($this->isModified($task, $changes)) { $events[] = TaskModel::EVENT_CREATE_UPDATE; $events[] = TaskModel::EVENT_UPDATE; } - foreach ($events as $event) { - $this->logger->debug('Event fired: '.$event); - $this->dispatcher->dispatch($event, new TaskEvent($event_data)); + if (! empty($events)) { + $this->queueManager->push($this->taskEventJob + ->withParams($task['id'], $events, $changes, array(), $task) + ); } } /** + * Return true if the task have been modified + * + * @access protected + * @param array $task + * @param array $changes + * @return bool + */ + protected function isModified(array $task, array $changes) + { + $diff = array_diff_assoc($changes, $task); + unset($diff['date_modification']); + return count($diff) > 0; + } + + /** * Return true if the field is the only modified value * - * @access public - * @param string $field - * @param array $changes - * @return boolean + * @access protected + * @param array $task + * @param array $changes + * @return bool */ - public function isFieldModified($field, array $changes) + protected function isAssigneeChanged(array $task, array $changes) { - return isset($changes[$field]) && count($changes) === 1; + $diff = array_diff_assoc($changes, $task); + unset($diff['date_modification']); + return isset($changes['owner_id']) && $task['owner_id'] != $changes['owner_id'] && count($diff) === 1; } /** diff --git a/app/Model/TaskPositionModel.php b/app/Model/TaskPositionModel.php index 9fdb8f7d..d6d2a0af 100644 --- a/app/Model/TaskPositionModel.php +++ b/app/Model/TaskPositionModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Position @@ -212,8 +211,7 @@ class TaskPositionModel extends Base */ private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id) { - $event_data = array( - 'task_id' => $task['id'], + $changes = array( 'project_id' => $task['project_id'], 'position' => $new_position, 'column_id' => $new_column_id, @@ -226,14 +224,26 @@ class TaskPositionModel extends Base ); if ($task['swimlane_id'] != $new_swimlane_id) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_SWIMLANE); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_SWIMLANE), + $changes, + $changes + )); } elseif ($task['column_id'] != $new_column_id) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_COLUMN); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_COLUMN), + $changes, + $changes + )); } elseif ($task['position'] != $new_position) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_POSITION); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_POSITION, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_POSITION), + $changes, + $changes + )); } } } diff --git a/app/Model/TaskProjectMoveModel.php b/app/Model/TaskProjectMoveModel.php index eda23c0b..ae3ae084 100644 --- a/app/Model/TaskProjectMoveModel.php +++ b/app/Model/TaskProjectMoveModel.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use Kanboard\Event\TaskEvent; - /** * Task Project Move * @@ -32,9 +30,8 @@ class TaskProjectMoveModel extends TaskDuplicationModel $this->checkDestinationProjectValues($values); $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id); - if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) { - $event = new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_PROJECT, $event); + if ($this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update($values)) { + $this->queueManager->push($this->taskEventJob->withParams($task_id, array(TaskModel::EVENT_MOVE_PROJECT), $values)); } return true; diff --git a/app/Model/TaskStatusModel.php b/app/Model/TaskStatusModel.php index 4d573f0e..ea304beb 100644 --- a/app/Model/TaskStatusModel.php +++ b/app/Model/TaskStatusModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Status @@ -101,10 +100,10 @@ class TaskStatusModel extends Base * @param integer $task_id Task id * @param integer $status Task status * @param integer $date_completed Timestamp - * @param string $event Event name + * @param string $event_name Event name * @return boolean */ - private function changeStatus($task_id, $status, $date_completed, $event) + private function changeStatus($task_id, $status, $date_completed, $event_name) { if (! $this->taskFinderModel->exists($task_id)) { return false; @@ -120,8 +119,7 @@ class TaskStatusModel extends Base )); if ($result) { - $this->logger->debug('Event fired: '.$event); - $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinderModel->getById($task_id))); + $this->queueManager->push($this->taskEventJob->withParams($task_id, array($event_name))); } return $result; diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql index 8d2f2dc0..8d494dcf 100644 --- a/app/Schema/Sql/mysql.sql +++ b/app/Schema/Sql/mysql.sql @@ -44,8 +44,8 @@ CREATE TABLE `columns` ( `position` int(11) NOT NULL, `project_id` int(11) NOT NULL, `task_limit` int(11) DEFAULT '0', - `hide_in_dashboard` int(11) DEFAULT '0', `description` text, + `hide_in_dashboard` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_title_project` (`title`,`project_id`), KEY `columns_project_idx` (`project_id`), @@ -671,7 +671,7 @@ CREATE TABLE `users` ( LOCK TABLES `settings` WRITE; /*!40000 ALTER TABLE `settings` DISABLE KEYS */; -INSERT INTO `settings` VALUES ('api_token','19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','1d62395a742260738a406789366a84138ced50a1be62e8862c5cf8d0a561',0,0),('webhook_url','',0,0); +INSERT INTO `settings` VALUES ('api_token','4064ef3d26efa9a0ff78fa7067d8bb9d99323455128edd89e9dc7c53ed76',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','c8f53c0bcd8aead902ad04f180ffafd7889b9c0062c2d510e2297ef543b8',0,0),('webhook_url','',0,0); /*!40000 ALTER TABLE `settings` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -700,4 +700,4 @@ UNLOCK TABLES; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$Kv6fus67I/ZG/3LYJ7bRLeis8bk8455Lwtu12ElgnGm3lhRs/z7Ni', 'app-admin');INSERT INTO schema_version VALUES ('111'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$yUJ9QnhG.f47yO.YvWKo3eMAHULukpluDNTOF9.Z7QQg0vOfFRB6u', 'app-admin');INSERT INTO schema_version VALUES ('112'); diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql index ae8b4fd5..0add9c91 100644 --- a/app/Schema/Sql/postgres.sql +++ b/app/Schema/Sql/postgres.sql @@ -98,8 +98,8 @@ CREATE TABLE "columns" ( "position" integer, "project_id" integer NOT NULL, "task_limit" integer DEFAULT 0, - "hide_in_dashboard" integer DEFAULT 0, - "description" "text" + "description" "text", + "hide_in_dashboard" boolean DEFAULT false ); @@ -2243,7 +2243,8 @@ INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_high INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_public_refresh_interval', '60', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_private_refresh_interval', '10', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_columns', '', 0, 0); -INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', '1aff324d30632aaed0d4f4dc1281be0d5bbc7b4fcddccc4badcd6c8f3d43', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', 'c9a7c2a4523f1724b2ca047c5685f8e2b26bba47eb69baf4f22d5d50d837', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', 'c57a6cb1789269547b616454e4e2f06d3de0514f83baf8fa5b5a8af44a08', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_language', 'en_US', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_timezone', 'UTC', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_url', '', 0, 0); @@ -2261,7 +2262,6 @@ INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('default_co INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('subtask_time_tracking', '1', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('cfd_include_closed_tasks', '1', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('password_reset', '1', 0, 0); -INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', '19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929', 0, 0); -- @@ -2313,4 +2313,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true); -- PostgreSQL database dump complete -- -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$Kv6fus67I/ZG/3LYJ7bRLeis8bk8455Lwtu12ElgnGm3lhRs/z7Ni', 'app-admin');INSERT INTO schema_version VALUES ('90'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$yUJ9QnhG.f47yO.YvWKo3eMAHULukpluDNTOF9.Z7QQg0vOfFRB6u', 'app-admin');INSERT INTO schema_version VALUES ('91'); diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php index 34202052..9383be12 100644 --- a/app/ServiceProvider/ActionProvider.php +++ b/app/ServiceProvider/ActionProvider.php @@ -32,6 +32,7 @@ use Kanboard\Action\TaskMoveColumnUnAssigned; use Kanboard\Action\TaskOpen; use Kanboard\Action\TaskUpdateStartDate; use Kanboard\Action\TaskCloseNoActivity; +use Kanboard\Action\TaskCloseNoActivityColumn; /** * Action Provider @@ -68,6 +69,7 @@ class ActionProvider implements ServiceProviderInterface $container['actionManager']->register(new TaskClose($container)); $container['actionManager']->register(new TaskCloseColumn($container)); $container['actionManager']->register(new TaskCloseNoActivity($container)); + $container['actionManager']->register(new TaskCloseNoActivityColumn($container)); $container['actionManager']->register(new TaskCreation($container)); $container['actionManager']->register(new TaskDuplicateAnotherProject($container)); $container['actionManager']->register(new TaskEmail($container)); diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php index 20281a09..436288dc 100644 --- a/app/ServiceProvider/FilterProvider.php +++ b/app/ServiceProvider/FilterProvider.php @@ -21,6 +21,7 @@ use Kanboard\Filter\TaskDueDateFilter; use Kanboard\Filter\TaskIdFilter; use Kanboard\Filter\TaskLinkFilter; use Kanboard\Filter\TaskModificationDateFilter; +use Kanboard\Filter\TaskPriorityFilter; use Kanboard\Filter\TaskProjectFilter; use Kanboard\Filter\TaskReferenceFilter; use Kanboard\Filter\TaskStatusFilter; @@ -137,6 +138,7 @@ class FilterProvider implements ServiceProviderInterface ->withFilter(TaskColorFilter::getInstance() ->setColorModel($c['colorModel']) ) + ->withFilter(new TaskPriorityFilter()) ->withFilter(new TaskColumnFilter()) ->withFilter(new TaskCommentFilter()) ->withFilter(TaskCreationDateFilter::getInstance() diff --git a/app/ServiceProvider/JobProvider.php b/app/ServiceProvider/JobProvider.php new file mode 100644 index 00000000..c7f323f1 --- /dev/null +++ b/app/ServiceProvider/JobProvider.php @@ -0,0 +1,57 @@ +<?php + +namespace Kanboard\ServiceProvider; + +use Kanboard\Job\CommentEventJob; +use Kanboard\Job\NotificationJob; +use Kanboard\Job\ProjectFileEventJob; +use Kanboard\Job\SubtaskEventJob; +use Kanboard\Job\TaskEventJob; +use Kanboard\Job\TaskFileEventJob; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class JobProvider + * + * @package Kanboard\ServiceProvider + * @author Frederic Guillot + */ +class JobProvider implements ServiceProviderInterface +{ + /** + * Register providers + * + * @access public + * @param \Pimple\Container $container + * @return \Pimple\Container + */ + public function register(Container $container) + { + $container['commentEventJob'] = $container->factory(function ($c) { + return new CommentEventJob($c); + }); + + $container['subtaskEventJob'] = $container->factory(function ($c) { + return new SubtaskEventJob($c); + }); + + $container['taskEventJob'] = $container->factory(function ($c) { + return new TaskEventJob($c); + }); + + $container['taskFileEventJob'] = $container->factory(function ($c) { + return new TaskFileEventJob($c); + }); + + $container['projectFileEventJob'] = $container->factory(function ($c) { + return new ProjectFileEventJob($c); + }); + + $container['notificationJob'] = $container->factory(function ($c) { + return new NotificationJob($c); + }); + + return $container; + } +} diff --git a/app/ServiceProvider/QueueProvider.php b/app/ServiceProvider/QueueProvider.php index 946b436a..570f2e77 100644 --- a/app/ServiceProvider/QueueProvider.php +++ b/app/ServiceProvider/QueueProvider.php @@ -15,9 +15,11 @@ use Pimple\ServiceProviderInterface; class QueueProvider implements ServiceProviderInterface { /** - * Registers services on the given container. + * Register providers * - * @param Container $container + * @access public + * @param \Pimple\Container $container + * @return \Pimple\Container */ public function register(Container $container) { diff --git a/app/Subscriber/BaseSubscriber.php b/app/Subscriber/BaseSubscriber.php index fdea29f6..92441962 100644 --- a/app/Subscriber/BaseSubscriber.php +++ b/app/Subscriber/BaseSubscriber.php @@ -12,28 +12,4 @@ use Kanboard\Core\Base; */ class BaseSubscriber extends Base { - /** - * Method called - * - * @access private - * @var array - */ - private $called = array(); - - /** - * Check if a listener has been executed - * - * @access public - * @param string $key - * @return boolean - */ - public function isExecuted($key = '') - { - if (isset($this->called[$key])) { - return true; - } - - $this->called[$key] = true; - return false; - } } diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php index db11e585..7de24e49 100644 --- a/app/Subscriber/NotificationSubscriber.php +++ b/app/Subscriber/NotificationSubscriber.php @@ -3,7 +3,6 @@ namespace Kanboard\Subscriber; use Kanboard\Event\GenericEvent; -use Kanboard\Job\NotificationJob; use Kanboard\Model\TaskModel; use Kanboard\Model\CommentModel; use Kanboard\Model\SubtaskModel; @@ -26,8 +25,10 @@ class NotificationSubscriber extends BaseSubscriber implements EventSubscriberIn TaskModel::EVENT_ASSIGNEE_CHANGE => 'handleEvent', SubtaskModel::EVENT_CREATE => 'handleEvent', SubtaskModel::EVENT_UPDATE => 'handleEvent', + SubtaskModel::EVENT_DELETE => 'handleEvent', CommentModel::EVENT_CREATE => 'handleEvent', CommentModel::EVENT_UPDATE => 'handleEvent', + CommentModel::EVENT_DELETE => 'handleEvent', CommentModel::EVENT_USER_MENTION => 'handleEvent', TaskFileModel::EVENT_CREATE => 'handleEvent', ); @@ -35,12 +36,7 @@ class NotificationSubscriber extends BaseSubscriber implements EventSubscriberIn public function handleEvent(GenericEvent $event, $eventName) { - if (!$this->isExecuted($eventName)) { - $this->logger->debug('Subscriber executed: ' . __METHOD__); - - $this->queueManager->push(NotificationJob::getInstance($this->container) - ->withParams($event, $eventName, get_class($event)) - ); - } + $this->logger->debug('Subscriber executed: ' . __METHOD__); + $this->queueManager->push($this->notificationJob->withParams($event, $eventName)); } } diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php index 6971a121..7e3c11c3 100644 --- a/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/app/Subscriber/ProjectDailySummarySubscriber.php @@ -22,7 +22,7 @@ class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubsc public function execute(TaskEvent $event) { - if (isset($event['project_id']) && !$this->isExecuted()) { + if (isset($event['project_id'])) { $this->logger->debug('Subscriber executed: '.__METHOD__); $this->queueManager->push(ProjectMetricJob::getInstance($this->container)->withParams($event['project_id'])); } diff --git a/app/Subscriber/ProjectModificationDateSubscriber.php b/app/Subscriber/ProjectModificationDateSubscriber.php index fee04eaa..1ffe0248 100644 --- a/app/Subscriber/ProjectModificationDateSubscriber.php +++ b/app/Subscriber/ProjectModificationDateSubscriber.php @@ -24,9 +24,7 @@ class ProjectModificationDateSubscriber extends BaseSubscriber implements EventS public function execute(GenericEvent $event) { - if (isset($event['project_id']) && !$this->isExecuted()) { - $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->projectModel->updateModificationDate($event['project_id']); - } + $this->logger->debug('Subscriber executed: '.__METHOD__); + $this->projectModel->updateModificationDate($event['task']['project_id']); } } diff --git a/app/Template/column/create.php b/app/Template/column/create.php index 387b6a47..812e9139 100644 --- a/app/Template/column/create.php +++ b/app/Template/column/create.php @@ -13,7 +13,7 @@ <?= $this->form->label(t('Task limit'), 'task_limit') ?> <?= $this->form->number('task_limit', $values, $errors) ?> - <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the Dashboard'), 1) ?> + <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1) ?> <?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php index abd70119..89487298 100644 --- a/app/Template/column/edit.php +++ b/app/Template/column/edit.php @@ -15,7 +15,7 @@ <?= $this->form->label(t('Task limit'), 'task_limit') ?> <?= $this->form->number('task_limit', $values, $errors) ?> - <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the Dashboard'), 1, $values['hide_in_dashboard'] == 1) ?> + <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1, $values['hide_in_dashboard'] == 1) ?> <?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> diff --git a/app/Template/config/about.php b/app/Template/config/about.php index 8e2d1325..8d5a575d 100644 --- a/app/Template/config/about.php +++ b/app/Template/config/about.php @@ -9,7 +9,7 @@ </li> <li> <?= t('Author:') ?> - <strong>Frédéric Guillot</strong> (<a href="https://github.com/fguillot/kanboard/blob/master/CONTRIBUTORS.md" target="_blank"><?= t('contributors') ?></a>) + <strong>Frédéric Guillot</strong> (<a href="https://github.com/kanboard/kanboard/blob/master/CONTRIBUTORS.md" target="_blank"><?= t('contributors') ?></a>) </li> <li> <?= t('License:') ?> diff --git a/app/Template/dashboard/tasks.php b/app/Template/dashboard/tasks.php index 4b83a96a..b3257c33 100644 --- a/app/Template/dashboard/tasks.php +++ b/app/Template/dashboard/tasks.php @@ -9,7 +9,7 @@ <th class="column-5"><?= $paginator->order('Id', 'tasks.id') ?></th> <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> <th><?= $paginator->order(t('Task'), 'title') ?></th> - <th class="column-5"><?= $paginator->order('Priority', 'tasks.priority') ?></th> + <th class="column-5"><?= $paginator->order(t('Priority'), 'tasks.priority') ?></th> <th class="column-20"><?= t('Time tracking') ?></th> <th class="column-10"><?= $paginator->order(t('Due date'), 'date_due') ?></th> <th class="column-10"><?= $paginator->order(t('Column'), 'column_title') ?></th> diff --git a/app/Template/event/comment_delete.php b/app/Template/event/comment_delete.php new file mode 100644 index 00000000..ead7d56a --- /dev/null +++ b/app/Template/event/comment_delete.php @@ -0,0 +1,11 @@ +<p class="activity-title"> + <?= e('%s removed a comment on the task %s', + $this->text->e($author), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> + <span class="activity-date"><?= $this->dt->datetime($date_creation) ?></span> +</p> +<div class="activity-description"> + <p class="activity-task-title"><?= $this->text->e($task['title']) ?></p> + <div class="markdown"><?= $this->text->markdown($comment['comment']) ?></div> +</div> diff --git a/app/Template/event/comment_update.php b/app/Template/event/comment_update.php index 5a0821bd..5be598ac 100644 --- a/app/Template/event/comment_update.php +++ b/app/Template/event/comment_update.php @@ -7,4 +7,7 @@ </p> <div class="activity-description"> <p class="activity-task-title"><?= $this->text->e($task['title']) ?></p> + <?php if (! empty($comment['comment'])): ?> + <div class="markdown"><?= $this->text->markdown($comment['comment']) ?></div> + <?php endif ?> </div> diff --git a/app/Template/event/subtask_delete.php b/app/Template/event/subtask_delete.php new file mode 100644 index 00000000..8ac11853 --- /dev/null +++ b/app/Template/event/subtask_delete.php @@ -0,0 +1,15 @@ +<p class="activity-title"> + <?= e('%s removed a subtask for the task %s', + $this->text->e($author), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> + <span class="activity-date"><?= $this->dt->datetime($date_creation) ?></span> +</p> +<div class="activity-description"> + <p class="activity-task-title"><?= $this->text->e($task['title']) ?></p> + <ul> + <li> + <?= $this->text->e($subtask['title']) ?> (<strong><?= $this->text->e($subtask['status_name']) ?></strong>) + </li> + </ul> +</div> diff --git a/app/Template/notification/comment_delete.php b/app/Template/notification/comment_delete.php new file mode 100644 index 00000000..928623ec --- /dev/null +++ b/app/Template/notification/comment_delete.php @@ -0,0 +1,7 @@ +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Comment removed') ?></h3> + +<?= $this->text->markdown($comment['comment']) ?> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> diff --git a/app/Template/notification/subtask_delete.php b/app/Template/notification/subtask_delete.php new file mode 100644 index 00000000..8c5f262c --- /dev/null +++ b/app/Template/notification/subtask_delete.php @@ -0,0 +1,11 @@ +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Subtask removed') ?></h3> + +<ul> + <li><?= t('Title:') ?> <?= $this->text->e($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= $this->text->e($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= $this->text->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> +</ul> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> diff --git a/app/Template/project_view/show.php b/app/Template/project_view/show.php index 0ad8852b..667a576c 100644 --- a/app/Template/project_view/show.php +++ b/app/Template/project_view/show.php @@ -57,7 +57,7 @@ <th class="column-40"><?= t('Column') ?></th> <th class="column-20"><?= t('Task limit') ?></th> <th class="column-20"><?= t('Active tasks') ?></th> - <th class="column-20"><?= t('Hide tasks in this column in the Dashboard') ?></th> + <th class="column-20"><?= t('Hide tasks in this column in the dashboard') ?></th> </tr> <?php foreach ($stats['columns'] as $column): ?> <tr> diff --git a/app/Template/user_modification/show.php b/app/Template/user_modification/show.php index 396d550d..506c9161 100644 --- a/app/Template/user_modification/show.php +++ b/app/Template/user_modification/show.php @@ -11,16 +11,16 @@ <?= $this->form->text('username', $values, $errors, array('required', isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?> <?= $this->form->label(t('Name'), 'name') ?> - <?= $this->form->text('name', $values, $errors) ?> + <?= $this->form->text('name', $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_name') ? '' : 'readonly')) ?> <?= $this->form->label(t('Email'), 'email') ?> - <?= $this->form->email('email', $values, $errors) ?> + <?= $this->form->email('email', $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_email') ? '' : 'readonly')) ?> <?= $this->form->label(t('Timezone'), 'timezone') ?> - <?= $this->form->select('timezone', $timezones, $values, $errors) ?> + <?= $this->form->select('timezone', $timezones, $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_timezone') ? '' : 'disabled')) ?> <?= $this->form->label(t('Language'), 'language') ?> - <?= $this->form->select('language', $languages, $values, $errors) ?> + <?= $this->form->select('language', $languages, $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_language') ? '' : 'disabled')) ?> <?php if ($this->user->isAdmin()): ?> <?= $this->form->label(t('Role'), 'role') ?> diff --git a/app/Template/user_view/sidebar.php b/app/Template/user_view/sidebar.php index d200a7f5..3dc6b7bc 100644 --- a/app/Template/user_view/sidebar.php +++ b/app/Template/user_view/sidebar.php @@ -12,18 +12,26 @@ </li> <?php endif ?> <?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?> - <li <?= $this->app->checkMenuSelection('UserViewController', 'timesheet') ?>> - <?= $this->url->link(t('Time tracking'), 'UserViewController', 'timesheet', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'lastLogin') ?>> - <?= $this->url->link(t('Last logins'), 'UserViewController', 'lastLogin', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'sessions') ?>> - <?= $this->url->link(t('Persistent connections'), 'UserViewController', 'sessions', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'passwordReset') ?>> - <?= $this->url->link(t('Password reset history'), 'UserViewController', 'passwordReset', array('user_id' => $user['id'])) ?> - </li> + <?php if ($this->user->hasAccess('UserViewController', 'timesheet')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'timesheet') ?>> + <?= $this->url->link(t('Time tracking'), 'UserViewController', 'timesheet', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'lastLogin')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'lastLogin') ?>> + <?= $this->url->link(t('Last logins'), 'UserViewController', 'lastLogin', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'sessions')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'sessions') ?>> + <?= $this->url->link(t('Persistent connections'), 'UserViewController', 'sessions', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'passwordReset')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'passwordReset') ?>> + <?= $this->url->link(t('Password reset history'), 'UserViewController', 'passwordReset', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> <?php endif ?> <?= $this->hook->render('template:user:sidebar:information', array('user' => $user)) ?> @@ -42,13 +50,13 @@ </li> <?php endif ?> - <?php if ($user['is_ldap_user'] == 0): ?> + <?php if ($user['is_ldap_user'] == 0 && $this->user->hasAccess('UserCredentialController', 'changePassword')): ?> <li <?= $this->app->checkMenuSelection('UserCredentialController', 'changePassword') ?>> <?= $this->url->link(t('Change password'), 'UserCredentialController', 'changePassword', array('user_id' => $user['id'])) ?> </li> <?php endif ?> - <?php if ($this->user->isCurrentUser($user['id'])): ?> + <?php if ($this->user->isCurrentUser($user['id']) && $this->user->hasAccess('TwoFactorController', 'index')): ?> <li <?= $this->app->checkMenuSelection('TwoFactorController', 'index') ?>> <?= $this->url->link(t('Two factor authentication'), 'TwoFactorController', 'index', array('user_id' => $user['id'])) ?> </li> @@ -58,18 +66,26 @@ </li> <?php endif ?> - <li <?= $this->app->checkMenuSelection('UserViewController', 'share') ?>> - <?= $this->url->link(t('Public access'), 'UserViewController', 'share', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'notifications') ?>> - <?= $this->url->link(t('Notifications'), 'UserViewController', 'notifications', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'external') ?>> - <?= $this->url->link(t('External accounts'), 'UserViewController', 'external', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'integrations') ?>> - <?= $this->url->link(t('Integrations'), 'UserViewController', 'integrations', array('user_id' => $user['id'])) ?> - </li> + <?php if ($this->user->hasAccess('UserViewController', 'share')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'share') ?>> + <?= $this->url->link(t('Public access'), 'UserViewController', 'share', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'notifications')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'notifications') ?>> + <?= $this->url->link(t('Notifications'), 'UserViewController', 'notifications', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'external')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'external') ?>> + <?= $this->url->link(t('External accounts'), 'UserViewController', 'external', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'integrations')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'integrations') ?>> + <?= $this->url->link(t('Integrations'), 'UserViewController', 'integrations', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> <?php endif ?> <?php if ($this->user->hasAccess('UserCredentialController', 'changeAuthentication')): ?> diff --git a/app/User/OAuthUserProvider.php b/app/User/OAuthUserProvider.php index dec26250..e5fedcca 100644 --- a/app/User/OAuthUserProvider.php +++ b/app/User/OAuthUserProvider.php @@ -13,14 +13,6 @@ use Kanboard\Core\User\UserProviderInterface; abstract class OAuthUserProvider implements UserProviderInterface { /** - * Get external id column name - * - * @access public - * @return string - */ - abstract public function getExternalIdColumn(); - - /** * User properties * * @access protected @@ -69,7 +61,7 @@ abstract class OAuthUserProvider implements UserProviderInterface */ public function getExternalId() { - return $this->user['id']; + return isset($this->user['id']) ? $this->user['id'] : ''; } /** @@ -102,7 +94,7 @@ abstract class OAuthUserProvider implements UserProviderInterface */ public function getName() { - return $this->user['name']; + return isset($this->user['name']) ? $this->user['name'] : ''; } /** @@ -113,7 +105,7 @@ abstract class OAuthUserProvider implements UserProviderInterface */ public function getEmail() { - return $this->user['email']; + return isset($this->user['email']) ? $this->user['email'] : ''; } /** diff --git a/app/common.php b/app/common.php index 72be3603..15fd7a75 100644 --- a/app/common.php +++ b/app/common.php @@ -46,6 +46,7 @@ $container->register(new Kanboard\ServiceProvider\ActionProvider()); $container->register(new Kanboard\ServiceProvider\ExternalLinkProvider()); $container->register(new Kanboard\ServiceProvider\AvatarProvider()); $container->register(new Kanboard\ServiceProvider\FilterProvider()); +$container->register(new Kanboard\ServiceProvider\JobProvider()); $container->register(new Kanboard\ServiceProvider\QueueProvider()); $container->register(new Kanboard\ServiceProvider\ApiProvider()); $container->register(new Kanboard\ServiceProvider\CommandProvider()); diff --git a/app/constants.php b/app/constants.php index fc120692..40b88fe9 100644 --- a/app/constants.php +++ b/app/constants.php @@ -134,3 +134,5 @@ defined('HTTP_PROXY_PORT') or define('HTTP_PROXY_PORT', '3128'); defined('HTTP_PROXY_USERNAME') or define('HTTP_PROXY_USERNAME', ''); defined('HTTP_PROXY_PASSWORD') or define('HTTP_PROXY_PASSWORD', ''); defined('HTTP_VERIFY_SSL_CERTIFICATE') or define('HTTP_VERIFY_SSL_CERTIFICATE', true); + +defined('TOTP_ISSUER') or define('TOTP_ISSUER', 'Kanboard'); |