diff --git a/sources/ChangeLog b/sources/ChangeLog index 7ad0649..5ae4fb7 100644 --- a/sources/ChangeLog +++ b/sources/ChangeLog @@ -1,3 +1,80 @@ +Version 1.0.24 +-------------- + +New features: + +* Forgot Password +* Add drop-down menu on each board column title to close all tasks +* Add Malay language +* Add new API procedures for groups, roles, project permissions and to move/duplicate tasks to another project + +Improvements: + +* Avoid to send XHR request when a task has not moved after a drag and drop +* Set maximum dropzone height when the individual column scrolling is disabled +* Always show the search box in board selector +* Replace logout link by a drop-down menu +* Handle notification for group members attached to a project +* Return the highest role for a project when a user is member of multiple groups +* Show in user interface the saving state of the task +* Add drop-down menu for subtasks, categories, swimlanes, columns, custom filters, task links and groups +* Add new template hooks +* Application settings are not cached anymore in the session +* Do not check board status during task move +* Move validators to a separate namespace +* Improve and write unit tests for reports +* Reduce the number of SQL queries for project daily column stats +* Remove event subscriber to update date_moved field +* Make sure that some event subscribers are not executed multiple times +* Show rendering time of individual templates when debug mode is enabled +* Make sure that no events are fired if nothing has been modified in the task +* Make dashboard section title clickable +* Add unit tests for LastLogin + +Bug fixes: + +* Automatic action listeners were using the same instance +* Fix wrong link for category in task footer +* Unable to set currency rate with Postgres database +* Avoid automatic actions that change the color to fire subsequent events +* Unable to unassign a task from the API +* Revert back previous optimizations of TaskPosition (incompatibility with some environment) + +Version 1.0.23 +-------------- + +Breaking changes: + +* Plugin API changes for Automatic Actions +* Automatic Action to close a task doesn't have the column parameter anymore (use the action "Close a task in a specific column") +* Action name stored in the database is now the absolute class name +* Core functionalities moved to external plugins: + - Github Webhook: https://github.com/kanboard/plugin-github-webhook + - Gitlab Webhook: https://github.com/kanboard/plugin-gitlab-webhook + - Bitbucket Webhook: https://github.com/kanboard/plugin-bitbucket-webhook + +New features: + +* Added support of user mentions (@username) +* Added report to compare working hours between open and closed tasks +* Added the possibility to define custom routes from plugins +* Added new method to remove metadata + +Improvements: + +* Improve Two-Factor activation and plugin API +* Improving performance during task position change (SQL queries are 3 times faster than before) +* Do not show window scrollbars when individual column scrolling is enabled +* Automatic Actions code improvements and unit tests +* Increase action name column length in actions table + +Bug fixes: + +* Fix compatibility issue with FreeBSD for session.hash_function parameter +* Fix wrong constant name that causes a PHP error in project management section +* Fix pagination in group members listing +* Avoid PHP error when enabling LDAP group provider with PHP < 5.5 + Version 1.0.22 -------------- @@ -33,7 +110,7 @@ Version 1.0.21 Breaking changes: -* Projects with duplicate name are now allowed: +* Projects with duplicate names are now allowed: - For Postgres and Mysql the unique constraint is removed by database migration - However Sqlite does not support alter table, only new databases will have the unique constraint removed @@ -44,7 +121,7 @@ New features: Improvements: -* Dropdown menu entry are now clickable outside of the html link +* Dropdown menu entries are now clickable outside of the html link * Improve error handling of plugins * Use PHP7 function random_bytes() to generate tokens if available * CSV task export show the assignee name in addition to the assignee username @@ -92,7 +169,7 @@ Improvements: Bug fixes: * People should not see any tasks during a search when they are not associated to a project -* Avoid to disable the default swimlane during renaming when there is no other activated swimlane +* Avoid disabling the default swimlane during renaming when there is no other activated swimlane Version 1.0.19 -------------- @@ -124,15 +201,15 @@ Improvements: * Offer alternative method to create Mysql and Postgres databases (import sql dump) * Make sure there is always a trailing slash for application_url * Do not show the checkbox "Show default swimlane" when there is no active swimlanes -* Append filters instead of replacing value for users and categories dropdowns +* Append filters instead of replacing value for users and categories drop-downs * Do not show empty swimlanes in public view * Change swimlane layout to save space on the screen * Add the possibility to set/unset max column height (column scrolling) -* Show "Open this task" in dropdown menu for closed tasks +* Show "Open this task" in drop-down menu for closed tasks * Show assignee on card only when someone is assigned (hide nobody text) -* Highlight selected item in dropdown menus +* Highlight selected item in drop-down menus * Gantt chart: change bar color according to task progress -* Replace color dropdown by color picker in task forms +* Replace color drop-down by color picker in task forms * Creating another task stay in the popover (no full page refresh anymore) * Avoid scrollbar in Gantt chart for row title on Windows platform * Remove unnecessary margin for calendar header @@ -144,14 +221,14 @@ Improvements: Others: -* Data directory permissions are not checked anymore +* Data directory permission are not checked anymore * Data directory is not mandatory anymore for people that use a remote database and remote object storage Bug fixes: -* Fix typo in template that prevent the Gitlab OAuth link to be displayed +* Fix typo in template that prevents Gitlab OAuth link to be displayed * Fix Markdown preview links focus -* Avoid dropdown menu to be truncated inside a column with scrolling +* Avoid drop-down menu to be truncated inside a column with scrolling * Deleting subtask doesn't update task time tracking * Fix Mysql error about gitlab_id when creating remote user * Fix subtask timer bug (event called recursively) @@ -169,7 +246,7 @@ New features: * Add hide/show columns * Add Gantt chart for projects and tasks * Add new role "Project Administrator" -* Add login bruteforce protection with captcha and account lockdown +* Add login brute force protection with captcha and account lockdown * Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList() * Add user api access * Add config parameter to define session duration @@ -177,7 +254,7 @@ New features: * Add start/end date for projects * Add new automated action to change task color based on the task link * Add milestone marker in board task -* Add search in task title when using an integer only input +* Add search for task title when using an integer only input * Add Portuguese (European) translation * Add Norwegian translation @@ -194,16 +271,16 @@ Improvements: * Improve sidebar menus * Add no referrer policy in meta tags * Run automated unit tests with Sqlite/Mysql/Postgres on Travis-ci -* Add Makefile and remove the scripts directory +* Add Makefile and remove the "scripts" directory Bug fixes: * Wrong template name for subtasks tooltip due to previous refactoring * Fix broken url for closed tasks in project view * Fix permission issue when changing the url manually -* Fix bug task estimate is reseted when using subtask timer +* Fix bug task estimate is reset when using subtask timer * Fix screenshot feature with Firefox 40 -* Fix bug when uploading files with cyrilic characters +* Fix bug when uploading files with Cyrilic characters Version 1.0.17 -------------- @@ -217,14 +294,14 @@ New features: * Added new dashboard layout * Added new layout for board/calendar/list views * Added filters helper for search forms -* Added settings option to disable subtask timer -* Added settings option to include or exclude closed tasks into CFD -* Added settings option to define the default task color +* Added setting option to disable subtask timer +* Added setting option to include or exclude closed tasks into CFD +* Added setting option to define the default task color * Added new config option to disable automatic creation of LDAP accounts * Added loading icon on board view * Prompt user when moving or duplicate a task to another project * Added current values when moving/duplicate a task to another project and add a loading icon -* Added memory consumption in debug log +* Added memory consumption to debug log * Added form to create remote user * Added edit form for user authentication * Added config option to hide login form @@ -235,7 +312,7 @@ New features: * Added new report: Lead and cycle time for projects * Added new report: Average time spent into each column * Added task analytics -* Added icon to set automatically the start date +* Added icon to set the start date automatically * Added datetime picker for start date Improvements: @@ -244,8 +321,8 @@ Improvements: * Display user initials when tasks are in collapsed mode * Show title in tooltip for collapsed tasks * Improve alert box fadeout to avoid an empty space -* Set focus on the dropdown for category popover -* Make escape keyboard shorcut global +* Set focus on the drop-down for category popover +* Make escape keyboard shortcut global * Check the box remember me by default * Store redirect login url in session instead of using url parameter * Update Gitlab webhook @@ -272,7 +349,7 @@ Translations: Bug fixes: -* Screenshot dropdown: unexpected scroll down on the board view and focus lost when clicking on the drop zone +* Screenshot drop-down: unexpected scroll down on the board view and focus lost when clicking on the drop zone * No creator when duplicating a task * Avoid the creation of multiple subtask timer for the same task and user diff --git a/sources/LICENSE b/sources/LICENSE index dbe5032..c1efa20 100644 --- a/sources/LICENSE +++ b/sources/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2015 Frédéric Guillot +Copyright (c) 2014-2016 Frédéric Guillot Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/sources/app/Action/Base.php b/sources/app/Action/Base.php index 81e2ccc..efc52f0 100644 --- a/sources/app/Action/Base.php +++ b/sources/app/Action/Base.php @@ -3,7 +3,6 @@ namespace Kanboard\Action; use Kanboard\Event\GenericEvent; -use Pimple\Container; /** * Base class for automatic actions @@ -13,6 +12,14 @@ use Pimple\Container; */ abstract class Base extends \Kanboard\Core\Base { + /** + * Extended events + * + * @access private + * @var array + */ + private $compatibleEvents = array(); + /** * Flag for called listener * @@ -27,7 +34,7 @@ abstract class Base extends \Kanboard\Core\Base * @access private * @var integer */ - private $project_id = 0; + private $projectId = 0; /** * User parameters @@ -38,20 +45,25 @@ abstract class Base extends \Kanboard\Core\Base private $params = array(); /** - * Attached event name + * Get automatic action name * - * @access protected - * @var string + * @final + * @access public + * @return string */ - protected $event_name = ''; + final public function getName() + { + return '\\'.get_called_class(); + } /** - * Container instance + * Get automatic action description * - * @access protected - * @var \Pimple\Container + * @abstract + * @access public + * @return string */ - protected $container; + abstract public function getDescription(); /** * Execute the action @@ -99,22 +111,6 @@ abstract class Base extends \Kanboard\Core\Base */ abstract public function hasRequiredCondition(array $data); - /** - * Constructor - * - * @access public - * @param \Pimple\Container $container Container - * @param integer $project_id Project id - * @param string $event_name Attached event name - */ - public function __construct(Container $container, $project_id, $event_name) - { - $this->container = $container; - $this->project_id = $project_id; - $this->event_name = $event_name; - $this->called = false; - } - /** * Return class information * @@ -123,7 +119,25 @@ abstract class Base extends \Kanboard\Core\Base */ public function __toString() { - return get_called_class(); + $params = array(); + + foreach ($this->params as $key => $value) { + $params[] = $key.'='.var_export($value, true); + } + + return $this->getName().'('.implode('|', $params).'])'; + } + + /** + * Set project id + * + * @access public + * @return Base + */ + public function setProjectId($project_id) + { + $this->projectId = $project_id; + return $this; } /** @@ -134,7 +148,7 @@ abstract class Base extends \Kanboard\Core\Base */ public function getProjectId() { - return $this->project_id; + return $this->projectId; } /** @@ -143,10 +157,12 @@ abstract class Base extends \Kanboard\Core\Base * @access public * @param string $name Parameter name * @param mixed $value Value + * @param Base */ public function setParam($name, $value) { $this->params[$name] = $value; + return $this; } /** @@ -154,24 +170,25 @@ abstract class Base extends \Kanboard\Core\Base * * @access public * @param string $name Parameter name - * @param mixed $default_value Default value + * @param mixed $default Default value * @return mixed */ - public function getParam($name, $default_value = null) + public function getParam($name, $default = null) { - return isset($this->params[$name]) ? $this->params[$name] : $default_value; + return isset($this->params[$name]) ? $this->params[$name] : $default; } /** * Check if an action is executable (right project and required parameters) * * @access public - * @param array $data Event data dictionary - * @return bool True if the action is executable + * @param array $data + * @param string $eventName + * @return bool */ - public function isExecutable(array $data) + public function isExecutable(array $data, $eventName) { - return $this->hasCompatibleEvent() && + return $this->hasCompatibleEvent($eventName) && $this->hasRequiredProject($data) && $this->hasRequiredParameters($data) && $this->hasRequiredCondition($data); @@ -181,11 +198,12 @@ abstract class Base extends \Kanboard\Core\Base * Check if the event is compatible with the action * * @access public + * @param string $eventName * @return bool */ - public function hasCompatibleEvent() + public function hasCompatibleEvent($eventName) { - return in_array($this->event_name, $this->getCompatibleEvents()); + return in_array($eventName, $this->getEvents()); } /** @@ -197,7 +215,7 @@ abstract class Base extends \Kanboard\Core\Base */ public function hasRequiredProject(array $data) { - return isset($data['project_id']) && $data['project_id'] == $this->project_id; + return isset($data['project_id']) && $data['project_id'] == $this->getProjectId(); } /** @@ -222,10 +240,11 @@ abstract class Base extends \Kanboard\Core\Base * Execute the action * * @access public - * @param \Event\GenericEvent $event Event data dictionary - * @return bool True if the action was executed or false when not executed + * @param \Kanboard\Event\GenericEvent $event + * @param string $eventName + * @return bool */ - public function execute(GenericEvent $event) + public function execute(GenericEvent $event, $eventName) { // Avoid infinite loop, a listener instance can be called only one time if ($this->called) { @@ -233,17 +252,44 @@ abstract class Base extends \Kanboard\Core\Base } $data = $event->getAll(); - $result = false; + $executable = $this->isExecutable($data, $eventName); + $executed = false; - if ($this->isExecutable($data)) { + if ($executable) { $this->called = true; - $result = $this->doAction($data); + $executed = $this->doAction($data); } - if (DEBUG) { - $this->logger->debug(get_called_class().' => '.($result ? 'true' : 'false')); + $this->logger->debug($this.' ['.$eventName.'] => executable='.var_export($executable, true).' exec_success='.var_export($executed, true)); + + return $executed; + } + + /** + * Register a new event for the automatic action + * + * @access public + * @param string $event + * @param string $description + */ + public function addEvent($event, $description = '') + { + if ($description !== '') { + $this->eventManager->register($event, $description); } - return $result; + $this->compatibleEvents[] = $event; + return $this; + } + + /** + * Get all compatible events of an automatic action + * + * @access public + * @return array + */ + public function getEvents() + { + return array_unique(array_merge($this->getCompatibleEvents(), $this->compatibleEvents)); } } diff --git a/sources/app/Action/CommentCreation.php b/sources/app/Action/CommentCreation.php index 73fedc3..b91e39e 100644 --- a/sources/app/Action/CommentCreation.php +++ b/sources/app/Action/CommentCreation.php @@ -2,10 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\BitbucketWebhook; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\GitlabWebhook; - /** * Create automatically a comment from a webhook * @@ -14,6 +10,17 @@ use Kanboard\Integration\GitlabWebhook; */ class CommentCreation extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Create a comment from an external provider'); + } + /** * Get the list of compatible events * @@ -22,14 +29,7 @@ class CommentCreation extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_COMMENT, - GithubWebhook::EVENT_COMMIT, - BitbucketWebhook::EVENT_ISSUE_COMMENT, - BitbucketWebhook::EVENT_COMMIT, - GitlabWebhook::EVENT_COMMIT, - GitlabWebhook::EVENT_ISSUE_COMMENT, - ); + return array(); } /** @@ -67,9 +67,9 @@ class CommentCreation extends Base { return (bool) $this->comment->create(array( 'reference' => isset($data['reference']) ? $data['reference'] : '', - 'comment' => empty($data['comment']) ? $data['commit_comment'] : $data['comment'], + 'comment' => $data['comment'], 'task_id' => $data['task_id'], - 'user_id' => empty($data['user_id']) ? 0 : $data['user_id'], + 'user_id' => isset($data['user_id']) && $this->projectPermission->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0, )); } @@ -82,6 +82,6 @@ class CommentCreation extends Base */ public function hasRequiredCondition(array $data) { - return ! empty($data['comment']) || ! empty($data['commit_comment']); + return ! empty($data['comment']); } } diff --git a/sources/app/Action/TaskLogMoveAnotherColumn.php b/sources/app/Action/CommentCreationMoveTaskColumn.php similarity index 83% rename from sources/app/Action/TaskLogMoveAnotherColumn.php rename to sources/app/Action/CommentCreationMoveTaskColumn.php index a699c4a..4473cf9 100644 --- a/sources/app/Action/TaskLogMoveAnotherColumn.php +++ b/sources/app/Action/CommentCreationMoveTaskColumn.php @@ -5,13 +5,24 @@ namespace Kanboard\Action; use Kanboard\Model\Task; /** - * Add a log of the triggering event to the task description. + * Add a comment of the triggering event to the task description. * * @package action * @author Oren Ben-Kiki */ -class TaskLogMoveAnotherColumn extends Base +class CommentCreationMoveTaskColumn extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Add a comment log when moving the task between columns'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskAssignCategoryColor.php b/sources/app/Action/TaskAssignCategoryColor.php index ffa1ac2..f5085cb 100644 --- a/sources/app/Action/TaskAssignCategoryColor.php +++ b/sources/app/Action/TaskAssignCategoryColor.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignCategoryColor extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a category based on a color'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskAssignCategoryLabel.php b/sources/app/Action/TaskAssignCategoryLabel.php index 0ef474b..95fa116 100644 --- a/sources/app/Action/TaskAssignCategoryLabel.php +++ b/sources/app/Action/TaskAssignCategoryLabel.php @@ -2,8 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; - /** * Set a category automatically according to a label * @@ -12,6 +10,17 @@ use Kanboard\Integration\GithubWebhook; */ class TaskAssignCategoryLabel extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change the category based on an external label'); + } + /** * Get the list of compatible events * @@ -20,9 +29,7 @@ class TaskAssignCategoryLabel extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_LABEL_CHANGE, - ); + return array(); } /** @@ -64,7 +71,7 @@ class TaskAssignCategoryLabel extends Base { $values = array( 'id' => $data['task_id'], - 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), + 'category_id' => $this->getParam('category_id'), ); return $this->taskModification->update($values); @@ -79,6 +86,6 @@ class TaskAssignCategoryLabel extends Base */ public function hasRequiredCondition(array $data) { - return $data['label'] == $this->getParam('label'); + return $data['label'] == $this->getParam('label') && empty($data['category_id']); } } diff --git a/sources/app/Action/TaskAssignCategoryLink.php b/sources/app/Action/TaskAssignCategoryLink.php index 3d00e8d..b39e41b 100644 --- a/sources/app/Action/TaskAssignCategoryLink.php +++ b/sources/app/Action/TaskAssignCategoryLink.php @@ -13,6 +13,17 @@ use Kanboard\Model\TaskLink; */ class TaskAssignCategoryLink extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a category based on a link'); + } + /** * Get the list of compatible events * @@ -65,7 +76,7 @@ class TaskAssignCategoryLink extends Base { $values = array( 'id' => $data['task_id'], - 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), + 'category_id' => $this->getParam('category_id'), ); return $this->taskModification->update($values); diff --git a/sources/app/Action/TaskAssignColorCategory.php b/sources/app/Action/TaskAssignColorCategory.php index a2332f7..139c24c 100644 --- a/sources/app/Action/TaskAssignColorCategory.php +++ b/sources/app/Action/TaskAssignColorCategory.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorCategory extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a color based on a category'); + } + /** * Get the list of compatible events * @@ -67,7 +78,7 @@ class TaskAssignColorCategory extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorColumn.php b/sources/app/Action/TaskAssignColorColumn.php index 5314073..9241273 100644 --- a/sources/app/Action/TaskAssignColorColumn.php +++ b/sources/app/Action/TaskAssignColorColumn.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorColumn extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign a color when the task is moved to a specific column'); + } + /** * Get the list of compatible events * @@ -68,7 +79,7 @@ class TaskAssignColorColumn extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorLink.php b/sources/app/Action/TaskAssignColorLink.php index 67b2ef6..12ceabb 100644 --- a/sources/app/Action/TaskAssignColorLink.php +++ b/sources/app/Action/TaskAssignColorLink.php @@ -12,6 +12,17 @@ use Kanboard\Model\TaskLink; */ class TaskAssignColorLink extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change task color when using a specific task link'); + } + /** * Get the list of compatible events * @@ -67,7 +78,7 @@ class TaskAssignColorLink extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorUser.php b/sources/app/Action/TaskAssignColorUser.php index 6bf02c3..6ec8ce9 100644 --- a/sources/app/Action/TaskAssignColorUser.php +++ b/sources/app/Action/TaskAssignColorUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign a color to a specific user'); + } + /** * Get the list of compatible events * @@ -68,7 +79,7 @@ class TaskAssignColorUser extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignCurrentUser.php b/sources/app/Action/TaskAssignCurrentUser.php index f34c4f3..192a120 100644 --- a/sources/app/Action/TaskAssignCurrentUser.php +++ b/sources/app/Action/TaskAssignCurrentUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignCurrentUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign the task to the person who does the action'); + } + /** * Get the list of compatible events * @@ -22,7 +33,6 @@ class TaskAssignCurrentUser extends Base { return array( Task::EVENT_CREATE, - Task::EVENT_MOVE_COLUMN, ); } @@ -34,9 +44,7 @@ class TaskAssignCurrentUser extends Base */ public function getActionRequiredParameters() { - return array( - 'column_id' => t('Column'), - ); + return array(); } /** @@ -49,7 +57,6 @@ class TaskAssignCurrentUser extends Base { return array( 'task_id', - 'column_id', ); } @@ -83,6 +90,6 @@ class TaskAssignCurrentUser extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return true; } } diff --git a/sources/app/Action/TaskAssignCurrentUserColumn.php b/sources/app/Action/TaskAssignCurrentUserColumn.php new file mode 100644 index 0000000..05d08dd --- /dev/null +++ b/sources/app/Action/TaskAssignCurrentUserColumn.php @@ -0,0 +1,98 @@ + t('Column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if (! $this->userSession->isLogged()) { + return false; + } + + $values = array( + 'id' => $data['task_id'], + 'owner_id' => $this->userSession->getId(), + ); + + return $this->taskModification->update($values); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/sources/app/Action/TaskAssignSpecificUser.php b/sources/app/Action/TaskAssignSpecificUser.php index dfcb281..2dc3e96 100644 --- a/sources/app/Action/TaskAssignSpecificUser.php +++ b/sources/app/Action/TaskAssignSpecificUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignSpecificUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign the task to a specific user'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskAssignUser.php b/sources/app/Action/TaskAssignUser.php index a582172..da54d18 100644 --- a/sources/app/Action/TaskAssignUser.php +++ b/sources/app/Action/TaskAssignUser.php @@ -2,9 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; - /** * Assign a task to someone * @@ -13,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskAssignUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change the assignee based on an external username'); + } + /** * Get the list of compatible events * @@ -21,10 +29,7 @@ class TaskAssignUser extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE, - BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE, - ); + return array(); } /** @@ -78,6 +83,6 @@ class TaskAssignUser extends Base */ public function hasRequiredCondition(array $data) { - return true; + return $this->projectPermission->isAssignable($this->getProjectId(), $data['owner_id']); } } diff --git a/sources/app/Action/TaskClose.php b/sources/app/Action/TaskClose.php index d80bd02..cf91e83 100644 --- a/sources/app/Action/TaskClose.php +++ b/sources/app/Action/TaskClose.php @@ -2,11 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; -use Kanboard\Model\Task; - /** * Close automatically a task * @@ -15,6 +10,17 @@ use Kanboard\Model\Task; */ class TaskClose extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Close a task'); + } + /** * Get the list of compatible events * @@ -23,15 +29,7 @@ class TaskClose extends Base */ public function getCompatibleEvents() { - return array( - Task::EVENT_MOVE_COLUMN, - GithubWebhook::EVENT_COMMIT, - GithubWebhook::EVENT_ISSUE_CLOSED, - GitlabWebhook::EVENT_COMMIT, - GitlabWebhook::EVENT_ISSUE_CLOSED, - BitbucketWebhook::EVENT_COMMIT, - BitbucketWebhook::EVENT_ISSUE_CLOSED, - ); + return array(); } /** @@ -42,17 +40,7 @@ class TaskClose extends Base */ public function getActionRequiredParameters() { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return array(); - default: - return array('column_id' => t('Column')); - } + return array(); } /** @@ -63,17 +51,7 @@ class TaskClose extends Base */ public function getEventRequiredParameters() { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return array('task_id'); - default: - return array('task_id', 'column_id'); - } + return array('task_id'); } /** @@ -97,16 +75,6 @@ class TaskClose extends Base */ public function hasRequiredCondition(array $data) { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return true; - default: - return $data['column_id'] == $this->getParam('column_id'); - } + return true; } } diff --git a/sources/app/Action/TaskCloseColumn.php b/sources/app/Action/TaskCloseColumn.php new file mode 100644 index 0000000..09af3b9 --- /dev/null +++ b/sources/app/Action/TaskCloseColumn.php @@ -0,0 +1,84 @@ + t('Column')); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('task_id', 'column_id'); + } + + /** + * Execute the action (close the task) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + return $this->taskStatus->close($data['task_id']); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/sources/app/Action/TaskCreation.php b/sources/app/Action/TaskCreation.php index af1403f..290c31e 100644 --- a/sources/app/Action/TaskCreation.php +++ b/sources/app/Action/TaskCreation.php @@ -2,10 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\BitbucketWebhook; - /** * Create automatically a task from a webhook * @@ -14,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskCreation extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Create a task from an external provider'); + } + /** * Get the list of compatible events * @@ -22,11 +29,7 @@ class TaskCreation extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_OPENED, - GitlabWebhook::EVENT_ISSUE_OPENED, - BitbucketWebhook::EVENT_ISSUE_OPENED, - ); + return array(); } /** diff --git a/sources/app/Action/TaskDuplicateAnotherProject.php b/sources/app/Action/TaskDuplicateAnotherProject.php index 1f6684d..5bcdce0 100644 --- a/sources/app/Action/TaskDuplicateAnotherProject.php +++ b/sources/app/Action/TaskDuplicateAnotherProject.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskDuplicateAnotherProject extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Duplicate the task to another project'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskDuplicateAnotherProject extends Base return array( 'task_id', 'column_id', - 'project_id', ); } @@ -65,7 +75,6 @@ class TaskDuplicateAnotherProject extends Base public function doAction(array $data) { $destination_column_id = $this->board->getFirstColumn($this->getParam('project_id')); - return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); } diff --git a/sources/app/Action/TaskEmail.php b/sources/app/Action/TaskEmail.php index 7fb76c4..4e0e06a 100644 --- a/sources/app/Action/TaskEmail.php +++ b/sources/app/Action/TaskEmail.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskEmail extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Send a task by email to someone'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskMoveAnotherProject.php b/sources/app/Action/TaskMoveAnotherProject.php index 476e203..fdff0d8 100644 --- a/sources/app/Action/TaskMoveAnotherProject.php +++ b/sources/app/Action/TaskMoveAnotherProject.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveAnotherProject extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another project'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskMoveColumnAssigned.php b/sources/app/Action/TaskMoveColumnAssigned.php index 16622ee..1b23a59 100644 --- a/sources/app/Action/TaskMoveColumnAssigned.php +++ b/sources/app/Action/TaskMoveColumnAssigned.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnAssigned extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when assigned to a user'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskMoveColumnAssigned extends Base return array( 'task_id', 'column_id', - 'project_id', 'owner_id' ); } diff --git a/sources/app/Action/TaskMoveColumnCategoryChange.php b/sources/app/Action/TaskMoveColumnCategoryChange.php index 1e12be4..0f591ed 100644 --- a/sources/app/Action/TaskMoveColumnCategoryChange.php +++ b/sources/app/Action/TaskMoveColumnCategoryChange.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnCategoryChange extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when the category is changed'); + } + /** * Get the list of compatible events * @@ -50,7 +61,6 @@ class TaskMoveColumnCategoryChange extends Base return array( 'task_id', 'column_id', - 'project_id', 'category_id', ); } @@ -71,7 +81,8 @@ class TaskMoveColumnCategoryChange extends Base $data['task_id'], $this->getParam('dest_column_id'), $original_task['position'], - $original_task['swimlane_id'] + $original_task['swimlane_id'], + false ); } diff --git a/sources/app/Action/TaskMoveColumnUnAssigned.php b/sources/app/Action/TaskMoveColumnUnAssigned.php index 617c75a..99ef935 100644 --- a/sources/app/Action/TaskMoveColumnUnAssigned.php +++ b/sources/app/Action/TaskMoveColumnUnAssigned.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnUnAssigned extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when assignee is cleared'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskMoveColumnUnAssigned extends Base return array( 'task_id', 'column_id', - 'project_id', 'owner_id' ); } diff --git a/sources/app/Action/TaskOpen.php b/sources/app/Action/TaskOpen.php index 2e84c69..ec0f96f 100644 --- a/sources/app/Action/TaskOpen.php +++ b/sources/app/Action/TaskOpen.php @@ -2,10 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\BitbucketWebhook; - /** * Open automatically a task * @@ -14,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskOpen extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Open a task'); + } + /** * Get the list of compatible events * @@ -22,11 +29,7 @@ class TaskOpen extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_REOPENED, - GitlabWebhook::EVENT_ISSUE_REOPENED, - BitbucketWebhook::EVENT_ISSUE_REOPENED, - ); + return array(); } /** diff --git a/sources/app/Action/TaskUpdateStartDate.php b/sources/app/Action/TaskUpdateStartDate.php index 4cd548a..e5cea01 100644 --- a/sources/app/Action/TaskUpdateStartDate.php +++ b/sources/app/Action/TaskUpdateStartDate.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskUpdateStartDate extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Automatically update the start date'); + } + /** * Get the list of compatible events * @@ -66,7 +77,7 @@ class TaskUpdateStartDate extends Base 'date_started' => time(), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php b/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php new file mode 100644 index 0000000..fd85f86 --- /dev/null +++ b/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php @@ -0,0 +1,114 @@ + 0, + 'total_lead_time' => 0, + 'total_cycle_time' => 0, + 'avg_lead_time' => 0, + 'avg_cycle_time' => 0, + ); + + $tasks = $this->getTasks($project_id); + + foreach ($tasks as &$task) { + $stats['count']++; + $stats['total_lead_time'] += $this->calculateLeadTime($task); + $stats['total_cycle_time'] += $this->calculateCycleTime($task); + } + + $stats['avg_lead_time'] = $this->calculateAverage($stats, 'total_lead_time'); + $stats['avg_cycle_time'] = $this->calculateAverage($stats, 'total_cycle_time'); + + return $stats; + } + + /** + * Calculate average + * + * @access private + * @param array &$stats + * @param string $field + * @return float + */ + private function calculateAverage(array &$stats, $field) + { + if ($stats['count'] > 0) { + return (int) ($stats[$field] / $stats['count']); + } + + return 0; + } + + /** + * Calculate lead time + * + * @access private + * @param array &$task + * @return integer + */ + private function calculateLeadTime(array &$task) + { + $end = $task['date_completed'] ?: time(); + $start = $task['date_creation']; + + return $end - $start; + } + + /** + * Calculate cycle time + * + * @access private + * @param array &$task + * @return integer + */ + private function calculateCycleTime(array &$task) + { + if (empty($task['date_started'])) { + return 0; + } + + $end = $task['date_completed'] ?: time(); + $start = $task['date_started']; + + return $end - $start; + } + + /** + * Get the 1000 last created tasks + * + * @access private + * @return array + */ + private function getTasks($project_id) + { + return $this->db + ->table(Task::TABLE) + ->columns('date_completed', 'date_creation', 'date_started') + ->eq('project_id', $project_id) + ->desc('id') + ->limit(1000) + ->findAll(); + } +} diff --git a/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php b/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php new file mode 100644 index 0000000..c3cff54 --- /dev/null +++ b/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php @@ -0,0 +1,153 @@ +initialize($project_id); + + $this->processTasks($stats, $project_id); + $this->calculateAverage($stats); + + return $stats; + } + + /** + * Initialize default values for each column + * + * @access private + * @param integer $project_id + * @return array + */ + private function initialize($project_id) + { + $stats = array(); + $columns = $this->board->getColumnsList($project_id); + + foreach ($columns as $column_id => $column_title) { + $stats[$column_id] = array( + 'count' => 0, + 'time_spent' => 0, + 'average' => 0, + 'title' => $column_title, + ); + } + + return $stats; + } + + /** + * Calculate time spent for each tasks for each columns + * + * @access private + * @param array $stats + * @param integer $project_id + */ + private function processTasks(array &$stats, $project_id) + { + $tasks = $this->getTasks($project_id); + + foreach ($tasks as &$task) { + foreach ($this->getTaskTimeByColumns($task) as $column_id => $time_spent) { + if (isset($stats[$column_id])) { + $stats[$column_id]['count']++; + $stats[$column_id]['time_spent'] += $time_spent; + } + } + } + } + + /** + * Calculate averages + * + * @access private + * @param array $stats + */ + private function calculateAverage(array &$stats) + { + foreach ($stats as &$column) { + $this->calculateColumnAverage($column); + } + } + + /** + * Calculate column average + * + * @access private + * @param array $column + */ + private function calculateColumnAverage(array &$column) + { + if ($column['count'] > 0) { + $column['average'] = (int) ($column['time_spent'] / $column['count']); + } + } + + /** + * Get time spent for each column for a given task + * + * @access private + * @param array $task + * @return array + */ + private function getTaskTimeByColumns(array &$task) + { + $columns = $this->transition->getTimeSpentByTask($task['id']); + + if (! isset($columns[$task['column_id']])) { + $columns[$task['column_id']] = 0; + } + + $columns[$task['column_id']] += $this->getTaskTimeSpentInCurrentColumn($task); + + return $columns; + } + + /** + * Calculate time spent of a task in the current column + * + * @access private + * @param array $task + */ + private function getTaskTimeSpentInCurrentColumn(array &$task) + { + $end = $task['date_completed'] ?: time(); + return $end - $task['date_moved']; + } + + /** + * Fetch the last 1000 tasks + * + * @access private + * @param integer $project_id + * @return array + */ + private function getTasks($project_id) + { + return $this->db + ->table(Task::TABLE) + ->columns('id', 'date_completed', 'date_moved', 'column_id') + ->eq('project_id', $project_id) + ->desc('id') + ->limit(1000) + ->findAll(); + } +} diff --git a/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php b/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php new file mode 100644 index 0000000..490bcd5 --- /dev/null +++ b/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php @@ -0,0 +1,50 @@ +db->table(Task::TABLE) + ->columns('SUM(time_estimated) AS time_estimated', 'SUM(time_spent) AS time_spent', 'is_active') + ->eq('project_id', $project_id) + ->groupBy('is_active') + ->findAll(); + + $metrics = array( + 'open' => array( + 'time_spent' => 0, + 'time_estimated' => 0, + ), + 'closed' => array( + 'time_spent' => 0, + 'time_estimated' => 0, + ), + ); + + foreach ($rows as $row) { + $key = $row['is_active'] == Task::STATUS_OPEN ? 'open' : 'closed'; + $metrics[$key]['time_spent'] = $row['time_spent']; + $metrics[$key]['time_estimated'] = $row['time_estimated']; + } + + return $metrics; + } +} diff --git a/sources/app/Analytic/TaskDistributionAnalytic.php b/sources/app/Analytic/TaskDistributionAnalytic.php new file mode 100644 index 0000000..614c5f7 --- /dev/null +++ b/sources/app/Analytic/TaskDistributionAnalytic.php @@ -0,0 +1,48 @@ +board->getColumns($project_id); + + foreach ($columns as $column) { + $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']); + $total += $nb_tasks; + + $metrics[] = array( + 'column_title' => $column['title'], + 'nb_tasks' => $nb_tasks, + ); + } + + if ($total === 0) { + return array(); + } + + foreach ($metrics as &$metric) { + $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); + } + + return $metrics; + } +} diff --git a/sources/app/Analytic/UserDistributionAnalytic.php b/sources/app/Analytic/UserDistributionAnalytic.php new file mode 100644 index 0000000..e1815f9 --- /dev/null +++ b/sources/app/Analytic/UserDistributionAnalytic.php @@ -0,0 +1,56 @@ +taskFinder->getAll($project_id); + $users = $this->projectUserRole->getAssignableUsersList($project_id); + + foreach ($tasks as $task) { + $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0]; + $total++; + + if (! isset($metrics[$user])) { + $metrics[$user] = array( + 'nb_tasks' => 0, + 'percentage' => 0, + 'user' => $user, + ); + } + + $metrics[$user]['nb_tasks']++; + } + + if ($total === 0) { + return array(); + } + + foreach ($metrics as &$metric) { + $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); + } + + ksort($metrics); + + return array_values($metrics); + } +} diff --git a/sources/app/Api/Action.php b/sources/app/Api/Action.php index 0ae91f1..9e3b86f 100644 --- a/sources/app/Api/Action.php +++ b/sources/app/Api/Action.php @@ -12,17 +12,17 @@ class Action extends \Kanboard\Core\Base { public function getAvailableActions() { - return $this->action->getAvailableActions(); + return $this->actionManager->getAvailableActions(); } public function getAvailableActionEvents() { - return $this->action->getAvailableEvents(); + return $this->eventManager->getAll(); } public function getCompatibleActionEvents($action_name) { - return $this->action->getCompatibleEvents($action_name); + return $this->actionManager->getCompatibleEvents($action_name); } public function removeAction($action_id) @@ -32,22 +32,10 @@ class Action extends \Kanboard\Core\Base public function getActions($project_id) { - $actions = $this->action->getAllByProject($project_id); - - foreach ($actions as $index => $action) { - $params = array(); - - foreach ($action['params'] as $param) { - $params[$param['name']] = $param['value']; - } - - $actions[$index]['params'] = $params; - } - - return $actions; + return $this->action->getAllByProject($project_id); } - public function createAction($project_id, $event_name, $action_name, $params) + public function createAction($project_id, $event_name, $action_name, array $params) { $values = array( 'project_id' => $project_id, @@ -56,23 +44,23 @@ class Action extends \Kanboard\Core\Base 'params' => $params, ); - list($valid, ) = $this->action->validateCreation($values); + list($valid, ) = $this->actionValidator->validateCreation($values); if (! $valid) { return false; } // Check if the action exists - $actions = $this->action->getAvailableActions(); + $actions = $this->actionManager->getAvailableActions(); if (! isset($actions[$action_name])) { return false; } // Check the event - $action = $this->action->load($action_name, $project_id, $event_name); + $action = $this->actionManager->getAction($action_name); - if (! in_array($event_name, $action->getCompatibleEvents())) { + if (! in_array($event_name, $action->getEvents())) { return false; } diff --git a/sources/app/Api/App.php b/sources/app/Api/App.php index d082bcf..635f1ce 100644 --- a/sources/app/Api/App.php +++ b/sources/app/Api/App.php @@ -34,4 +34,14 @@ class App extends \Kanboard\Core\Base { return $this->color->getList(); } + + public function getApplicationRoles() + { + return $this->role->getApplicationRoles(); + } + + public function getProjectRoles() + { + return $this->role->getProjectRoles(); + } } diff --git a/sources/app/Api/Auth.php b/sources/app/Api/Auth.php index a9d1617..c7c5298 100644 --- a/sources/app/Api/Auth.php +++ b/sources/app/Api/Auth.php @@ -23,7 +23,7 @@ class Auth extends Base */ public function checkCredentials($username, $password, $class, $method) { - $this->container['dispatcher']->dispatch('app.bootstrap'); + $this->dispatcher->dispatch('app.bootstrap'); if ($this->isUserAuthenticated($username, $password)) { $this->checkProcedurePermission(true, $method); diff --git a/sources/app/Api/Category.php b/sources/app/Api/Category.php index 458eaef..fbd61c5 100644 --- a/sources/app/Api/Category.php +++ b/sources/app/Api/Category.php @@ -32,7 +32,7 @@ class Category extends \Kanboard\Core\Base 'name' => $name, ); - list($valid, ) = $this->category->validateCreation($values); + list($valid, ) = $this->categoryValidator->validateCreation($values); return $valid ? $this->category->create($values) : false; } @@ -43,7 +43,7 @@ class Category extends \Kanboard\Core\Base 'name' => $name, ); - list($valid, ) = $this->category->validateModification($values); + list($valid, ) = $this->categoryValidator->validateModification($values); return $valid && $this->category->update($values); } } diff --git a/sources/app/Api/Comment.php b/sources/app/Api/Comment.php index 26b632e..1fc1c70 100644 --- a/sources/app/Api/Comment.php +++ b/sources/app/Api/Comment.php @@ -25,15 +25,16 @@ class Comment extends \Kanboard\Core\Base return $this->comment->remove($comment_id); } - public function createComment($task_id, $user_id, $content) + public function createComment($task_id, $user_id, $content, $reference = '') { $values = array( 'task_id' => $task_id, 'user_id' => $user_id, 'comment' => $content, + 'reference' => $reference, ); - list($valid, ) = $this->comment->validateCreation($values); + list($valid, ) = $this->commentValidator->validateCreation($values); return $valid ? $this->comment->create($values) : false; } @@ -45,7 +46,7 @@ class Comment extends \Kanboard\Core\Base 'comment' => $content, ); - list($valid, ) = $this->comment->validateModification($values); + list($valid, ) = $this->commentValidator->validateModification($values); return $valid && $this->comment->update($values); } } diff --git a/sources/app/Api/File.php b/sources/app/Api/File.php index be415ec..269803e 100644 --- a/sources/app/Api/File.php +++ b/sources/app/Api/File.php @@ -32,14 +32,18 @@ class File extends \Kanboard\Core\Base } } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); + return ''; } - - return ''; } public function createFile($project_id, $task_id, $filename, $blob) { - return $this->file->uploadContent($project_id, $task_id, $filename, $blob); + try { + return $this->file->uploadContent($project_id, $task_id, $filename, $blob); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + return false; + } } public function removeFile($file_id) diff --git a/sources/app/Api/Group.php b/sources/app/Api/Group.php new file mode 100644 index 0000000..a1e0a73 --- /dev/null +++ b/sources/app/Api/Group.php @@ -0,0 +1,49 @@ +group->create($name, $external_id); + } + + public function updateGroup($group_id, $name = null, $external_id = null) + { + $values = array( + 'id' => $group_id, + 'name' => $name, + 'external_id' => $external_id, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + return $this->group->update($values); + } + + public function removeGroup($group_id) + { + return $this->group->remove($group_id); + } + + public function getGroup($group_id) + { + return $this->group->getById($group_id); + } + + public function getAllGroups() + { + return $this->group->getAll(); + } +} diff --git a/sources/app/Api/GroupMember.php b/sources/app/Api/GroupMember.php new file mode 100644 index 0000000..de62f0c --- /dev/null +++ b/sources/app/Api/GroupMember.php @@ -0,0 +1,32 @@ +groupMember->getMembers($group_id); + } + + public function addGroupMember($group_id, $user_id) + { + return $this->groupMember->addUser($group_id, $user_id); + } + + public function removeGroupMember($group_id, $user_id) + { + return $this->groupMember->removeUser($group_id, $user_id); + } + + public function isGroupMember($group_id, $user_id) + { + return $this->groupMember->isMember($group_id, $user_id); + } +} diff --git a/sources/app/Api/Link.php b/sources/app/Api/Link.php index d4df18f..23a9916 100644 --- a/sources/app/Api/Link.php +++ b/sources/app/Api/Link.php @@ -72,7 +72,7 @@ class Link extends \Kanboard\Core\Base 'opposite_label' => $opposite_label, ); - list($valid, ) = $this->link->validateCreation($values); + list($valid, ) = $this->linkValidator->validateCreation($values); return $valid ? $this->link->create($label, $opposite_label) : false; } @@ -93,7 +93,7 @@ class Link extends \Kanboard\Core\Base 'label' => $label, ); - list($valid, ) = $this->link->validateModification($values); + list($valid, ) = $this->linkValidator->validateModification($values); return $valid && $this->link->update($values); } diff --git a/sources/app/Api/Me.php b/sources/app/Api/Me.php index 3785173..df8ec07 100644 --- a/sources/app/Api/Me.php +++ b/sources/app/Api/Me.php @@ -44,7 +44,7 @@ class Me extends Base 'is_private' => 1, ); - list($valid, ) = $this->project->validateCreation($values); + list($valid, ) = $this->projectValidator->validateCreation($values); return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false; } diff --git a/sources/app/Api/Project.php b/sources/app/Api/Project.php index f934432..8e311f7 100644 --- a/sources/app/Api/Project.php +++ b/sources/app/Api/Project.php @@ -69,7 +69,7 @@ class Project extends Base 'description' => $description ); - list($valid, ) = $this->project->validateCreation($values); + list($valid, ) = $this->projectValidator->validateCreation($values); return $valid ? $this->project->create($values) : false; } @@ -81,7 +81,7 @@ class Project extends Base 'description' => $description ); - list($valid, ) = $this->project->validateModification($values); + list($valid, ) = $this->projectValidator->validateModification($values); return $valid && $this->project->update($values); } } diff --git a/sources/app/Api/ProjectPermission.php b/sources/app/Api/ProjectPermission.php index d440819..11e92af 100644 --- a/sources/app/Api/ProjectPermission.php +++ b/sources/app/Api/ProjectPermission.php @@ -5,25 +5,68 @@ namespace Kanboard\Api; use Kanboard\Core\Security\Role; /** - * ProjectPermission API controller + * Project Permission API controller * * @package api * @author Frederic Guillot */ class ProjectPermission extends \Kanboard\Core\Base { - public function getMembers($project_id) + public function getProjectUsers($project_id) { return $this->projectUserRole->getAllUsers($project_id); } - public function revokeUser($project_id, $user_id) + public function getAssignableUsers($project_id, $prepend_unassigned = false) + { + return $this->projectUserRole->getAssignableUsersList($project_id, $prepend_unassigned); + } + + public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER) + { + return $this->projectUserRole->addUser($project_id, $user_id, $role); + } + + public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER) + { + return $this->projectGroupRole->addGroup($project_id, $group_id, $role); + } + + public function removeProjectUser($project_id, $user_id) { return $this->projectUserRole->removeUser($project_id, $user_id); } + public function removeProjectGroup($project_id, $group_id) + { + return $this->projectGroupRole->removeGroup($project_id, $group_id); + } + + public function changeProjectUserRole($project_id, $user_id, $role) + { + return $this->projectUserRole->changeUserRole($project_id, $user_id, $role); + } + + public function changeProjectGroupRole($project_id, $group_id, $role) + { + return $this->projectGroupRole->changeGroupRole($project_id, $group_id, $role); + } + + // Deprecated + public function getMembers($project_id) + { + return $this->getProjectUsers($project_id); + } + + // Deprecated + public function revokeUser($project_id, $user_id) + { + return $this->removeProjectUser($project_id, $user_id); + } + + // Deprecated public function allowUser($project_id, $user_id) { - return $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER); + return $this->addProjectUser($project_id, $user_id); } } diff --git a/sources/app/Api/Subtask.php b/sources/app/Api/Subtask.php index 7baee3d..782fdb0 100644 --- a/sources/app/Api/Subtask.php +++ b/sources/app/Api/Subtask.php @@ -36,7 +36,7 @@ class Subtask extends \Kanboard\Core\Base 'status' => $status, ); - list($valid, ) = $this->subtask->validateCreation($values); + list($valid, ) = $this->subtaskValidator->validateCreation($values); return $valid ? $this->subtask->create($values) : false; } @@ -58,7 +58,7 @@ class Subtask extends \Kanboard\Core\Base } } - list($valid, ) = $this->subtask->validateApiModification($values); + list($valid, ) = $this->subtaskValidator->validateApiModification($values); return $valid && $this->subtask->update($values); } } diff --git a/sources/app/Api/Task.php b/sources/app/Api/Task.php index 4a7ee93..f132bcd 100644 --- a/sources/app/Api/Task.php +++ b/sources/app/Api/Task.php @@ -64,6 +64,16 @@ class Task extends Base return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); } + public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) + { + return $this->taskDuplication->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + } + + public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) + { + return $this->taskDuplication->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + } + public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, @@ -71,7 +81,7 @@ class Task extends Base { $this->checkProjectPermission($project_id); - if ($owner_id !== 0 && ! $this->projectPermission->isMember($project_id, $owner_id)) { + if ($owner_id !== 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) { return false; } @@ -117,7 +127,7 @@ class Task extends Base return false; } - if ($owner_id !== null && ! $this->projectPermission->isMember($project_id, $owner_id)) { + if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) { return false; } diff --git a/sources/app/Api/User.php b/sources/app/Api/User.php index 06e305f..63c222f 100644 --- a/sources/app/Api/User.php +++ b/sources/app/Api/User.php @@ -42,7 +42,7 @@ class User extends \Kanboard\Core\Base 'role' => $role, ); - list($valid, ) = $this->user->validateCreation($values); + list($valid, ) = $this->userValidator->validateCreation($values); return $valid ? $this->user->create($values) : false; } @@ -94,7 +94,7 @@ class User extends \Kanboard\Core\Base } } - list($valid, ) = $this->user->validateApiModification($values); + list($valid, ) = $this->userValidator->validateApiModification($values); return $valid && $this->user->update($values); } } diff --git a/sources/app/Auth/DatabaseAuth.php b/sources/app/Auth/DatabaseAuth.php index 69260fe..5a8ee64 100644 --- a/sources/app/Auth/DatabaseAuth.php +++ b/sources/app/Auth/DatabaseAuth.php @@ -19,26 +19,26 @@ class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterfa /** * User properties * - * @access private + * @access protected * @var array */ - private $userInfo = array(); + protected $userInfo = array(); /** * Username * - * @access private + * @access protected * @var string */ - private $username = ''; + protected $username = ''; /** * Password * - * @access private + * @access protected * @var string */ - private $password = ''; + protected $password = ''; /** * Get authentication provider name diff --git a/sources/app/Auth/GithubAuth.php b/sources/app/Auth/GithubAuth.php index 606ab30..8369958 100644 --- a/sources/app/Auth/GithubAuth.php +++ b/sources/app/Auth/GithubAuth.php @@ -17,26 +17,26 @@ class GithubAuth extends Base implements OAuthAuthenticationProviderInterface /** * User properties * - * @access private + * @access protected * @var \Kanboard\User\GithubUserProvider */ - private $userInfo = null; + protected $userInfo = null; /** * OAuth2 instance * - * @access private + * @access protected * @var \Kanboard\Core\Http\OAuth2 */ - private $service; + protected $service; /** * OAuth2 code * - * @access private + * @access protected * @var string */ - private $code = ''; + protected $code = ''; /** * Get authentication provider name diff --git a/sources/app/Auth/GitlabAuth.php b/sources/app/Auth/GitlabAuth.php index 084e8ab..c0a2cf9 100644 --- a/sources/app/Auth/GitlabAuth.php +++ b/sources/app/Auth/GitlabAuth.php @@ -25,18 +25,18 @@ class GitlabAuth extends Base implements OAuthAuthenticationProviderInterface /** * OAuth2 instance * - * @access private + * @access protected * @var \Kanboard\Core\Http\OAuth2 */ - private $service; + protected $service; /** * OAuth2 code * - * @access private + * @access protected * @var string */ - private $code = ''; + protected $code = ''; /** * Get authentication provider name diff --git a/sources/app/Auth/GoogleAuth.php b/sources/app/Auth/GoogleAuth.php index 2a1f1b4..6eacf0b 100644 --- a/sources/app/Auth/GoogleAuth.php +++ b/sources/app/Auth/GoogleAuth.php @@ -17,26 +17,26 @@ class GoogleAuth extends Base implements OAuthAuthenticationProviderInterface /** * User properties * - * @access private + * @access protected * @var \Kanboard\User\GoogleUserProvider */ - private $userInfo = null; + protected $userInfo = null; /** * OAuth2 instance * - * @access private + * @access protected * @var \Kanboard\Core\Http\OAuth2 */ - private $service; + protected $service; /** * OAuth2 code * - * @access private + * @access protected * @var string */ - private $code = ''; + protected $code = ''; /** * Get authentication provider name diff --git a/sources/app/Auth/LdapAuth.php b/sources/app/Auth/LdapAuth.php index 85234ed..b4efbb5 100644 --- a/sources/app/Auth/LdapAuth.php +++ b/sources/app/Auth/LdapAuth.php @@ -20,26 +20,26 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface /** * User properties * - * @access private + * @access protected * @var \Kanboard\User\LdapUserProvider */ - private $userInfo = null; + protected $userInfo = null; /** * Username * - * @access private + * @access protected * @var string */ - private $username = ''; + protected $username = ''; /** * Password * - * @access private + * @access protected * @var string */ - private $password = ''; + protected $password = ''; /** * Get authentication provider name diff --git a/sources/app/Auth/RememberMeAuth.php b/sources/app/Auth/RememberMeAuth.php index 5a0e48e..509a511 100644 --- a/sources/app/Auth/RememberMeAuth.php +++ b/sources/app/Auth/RememberMeAuth.php @@ -17,10 +17,10 @@ class RememberMeAuth extends Base implements PreAuthenticationProviderInterface /** * User properties * - * @access private + * @access protected * @var array */ - private $userInfo = array(); + protected $userInfo = array(); /** * Get authentication provider name diff --git a/sources/app/Auth/TotpAuth.php b/sources/app/Auth/TotpAuth.php index f41fabd..f430493 100644 --- a/sources/app/Auth/TotpAuth.php +++ b/sources/app/Auth/TotpAuth.php @@ -19,18 +19,18 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface /** * User pin code * - * @access private + * @access protected * @var string */ - private $code = ''; + protected $code = ''; /** * Private key * - * @access private + * @access protected * @var string */ - private $secret = ''; + protected $secret = ''; /** * Get authentication provider name @@ -40,7 +40,7 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface */ public function getName() { - return 'Time-based One-time Password Algorithm'; + return t('Time-based One-time Password Algorithm'); } /** @@ -55,6 +55,16 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface return $otp->checkTotp(Base32::decode($this->secret), $this->code); } + /** + * Called before to prompt the user + * + * @access public + */ + public function beforeCode() + { + + } + /** * Set validation code * @@ -66,6 +76,18 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface $this->code = $code; } + /** + * Generate secret + * + * @access public + * @return string + */ + public function generateSecret() + { + $this->secret = GoogleAuthenticator::generateRandom(); + return $this->secret; + } + /** * Set secret token * @@ -85,10 +107,6 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface */ public function getSecret() { - if (empty($this->secret)) { - $this->secret = GoogleAuthenticator::generateRandom(); - } - return $this->secret; } diff --git a/sources/app/Controller/Action.php b/sources/app/Controller/Action.php index 3caea45..645b53b 100644 --- a/sources/app/Controller/Action.php +++ b/sources/app/Controller/Action.php @@ -18,17 +18,18 @@ class Action extends Base public function index() { $project = $this->getProject(); + $actions = $this->action->getAllByProject($project['id']); $this->response->html($this->projectLayout('action/index', array( 'values' => array('project_id' => $project['id']), 'project' => $project, - 'actions' => $this->action->getAllByProject($project['id']), - 'available_actions' => $this->action->getAvailableActions(), - 'available_events' => $this->action->getAvailableEvents(), - 'available_params' => $this->action->getAllActionParameters(), + 'actions' => $actions, + 'available_actions' => $this->actionManager->getAvailableActions(), + 'available_events' => $this->eventManager->getAll(), + 'available_params' => $this->actionManager->getAvailableParameters($actions), 'columns_list' => $this->board->getColumnsList($project['id']), 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), - 'projects_list' => $this->project->getList(false), + 'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'links_list' => $this->link->getList(0, false), @@ -53,7 +54,7 @@ class Action extends Base $this->response->html($this->projectLayout('action/event', array( 'values' => $values, 'project' => $project, - 'events' => $this->action->getCompatibleEvents($values['action_name']), + 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), 'title' => t('Automatic actions') ))); } @@ -72,14 +73,14 @@ class Action extends Base $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); } - $action = $this->action->load($values['action_name'], $values['project_id'], $values['event_name']); + $action = $this->actionManager->getAction($values['action_name']); $action_params = $action->getActionRequiredParameters(); if (empty($action_params)) { $this->doCreation($project, $values + array('params' => array())); } - $projects_list = $this->project->getList(false); + $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); unset($projects_list[$project['id']]); $this->response->html($this->projectLayout('action/params', array( @@ -115,7 +116,7 @@ class Action extends Base */ private function doCreation(array $project, array $values) { - list($valid, ) = $this->action->validateCreation($values); + list($valid, ) = $this->actionValidator->validateCreation($values); if ($valid) { if ($this->action->create($values) !== false) { @@ -139,8 +140,8 @@ class Action extends Base $this->response->html($this->projectLayout('action/remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), - 'available_events' => $this->action->getAvailableEvents(), - 'available_actions' => $this->action->getAvailableActions(), + 'available_events' => $this->eventManager->getAll(), + 'available_actions' => $this->actionManager->getAvailableActions(), 'project' => $project, 'title' => t('Remove an action') ))); diff --git a/sources/app/Controller/Activity.php b/sources/app/Controller/Activity.php index 71d5e94..3865834 100644 --- a/sources/app/Controller/Activity.php +++ b/sources/app/Controller/Activity.php @@ -20,7 +20,7 @@ class Activity extends Base $project = $this->getProject(); $this->response->html($this->template->layout('activity/project', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'events' => $this->projectActivity->getProject($project['id']), 'project' => $project, 'title' => t('%s\'s activity', $project['name']) diff --git a/sources/app/Controller/Analytic.php b/sources/app/Controller/Analytic.php index e03d8ca..d203fb8 100644 --- a/sources/app/Controller/Analytic.php +++ b/sources/app/Controller/Analytic.php @@ -1,6 +1,7 @@ projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['content_for_sublayout'] = $this->template->render($template, $params); return $this->template->layout('analytic/layout', $params); @@ -34,17 +35,7 @@ class Analytic extends Base public function leadAndCycleTime() { $project = $this->getProject(); - $values = $this->request->getValues(); - - $this->projectDailyStats->updateTotals($project['id'], date('Y-m-d')); - - $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); - $to = $this->request->getStringParam('to', date('Y-m-d')); - - if (! empty($values)) { - $from = $values['from']; - $to = $values['to']; - } + list($from, $to) = $this->getDates(); $this->response->html($this->layout('analytic/lead_cycle_time', array( 'values' => array( @@ -52,7 +43,7 @@ class Analytic extends Base 'to' => $to, ), 'project' => $project, - 'average' => $this->projectAnalytic->getAverageLeadAndCycleTime($project['id']), + 'average' => $this->averageLeadCycleTimeAnalytic->build($project['id']), 'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to), 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), @@ -60,6 +51,32 @@ class Analytic extends Base ))); } + /** + * Show comparison between actual and estimated hours chart + * + * @access public + */ + public function compareHours() + { + $project = $this->getProject(); + $params = $this->getProjectFilters('analytic', 'compareHours'); + $query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery(); + + $paginator = $this->paginator + ->setUrl('analytic', 'compareHours', array('project_id' => $project['id'])) + ->setMax(30) + ->setOrder(TaskModel::TABLE.'.id') + ->setQuery($query) + ->calculate(); + + $this->response->html($this->layout('analytic/compare_hours', array( + 'project' => $project, + 'paginator' => $paginator, + 'metrics' => $this->estimatedTimeComparisonAnalytic->build($project['id']), + 'title' => t('Compare hours for "%s"', $project['name']), + ))); + } + /** * Show average time spent by column * @@ -71,7 +88,7 @@ class Analytic extends Base $this->response->html($this->layout('analytic/avg_time_columns', array( 'project' => $project, - 'metrics' => $this->projectAnalytic->getAverageTimeSpentByColumn($project['id']), + 'metrics' => $this->averageTimeSpentColumnAnalytic->build($project['id']), 'title' => t('Average time spent into each column for "%s"', $project['name']), ))); } @@ -87,7 +104,7 @@ class Analytic extends Base $this->response->html($this->layout('analytic/tasks', array( 'project' => $project, - 'metrics' => $this->projectAnalytic->getTaskRepartition($project['id']), + 'metrics' => $this->taskDistributionAnalytic->build($project['id']), 'title' => t('Task repartition for "%s"', $project['name']), ))); } @@ -103,7 +120,7 @@ class Analytic extends Base $this->response->html($this->layout('analytic/users', array( 'project' => $project, - 'metrics' => $this->projectAnalytic->getUserRepartition($project['id']), + 'metrics' => $this->userDistributionAnalytic->build($project['id']), 'title' => t('User repartition for "%s"', $project['name']), ))); } @@ -139,17 +156,7 @@ class Analytic extends Base private function commonAggregateMetrics($template, $column, $title) { $project = $this->getProject(); - $values = $this->request->getValues(); - - $this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d')); - - $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); - $to = $this->request->getStringParam('to', date('Y-m-d')); - - if (! empty($values)) { - $from = $values['from']; - $to = $values['to']; - } + list($from, $to) = $this->getDates(); $display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2; @@ -166,4 +173,19 @@ class Analytic extends Base 'title' => t($title, $project['name']), ))); } + + private function getDates() + { + $values = $this->request->getValues(); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + return array($from, $to); + } } diff --git a/sources/app/Controller/App.php b/sources/app/Controller/App.php index c596b4a..bdd7fbc 100644 --- a/sources/app/Controller/App.php +++ b/sources/app/Controller/App.php @@ -22,7 +22,7 @@ class App extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['content_for_sublayout'] = $this->template->render($template, $params); return $this->template->layout('app/layout', $params); diff --git a/sources/app/Controller/Auth.php b/sources/app/Controller/Auth.php index cd1dd16..5284e12 100644 --- a/sources/app/Controller/Auth.php +++ b/sources/app/Controller/Auth.php @@ -2,8 +2,6 @@ namespace Kanboard\Controller; -use Gregwar\Captcha\CaptchaBuilder; - /** * Authentication controller * @@ -41,7 +39,7 @@ class Auth extends Base { $values = $this->request->getValues(); $this->sessionStorage->hasRememberMe = ! empty($values['remember_me']); - list($valid, $errors) = $this->authentication->validateForm($values); + list($valid, $errors) = $this->authValidator->validateForm($values); if ($valid) { $this->redirectAfterLogin(); @@ -61,21 +59,6 @@ class Auth extends Base $this->response->redirect($this->helper->url->to('auth', 'login')); } - /** - * Display captcha image - * - * @access public - */ - public function captcha() - { - $this->response->contentType('image/jpeg'); - - $builder = new CaptchaBuilder; - $builder->build(); - $this->sessionStorage->captcha = $builder->getPhrase(); - $builder->output(); - } - /** * Redirect the user after the authentication * diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php index 35ceee0..66a9e84 100644 --- a/sources/app/Controller/Base.php +++ b/sources/app/Controller/Base.php @@ -17,18 +17,18 @@ abstract class Base extends \Kanboard\Core\Base * * @access public */ - public function beforeAction($controller, $action) + public function beforeAction() { $this->sessionManager->open(); $this->dispatcher->dispatch('app.bootstrap'); - $this->sendHeaders($action); + $this->sendHeaders(); $this->authenticationManager->checkCurrentSession(); - if (! $this->applicationAuthorization->isAllowed($controller, $action, Role::APP_PUBLIC)) { + if (! $this->applicationAuthorization->isAllowed($this->router->getController(), $this->router->getAction(), Role::APP_PUBLIC)) { $this->handleAuthentication(); - $this->handlePostAuthentication($controller, $action); - $this->checkApplicationAuthorization($controller, $action); - $this->checkProjectAuthorization($controller, $action); + $this->handlePostAuthentication(); + $this->checkApplicationAuthorization(); + $this->checkProjectAuthorization(); } } @@ -37,7 +37,7 @@ abstract class Base extends \Kanboard\Core\Base * * @access private */ - private function sendHeaders($action) + private function sendHeaders() { // HTTP secure headers $this->response->csp($this->container['cspRules']); @@ -45,7 +45,7 @@ abstract class Base extends \Kanboard\Core\Base $this->response->xss(); // Allow the public board iframe inclusion - if (ENABLE_XFRAME && $action !== 'readonly') { + if (ENABLE_XFRAME && $this->router->getAction() !== 'readonly') { $this->response->xframe(); } @@ -76,8 +76,10 @@ abstract class Base extends \Kanboard\Core\Base * * @access private */ - private function handlePostAuthentication($controller, $action) + private function handlePostAuthentication() { + $controller = strtolower($this->router->getController()); + $action = strtolower($this->router->getAction()); $ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout'); if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) { @@ -94,9 +96,9 @@ abstract class Base extends \Kanboard\Core\Base * * @access private */ - private function checkApplicationAuthorization($controller, $action) + private function checkApplicationAuthorization() { - if (! $this->helper->user->hasAccess($controller, $action)) { + if (! $this->helper->user->hasAccess($this->router->getController(), $this->router->getAction())) { $this->forbidden(); } } @@ -106,7 +108,7 @@ abstract class Base extends \Kanboard\Core\Base * * @access private */ - private function checkProjectAuthorization($controller, $action) + private function checkProjectAuthorization() { $project_id = $this->request->getIntegerParam('project_id'); $task_id = $this->request->getIntegerParam('task_id'); @@ -116,7 +118,7 @@ abstract class Base extends \Kanboard\Core\Base $project_id = $this->taskFinder->getProjectId($task_id); } - if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($controller, $action, $project_id)) { + if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($this->router->getController(), $this->router->getAction(), $project_id)) { $this->forbidden(); } } @@ -144,7 +146,7 @@ abstract class Base extends \Kanboard\Core\Base protected function forbidden($no_layout = false) { if ($this->request->isAjax()) { - $this->response->text('Not Authorized', 401); + $this->response->text('Access Forbidden', 403); } $this->response->html($this->template->layout('app/forbidden', array( @@ -190,7 +192,7 @@ abstract class Base extends \Kanboard\Core\Base $content = $this->template->render($template, $params); $params['task_content_for_layout'] = $content; $params['title'] = $params['task']['project_name'].' > '.$params['task']['title']; - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); return $this->template->layout('task/layout', $params); } @@ -208,7 +210,7 @@ abstract class Base extends \Kanboard\Core\Base $content = $this->template->render($template, $params); $params['project_content_for_layout'] = $content; $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title']; - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['sidebar_template'] = $sidebar_template; return $this->template->layout('project/layout', $params); @@ -289,7 +291,7 @@ abstract class Base extends \Kanboard\Core\Base { $project = $this->getProject(); $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id'])); - $board_selector = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); unset($board_selector[$project['id']]); $filters = array( diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php index a75fea3..06736cc 100644 --- a/sources/app/Controller/Board.php +++ b/sources/app/Controller/Board.php @@ -73,10 +73,6 @@ class Board extends Base return $this->response->status(403); } - if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { - $this->response->text('Forbidden', 403); - } - $values = $this->request->getJson(); $result =$this->taskPosition->movePosition( @@ -101,22 +97,18 @@ class Board extends Base */ public function check() { - if (! $this->request->isAjax()) { - return $this->response->status(403); - } - $project_id = $this->request->getIntegerParam('project_id'); $timestamp = $this->request->getIntegerParam('timestamp'); - if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { - $this->response->text('Forbidden', 403); + if (! $project_id || ! $this->request->isAjax()) { + return $this->response->status(403); } if (! $this->project->isModifiedSince($project_id, $timestamp)) { return $this->response->status(304); } - $this->response->html($this->renderBoard($project_id)); + return $this->response->html($this->renderBoard($project_id)); } /** @@ -126,14 +118,10 @@ class Board extends Base */ public function reload() { - if (! $this->request->isAjax()) { - return $this->response->status(403); - } - $project_id = $this->request->getIntegerParam('project_id'); - if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { - $this->response->text('Forbidden', 403); + if (! $project_id || ! $this->request->isAjax()) { + return $this->response->status(403); } $values = $this->request->getJson(); diff --git a/sources/app/Controller/BoardPopover.php b/sources/app/Controller/BoardPopover.php index 51ec9bc..a214439 100644 --- a/sources/app/Controller/BoardPopover.php +++ b/sources/app/Controller/BoardPopover.php @@ -98,4 +98,39 @@ class BoardPopover extends Base 'redirect' => 'board', ))); } + + /** + * Confirmation before to close all column tasks + * + * @access public + */ + public function confirmCloseColumnTasks() + { + $project = $this->getProject(); + $column_id = $this->request->getIntegerParam('column_id'); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->response->html($this->template->render('board/popover_close_all_tasks_column', array( + 'project' => $project, + 'nb_tasks' => $this->taskFinder->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id), + 'column' => $this->board->getColumnTitleById($column_id), + 'swimlane' => $this->swimlane->getNameById($swimlane_id) ?: t($project['default_swimlane']), + 'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id), + ))); + } + + /** + * Close all column tasks + * + * @access public + */ + public function closeColumnTasks() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $this->taskStatus->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']); + $this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->board->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane']))); + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); + } } diff --git a/sources/app/Controller/Captcha.php b/sources/app/Controller/Captcha.php new file mode 100644 index 0000000..fcf081e --- /dev/null +++ b/sources/app/Controller/Captcha.php @@ -0,0 +1,29 @@ +response->contentType('image/jpeg'); + + $builder = new CaptchaBuilder; + $builder->build(); + $this->sessionStorage->captcha = $builder->getPhrase(); + $builder->output(); + } +} diff --git a/sources/app/Controller/Category.php b/sources/app/Controller/Category.php index 9864348..a0af413 100644 --- a/sources/app/Controller/Category.php +++ b/sources/app/Controller/Category.php @@ -57,7 +57,7 @@ class Category extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->category->validateCreation($values); + list($valid, $errors) = $this->categoryValidator->validateCreation($values); if ($valid) { if ($this->category->create($values)) { @@ -99,7 +99,7 @@ class Category extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->category->validateModification($values); + list($valid, $errors) = $this->categoryValidator->validateModification($values); if ($valid) { if ($this->category->update($values)) { diff --git a/sources/app/Controller/Column.php b/sources/app/Controller/Column.php index b484fe1..1ce575d 100644 --- a/sources/app/Controller/Column.php +++ b/sources/app/Controller/Column.php @@ -51,7 +51,7 @@ class Column extends Base $values['title['.$column_id.']'] = $column_title; } - list($valid, $errors) = $this->board->validateCreation($data); + list($valid, $errors) = $this->columnValidator->validateCreation($data); if ($valid) { if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) { @@ -94,7 +94,7 @@ class Column extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->board->validateModification($values); + list($valid, $errors) = $this->columnValidator->validateModification($values); if ($valid) { if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) { diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/Comment.php index 54339e4..a608dd1 100644 --- a/sources/app/Controller/Comment.php +++ b/sources/app/Controller/Comment.php @@ -78,7 +78,7 @@ class Comment extends Base $values = $this->request->getValues(); $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - list($valid, $errors) = $this->comment->validateCreation($values); + list($valid, $errors) = $this->commentValidator->validateCreation($values); if ($valid) { if ($this->comment->create($values)) { @@ -127,7 +127,7 @@ class Comment extends Base $comment = $this->getComment(); $values = $this->request->getValues(); - list($valid, $errors) = $this->comment->validateModification($values); + list($valid, $errors) = $this->commentValidator->validateModification($values); if ($valid) { if ($this->comment->update($values)) { diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/Config.php index c813c79..4aee855 100644 --- a/sources/app/Controller/Config.php +++ b/sources/app/Controller/Config.php @@ -20,7 +20,7 @@ class Config extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['values'] = $this->config->getAll(); $params['errors'] = array(); $params['config_content_for_layout'] = $this->template->render($template, $params); @@ -40,6 +40,9 @@ class Config extends Base $values = $this->request->getValues(); switch ($redirect) { + case 'application': + $values += array('password_reset' => 0); + break; case 'project': $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0); break; diff --git a/sources/app/Controller/Currency.php b/sources/app/Controller/Currency.php index 89e3856..42e404f 100644 --- a/sources/app/Controller/Currency.php +++ b/sources/app/Controller/Currency.php @@ -20,7 +20,7 @@ class Currency extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['config_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('config/layout', $params); @@ -38,7 +38,7 @@ class Currency extends Base 'values' => $values, 'errors' => $errors, 'rates' => $this->currency->getAll(), - 'currencies' => $this->config->getCurrencies(), + 'currencies' => $this->currency->getCurrencies(), 'title' => t('Settings').' > '.t('Currency rates'), ))); } @@ -51,7 +51,7 @@ class Currency extends Base public function create() { $values = $this->request->getValues(); - list($valid, $errors) = $this->currency->validate($values); + list($valid, $errors) = $this->currencyValidator->validateCreation($values); if ($valid) { if ($this->currency->create($values['currency'], $values['rate'])) { diff --git a/sources/app/Controller/Customfilter.php b/sources/app/Controller/Customfilter.php index 12cc8e7..1b43f1d 100644 --- a/sources/app/Controller/Customfilter.php +++ b/sources/app/Controller/Customfilter.php @@ -42,7 +42,7 @@ class Customfilter extends Base $values = $this->request->getValues(); $values['user_id'] = $this->userSession->getId(); - list($valid, $errors) = $this->customFilter->validateCreation($values); + list($valid, $errors) = $this->customFilterValidator->validateCreation($values); if ($valid) { if ($this->customFilter->create($values)) { @@ -121,7 +121,7 @@ class Customfilter extends Base $values += array('append' => 0); } - list($valid, $errors) = $this->customFilter->validateModification($values); + list($valid, $errors) = $this->customFilterValidator->validateModification($values); if ($valid) { if ($this->customFilter->update($values)) { diff --git a/sources/app/Controller/Doc.php b/sources/app/Controller/Doc.php index 08561aa..a233b12 100644 --- a/sources/app/Controller/Doc.php +++ b/sources/app/Controller/Doc.php @@ -53,7 +53,7 @@ class Doc extends Base } $this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), ))); } } diff --git a/sources/app/Controller/Gantt.php b/sources/app/Controller/Gantt.php index f3954a2..ac0e6fa 100644 --- a/sources/app/Controller/Gantt.php +++ b/sources/app/Controller/Gantt.php @@ -26,7 +26,7 @@ class Gantt extends Base $this->response->html($this->template->layout('gantt/projects', array( 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(), 'title' => t('Gantt chart for all projects'), - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), ))); } diff --git a/sources/app/Controller/Group.php b/sources/app/Controller/Group.php index 395a954..e952c0e 100644 --- a/sources/app/Controller/Group.php +++ b/sources/app/Controller/Group.php @@ -25,7 +25,7 @@ class Group extends Base ->calculate(); $this->response->html($this->template->layout('group/index', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'title' => t('Groups').' ('.$paginator->getTotal().')', 'paginator' => $paginator, ))); @@ -42,14 +42,14 @@ class Group extends Base $group = $this->group->getById($group_id); $paginator = $this->paginator - ->setUrl('group', 'users') + ->setUrl('group', 'users', array('group_id' => $group_id)) ->setMax(30) ->setOrder('username') ->setQuery($this->groupMember->getQuery($group_id)) ->calculate(); $this->response->html($this->template->layout('group/users', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')', 'paginator' => $paginator, 'group' => $group, @@ -64,7 +64,7 @@ class Group extends Base public function create(array $values = array(), array $errors = array()) { $this->response->html($this->template->layout('group/create', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'errors' => $errors, 'values' => $values, 'title' => t('New group') @@ -79,7 +79,7 @@ class Group extends Base public function save() { $values = $this->request->getValues(); - list($valid, $errors) = $this->group->validateCreation($values); + list($valid, $errors) = $this->groupValidator->validateCreation($values); if ($valid) { if ($this->group->create($values['name']) !== false) { @@ -105,7 +105,7 @@ class Group extends Base } $this->response->html($this->template->layout('group/edit', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'errors' => $errors, 'values' => $values, 'title' => t('Edit group') @@ -120,7 +120,7 @@ class Group extends Base public function update() { $values = $this->request->getValues(); - list($valid, $errors) = $this->group->validateModification($values); + list($valid, $errors) = $this->groupValidator->validateModification($values); if ($valid) { if ($this->group->update($values) !== false) { @@ -149,7 +149,7 @@ class Group extends Base } $this->response->html($this->template->layout('group/associate', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)), 'group' => $group, 'errors' => $errors, diff --git a/sources/app/Controller/Link.php b/sources/app/Controller/Link.php index 33ec668..d52d1f9 100644 --- a/sources/app/Controller/Link.php +++ b/sources/app/Controller/Link.php @@ -21,7 +21,7 @@ class Link extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['config_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('config/layout', $params); @@ -67,7 +67,7 @@ class Link extends Base public function save() { $values = $this->request->getValues(); - list($valid, $errors) = $this->link->validateCreation($values); + list($valid, $errors) = $this->linkValidator->validateCreation($values); if ($valid) { if ($this->link->create($values['label'], $values['opposite_label']) !== false) { @@ -108,7 +108,7 @@ class Link extends Base public function update() { $values = $this->request->getValues(); - list($valid, $errors) = $this->link->validateModification($values); + list($valid, $errors) = $this->linkValidator->validateModification($values); if ($valid) { if ($this->link->update($values)) { diff --git a/sources/app/Controller/PasswordReset.php b/sources/app/Controller/PasswordReset.php new file mode 100644 index 0000000..23567c9 --- /dev/null +++ b/sources/app/Controller/PasswordReset.php @@ -0,0 +1,120 @@ +checkActivation(); + + $this->response->html($this->template->layout('password_reset/create', array( + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + ))); + } + + /** + * Validate and send the email + */ + public function save() + { + $this->checkActivation(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->passwordResetValidator->validateCreation($values); + + if ($valid) { + $this->sendEmail($values['username']); + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + + $this->create($values, $errors); + } + + /** + * Show the form to set a new password + */ + public function change(array $values = array(), array $errors = array()) + { + $this->checkActivation(); + + $token = $this->request->getStringParam('token'); + $user_id = $this->passwordReset->getUserIdByToken($token); + + if ($user_id !== false) { + $this->response->html($this->template->layout('password_reset/change', array( + 'token' => $token, + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + ))); + } + + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + + /** + * Set the new password + */ + public function update() + { + $this->checkActivation(); + + $token = $this->request->getStringParam('token'); + $values = $this->request->getValues(); + list($valid, $errors) = $this->passwordResetValidator->validateModification($values); + + if ($valid) { + $user_id = $this->passwordReset->getUserIdByToken($token); + + if ($user_id !== false) { + $this->user->update(array('id' => $user_id, 'password' => $values['password'])); + $this->passwordReset->disable($user_id); + } + + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + + $this->change($values, $errors); + } + + /** + * Send the email + */ + private function sendEmail($username) + { + $token = $this->passwordReset->create($username); + + if ($token !== false) { + $user = $this->user->getByUsername($username); + + $this->emailClient->send( + $user['email'], + $user['name'] ?: $user['username'], + t('Password Reset for Kanboard'), + $this->template->render('password_reset/email', array('token' => $token)) + ); + } + } + + /** + * Check feature availability + */ + private function checkActivation() + { + if ($this->config->get('password_reset', 0) == 0) { + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + } +} diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php index 80c95aa..27c827d 100644 --- a/sources/app/Controller/Project.php +++ b/sources/app/Controller/Project.php @@ -33,7 +33,7 @@ class Project extends Base ->calculate(); $this->response->html($this->template->layout('project/index', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'paginator' => $paginator, 'nb_projects' => $nb_projects, 'title' => t('Projects').' ('.$nb_projects.')' @@ -169,7 +169,7 @@ class Project extends Base } } - list($valid, $errors) = $this->project->validateModification($values); + list($valid, $errors) = $this->projectValidator->validateModification($values); if ($valid) { if ($this->project->update($values)) { @@ -302,7 +302,7 @@ class Project extends Base $is_private = isset($values['is_private']) && $values['is_private'] == 1; $this->response->html($this->template->layout('project/new', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'values' => $values, 'errors' => $errors, 'is_private' => $is_private, @@ -329,7 +329,7 @@ class Project extends Base public function save() { $values = $this->request->getValues(); - list($valid, $errors) = $this->project->validateCreation($values); + list($valid, $errors) = $this->projectValidator->validateCreation($values); if ($valid) { $project_id = $this->project->create($values, $this->userSession->getId(), true); diff --git a/sources/app/Controller/Projectuser.php b/sources/app/Controller/Projectuser.php index 3459576..806ede7 100644 --- a/sources/app/Controller/Projectuser.php +++ b/sources/app/Controller/Projectuser.php @@ -24,7 +24,7 @@ class Projectuser extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['content_for_sublayout'] = $this->template->render($template, $params); $params['filter'] = array('user_id' => $params['user_id']); diff --git a/sources/app/Controller/Subtask.php b/sources/app/Controller/Subtask.php index c93b637..caaaa85 100644 --- a/sources/app/Controller/Subtask.php +++ b/sources/app/Controller/Subtask.php @@ -63,7 +63,7 @@ class Subtask extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subtask->validateCreation($values); + list($valid, $errors) = $this->subtaskValidator->validateCreation($values); if ($valid) { if ($this->subtask->create($values)) { @@ -113,7 +113,7 @@ class Subtask extends Base $this->getSubtask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subtask->validateModification($values); + list($valid, $errors) = $this->subtaskValidator->validateModification($values); if ($valid) { if ($this->subtask->update($values)) { diff --git a/sources/app/Controller/Swimlane.php b/sources/app/Controller/Swimlane.php index 5229621..6641088 100644 --- a/sources/app/Controller/Swimlane.php +++ b/sources/app/Controller/Swimlane.php @@ -60,7 +60,7 @@ class Swimlane extends Base { $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->swimlane->validateCreation($values); + list($valid, $errors) = $this->swimlaneValidator->validateCreation($values); if ($valid) { if ($this->swimlane->create($values)) { @@ -84,7 +84,7 @@ class Swimlane extends Base $project = $this->getProject(); $values = $this->request->getValues() + array('show_default_swimlane' => 0); - list($valid, ) = $this->swimlane->validateDefaultModification($values); + list($valid, ) = $this->swimlaneValidator->validateDefaultModification($values); if ($valid) { if ($this->swimlane->updateDefault($values)) { @@ -126,7 +126,7 @@ class Swimlane extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->swimlane->validateModification($values); + list($valid, $errors) = $this->swimlaneValidator->validateModification($values); if ($valid) { if ($this->swimlane->update($values)) { diff --git a/sources/app/Controller/Tasklink.php b/sources/app/Controller/Tasklink.php index 068bf16..a81d3ee 100644 --- a/sources/app/Controller/Tasklink.php +++ b/sources/app/Controller/Tasklink.php @@ -69,7 +69,7 @@ class Tasklink extends Base $values = $this->request->getValues(); $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - list($valid, $errors) = $this->taskLink->validateCreation($values); + list($valid, $errors) = $this->taskLinkValidator->validateCreation($values); if ($valid) { if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { @@ -125,7 +125,7 @@ class Tasklink extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->taskLink->validateModification($values); + list($valid, $errors) = $this->taskLinkValidator->validateModification($values); if ($valid) { if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) { diff --git a/sources/app/Controller/Twofactor.php b/sources/app/Controller/Twofactor.php index aeb13ac..8dbfcf6 100644 --- a/sources/app/Controller/Twofactor.php +++ b/sources/app/Controller/Twofactor.php @@ -23,7 +23,7 @@ class Twofactor extends User } /** - * Index + * Show form to disable/enable 2FA * * @access public */ @@ -31,54 +31,45 @@ class Twofactor extends User { $user = $this->getUser(); $this->checkCurrentUser($user); - - $provider = $this->authenticationManager->getPostAuthenticationProvider(); - $label = $user['email'] ?: $user['username']; - - $provider->setSecret($user['twofactor_secret']); + unset($this->sessionStorage->twoFactorSecret); $this->response->html($this->layout('twofactor/index', array( 'user' => $user, - 'qrcode_url' => $user['twofactor_activated'] == 1 ? $provider->getQrCodeUrl($label) : '', - 'key_url' => $user['twofactor_activated'] == 1 ? $provider->getKeyUrl($label) : '', + 'provider' => $this->authenticationManager->getPostAuthenticationProvider()->getName(), ))); } /** - * Enable/disable 2FA + * Show page with secret and test form * * @access public */ - public function save() + public function show() { $user = $this->getUser(); $this->checkCurrentUser($user); - $values = $this->request->getValues(); + $label = $user['email'] ?: $user['username']; + $provider = $this->authenticationManager->getPostAuthenticationProvider(); - if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) { - $this->user->update(array( - 'id' => $user['id'], - 'twofactor_activated' => 1, - 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(), - )); + if (! isset($this->sessionStorage->twoFactorSecret)) { + $provider->generateSecret(); + $provider->beforeCode(); + $this->sessionStorage->twoFactorSecret = $provider->getSecret(); } else { - $this->user->update(array( - 'id' => $user['id'], - 'twofactor_activated' => 0, - 'twofactor_secret' => '', - )); + $provider->setSecret($this->sessionStorage->twoFactorSecret); } - // Allow the user to test or disable the feature - $this->userSession->disablePostAuthentication(); - - $this->flash->success(t('User updated successfully.')); - $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + $this->response->html($this->layout('twofactor/show', array( + 'user' => $user, + 'secret' => $this->sessionStorage->twoFactorSecret, + 'qrcode_url' => $provider->getQrCodeUrl($label), + 'key_url' => $provider->getKeyUrl($label), + ))); } /** - * Test code + * Test code and save secret * * @access public */ @@ -91,14 +82,47 @@ class Twofactor extends User $provider = $this->authenticationManager->getPostAuthenticationProvider(); $provider->setCode(empty($values['code']) ? '' : $values['code']); - $provider->setSecret($user['twofactor_secret']); + $provider->setSecret($this->sessionStorage->twoFactorSecret); if ($provider->authenticate()) { $this->flash->success(t('The two factor authentication code is valid.')); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 1, + 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(), + )); + + unset($this->sessionStorage->twoFactorSecret); + $this->userSession->disablePostAuthentication(); + + $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); } else { $this->flash->failure(t('The two factor authentication code is not valid.')); + $this->response->redirect($this->helper->url->to('twofactor', 'show', array('user_id' => $user['id']))); } + } + /** + * Disable 2FA for the current user + * + * @access public + */ + public function deactivate() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 0, + 'twofactor_secret' => '', + )); + + // Allow the user to test or disable the feature + $this->userSession->disablePostAuthentication(); + + $this->flash->success(t('User updated successfully.')); $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); } @@ -135,6 +159,12 @@ class Twofactor extends User */ public function code() { + if (! isset($this->sessionStorage->twoFactorBeforeCodeCalled)) { + $provider = $this->authenticationManager->getPostAuthenticationProvider(); + $provider->beforeCode(); + $this->sessionStorage->twoFactorBeforeCodeCalled = true; + } + $this->response->html($this->template->layout('twofactor/check', array( 'title' => t('Check two factor authentication code'), ))); diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php index aa54864..97e0155 100644 --- a/sources/app/Controller/User.php +++ b/sources/app/Controller/User.php @@ -26,7 +26,7 @@ class User extends Base { $content = $this->template->render($template, $params); $params['user_content_for_layout'] = $content; - $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); if (isset($params['user'])) { $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; @@ -51,12 +51,34 @@ class User extends Base $this->response->html( $this->template->layout('user/index', array( - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'title' => t('Users').' ('.$paginator->getTotal().')', 'paginator' => $paginator, ))); } + /** + * Public user profile + * + * @access public + */ + public function profile() + { + $user = $this->user->getById($this->request->getIntegerParam('user_id')); + + if (empty($user)) { + $this->notfound(); + } + + $this->response->html( + $this->template->layout('user/profile', array( + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + 'title' => $user['name'] ?: $user['username'], + 'user' => $user, + ) + )); + } + /** * Display a form to create a new user * @@ -70,7 +92,7 @@ class User extends Base 'timezones' => $this->config->getTimezones(true), 'languages' => $this->config->getLanguages(true), 'roles' => $this->role->getApplicationRoles(), - 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'projects' => $this->project->getList(), 'errors' => $errors, 'values' => $values + array('role' => Role::APP_USER), @@ -86,7 +108,7 @@ class User extends Base public function save() { $values = $this->request->getValues(); - list($valid, $errors) = $this->user->validateCreation($values); + list($valid, $errors) = $this->userValidator->validateCreation($values); if ($valid) { $project_id = empty($values['project_id']) ? 0 : $values['project_id']; @@ -150,6 +172,20 @@ class User extends Base ))); } + /** + * Display last password reset + * + * @access public + */ + public function passwordReset() + { + $user = $this->getUser(); + $this->response->html($this->layout('user/password_reset', array( + 'tokens' => $this->passwordReset->getAll($user['id']), + 'user' => $user, + ))); + } + /** * Display last connections * @@ -293,7 +329,7 @@ class User extends Base if ($this->request->isPost()) { $values = $this->request->getValues(); - list($valid, $errors) = $this->user->validatePasswordModification($values); + list($valid, $errors) = $this->userValidator->validatePasswordModification($values); if ($valid) { if ($this->user->update($values)) { @@ -335,7 +371,7 @@ class User extends Base } } - list($valid, $errors) = $this->user->validateModification($values); + list($valid, $errors) = $this->userValidator->validateModification($values); if ($valid) { if ($this->user->update($values)) { @@ -373,7 +409,7 @@ class User extends Base if ($this->request->isPost()) { $values = $this->request->getValues() + array('disable_login_form' => 0, 'is_ldap_user' => 0); - list($valid, $errors) = $this->user->validateModification($values); + list($valid, $errors) = $this->userValidator->validateModification($values); if ($valid) { if ($this->user->update($values)) { diff --git a/sources/app/Controller/UserHelper.php b/sources/app/Controller/UserHelper.php index f164d0a..041ed2c 100644 --- a/sources/app/Controller/UserHelper.php +++ b/sources/app/Controller/UserHelper.php @@ -21,4 +21,17 @@ class UserHelper extends Base $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format(); $this->response->json($users); } + + /** + * User mention autocompletion (Ajax) + * + * @access public + */ + public function mention() + { + $project_id = $this->request->getStringParam('project_id'); + $query = $this->request->getStringParam('q'); + $users = $this->projectPermission->findUsernames($project_id, $query); + $this->response->json($users); + } } diff --git a/sources/app/Controller/Webhook.php b/sources/app/Controller/Webhook.php index a7e9bde..0eafe3e 100644 --- a/sources/app/Controller/Webhook.php +++ b/sources/app/Controller/Webhook.php @@ -39,57 +39,4 @@ class Webhook extends Base $this->response->text('FAILED'); } - - /** - * Handle Github webhooks - * - * @access public - */ - public function github() - { - $this->checkWebhookToken(); - - $this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id')); - - $result = $this->githubWebhook->parsePayload( - $this->request->getHeader('X-Github-Event'), - $this->request->getJson() - ); - - echo $result ? 'PARSED' : 'IGNORED'; - } - - /** - * Handle Gitlab webhooks - * - * @access public - */ - public function gitlab() - { - $this->checkWebhookToken(); - - $this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id')); - $result = $this->gitlabWebhook->parsePayload($this->request->getJson()); - - echo $result ? 'PARSED' : 'IGNORED'; - } - - /** - * Handle Bitbucket webhooks - * - * @access public - */ - public function bitbucket() - { - $this->checkWebhookToken(); - - $this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id')); - - $result = $this->bitbucketWebhook->parsePayload( - $this->request->getHeader('X-Event-Key'), - $this->request->getJson() - ); - - echo $result ? 'PARSED' : 'IGNORED'; - } } diff --git a/sources/app/Core/Action/ActionManager.php b/sources/app/Core/Action/ActionManager.php new file mode 100644 index 0000000..f1ea8ab --- /dev/null +++ b/sources/app/Core/Action/ActionManager.php @@ -0,0 +1,142 @@ +actions[$action->getName()] = $action; + return $this; + } + + /** + * Get automatic action instance + * + * @access public + * @param string $name Absolute class name with namespace + * @return ActionBase + */ + public function getAction($name) + { + if (isset($this->actions[$name])) { + return $this->actions[$name]; + } + + throw new RuntimeException('Automatic Action Not Found: '.$name); + } + + /** + * Get available automatic actions + * + * @access public + * @return array + */ + public function getAvailableActions() + { + $actions = array(); + + foreach ($this->actions as $action) { + if (count($action->getEvents()) > 0) { + $actions[$action->getName()] = $action->getDescription(); + } + } + + asort($actions); + + return $actions; + } + + /** + * Get all available action parameters + * + * @access public + * @param array $actions + * @return array + */ + public function getAvailableParameters(array $actions) + { + $params = array(); + + foreach ($actions as $action) { + $currentAction = $this->getAction($action['action_name']); + $params[$currentAction->getName()] = $currentAction->getActionRequiredParameters(); + } + + return $params; + } + + /** + * Get list of compatible events for a given action + * + * @access public + * @param string $name + * @return array + */ + public function getCompatibleEvents($name) + { + $events = array(); + $actionEvents = $this->getAction($name)->getEvents(); + + foreach ($this->eventManager->getAll() as $event => $description) { + if (in_array($event, $actionEvents)) { + $events[$event] = $description; + } + } + + return $events; + } + + /** + * Bind automatic actions to events + * + * @access public + * @return ActionManager + */ + public function attachEvents() + { + if ($this->userSession->isLogged()) { + $actions = $this->action->getAllByUser($this->userSession->getId()); + } else { + $actions = $this->action->getAll(); + } + + foreach ($actions as $action) { + $listener = clone $this->getAction($action['action_name']); + $listener->setProjectId($action['project_id']); + + foreach ($action['params'] as $param_name => $param_value) { + $listener->setParam($param_name, $param_value); + } + + $this->dispatcher->addListener($action['event_name'], array($listener, 'execute')); + } + + return $this; + } +} diff --git a/sources/app/Core/Base.php b/sources/app/Core/Base.php index 2d00e52..2821e5a 100644 --- a/sources/app/Core/Base.php +++ b/sources/app/Core/Base.php @@ -10,7 +10,14 @@ use Pimple\Container; * @package core * @author Frederic Guillot * + * @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic + * @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic + * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic + * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic + * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic + * @property \Kanboard\Core\Action\ActionManager $actionManager * @property \Kanboard\Core\Cache\MemoryCache $memoryCache + * @property \Kanboard\Core\Event\EventManager $eventManager * @property \Kanboard\Core\Group\GroupManager $groupManager * @property \Kanboard\Core\Http\Client $httpClient * @property \Kanboard\Core\Http\OAuth2 $oauth @@ -18,6 +25,7 @@ use Pimple\Container; * @property \Kanboard\Core\Http\Request $request * @property \Kanboard\Core\Http\Response $response * @property \Kanboard\Core\Http\Router $router + * @property \Kanboard\Core\Http\Route $route * @property \Kanboard\Core\Mail\Client $emailClient * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage * @property \Kanboard\Core\Plugin\Hook $hook @@ -42,9 +50,6 @@ use Pimple\Container; * @property \Kanboard\Core\Lexer $lexer * @property \Kanboard\Core\Paginator $paginator * @property \Kanboard\Core\Template $template - * @property \Kanboard\Integration\BitbucketWebhook $bitbucketWebhook - * @property \Kanboard\Integration\GithubWebhook $githubWebhook - * @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter @@ -53,7 +58,7 @@ use Pimple\Container; * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter * @property \Kanboard\Model\Action $action - * @property \Kanboard\Model\Authentication $authentication + * @property \Kanboard\Model\ActionParameter $actionParameter * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category * @property \Kanboard\Model\Color $color @@ -68,6 +73,7 @@ use Pimple\Container; * @property \Kanboard\Model\Link $link * @property \Kanboard\Model\Notification $notification * @property \Kanboard\Model\OverdueNotification $overdueNotification + * @property \Kanboard\Model\PasswordReset $passwordReset * @property \Kanboard\Model\Project $project * @property \Kanboard\Model\ProjectActivity $projectActivity * @property \Kanboard\Model\ProjectAnalytic $projectAnalytic @@ -77,6 +83,7 @@ use Pimple\Container; * @property \Kanboard\Model\ProjectMetadata $projectMetadata * @property \Kanboard\Model\ProjectPermission $projectPermission * @property \Kanboard\Model\ProjectUserRole $projectUserRole + * @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter * @property \Kanboard\Model\ProjectGroupRole $projectGroupRole * @property \Kanboard\Model\ProjectNotification $projectNotification * @property \Kanboard\Model\ProjectNotificationType $projectNotificationType @@ -98,20 +105,36 @@ use Pimple\Container; * @property \Kanboard\Model\TaskPermission $taskPermission * @property \Kanboard\Model\TaskPosition $taskPosition * @property \Kanboard\Model\TaskStatus $taskStatus - * @property \Kanboard\Model\TaskValidator $taskValidator * @property \Kanboard\Model\TaskMetadata $taskMetadata * @property \Kanboard\Model\Transition $transition * @property \Kanboard\Model\User $user * @property \Kanboard\Model\UserImport $userImport * @property \Kanboard\Model\UserLocking $userLocking + * @property \Kanboard\Model\UserMention $userMention * @property \Kanboard\Model\UserNotification $userNotification * @property \Kanboard\Model\UserNotificationType $userNotificationType * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification * @property \Kanboard\Model\UserMetadata $userMetadata * @property \Kanboard\Model\Webhook $webhook + * @property \Kanboard\Validator\ActionValidator $actionValidator + * @property \Kanboard\Validator\AuthValidator $authValidator + * @property \Kanboard\Validator\ColumnValidator $columnValidator + * @property \Kanboard\Validator\CategoryValidator $categoryValidator + * @property \Kanboard\Validator\ColumnValidator $columnValidator + * @property \Kanboard\Validator\CommentValidator $commentValidator + * @property \Kanboard\Validator\CurrencyValidator $currencyValidator + * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator + * @property \Kanboard\Validator\GroupValidator $groupValidator + * @property \Kanboard\Validator\LinkValidator $linkValidator + * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator + * @property \Kanboard\Validator\ProjectValidator $projectValidator + * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator + * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator + * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator + * @property \Kanboard\Validator\TaskValidator $taskValidator + * @property \Kanboard\Validator\UserValidator $userValidator * @property \Psr\Log\LoggerInterface $logger - * @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */ diff --git a/sources/app/Core/Event/EventManager.php b/sources/app/Core/Event/EventManager.php new file mode 100644 index 0000000..8d76bfc --- /dev/null +++ b/sources/app/Core/Event/EventManager.php @@ -0,0 +1,62 @@ +events[$event] = $description; + return $this; + } + + /** + * Get the list of events and description that can be used from the user interface + * + * @access public + * @return array + */ + public function getAll() + { + $events = array( + TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'), + Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), + Task::EVENT_UPDATE => t('Task modification'), + Task::EVENT_CREATE => t('Task creation'), + Task::EVENT_OPEN => t('Reopen a task'), + Task::EVENT_CLOSE => t('Closing a task'), + Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), + Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), + ); + + $events = array_merge($events, $this->events); + asort($events); + + return $events; + } +} diff --git a/sources/app/Core/Http/Request.php b/sources/app/Core/Http/Request.php index c626f5b..1b3036d 100644 --- a/sources/app/Core/Http/Request.php +++ b/sources/app/Core/Http/Request.php @@ -41,6 +41,16 @@ class Request extends Base $this->cookies = empty($cookies) ? $_COOKIE : $cookies; } + /** + * Set GET parameters + * + * @param array $params + */ + public function setParams(array $params) + { + $this->get = array_merge($this->get, $params); + } + /** * Get query string string parameter * @@ -146,6 +156,29 @@ class Request extends Base return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : ''; } + /** + * Get info of an uploaded file + * + * @access public + * @param string $name Form file name + * @return array + */ + public function getFileInfo($name) + { + return isset($this->files[$name]) ? $this->files[$name] : array(); + } + + /** + * Return HTTP method + * + * @access public + * @return bool + */ + public function getMethod() + { + return $this->getServerVariable('REQUEST_METHOD'); + } + /** * Return true if the HTTP request is sent with the POST method * @@ -154,7 +187,7 @@ class Request extends Base */ public function isPost() { - return isset($this->server['REQUEST_METHOD']) && $this->server['REQUEST_METHOD'] === 'POST'; + return $this->getServerVariable('REQUEST_METHOD') === 'POST'; } /** @@ -203,7 +236,7 @@ class Request extends Base public function getHeader($name) { $name = 'HTTP_'.str_replace('-', '_', strtoupper($name)); - return isset($this->server[$name]) ? $this->server[$name] : ''; + return $this->getServerVariable($name); } /** @@ -214,18 +247,18 @@ class Request extends Base */ public function getRemoteUser() { - return isset($this->server[REVERSE_PROXY_USER_HEADER]) ? $this->server[REVERSE_PROXY_USER_HEADER] : ''; + return $this->getServerVariable(REVERSE_PROXY_USER_HEADER); } /** - * Returns current request's query string, useful for redirecting + * Returns query string * * @access public * @return string */ public function getQueryString() { - return isset($this->server['QUERY_STRING']) ? $this->server['QUERY_STRING'] : ''; + return $this->getServerVariable('QUERY_STRING'); } /** @@ -236,7 +269,7 @@ class Request extends Base */ public function getUri() { - return isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; + return $this->getServerVariable('REQUEST_URI'); } /** @@ -269,7 +302,7 @@ class Request extends Base ); foreach ($keys as $key) { - if (! empty($this->server[$key])) { + if ($this->getServerVariable($key) !== '') { foreach (explode(',', $this->server[$key]) as $ipAddress) { return trim($ipAddress); } @@ -287,6 +320,18 @@ class Request extends Base */ public function getStartTime() { - return isset($this->server['REQUEST_TIME_FLOAT']) ? $this->server['REQUEST_TIME_FLOAT'] : 0; + return $this->getServerVariable('REQUEST_TIME_FLOAT') ?: 0; + } + + /** + * Get server variable + * + * @access public + * @param string $variable + * @return string + */ + public function getServerVariable($variable) + { + return isset($this->server[$variable]) ? $this->server[$variable] : ''; } } diff --git a/sources/app/Core/Http/Response.php b/sources/app/Core/Http/Response.php index fc21401..7fefdde 100644 --- a/sources/app/Core/Http/Response.php +++ b/sources/app/Core/Http/Response.php @@ -60,7 +60,7 @@ class Response extends Base public function status($status_code) { header('Status: '.$status_code); - header($_SERVER['SERVER_PROTOCOL'].' '.$status_code); + header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code); } /** @@ -71,7 +71,7 @@ class Response extends Base */ public function redirect($url) { - if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { + if ($this->request->getServerVariable('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest') { header('X-Ajax-Redirect: '.$url); } else { header('Location: '.$url); @@ -220,7 +220,6 @@ class Response extends Base */ public function csp(array $policies = array()) { - $policies['default-src'] = "'self'"; $values = ''; foreach ($policies as $policy => $acl) { diff --git a/sources/app/Core/Http/Route.php b/sources/app/Core/Http/Route.php new file mode 100644 index 0000000..7836146 --- /dev/null +++ b/sources/app/Core/Http/Route.php @@ -0,0 +1,187 @@ +activated = true; + return $this; + } + + /** + * Add route + * + * @access public + * @param string $path + * @param string $controller + * @param string $action + * @param string $plugin + * @return Route + */ + public function addRoute($path, $controller, $action, $plugin = '') + { + if ($this->activated) { + $path = ltrim($path, '/'); + $items = explode('/', $path); + $params = $this->findParams($items); + + $this->paths[] = array( + 'items' => $items, + 'count' => count($items), + 'controller' => $controller, + 'action' => $action, + 'plugin' => $plugin, + ); + + $this->urls[$plugin][$controller][$action][] = array( + 'path' => $path, + 'params' => $params, + 'count' => count($params), + ); + } + + return $this; + } + + /** + * Find a route according to the given path + * + * @access public + * @param string $path + * @return array + */ + public function findRoute($path) + { + $items = explode('/', ltrim($path, '/')); + $count = count($items); + + foreach ($this->paths as $route) { + if ($count === $route['count']) { + $params = array(); + + for ($i = 0; $i < $count; $i++) { + if ($route['items'][$i]{0} === ':') { + $params[substr($route['items'][$i], 1)] = $items[$i]; + } elseif ($route['items'][$i] !== $items[$i]) { + break; + } + } + + if ($i === $count) { + $this->request->setParams($params); + return array( + 'controller' => $route['controller'], + 'action' => $route['action'], + 'plugin' => $route['plugin'], + ); + } + } + } + + return array( + 'controller' => 'app', + 'action' => 'index', + 'plugin' => '', + ); + } + + /** + * Find route url + * + * @access public + * @param string $controller + * @param string $action + * @param array $params + * @param string $plugin + * @return string + */ + public function findUrl($controller, $action, array $params = array(), $plugin = '') + { + if ($plugin === '' && isset($params['plugin'])) { + $plugin = $params['plugin']; + unset($params['plugin']); + } + + if (! isset($this->urls[$plugin][$controller][$action])) { + return ''; + } + + foreach ($this->urls[$plugin][$controller][$action] as $route) { + if (array_diff_key($params, $route['params']) === array()) { + $url = $route['path']; + $i = 0; + + foreach ($params as $variable => $value) { + $url = str_replace(':'.$variable, $value, $url); + $i++; + } + + if ($i === $route['count']) { + return $url; + } + } + } + + return ''; + } + + /** + * Find url params + * + * @access public + * @param array $items + * @return array + */ + public function findParams(array $items) + { + $params = array(); + + foreach ($items as $item) { + if ($item !== '' && $item{0} === ':') { + $params[substr($item, 1)] = true; + } + } + + return $params; + } +} diff --git a/sources/app/Core/Http/Router.php b/sources/app/Core/Http/Router.php index 0080b23..0fe80ec 100644 --- a/sources/app/Core/Http/Router.php +++ b/sources/app/Core/Http/Router.php @@ -6,13 +6,21 @@ use RuntimeException; use Kanboard\Core\Base; /** - * Router class + * Route Dispatcher * * @package http * @author Frederic Guillot */ class Router extends Base { + /** + * Plugin name + * + * @access private + * @var string + */ + private $plugin = ''; + /** * Controller * @@ -30,30 +38,14 @@ class Router extends Base private $action = ''; /** - * Store routes for path lookup - * - * @access private - * @var array - */ - private $paths = array(); - - /** - * Store routes for url lookup - * - * @access private - * @var array - */ - private $urls = array(); - - /** - * Get action + * Get plugin name * * @access public * @return string */ - public function getAction() + public function getPlugin() { - return $this->action; + return $this->plugin; } /** @@ -67,23 +59,32 @@ class Router extends Base return $this->controller; } + /** + * Get action + * + * @access public + * @return string + */ + public function getAction() + { + return $this->action; + } + /** * Get the path to compare patterns * * @access public - * @param string $uri - * @param string $query_string * @return string */ - public function getPath($uri, $query_string = '') + public function getPath() { - $path = substr($uri, strlen($this->helper->url->dir())); + $path = substr($this->request->getUri(), strlen($this->helper->url->dir())); - if (! empty($query_string)) { - $path = substr($path, 0, - strlen($query_string) - 1); + if ($this->request->getQueryString() !== '') { + $path = substr($path, 0, - strlen($this->request->getQueryString()) - 1); } - if (! empty($path) && $path{0} === '/') { + if ($path !== '' && $path{0} === '/') { $path = substr($path, 1); } @@ -91,140 +92,78 @@ class Router extends Base } /** - * Add route + * Find controller/action from the route table or from get arguments * * @access public - * @param string $path - * @param string $controller - * @param string $action - * @param array $params */ - public function addRoute($path, $controller, $action, array $params = array()) + public function dispatch() { - $pattern = explode('/', $path); + $controller = $this->request->getStringParam('controller'); + $action = $this->request->getStringParam('action'); + $plugin = $this->request->getStringParam('plugin'); - $this->paths[] = array( - 'pattern' => $pattern, - 'count' => count($pattern), - 'controller' => $controller, - 'action' => $action, - ); - - $this->urls[$controller][$action][] = array( - 'path' => $path, - 'params' => array_flip($params), - 'count' => count($params), - ); - } - - /** - * Find a route according to the given path - * - * @access public - * @param string $path - * @return array - */ - public function findRoute($path) - { - $parts = explode('/', $path); - $count = count($parts); - - foreach ($this->paths as $route) { - if ($count === $route['count']) { - $params = array(); - - for ($i = 0; $i < $count; $i++) { - if ($route['pattern'][$i]{0} === ':') { - $params[substr($route['pattern'][$i], 1)] = $parts[$i]; - } elseif ($route['pattern'][$i] !== $parts[$i]) { - break; - } - } - - if ($i === $count) { - $_GET = array_merge($_GET, $params); - return array($route['controller'], $route['action']); - } - } + if ($controller === '') { + $route = $this->route->findRoute($this->getPath()); + $controller = $route['controller']; + $action = $route['action']; + $plugin = $route['plugin']; } - return array('app', 'index'); - } + $this->controller = ucfirst($this->sanitize($controller, 'app')); + $this->action = $this->sanitize($action, 'index'); + $this->plugin = ucfirst($this->sanitize($plugin)); - /** - * Find route url - * - * @access public - * @param string $controller - * @param string $action - * @param array $params - * @return string - */ - public function findUrl($controller, $action, array $params = array()) - { - if (! isset($this->urls[$controller][$action])) { - return ''; - } - - foreach ($this->urls[$controller][$action] as $pattern) { - if (array_diff_key($params, $pattern['params']) === array()) { - $url = $pattern['path']; - $i = 0; - - foreach ($params as $variable => $value) { - $url = str_replace(':'.$variable, $value, $url); - $i++; - } - - if ($i === $pattern['count']) { - return $url; - } - } - } - - return ''; + return $this->executeAction(); } /** * Check controller and action parameter * * @access public - * @param string $value Controller or action name - * @param string $default_value Default value if validation fail + * @param string $value + * @param string $default * @return string */ - public function sanitize($value, $default_value) + public function sanitize($value, $default = '') { - return ! preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $default_value : $value; + return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default; } /** - * Find controller/action from the route table or from get arguments + * Execute controller action * - * @access public - * @param string $uri - * @param string $query_string + * @access private */ - public function dispatch($uri, $query_string = '') + private function executeAction() { - if (! empty($_GET['controller']) && ! empty($_GET['action'])) { - $this->controller = $this->sanitize($_GET['controller'], 'app'); - $this->action = $this->sanitize($_GET['action'], 'index'); - $plugin = ! empty($_GET['plugin']) ? $this->sanitize($_GET['plugin'], '') : ''; - } else { - list($this->controller, $this->action) = $this->findRoute($this->getPath($uri, $query_string)); // TODO: add plugin for routes - $plugin = ''; + $class = $this->getControllerClassName(); + + if (! class_exists($class)) { + throw new RuntimeException('Controller not found'); } - $class = '\Kanboard\\'; - $class .= empty($plugin) ? 'Controller\\'.ucfirst($this->controller) : 'Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($this->controller); - - if (! class_exists($class) || ! method_exists($class, $this->action)) { - throw new RuntimeException('Controller or method not found for the given url!'); + if (! method_exists($class, $this->action)) { + throw new RuntimeException('Action not implemented'); } $instance = new $class($this->container); - $instance->beforeAction($this->controller, $this->action); + $instance->beforeAction(); $instance->{$this->action}(); + return $instance; + } + + /** + * Get controller class name + * + * @access private + * @return string + */ + private function getControllerClassName() + { + if ($this->plugin !== '') { + return '\Kanboard\Plugin\\'.$this->plugin.'\Controller\\'.$this->controller; + } + + return '\Kanboard\Controller\\'.$this->controller; } } diff --git a/sources/app/Core/Ldap/Client.php b/sources/app/Core/Ldap/Client.php index 5d481cd..63149ae 100644 --- a/sources/app/Core/Ldap/Client.php +++ b/sources/app/Core/Ldap/Client.php @@ -15,10 +15,10 @@ class Client /** * LDAP resource * - * @access private + * @access protected * @var resource */ - private $ldap; + protected $ldap; /** * Establish LDAP connection diff --git a/sources/app/Core/Ldap/Entries.php b/sources/app/Core/Ldap/Entries.php index 3487541..0e77934 100644 --- a/sources/app/Core/Ldap/Entries.php +++ b/sources/app/Core/Ldap/Entries.php @@ -13,10 +13,10 @@ class Entries /** * LDAP entries * - * @access private + * @access protected * @var array */ - private $entries = array(); + protected $entries = array(); /** * Constructor diff --git a/sources/app/Core/Ldap/Entry.php b/sources/app/Core/Ldap/Entry.php index e67dd62..0b99a58 100644 --- a/sources/app/Core/Ldap/Entry.php +++ b/sources/app/Core/Ldap/Entry.php @@ -13,10 +13,10 @@ class Entry /** * LDAP entry * - * @access private + * @access protected * @var array */ - private $entry = array(); + protected $entry = array(); /** * Constructor diff --git a/sources/app/Core/Ldap/Group.php b/sources/app/Core/Ldap/Group.php index e11e8ec..634d47e 100644 --- a/sources/app/Core/Ldap/Group.php +++ b/sources/app/Core/Ldap/Group.php @@ -16,10 +16,10 @@ class Group /** * Query * - * @access private + * @access protected * @var Query */ - private $query; + protected $query; /** * Constructor @@ -43,7 +43,8 @@ class Group */ public static function getGroups(Client $client, $query) { - $self = new self(new Query($client)); + $className = get_called_class(); + $self = new $className(new Query($client)); return $self->find($query); } diff --git a/sources/app/Core/Ldap/Query.php b/sources/app/Core/Ldap/Query.php index 6ca4bc9..e03495e 100644 --- a/sources/app/Core/Ldap/Query.php +++ b/sources/app/Core/Ldap/Query.php @@ -13,18 +13,18 @@ class Query /** * LDAP client * - * @access private + * @access protected * @var Client */ - private $client = null; + protected $client = null; /** * Query result * - * @access private + * @access protected * @var array */ - private $entries = array(); + protected $entries = array(); /** * Constructor diff --git a/sources/app/Core/Ldap/User.php b/sources/app/Core/Ldap/User.php index 0c9df63..d36d6f3 100644 --- a/sources/app/Core/Ldap/User.php +++ b/sources/app/Core/Ldap/User.php @@ -17,10 +17,10 @@ class User /** * Query * - * @access private + * @access protected * @var Query */ - private $query; + protected $query; /** * Constructor @@ -40,11 +40,12 @@ class User * @access public * @param Client $client * @param string $username - * @return array + * @return LdapUserProvider */ public static function getUser(Client $client, $username) { - $self = new self(new Query($client)); + $className = get_called_class(); + $self = new $className(new Query($client)); return $self->find($self->getLdapUserPattern($username)); } diff --git a/sources/app/Core/Mail/Client.php b/sources/app/Core/Mail/Client.php index 7b4268b..e1f3169 100644 --- a/sources/app/Core/Mail/Client.php +++ b/sources/app/Core/Mail/Client.php @@ -45,19 +45,21 @@ class Client extends Base */ public function send($email, $name, $subject, $html) { - $this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); + if (! empty($email)) { + $this->logger->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); - $start_time = microtime(true); - $author = 'Kanboard'; + $start_time = microtime(true); + $author = 'Kanboard'; - if ($this->userSession->isLogged()) { - $author = e('%s via Kanboard', $this->helper->user->getFullname()); - } + if ($this->userSession->isLogged()) { + $author = e('%s via Kanboard', $this->helper->user->getFullname()); + } - $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author); + $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author); - if (DEBUG) { - $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + if (DEBUG) { + $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + } } return $this; diff --git a/sources/app/Core/Markdown.php b/sources/app/Core/Markdown.php index f08c486..827fd0d 100644 --- a/sources/app/Core/Markdown.php +++ b/sources/app/Core/Markdown.php @@ -3,7 +3,7 @@ namespace Kanboard\Core; use Parsedown; -use Kanboard\Helper\Url; +use Pimple\Container; /** * Specific Markdown rules for Kanboard @@ -14,22 +14,51 @@ use Kanboard\Helper\Url; */ class Markdown extends Parsedown { - private $link; - private $helper; + /** + * Link params for tasks + * + * @access private + * @var array + */ + private $link = array(); - public function __construct($link, Url $helper) + /** + * Container + * + * @access private + * @var Container + */ + private $container; + + /** + * Constructor + * + * @access public + * @param Container $container + * @param array $link + */ + public function __construct(Container $container, array $link) { $this->link = $link; - $this->helper = $helper; + $this->container = $container; $this->InlineTypes['#'][] = 'TaskLink'; - $this->inlineMarkerList .= '#'; + $this->InlineTypes['@'][] = 'UserLink'; + $this->inlineMarkerList .= '#@'; } - protected function inlineTaskLink($Excerpt) + /** + * Handle Task Links + * + * Replace "#123" by a link to the task + * + * @access public + * @param array $Excerpt + * @return array + */ + protected function inlineTaskLink(array $Excerpt) { - // Replace task #123 by a link to the task if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) { - $url = $this->helper->href( + $url = $this->container['helper']->url->href( $this->link['controller'], $this->link['action'], $this->link['params'] + array('task_id' => $matches[1]) @@ -40,7 +69,38 @@ class Markdown extends Parsedown 'element' => array( 'name' => 'a', 'text' => $matches[0], - 'attributes' => array('href' => $url))); + 'attributes' => array('href' => $url) + ), + ); + } + } + + /** + * Handle User Mentions + * + * Replace "@username" by a link to the user + * + * @access public + * @param array $Excerpt + * @return array + */ + protected function inlineUserLink(array $Excerpt) + { + if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) { + $user_id = $this->container['user']->getIdByUsername($matches[1]); + + if (! empty($user_id)) { + $url = $this->container['helper']->url->href('user', 'profile', array('user_id' => $user_id)); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[0], + 'attributes' => array('href' => $url, 'class' => 'user-mention-link'), + ), + ); + } } } } diff --git a/sources/app/Core/Security/AccessMap.php b/sources/app/Core/Security/AccessMap.php index 02a4ca4..f34c4b0 100644 --- a/sources/app/Core/Security/AccessMap.php +++ b/sources/app/Core/Security/AccessMap.php @@ -86,6 +86,26 @@ class AccessMap return $roles; } + /** + * Get the highest role from a list + * + * @access public + * @param array $roles + * @return string + */ + public function getHighestRole(array $roles) + { + $rank = array(); + + foreach ($roles as $role) { + $rank[$role] = count($this->getRoleHierarchy($role)); + } + + asort($rank); + + return key($rank); + } + /** * Add new access rules * diff --git a/sources/app/Core/Security/PostAuthenticationProviderInterface.php b/sources/app/Core/Security/PostAuthenticationProviderInterface.php index 88fc2fe..3f628bb 100644 --- a/sources/app/Core/Security/PostAuthenticationProviderInterface.php +++ b/sources/app/Core/Security/PostAuthenticationProviderInterface.php @@ -10,6 +10,13 @@ namespace Kanboard\Core\Security; */ interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface { + /** + * Called only one time before to prompt the user for pin code + * + * @access public + */ + public function beforeCode(); + /** * Set user pin-code * @@ -18,6 +25,14 @@ interface PostAuthenticationProviderInterface extends AuthenticationProviderInte */ public function setCode($code); + /** + * Generate secret if necessary + * + * @access public + * @return string + */ + public function generateSecret(); + /** * Set secret token (fetched from user profile) * diff --git a/sources/app/Core/Security/Role.php b/sources/app/Core/Security/Role.php index 85d8574..cb45a8a 100644 --- a/sources/app/Core/Security/Role.php +++ b/sources/app/Core/Security/Role.php @@ -50,7 +50,7 @@ class Role } /** - * Get application roles + * Get role name * * @access public * @param string $role diff --git a/sources/app/Core/Security/Token.php b/sources/app/Core/Security/Token.php index 9fd2d02..cbd784a 100644 --- a/sources/app/Core/Security/Token.php +++ b/sources/app/Core/Security/Token.php @@ -21,15 +21,7 @@ class Token extends Base */ public static function getToken() { - if (function_exists('random_bytes')) { - return bin2hex(random_bytes(30)); - } elseif (function_exists('openssl_random_pseudo_bytes')) { - return bin2hex(openssl_random_pseudo_bytes(30)); - } elseif (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { - return hash('sha256', file_get_contents('/dev/urandom', false, null, 0, 30)); - } - - return hash('sha256', uniqid(mt_rand(), true)); + return bin2hex(random_bytes(30)); } /** diff --git a/sources/app/Core/Session/SessionManager.php b/sources/app/Core/Session/SessionManager.php index 776d02d..4f9f2c0 100644 --- a/sources/app/Core/Session/SessionManager.php +++ b/sources/app/Core/Session/SessionManager.php @@ -100,7 +100,7 @@ class SessionManager extends Base ini_set('session.use_strict_mode', '1'); // Better session hash - ini_set('session.hash_function', 'sha512'); + ini_set('session.hash_function', '1'); // 'sha512' is not compatible with FreeBSD, only MD5 '0' and SHA-1 '1' seems to work ini_set('session.hash_bits_per_character', 6); // Set an additional entropy diff --git a/sources/app/Core/Session/SessionStorage.php b/sources/app/Core/Session/SessionStorage.php index f55a7ee..667d925 100644 --- a/sources/app/Core/Session/SessionStorage.php +++ b/sources/app/Core/Session/SessionStorage.php @@ -8,7 +8,6 @@ namespace Kanboard\Core\Session; * @package session * @author Frederic Guillot * - * @property array $config * @property array $user * @property array $flash * @property array $csrf @@ -20,6 +19,8 @@ namespace Kanboard\Core\Session; * @property bool $hasSubtaskInProgress * @property bool $hasRememberMe * @property bool $boardCollapsed + * @property bool $twoFactorBeforeCodeCalled + * @property string $twoFactorSecret */ class SessionStorage { diff --git a/sources/app/Core/Template.php b/sources/app/Core/Template.php index ce2884a..8ded6f7 100644 --- a/sources/app/Core/Template.php +++ b/sources/app/Core/Template.php @@ -18,6 +18,50 @@ class Template extends Helper */ private $overrides = array(); + /** + * Rendering start time + * + * @access private + * @var float + */ + private $startTime = 0; + + /** + * Total rendering time + * + * @access private + * @var float + */ + private $renderingTime = 0; + + /** + * Method executed before the rendering + * + * @access protected + * @param string $template + */ + protected function beforeRender($template) + { + if (DEBUG) { + $this->startTime = microtime(true); + } + } + + /** + * Method executed after the rendering + * + * @access protected + * @param string $template + */ + protected function afterRender($template) + { + if (DEBUG) { + $duration = microtime(true) - $this->startTime; + $this->renderingTime += $duration; + $this->container['logger']->debug('Rendering '.$template.' in '.$duration.'s, total='.$this->renderingTime); + } + } + /** * Render a template * @@ -32,11 +76,16 @@ class Template extends Helper */ public function render($__template_name, array $__template_args = array()) { - extract($__template_args); + $this->beforeRender($__template_name); + extract($__template_args); ob_start(); include $this->getTemplateFile($__template_name); - return ob_get_clean(); + $html = ob_get_clean(); + + $this->afterRender($__template_name); + + return $html; } /** diff --git a/sources/app/Group/LdapBackendGroupProvider.php b/sources/app/Group/LdapBackendGroupProvider.php index d65a986..cad732c 100644 --- a/sources/app/Group/LdapBackendGroupProvider.php +++ b/sources/app/Group/LdapBackendGroupProvider.php @@ -45,7 +45,7 @@ class LdapBackendGroupProvider extends Base implements GroupBackendProviderInter */ public function getLdapGroupPattern($input) { - if (empty(LDAP_GROUP_FILTER)) { + if (LDAP_GROUP_FILTER === '') { throw new LogicException('LDAP group filter empty, check the parameter LDAP_GROUP_FILTER'); } diff --git a/sources/app/Helper/App.php b/sources/app/Helper/App.php index 33729f2..0593795 100644 --- a/sources/app/Helper/App.php +++ b/sources/app/Helper/App.php @@ -2,14 +2,63 @@ namespace Kanboard\Helper; +use Kanboard\Core\Base; + /** * Application helpers * * @package helper * @author Frederic Guillot */ -class App extends \Kanboard\Core\Base +class App extends Base { + /** + * Get config variable + * + * @access public + * @param string $param + * @return mixed + */ + public function config($param) + { + return $this->config->get($param); + } + + /** + * Make sidebar menu active + * + * @access public + * @param string $controller + * @param string $action + * @param string $plugin + * @return string + */ + public function checkMenuSelection($controller, $action = '', $plugin = '') + { + $result = strtolower($this->getRouterController()) === strtolower($controller); + + if ($result && $action !== '') { + $result = strtolower($this->getRouterAction()) === strtolower($action); + } + + if ($result && $plugin !== '') { + $result = strtolower($this->getPluginName()) === strtolower($plugin); + } + + return $result ? 'class="active"' : ''; + } + + /** + * Get plugin name from route + * + * @access public + * @return string + */ + public function getPluginName() + { + return $this->router->getPlugin(); + } + /** * Get router controller * diff --git a/sources/app/Helper/Text.php b/sources/app/Helper/Text.php index d2075fe..59bfd99 100644 --- a/sources/app/Helper/Text.php +++ b/sources/app/Helper/Text.php @@ -3,14 +3,15 @@ namespace Kanboard\Helper; use Kanboard\Core\Markdown; +use Kanboard\Core\Base; /** - * Text helpers + * Text Helpers * * @package helper * @author Frederic Guillot */ -class Text extends \Kanboard\Core\Base +class Text extends Base { /** * Markdown transformation @@ -21,7 +22,7 @@ class Text extends \Kanboard\Core\Base */ public function markdown($text, array $link = array()) { - $parser = new Markdown($link, $this->helper->url); + $parser = new Markdown($this->container, $link); $parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML); return $parser->text($text); } diff --git a/sources/app/Helper/Url.php b/sources/app/Helper/Url.php index 6ada806..720297c 100644 --- a/sources/app/Helper/Url.php +++ b/sources/app/Helper/Url.php @@ -103,8 +103,8 @@ class Url extends Base */ public function dir() { - if (empty($this->directory) && isset($_SERVER['REQUEST_METHOD'])) { - $this->directory = str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])); + if ($this->directory === '' && $this->request->getMethod() !== '') { + $this->directory = str_replace('\\', '/', dirname($this->request->getServerVariable('PHP_SELF'))); $this->directory = $this->directory !== '/' ? $this->directory.'/' : '/'; $this->directory = str_replace('//', '/', $this->directory); } @@ -120,13 +120,13 @@ class Url extends Base */ public function server() { - if (empty($_SERVER['SERVER_NAME'])) { + if ($this->request->getServerVariable('SERVER_NAME') === '') { return 'http://localhost/'; } $url = $this->request->isHTTPS() ? 'https://' : 'http://'; - $url .= $_SERVER['SERVER_NAME']; - $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; + $url .= $this->request->getServerVariable('SERVER_NAME'); + $url .= $this->request->getServerVariable('SERVER_PORT') == 80 || $this->request->getServerVariable('SERVER_PORT') == 443 ? '' : ':'.$this->request->getServerVariable('SERVER_PORT'); $url .= $this->dir() ?: '/'; return $url; @@ -147,13 +147,15 @@ class Url extends Base */ private function build($separator, $controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false) { - $path = $this->router->findUrl($controller, $action, $params); + $path = $this->route->findUrl($controller, $action, $params); $qs = array(); if (empty($path)) { $qs['controller'] = $controller; $qs['action'] = $action; $qs += $params; + } else { + unset($params['plugin']); } if ($csrf) { diff --git a/sources/app/Helper/User.php b/sources/app/Helper/User.php index 101d8db..29844df 100644 --- a/sources/app/Helper/User.php +++ b/sources/app/Helper/User.php @@ -50,22 +50,6 @@ class User extends \Kanboard\Core\Base return $this->userSession->getId(); } - /** - * Get user profile - * - * @access public - * @return string - */ - public function getProfileLink() - { - return $this->helper->url->link( - $this->helper->e($this->getFullname()), - 'user', - 'show', - array('user_id' => $this->userSession->getId()) - ); - } - /** * Check if the given user_id is the connected user * diff --git a/sources/app/Integration/BitbucketWebhook.php b/sources/app/Integration/BitbucketWebhook.php deleted file mode 100644 index 97a3943..0000000 --- a/sources/app/Integration/BitbucketWebhook.php +++ /dev/null @@ -1,312 +0,0 @@ -project_id = $project_id; - } - - /** - * Parse incoming events - * - * @access public - * @param string $type Bitbucket event type - * @param array $payload Bitbucket event - * @return boolean - */ - public function parsePayload($type, array $payload) - { - switch ($type) { - case 'issue:comment_created': - return $this->handleCommentCreated($payload); - case 'issue:created': - return $this->handleIssueOpened($payload); - case 'issue:updated': - return $this->handleIssueUpdated($payload); - case 'repo:push': - return $this->handlePush($payload); - } - - return false; - } - - /** - * Parse comment issue events - * - * @access public - * @param array $payload - * @return boolean - */ - public function handleCommentCreated(array $payload) - { - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']); - - if (! empty($task)) { - $user = $this->user->getByUsername($payload['actor']['username']); - - if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) { - $user = array(); - } - - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['comment']['id'], - 'comment' => $payload['comment']['content']['raw']."\n\n[".t('By @%s on Bitbucket', $payload['actor']['display_name']).']('.$payload['comment']['links']['html']['href'].')', - 'user_id' => ! empty($user) ? $user['id'] : 0, - 'task_id' => $task['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_COMMENT, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle new issues - * - * @access public - * @param array $payload - * @return boolean - */ - public function handleIssueOpened(array $payload) - { - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['issue']['id'], - 'title' => $payload['issue']['title'], - 'description' => $payload['issue']['content']['raw']."\n\n[".t('Bitbucket Issue').']('.$payload['issue']['links']['html']['href'].')', - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_OPENED, - new GenericEvent($event) - ); - - return true; - } - - /** - * Handle issue updates - * - * @access public - * @param array $payload - * @return boolean - */ - public function handleIssueUpdated(array $payload) - { - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']); - - if (empty($task)) { - return false; - } - - if (isset($payload['changes']['status'])) { - return $this->handleStatusChange($task, $payload); - } elseif (isset($payload['changes']['assignee'])) { - return $this->handleAssigneeChange($task, $payload); - } - - return false; - } - - /** - * Handle issue status change - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleStatusChange(array $task, array $payload) - { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $payload['issue']['id'], - ); - - switch ($payload['issue']['state']) { - case 'closed': - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_CLOSED, new GenericEvent($event)); - return true; - case 'open': - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_REOPENED, new GenericEvent($event)); - return true; - } - - return false; - } - - /** - * Handle issue assignee change - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleAssigneeChange(array $task, array $payload) - { - if (empty($payload['issue']['assignee'])) { - return $this->handleIssueUnassigned($task, $payload); - } - - return $this->handleIssueAssigned($task, $payload); - } - - /** - * Handle issue assigned - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleIssueAssigned(array $task, array $payload) - { - $user = $this->user->getByUsername($payload['issue']['assignee']['username']); - - if (empty($user)) { - return false; - } - - if (! $this->projectPermission->isMember($this->project_id, $user['id'])) { - return false; - } - - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => $user['id'], - 'reference' => $payload['issue']['id'], - ); - - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_ASSIGNEE_CHANGE, new GenericEvent($event)); - - return true; - } - - /** - * Handle issue unassigned - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleIssueUnassigned(array $task, array $payload) - { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => 0, - 'reference' => $payload['issue']['id'], - ); - - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_ASSIGNEE_CHANGE, new GenericEvent($event)); - - return true; - } - - /** - * Parse push events - * - * @access public - * @param array $payload - * @return boolean - */ - public function handlePush(array $payload) - { - if (isset($payload['push']['changes'])) { - foreach ($payload['push']['changes'] as $change) { - if (isset($change['new']['target']) && $this->handleCommit($change['new']['target'], $payload['actor'])) { - return true; - } - } - } - - return false; - } - - /** - * Parse commit - * - * @access public - * @param array $commit Bitbucket commit - * @param array $actor Bitbucket actor - * @return boolean - */ - public function handleCommit(array $commit, array $actor) - { - $task_id = $this->task->getTaskIdFromText($commit['message']); - - if (empty($task_id)) { - return false; - } - - $task = $this->taskFinder->getById($task_id); - - if (empty($task)) { - return false; - } - - if ($task['project_id'] != $this->project_id) { - return false; - } - - $this->container['dispatcher']->dispatch( - self::EVENT_COMMIT, - new GenericEvent(array( - 'task_id' => $task_id, - 'commit_message' => $commit['message'], - 'commit_url' => $commit['links']['html']['href'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Bitbucket', $actor['display_name']).']('.$commit['links']['html']['href'].')', - ) + $task) - ); - - return true; - } -} diff --git a/sources/app/Integration/GithubWebhook.php b/sources/app/Integration/GithubWebhook.php deleted file mode 100644 index c8b53e3..0000000 --- a/sources/app/Integration/GithubWebhook.php +++ /dev/null @@ -1,380 +0,0 @@ -project_id = $project_id; - } - - /** - * Parse Github events - * - * @access public - * @param string $type Github event type - * @param array $payload Github event - * @return boolean - */ - public function parsePayload($type, array $payload) - { - switch ($type) { - case 'push': - return $this->parsePushEvent($payload); - case 'issues': - return $this->parseIssueEvent($payload); - case 'issue_comment': - return $this->parseCommentIssueEvent($payload); - } - - return false; - } - - /** - * Parse Push events (list of commits) - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function parsePushEvent(array $payload) - { - foreach ($payload['commits'] as $commit) { - $task_id = $this->task->getTaskIdFromText($commit['message']); - - if (empty($task_id)) { - continue; - } - - $task = $this->taskFinder->getById($task_id); - - if (empty($task)) { - continue; - } - - if ($task['project_id'] != $this->project_id) { - continue; - } - - $this->container['dispatcher']->dispatch( - self::EVENT_COMMIT, - new GenericEvent(array( - 'task_id' => $task_id, - 'commit_message' => $commit['message'], - 'commit_url' => $commit['url'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Github', $commit['author']['username']).']('.$commit['url'].')' - ) + $task) - ); - } - - return true; - } - - /** - * Parse issue events - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function parseIssueEvent(array $payload) - { - switch ($payload['action']) { - case 'opened': - return $this->handleIssueOpened($payload['issue']); - case 'closed': - return $this->handleIssueClosed($payload['issue']); - case 'reopened': - return $this->handleIssueReopened($payload['issue']); - case 'assigned': - return $this->handleIssueAssigned($payload['issue']); - case 'unassigned': - return $this->handleIssueUnassigned($payload['issue']); - case 'labeled': - return $this->handleIssueLabeled($payload['issue'], $payload['label']); - case 'unlabeled': - return $this->handleIssueUnlabeled($payload['issue'], $payload['label']); - } - - return false; - } - - /** - * Parse comment issue events - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function parseCommentIssueEvent(array $payload) - { - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['number']); - - if (! empty($task)) { - $user = $this->user->getByUsername($payload['comment']['user']['login']); - - if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) { - $user = array(); - } - - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['comment']['id'], - 'comment' => $payload['comment']['body']."\n\n[".t('By @%s on Github', $payload['comment']['user']['login']).']('.$payload['comment']['html_url'].')', - 'user_id' => ! empty($user) ? $user['id'] : 0, - 'task_id' => $task['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_COMMENT, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle new issues - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueOpened(array $issue) - { - $event = array( - 'project_id' => $this->project_id, - 'reference' => $issue['number'], - 'title' => $issue['title'], - 'description' => $issue['body']."\n\n[".t('Github Issue').']('.$issue['html_url'].')', - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_OPENED, - new GenericEvent($event) - ); - - return true; - } - - /** - * Handle issue closing - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueClosed(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_CLOSED, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle issue reopened - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueReopened(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_REOPENED, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle issue assignee change - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueAssigned(array $issue) - { - $user = $this->user->getByUsername($issue['assignee']['login']); - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($user) && ! empty($task) && $this->projectPermission->isMember($this->project_id, $user['id'])) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => $user['id'], - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_ASSIGNEE_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle unassigned issue - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueUnassigned(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => 0, - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_ASSIGNEE_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle labeled issue - * - * @access public - * @param array $issue Issue data - * @param array $label Label data - * @return boolean - */ - public function handleIssueLabeled(array $issue, array $label) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - 'label' => $label['name'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_LABEL_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle unlabeled issue - * - * @access public - * @param array $issue Issue data - * @param array $label Label data - * @return boolean - */ - public function handleIssueUnlabeled(array $issue, array $label) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - 'label' => $label['name'], - 'category_id' => 0, - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_LABEL_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } -} diff --git a/sources/app/Integration/GitlabWebhook.php b/sources/app/Integration/GitlabWebhook.php deleted file mode 100644 index 17b6da7..0000000 --- a/sources/app/Integration/GitlabWebhook.php +++ /dev/null @@ -1,298 +0,0 @@ -project_id = $project_id; - } - - /** - * Parse events - * - * @access public - * @param array $payload Gitlab event - * @return boolean - */ - public function parsePayload(array $payload) - { - switch ($this->getType($payload)) { - case self::TYPE_PUSH: - return $this->handlePushEvent($payload); - case self::TYPE_ISSUE; - return $this->handleIssueEvent($payload); - case self::TYPE_COMMENT; - return $this->handleCommentEvent($payload); - } - - return false; - } - - /** - * Get event type - * - * @access public - * @param array $payload Gitlab event - * @return string - */ - public function getType(array $payload) - { - if (empty($payload['object_kind'])) { - return ''; - } - - switch ($payload['object_kind']) { - case 'issue': - return self::TYPE_ISSUE; - case 'note': - return self::TYPE_COMMENT; - case 'push': - return self::TYPE_PUSH; - default: - return ''; - } - } - - /** - * Parse push event - * - * @access public - * @param array $payload Gitlab event - * @return boolean - */ - public function handlePushEvent(array $payload) - { - foreach ($payload['commits'] as $commit) { - $this->handleCommit($commit); - } - - return true; - } - - /** - * Parse commit - * - * @access public - * @param array $commit Gitlab commit - * @return boolean - */ - public function handleCommit(array $commit) - { - $task_id = $this->task->getTaskIdFromText($commit['message']); - - if (empty($task_id)) { - return false; - } - - $task = $this->taskFinder->getById($task_id); - - if (empty($task)) { - return false; - } - - if ($task['project_id'] != $this->project_id) { - return false; - } - - $this->container['dispatcher']->dispatch( - self::EVENT_COMMIT, - new GenericEvent(array( - 'task_id' => $task_id, - 'commit_message' => $commit['message'], - 'commit_url' => $commit['url'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Gitlab', $commit['author']['name']).']('.$commit['url'].')' - ) + $task) - ); - - return true; - } - - /** - * Parse issue event - * - * @access public - * @param array $payload Gitlab event - * @return boolean - */ - public function handleIssueEvent(array $payload) - { - switch ($payload['object_attributes']['action']) { - case 'open': - return $this->handleIssueOpened($payload['object_attributes']); - case 'close': - return $this->handleIssueClosed($payload['object_attributes']); - case 'reopen': - return $this->handleIssueReopened($payload['object_attributes']); - } - - return false; - } - - /** - * Handle new issues - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueOpened(array $issue) - { - $event = array( - 'project_id' => $this->project_id, - 'reference' => $issue['id'], - 'title' => $issue['title'], - 'description' => $issue['description']."\n\n[".t('Gitlab Issue').']('.$issue['url'].')', - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_OPENED, - new GenericEvent($event) - ); - - return true; - } - - /** - * Handle issue reopening - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueReopened(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['id']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_REOPENED, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - - /** - * Handle issue closing - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueClosed(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['id']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_CLOSED, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Parse comment issue events - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function handleCommentEvent(array $payload) - { - if (! isset($payload['issue'])) { - return false; - } - - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']); - - if (! empty($task)) { - $user = $this->user->getByUsername($payload['user']['username']); - - if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) { - $user = array(); - } - - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['object_attributes']['id'], - 'comment' => $payload['object_attributes']['note']."\n\n[".t('By @%s on Gitlab', $payload['user']['username']).']('.$payload['object_attributes']['url'].')', - 'user_id' => ! empty($user) ? $user['id'] : 0, - 'task_id' => $task['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_COMMENT, - new GenericEvent($event) - ); - - return true; - } - - return false; - } -} diff --git a/sources/app/Locale/bs_BA/translations.php b/sources/app/Locale/bs_BA/translations.php index 08fd382..6ad3a78 100644 --- a/sources/app/Locale/bs_BA/translations.php +++ b/sources/app/Locale/bs_BA/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s promijenio izvršioca za zadatak %s u %s', 'New password for the user "%s"' => 'Nova šifra korisnika "%s"', 'Choose an event' => 'Izaberi događaj', - 'Github commit received' => 'Github: commit dobijen', - 'Github issue opened' => 'Github: otvoren problem', - 'Github issue closed' => 'Github: zatvoren problem', - 'Github issue reopened' => 'Github: ponovo otvoren problem', - 'Github issue assignee change' => 'Github: izmijenjen izvršioc problema', - 'Github issue label change' => 'Github: izmjena etikete problema', 'Create a task from an external provider' => 'Kreiraj zadatak preko posrednika', 'Change the assignee based on an external username' => 'Izmijene izvršioca bazirano na vanjskom korisničkom imenu', 'Change the category based on an external label' => 'Izmijene kategorije bazirano na vanjskoj etiketi', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Svima je dozvoljen pristup ovom projektu.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Pomoć na Github webhooks', 'Create a comment from an external provider' => 'Napravi komentar preko vanjskog posrednika', - 'Github issue comment created' => 'Github: dodan komentar za problem', 'Project management' => 'Upravljanje projektima', 'My projects' => 'Moji projekti', 'Columns' => 'Kolone', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Zaduženja korisnika za "%s"', 'Clone this project' => 'Kloniraj ovaj projekat', 'Column removed successfully.' => 'Kolona uspješno uklonjena.', - 'Github Issue' => 'Github problemi', 'Not enough data to show the graph.' => 'Nedovoljno podataka za prikaz na grafikonu.', 'Previous' => 'Prethodni', 'The id must be an integer' => 'ID mora biti cjeloviti broj', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Swimline traka je uspješno kreirana.', 'Example: "Bug, Feature Request, Improvement"' => 'Npr: "Greška, Zahtjev za izmjenama, Poboljšanje"', 'Default categories for new projects (Comma-separated)' => 'Podrazumijevane kategorije za novi projekat', - 'Gitlab commit received' => 'Gitlab: commit dobijen', - 'Gitlab issue opened' => 'Gitlab: problem otvoren', - 'Gitlab issue closed' => 'Gitlab: problem zatvoren', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Pomoc na Gitlab webhooks', 'Integrations' => 'Integracije', 'Integration with third-party services' => 'Integracija sa uslugama vanjskih servisa', - 'Gitlab Issue' => 'Gitlab problemi', 'Subtask Id' => 'ID pod-zadatka', 'Subtasks' => 'Pod-zadaci', 'Subtasks Export' => 'Izvoz pod-zadataka', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Već imaš jedan pod-zadatak "u radu"', 'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želiš duplicirati?', 'Disallow login form' => 'Zabrani prijavnu formu', - 'Bitbucket commit received' => 'Bitbucket: commit dobijen', - 'Bitbucket webhooks' => 'Bitbucket: webhooks', - 'Help on Bitbucket webhooks' => 'Pomoć na Bitbucket webhooks', 'Start' => 'Početak', 'End' => 'Kraj', 'Task age in days' => 'Trajanje zadatka u danima', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Faktor-dva autentifikacionog koda je validan.', 'Code' => 'Kod', 'Two factor authentication' => 'Faktor-dva autentifikacija', - 'Enable/disable two factor authentication' => 'Omogući/onemogući faktor-dva autentifikaciju', 'This QR code contains the key URI: ' => 'Ovaj QR kod sadržava ključni URL: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Sačuvaj tajni klju u svom TOTP softveru (npr. Google Authenticator or FreeOTP)', 'Check my code' => 'Provjeri moj kod', 'Secret key: ' => 'Tajni ključ: ', 'Test your device' => 'Testiraj svoj uređaj', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'Korisnik će dobiti email', 'Email subject' => 'Predmet email-a', 'Date' => 'Datum', - 'By @%s on Bitbucket' => 'Od @%s na Bitbucket', - 'Bitbucket Issue' => 'Bitbucket problem', - 'Commit made by @%s on Bitbucket' => 'Commit-ao @%s na Bitbucket', - 'Commit made by @%s on Github' => 'Commit-ao @%s na Github', - 'By @%s on Github' => '@%s na Github', - 'Commit made by @%s on Gitlab' => 'Commit-ao @%s na Gitlab', 'Add a comment log when moving the task between columns' => 'Dodaj komentar u dnevnik kada se pomjeri zadatak između kolona', 'Move the task to another column when the category is changed' => 'Pomjeri zadatak u drugu kolonu kada je kategorija promijenjena', 'Send a task by email to someone' => 'Pošalji zadatak nekome emailom', 'Reopen a task' => 'Ponovo otvori zadatak', - 'Bitbucket issue opened' => 'Bitbucket: otvoren problem', - 'Bitbucket issue closed' => 'Bitbucket: zatvoren problem', - 'Bitbucket issue reopened' => 'Bitbucket: problem ponovo otvoren', - 'Bitbucket issue assignee change' => 'Bitbucket: promijenjen izvršilac problema', - 'Bitbucket issue comment created' => 'Bitbucket: dodan komentar na problemu', 'Column change' => 'Promijena kolone', 'Position change' => 'Promjena pozicije', 'Swimlane change' => 'Promjena swimline trake', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Vanjski korisnik', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Vanjski korisnik ne čuva šifru u Kanboard bazi, npr: LDAP, Google i Github korisnički računi.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Ako ste označili kvadratić "Zabrani prijavnu formu", unos pristupnih podataka u prijavnoj formi će biti ignorisan.', - 'By @%s on Gitlab' => 'Od @%s s Gitlab-om', - 'Gitlab issue comment created' => 'Gitlab: dodan komentar za problem', 'New remote user' => 'Novi vanjski korisnik', 'New local user' => 'Novi lokalni korisnik', 'Default task color' => 'Podrazumijevana boja zadatka', @@ -1028,7 +994,6 @@ return array( 'Append/Replace' => 'Dodaj/Zamijeni', 'Append' => 'Dodaj', 'Replace' => 'Zamijeni', - 'There is no notification method registered.' => 'Nema registrovanih metoda za obavještenja', 'Import' => 'Uvoz', 'change sorting' => 'Promijeni sortiranje', 'Tasks Importation' => 'Uvoz zadataka', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', 'Project members' => 'Članovi projekta', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/cs_CZ/translations.php b/sources/app/Locale/cs_CZ/translations.php index 6b5ad8d..2c6cb7e 100644 --- a/sources/app/Locale/cs_CZ/translations.php +++ b/sources/app/Locale/cs_CZ/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s změnil řešitele úkolu %s na uživatele %s', 'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"', 'Choose an event' => 'Vybrat událost', - 'Github commit received' => 'Github commit empfangen', - 'Github issue opened' => 'Github Fehler geöffnet', - 'Github issue closed' => 'Github Fehler geschlossen', - 'Github issue reopened' => 'Github Fehler erneut geöffnet', - 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert', - 'Github issue label change' => 'Github Fehlerkennzeichnung verändert', 'Create a task from an external provider' => 'Vytvořit úkol externím poskytovatelem', 'Change the assignee based on an external username' => 'Změna přiřazení uživatele závislá na externím uživateli', 'Change the category based on an external label' => 'Změna kategorie závislá na externím popisku', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Přístup k tomuto projektu má kdokoliv.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github Webhook', - 'Help on Github webhooks' => 'Hilfe für Github Webhooks', 'Create a comment from an external provider' => 'Vytvořit komentář pomocí externího poskytovatele', - 'Github issue comment created' => 'Github Fehler Kommentar hinzugefügt', 'Project management' => 'Správa projektů', 'My projects' => 'Moje projekty', 'Columns' => 'Sloupce', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Rozdělení podle uživatelů pro "%s"', 'Clone this project' => 'Duplokovat projekt', 'Column removed successfully.' => 'Sloupec byl odstraněn.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Pro zobrazení grafu není dostatek dat.', 'Previous' => 'Předchozí', 'The id must be an integer' => 'ID musí být celé číslo', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"', 'Default categories for new projects (Comma-separated)' => 'Výchozí kategorie pro nové projekty (oddělené čárkou)', - 'Gitlab commit received' => 'Gitlab commit erhalten', - 'Gitlab issue opened' => 'Gitlab Fehler eröffnet', - 'Gitlab issue closed' => 'Gitlab Fehler geschlossen', - 'Gitlab webhooks' => 'Gitlab Webhook', - 'Help on Gitlab webhooks' => 'Hilfe für Gitlab Webhooks', 'Integrations' => 'Integrace', 'Integration with third-party services' => 'Integration von Fremdleistungen', - 'Gitlab Issue' => 'Gitlab Fehler', 'Subtask Id' => 'Dílčí úkol Id', 'Subtasks' => 'Dílčí úkoly', 'Subtasks Export' => 'Export dílčích úkolů', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Jeden dílčí úkol již aktuálně řešíte', 'Which parts of the project do you want to duplicate?' => 'Které části projektu chcete duplikovat?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit erhalten', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket webhooks', 'Start' => 'Začátek', 'End' => 'Konec', 'Task age in days' => 'Doba trvání úkolu ve dnech', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', 'Code' => 'Code', 'Two factor authentication' => 'Dvouúrovňová autorizace', - 'Enable/disable two factor authentication' => 'Povolit / zakázat dvou úrovňovou autorizaci', 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Speichere den geheimen Schlüssel in deiner TOTP software (z.B. Google Authenticator oder FreeOTP).', 'Check my code' => 'Kontrola mého kódu', 'Secret key: ' => 'Tajný klíč', 'Test your device' => 'Test Vašeho zařízení', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'Uživatel, který dostane E-mail', 'Email subject' => 'E-mail Předmět', 'Date' => 'Datum', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', 'Add a comment log when moving the task between columns' => 'Přidat komentář když je úkol přesouván mezi sloupci', 'Move the task to another column when the category is changed' => 'Přesun úkolu do jiného sloupce když je změněna kategorie', 'Send a task by email to someone' => 'Poslat někomu úkol poštou', 'Reopen a task' => 'Znovu otevřít úkol', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', 'Column change' => 'Spalte geändert', 'Position change' => 'Position geändert', 'Swimlane change' => 'Swimlane geändert', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Vzdálený uživatel', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Hesla vzdáleným uživatelům se neukládají do databáze Kanboard. Naříklad: LDAP, Google a Github účty.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Pokud zaškrtnete políčko "Zakázat přihlašovací formulář", budou pověření zadané do přihlašovacího formuláře ignorovány.', - 'By @%s on Gitlab' => 'uživatelem @%s na Gitlab', - 'Gitlab issue comment created' => 'Vytvořen komentář problému na Gitlab', 'New remote user' => 'Nový vzdálený uživatel', 'New local user' => 'Nový lokální uživatel', 'Default task color' => 'Výchozí barva úkolu', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/da_DK/translations.php b/sources/app/Locale/da_DK/translations.php index 1428626..820b4d1 100644 --- a/sources/app/Locale/da_DK/translations.php +++ b/sources/app/Locale/da_DK/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s skift ansvarlig for opgaven %s til %s', 'New password for the user "%s"' => 'Ny adgangskode for brugeren "%s"', 'Choose an event' => 'Vælg et event', - 'Github commit received' => 'Github commit modtaget', - 'Github issue opened' => 'Github problem åbet', - 'Github issue closed' => 'Github problem lukket', - 'Github issue reopened' => 'Github problem genåbnet', - 'Github issue assignee change' => 'Github problem ansvarlig skift', - 'Github issue label change' => 'Github problem label skift', 'Create a task from an external provider' => 'Opret en opgave fra en ekstern udbyder', 'Change the assignee based on an external username' => 'Skift den ansvarlige baseret på et eksternt brugernavn', 'Change the category based on an external label' => 'Skift kategorien baseret på en ekstern label', @@ -487,10 +481,7 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', // 'Project management' => '', // 'My projects' => '', // 'Columns' => '', @@ -508,7 +499,6 @@ return array( // 'User repartition for "%s"' => '', // 'Clone this project' => '', // 'Column removed successfully.' => '', - // 'Github Issue' => '', // 'Not enough data to show the graph.' => '', // 'Previous' => '', // 'The id must be an integer' => '', @@ -553,14 +543,8 @@ return array( // 'Your swimlane have been created successfully.' => '', // 'Example: "Bug, Feature Request, Improvement"' => '', // 'Default categories for new projects (Comma-separated)' => '', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', // 'Integrations' => '', // 'Integration with third-party services' => '', - // 'Gitlab Issue' => '', // 'Subtask Id' => '', // 'Subtasks' => '', // 'Subtasks Export' => '', @@ -588,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', // 'Which parts of the project do you want to duplicate?' => '', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', // 'Start' => '', // 'End' => '', // 'Task age in days' => '', @@ -688,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/de_DE/translations.php b/sources/app/Locale/de_DE/translations.php index e244ede..f1c89ca 100644 --- a/sources/app/Locale/de_DE/translations.php +++ b/sources/app/Locale/de_DE/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s', 'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"', 'Choose an event' => 'Aktion wählen', - 'Github commit received' => 'Github commit empfangen', - 'Github issue opened' => 'Github Fehler geöffnet', - 'Github issue closed' => 'Github Fehler geschlossen', - 'Github issue reopened' => 'Github Fehler erneut geöffnet', - 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert', - 'Github issue label change' => 'Github Fehlerkennzeichnung verändert', 'Create a task from an external provider' => 'Eine Aufgabe durch einen externen Provider hinzufügen', 'Change the assignee based on an external username' => 'Zuordnung ändern basierend auf externem Benutzernamen', 'Change the category based on an external label' => 'Kategorie basierend auf einer externen Kennzeichnung ändern', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Jeder hat Zugriff zu diesem Projekt', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github-Webhook', - 'Help on Github webhooks' => 'Hilfe für Github-Webhooks', 'Create a comment from an external provider' => 'Kommentar eines externen Providers hinzufügen', - 'Github issue comment created' => 'Kommentar zum Github-Issue hinzugefügt', 'Project management' => 'Projektmanagement', 'My projects' => 'Meine Projekte', 'Columns' => 'Spalten', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Benutzerverteilung für "%s"', 'Clone this project' => 'Projekt kopieren', 'Column removed successfully.' => 'Spalte erfolgreich entfernt.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Nicht genügend Daten, um die Grafik zu zeigen.', 'Previous' => 'Vorherige', 'The id must be an integer' => 'Die Id muss eine ganze Zahl sein', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"', 'Default categories for new projects (Comma-separated)' => 'Standard-Kategorien für neue Projekte (Komma-getrennt)', - 'Gitlab commit received' => 'Gitlab-Commit erhalten', - 'Gitlab issue opened' => 'Gitlab-Issue eröffnet', - 'Gitlab issue closed' => 'Gitlab-Issue geschlossen', - 'Gitlab webhooks' => 'Gitlab-Webhook', - 'Help on Gitlab webhooks' => 'Hilfe für Gitlab-Webhooks', 'Integrations' => 'Integration', 'Integration with third-party services' => 'Integration von externen Diensten', - 'Gitlab Issue' => 'Gitlab-Issue', 'Subtask Id' => 'Teilaufgaben-ID', 'Subtasks' => 'Teilaufgaben', 'Subtasks Export' => 'Export von Teilaufgaben', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Bereits eine Teilaufgabe in Bearbeitung', 'Which parts of the project do you want to duplicate?' => 'Welcher Teil des Projekts soll kopiert werden?', 'Disallow login form' => 'Verbiete Login-Formular', - 'Bitbucket commit received' => 'Bitbucket-Commit erhalten', - 'Bitbucket webhooks' => 'Bitbucket-Webhooks', - 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket-Webhooks', 'Start' => 'Start', 'End' => 'Ende', 'Task age in days' => 'Aufgabenalter in Tagen', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', 'Code' => 'Code', 'Two factor authentication' => 'Zwei-Faktor-Authentifizierung', - 'Enable/disable two factor authentication' => 'Aktiviere/Deaktiviere Zwei-Faktor-Authentifizierung', 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Speichere den geheimen Schlüssel in deiner TOTP software (z.B. Google Authenticator oder FreeOTP).', 'Check my code' => 'Überprüfe meinen Code', 'Secret key: ' => 'Geheimer Schlüssel', 'Test your device' => 'Teste dein Gerät', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'Empfänger der E-Mail', 'Email subject' => 'E-Mail-Betreff', 'Date' => 'Datum', - 'By @%s on Bitbucket' => 'Durch @%s auf Bitbucket', - 'Bitbucket Issue' => 'Bitbucket-Issue', - 'Commit made by @%s on Bitbucket' => 'Commit von @%s auf Bitbucket', - 'Commit made by @%s on Github' => 'Commit von @%s auf Github', - 'By @%s on Github' => 'Durch @%s auf Github', - 'Commit made by @%s on Gitlab' => 'Commit von @%s auf Gitlab', 'Add a comment log when moving the task between columns' => 'Kommentar hinzufügen, wenn Aufgabe in andere Spalte verschoben wird', 'Move the task to another column when the category is changed' => 'Aufgabe in andere Spalte verschieben, wenn Kategorie geändert wird', 'Send a task by email to someone' => 'Aufgabe per E-Mail versenden', 'Reopen a task' => 'Aufgabe wieder öffnen', - 'Bitbucket issue opened' => 'Bitbucket Ticket eröffnet', - 'Bitbucket issue closed' => 'Bitbucket Ticket geschlossen', - 'Bitbucket issue reopened' => 'Bitbucket Ticket wieder eröffnet', - 'Bitbucket issue assignee change' => 'Bitbucket Ticket Zuordnung geändert', - 'Bitbucket issue comment created' => 'Bitbucket Ticket Kommentar erstellt', 'Column change' => 'Spalte geändert', 'Position change' => 'Position geändert', 'Swimlane change' => 'Swimlane geändert', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Remote-Benutzer', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Goole und Github Accounts', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.', - 'By @%s on Gitlab' => 'Durch @%s auf Gitlab', - 'Gitlab issue comment created' => 'Gitlab Ticket Kommentar erstellt', 'New remote user' => 'Neuer Remote-Benutzer', 'New local user' => 'Neuer lokaler Benutzer', 'Default task color' => 'Voreingestellte Aufgabenfarbe', @@ -1028,7 +994,6 @@ return array( 'Append/Replace' => 'Anhängen/Ersetzen', 'Append' => 'Anhängen', 'Replace' => 'Ersetzen', - 'There is no notification method registered.' => 'Es ist keine Benachrichtigungsmethode registriert', 'Import' => 'Import', 'change sorting' => 'Sortierung ändern', 'Tasks Importation' => 'Aufgaben Import', @@ -1073,7 +1038,6 @@ return array( 'Project Manager' => 'Projekt Manager', 'Project Member' => 'Projekt Mitglied', 'Project Viewer' => 'Projekt Betrachter', - 'Gitlab issue reopened' => 'Gitlab Thema wiedereröffnet', 'Your account is locked for %d minutes' => 'Ihr Zugang wurde für %d Minuten gesperrt', 'Invalid captcha' => 'Ungültiges Captcha', 'The name must be unique' => 'Der Name muss eindeutig sein', @@ -1099,4 +1063,45 @@ return array( 'Enter group name...' => 'Geben Sie den Gruppennamen ein...', 'Role:' => 'Rolle:', 'Project members' => 'Projektmitglieder', + 'Compare hours for "%s"' => 'Vergleich der Stunden für %s', + '%s mentioned you in the task #%d' => '%s erwähnte Sie in Aufgabe #%d', + '%s mentioned you in a comment on the task #%d' => '%s erwähnte Sie in einem Kommentar zur Aufgabe #%d', + 'You were mentioned in the task #%d' => 'Sie wurden in der Aufgabe #%d erwähnt', + 'You were mentioned in a comment on the task #%d' => 'Sie wurden in einem Kommentar zur Aufgabe #%d erwähnt', + 'Mentioned' => 'Erwähnt', + 'Compare Estimated Time vs Actual Time' => 'Vergleich zwischen erwartetem und tatsächlichem Zeitaufwand', + 'Estimated hours: ' => 'Erwarteter Zeitaufwand (Stunden): ', + 'Actual hours: ' => 'Tatsächlich aufgewändete Stunden: ', + 'Hours Spent' => 'Stunden aufgewändet', + 'Hours Estimated' => 'Stunden erwartet', + 'Estimated Time' => 'Erwartete Zeit', + 'Actual Time' => 'Aktuelle Zeit', + 'Estimated vs actual time' => 'Erwarteter vs. tatsächlicher Zeitaufwand', + 'RUB - Russian Ruble' => 'Russischer Rubel', + 'Assign the task to the person who does the action when the column is changed' => 'Aufgabe der Person zuordnen, die die Aktion durchführt, wenn die Spalte geändert wird', + 'Close a task in a specific column' => 'Schliesse eine Aufgabe in einer bestimmten Spalte', + 'Time-based One-time Password Algorithm' => 'Zeitbasierter Einmalpasswort Algorithmus', + 'Two-Factor Provider: ' => '2FA Anbieter: ', + 'Disable two-factor authentication' => 'Zwei-Faktor-Authentifizierung deaktivieren', + 'Enable two-factor authentication' => 'Zwei-Faktor-Authentifizierung aktivieren', + 'There is no integration registered at the moment.' => 'Derzeit ist kein externer Dienst registriert.', + 'Password Reset for Kanboard' => 'Zurücksetzen des Passwortes für Kanboard', + 'Forgot password?' => 'Passwort vergessen?', + 'Enable "Forget Password"' => 'Passwortrücksetzung aktivieren', + 'Password Reset' => 'Passwort zurücksetzen', + 'New password' => 'Neues Passwort', + 'Change Password' => 'Passwort ändern', + 'To reset your password click on this link:' => 'Bitte auf den Link klicken, um Ihr Passwort zurückzusetzen.', + 'Last Password Reset' => 'Verlauf der Passwortrücksetzung', + 'The password has never been reinitialized.' => 'Das Passwort wurde noch nie zurückgesetzt.', + 'Creation' => 'Erstellung', + 'Expiration' => 'Ablauf', + 'Password reset history' => 'Verlauf Passwortrücksetzung', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/es_ES/translations.php b/sources/app/Locale/es_ES/translations.php index b6ad2df..9248943 100644 --- a/sources/app/Locale/es_ES/translations.php +++ b/sources/app/Locale/es_ES/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s cambió el concesionario de la tarea %s por %s', 'New password for the user "%s"' => 'Nueva contraseña para el usuario "%s"', 'Choose an event' => 'Seleccione un evento', - 'Github commit received' => 'Envío a Github recibido', - 'Github issue opened' => 'Abierto asunto en Github', - 'Github issue closed' => 'Cerrado asunto en Github', - 'Github issue reopened' => 'Reabierto asunto en Github', - 'Github issue assignee change' => 'Cambio en concesionario de asunto de Github', - 'Github issue label change' => 'Cambio en etiqueta de asunto de Github', 'Create a task from an external provider' => 'Crear una tarea a partir de un proveedor externo', 'Change the assignee based on an external username' => 'Cambiar el concesionario basado en un nombre de usuario externo', 'Change the category based on an external label' => 'Cambiar la categoría basado en una etiqueta externa', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Cualquiera tiene acceso a este proyecto', 'Webhooks' => 'Disparadores Web (Webhooks)', 'API' => 'API', - 'Github webhooks' => 'Disparadores Web (Webhooks) de Github', - 'Help on Github webhooks' => 'Ayuda con los Disparadores Web (Webhook) de Github', 'Create a comment from an external provider' => 'Crear un comentario a partir de un proveedor externo', - 'Github issue comment created' => 'Creado el comentario del problema en Github', 'Project management' => 'Administración del proyecto', 'My projects' => 'Mis proyectos', 'Columns' => 'Columnas', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Repartición para "%s"', 'Clone this project' => 'Clonar este proyecto', 'Column removed successfully.' => 'Columna eliminada correctamente', - 'Github Issue' => 'Problema con Github', 'Not enough data to show the graph.' => 'No hay suficiente información para mostrar el gráfico.', 'Previous' => 'Anterior', 'The id must be an integer' => 'El id debe ser un entero', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Su calle ha sido creada correctamente', 'Example: "Bug, Feature Request, Improvement"' => 'Ejemplo: "Error, Solicitud de característica, Mejora', 'Default categories for new projects (Comma-separated)' => 'Categorías por defecto para nuevos proyectos (separadas por comas)', - 'Gitlab commit received' => 'Recibido envío desde Gitlab', - 'Gitlab issue opened' => 'Abierto asunto de Gitlab', - 'Gitlab issue closed' => 'Cerrado asunto de Gitlab', - 'Gitlab webhooks' => 'Disparadores Web (Webhooks) de Gitlab', - 'Help on Gitlab webhooks' => 'Ayuda sobre Disparadores Web (Webhooks) de Gitlab', 'Integrations' => 'Integraciones', 'Integration with third-party services' => 'Integración con servicios de terceros', - 'Gitlab Issue' => 'Asunto Gitlab', 'Subtask Id' => 'Id de Subtarea', 'Subtasks' => 'Subtareas', 'Subtasks Export' => 'Exportación de Subtareas', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Ya dispones de una subtarea en progreso', 'Which parts of the project do you want to duplicate?' => '¿Qué partes del proyecto desea duplicar?', 'Disallow login form' => 'Deshabilitar formulario de ingreso', - 'Bitbucket commit received' => 'Recibido envío desde Bitbucket', - 'Bitbucket webhooks' => 'Disparadores Web (webhooks) de Bitbucket', - 'Help on Bitbucket webhooks' => 'Ayuda sobre disparadores web (webhooks) de Bitbucket', 'Start' => 'Inicio', 'End' => 'Fin', 'Task age in days' => 'Edad de la tarea en días', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'El código de autenticación de dos factores es válido', 'Code' => 'Código', 'Two factor authentication' => 'Autenticación de dos factores', - 'Enable/disable two factor authentication' => 'Activar/desactivar autenticación de dos factores', 'This QR code contains the key URI: ' => 'Este código QR contiene la clave URI: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Guarda la clave secreta en su software TOTP (por ejemplo Autenticación de Google o FreeOTP).', 'Check my code' => 'Revisar mi código', 'Secret key: ' => 'Clave secreta: ', 'Test your device' => 'Probar su dispositivo', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'Usuario que recibirá el correo', 'Email subject' => 'Asunto del correo', 'Date' => 'Fecha', - 'By @%s on Bitbucket' => 'Mediante @%s en Bitbucket', - 'Bitbucket Issue' => 'Asunto de Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Envío realizado por @%s en Bitbucket', - 'Commit made by @%s on Github' => 'Envío realizado por @%s en Github', - 'By @%s on Github' => 'Por @%s en Github', - 'Commit made by @%s on Gitlab' => 'Envío realizado por @%s en Gitlab', 'Add a comment log when moving the task between columns' => 'Añadir un comentario al mover la tarea entre columnas', 'Move the task to another column when the category is changed' => 'Mover la tarea a otra columna cuando cambia la categoría', 'Send a task by email to someone' => 'Enviar una tarea a alguien por correo', 'Reopen a task' => 'Reabrir tarea', - 'Bitbucket issue opened' => 'Abierto asunto de Bitbucket', - 'Bitbucket issue closed' => 'Cerrado asunto de Bitbucket', - 'Bitbucket issue reopened' => 'Reabierto asunto de Bitbucket', - 'Bitbucket issue assignee change' => 'Cambiado concesionario de asunto de Bitbucket', - 'Bitbucket issue comment created' => 'Creado comentario de asunto de Bitbucket', 'Column change' => 'Cambio de columna', 'Position change' => 'Cambio de posición', 'Swimlane change' => 'Cambio de calle', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Usuario remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Los usuarios remotos no almacenan sus contraseñas en la base de datos Kanboard, por ejemplo: cuentas de LDAP, Google y Github', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si marcas la caja de edición "Desactivar formulario de ingreso", se ignoran las credenciales entradas en el formulario de ingreso.', - 'By @%s on Gitlab' => 'Por @%s en Gitlab', - 'Gitlab issue comment created' => 'Creado comentario de asunto de Gitlab', 'New remote user' => 'Nuevo usuario remoto', 'New local user' => 'Nuevo usuario local', 'Default task color' => 'Color por defecto de tarea', @@ -1028,7 +994,6 @@ return array( 'Append/Replace' => 'Añadir/Reemplazar', 'Append' => 'Añadir', 'Replace' => 'Reemplazar', - 'There is no notification method registered.' => 'No hay método de notificación registrado', 'Import' => 'Importar', 'change sorting' => 'Cambiar orden', 'Tasks Importation' => 'Importación de tareas', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', 'Project members' => 'Miembros de proyecto', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + 'Estimated vs actual time' => 'Tiempo estimado vs real', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/fi_FI/translations.php b/sources/app/Locale/fi_FI/translations.php index e4e18ab..18553ec 100644 --- a/sources/app/Locale/fi_FI/translations.php +++ b/sources/app/Locale/fi_FI/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s vaihtoi tehtävän %s saajaksi %s', 'New password for the user "%s"' => 'Uusi salasana käyttäjälle "%s"', 'Choose an event' => 'Valitse toiminta', - 'Github commit received' => 'Github-kommitti vastaanotettu', - 'Github issue opened' => 'Github-issue avattu', - 'Github issue closed' => 'Github-issue suljettu', - 'Github issue reopened' => 'Github-issue uudelleenavattu', - 'Github issue assignee change' => 'Github-issuen saajan vaihto', - 'Github issue label change' => 'Github-issuen labelin vaihto', 'Create a task from an external provider' => 'Luo tehtävä ulkoiselta tarjoajalta', 'Change the assignee based on an external username' => 'Vaihda tehtävän saajaa perustuen ulkoiseen käyttäjänimeen', 'Change the category based on an external label' => 'Vaihda kategoriaa perustuen ulkoiseen labeliin', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Kaikilla on käyttöoikeus projektiin.', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', 'Project management' => 'Projektin hallinta', 'My projects' => 'Minun projektini', 'Columns' => 'Sarakkeet', @@ -508,7 +499,6 @@ return array( // 'User repartition for "%s"' => '', 'Clone this project' => 'Kahdenna projekti', 'Column removed successfully.' => 'Sarake poistettu onnstuneesti.', - 'Github Issue' => 'Github-issue', 'Not enough data to show the graph.' => 'Ei riittävästi dataa graafin näyttämiseksi.', 'Previous' => 'Edellinen', 'The id must be an integer' => 'ID:n on oltava kokonaisluku', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Kaista luotu onnistuneesti.', 'Example: "Bug, Feature Request, Improvement"' => 'Esimerkiksi: "Bugit, Ominaisuuspyynnöt, Parannukset"', 'Default categories for new projects (Comma-separated)' => 'Oletuskategoriat uusille projekteille (pilkuin eroteltu)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', // 'Integrations' => '', // 'Integration with third-party services' => '', - // 'Gitlab Issue' => '', // 'Subtask Id' => '', // 'Subtasks' => '', // 'Subtasks Export' => '', @@ -588,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', // 'Which parts of the project do you want to duplicate?' => '', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', // 'Start' => '', // 'End' => '', // 'Task age in days' => '', @@ -688,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/fr_FR/translations.php b/sources/app/Locale/fr_FR/translations.php index 5c730eb..26490b1 100644 --- a/sources/app/Locale/fr_FR/translations.php +++ b/sources/app/Locale/fr_FR/translations.php @@ -439,12 +439,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s a changé la personne assignée à la tâche %s pour %s', 'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »', 'Choose an event' => 'Choisir un événement', - 'Github commit received' => 'Commit reçu via Github', - 'Github issue opened' => 'Ouverture d\'un ticket sur Github', - 'Github issue closed' => 'Fermeture d\'un ticket sur Github', - 'Github issue reopened' => 'Réouverture d\'un ticket sur Github', - 'Github issue assignee change' => 'Changement d\'assigné sur un ticket Github', - 'Github issue label change' => 'Changement de libellé sur un ticket Github', 'Create a task from an external provider' => 'Créer une tâche depuis un fournisseur externe', 'Change the assignee based on an external username' => 'Changer l\'assigné en fonction d\'un utilisateur externe', 'Change the category based on an external label' => 'Changer la catégorie en fonction d\'un libellé externe', @@ -475,7 +469,7 @@ return array( 'This project is private' => 'Ce projet est privé', 'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici', 'Add' => 'Ajouter', - 'Estimated time: %s hours' => 'Temps estimé: %s hours', + 'Estimated time: %s hours' => 'Temps estimé: %s heures', 'Time spent: %s hours' => 'Temps passé : %s heures', 'Started on %B %e, %Y' => 'Commençé le %d/%m/%Y', 'Start date' => 'Date de début', @@ -489,10 +483,7 @@ return array( 'Everybody have access to this project.' => 'Tout le monde a accès à ce projet.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Webhook Github', - 'Help on Github webhooks' => 'Aide sur les webhooks Github', 'Create a comment from an external provider' => 'Créer un commentaire depuis un fournisseur externe', - 'Github issue comment created' => 'Commentaire créé sur un ticket Github', 'Project management' => 'Gestion des projets', 'My projects' => 'Mes projets', 'Columns' => 'Colonnes', @@ -510,7 +501,6 @@ return array( 'User repartition for "%s"' => 'Répartition des utilisateurs pour « %s »', 'Clone this project' => 'Cloner ce projet', 'Column removed successfully.' => 'Colonne supprimée avec succès.', - 'Github Issue' => 'Ticket Github', 'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique.', 'Previous' => 'Précédent', 'The id must be an integer' => 'L\'id doit être un entier', @@ -555,14 +545,8 @@ return array( 'Your swimlane have been created successfully.' => 'Votre swimlane a été créée avec succès.', 'Example: "Bug, Feature Request, Improvement"' => 'Exemple: « Incident, Demande de fonctionnalité, Amélioration »', 'Default categories for new projects (Comma-separated)' => 'Catégories par défaut pour les nouveaux projets (séparation par des virgules)', - 'Gitlab commit received' => 'Commit reçu via Gitlab', - 'Gitlab issue opened' => 'Ouverture d\'un ticket sur Gitlab', - 'Gitlab issue closed' => 'Fermeture d\'un ticket sur Gitlab', - 'Gitlab webhooks' => 'Webhook Gitlab', - 'Help on Gitlab webhooks' => 'Aide sur les webhooks Gitlab', 'Integrations' => 'Intégrations', 'Integration with third-party services' => 'Intégration avec des services externes', - 'Gitlab Issue' => 'Ticket Gitlab', 'Subtask Id' => 'Identifiant de la sous-tâche', 'Subtasks' => 'Sous-tâches', 'Subtasks Export' => 'Exportation des sous-tâches', @@ -590,9 +574,6 @@ return array( 'You already have one subtask in progress' => 'Vous avez déjà une sous-tâche en progrès', 'Which parts of the project do you want to duplicate?' => 'Quelles parties du projet voulez-vous dupliquer ?', 'Disallow login form' => 'Interdire le formulaire d\'authentification', - 'Bitbucket commit received' => 'Commit reçu via Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Aide sur les webhooks Bitbucket', 'Start' => 'Début', 'End' => 'Fin', 'Task age in days' => 'Âge de la tâche en jours', @@ -690,9 +671,7 @@ return array( 'The two factor authentication code is valid.' => 'Le code pour l\'authentification à deux-facteurs est valide.', 'Code' => 'Code', 'Two factor authentication' => 'Authentification à deux-facteurs', - 'Enable/disable two factor authentication' => 'Activer/désactiver l\'authentification à deux-facteurs', 'This QR code contains the key URI: ' => 'Ce code QR contient l\'url de la clé : ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Sauvegardez cette clé secrète dans votre logiciel TOTP (par exemple Google Authenticator ou FreeOTP).', 'Check my code' => 'Vérifier mon code', 'Secret key: ' => 'Clé secrète : ', 'Test your device' => 'Testez votre appareil', @@ -714,7 +693,7 @@ return array( 'Disable two factor authentication' => 'Désactiver l\'authentification à deux facteurs', 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Voulez-vous vraiment désactiver l\'authentification à deux facteurs pour cet utilisateur : « %s » ?', 'Edit link' => 'Modifier un lien', - 'Start to type task title...' => 'Entrez le titre de la tâche…', + 'Start to type task title...' => 'Entrez le titre de la tâche...', 'A task cannot be linked to itself' => 'Une tâche ne peut être liée à elle-même', 'The exact same link already exists' => 'Un lien identique existe déjà', 'Recurrent task is scheduled to be generated' => 'La tâche récurrente est programmée pour être créée', @@ -764,21 +743,10 @@ return array( 'User that will receive the email' => 'Utilisateur qui va reçevoir l\'email', 'Email subject' => 'Sujet de l\'email', 'Date' => 'Date', - 'By @%s on Bitbucket' => 'Par @%s sur Bitbucket', - 'Bitbucket Issue' => 'Ticket Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Commit fait par @%s sur Bitbucket', - 'Commit made by @%s on Github' => 'Commit fait par @%s sur Github', - 'By @%s on Github' => 'Par @%s sur Github', - 'Commit made by @%s on Gitlab' => 'Commit fait par @%s sur Gitlab', 'Add a comment log when moving the task between columns' => 'Ajouter un commentaire d\'information lorsque une tâche est déplacée dans une autre colonne', 'Move the task to another column when the category is changed' => 'Déplacer une tâche vers une autre colonne lorsque la catégorie a changé', 'Send a task by email to someone' => 'Envoyer une tâche par email à quelqu\'un', 'Reopen a task' => 'Rouvrir une tâche', - 'Bitbucket issue opened' => 'Ticket Bitbucket ouvert', - 'Bitbucket issue closed' => 'Ticket Bitbucket fermé', - 'Bitbucket issue reopened' => 'Ticket Bitbucket rouvert', - 'Bitbucket issue assignee change' => 'Changement d\'assigné sur un ticket Bitbucket', - 'Bitbucket issue comment created' => 'Commentaire créé sur un ticket Bitbucket', 'Column change' => 'Changement de colonne', 'Position change' => 'Changement de position', 'Swimlane change' => 'Changement de swimlane', @@ -898,8 +866,6 @@ return array( 'Remote user' => 'Utilisateur distant', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Les utilisateurs distants ne stockent pas leur mot de passe dans la base de données de Kanboard, exemples : comptes LDAP, Github ou Google.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si vous cochez la case « Interdire le formulaire d\'authentification », les identifiants entrés dans le formulaire d\'authentification seront ignorés.', - 'By @%s on Gitlab' => 'Par @%s sur Gitlab', - 'Gitlab issue comment created' => 'Commentaire créé sur un ticket Gitlab', 'New remote user' => 'Créer un utilisateur distant', 'New local user' => 'Créer un utilisateur local', 'Default task color' => 'Couleur par défaut des tâches', @@ -1030,7 +996,6 @@ return array( 'Append/Replace' => 'Ajouter/Remplaçer', 'Append' => 'Ajouter', 'Replace' => 'Remplaçer', - 'There is no notification method registered.' => 'Il n\'y a aucune méthode de notification enregistrée.', 'Import' => 'Importation', 'change sorting' => 'changer l\'ordre', 'Tasks Importation' => 'Importation des tâches', @@ -1076,7 +1041,6 @@ return array( 'Project Manager' => 'Chef de projet', 'Project Member' => 'Membre du projet', 'Project Viewer' => 'Visualiseur de projet', - 'Gitlab issue reopened' => 'Ticket Gitlab rouvert', 'Your account is locked for %d minutes' => 'Votre compte est vérouillé pour %d minutes', 'Invalid captcha' => 'Captcha invalid', 'The name must be unique' => 'Le nom doit être unique', @@ -1101,5 +1065,46 @@ return array( 'Group Name' => 'Nom du groupe', 'Enter group name...' => 'Entrez le nom du groupe...', 'Role:' => 'Rôle :', - 'Project members' => 'Membres du project', + 'Project members' => 'Membres du projet', + 'Compare hours for "%s"' => 'Comparer les heures pour « %s »', + '%s mentioned you in the task #%d' => '%s vous a mentionné dans la tâche n°%d', + '%s mentioned you in a comment on the task #%d' => '%s vous a mentionné dans un commentaire de la tâche n°%d', + 'You were mentioned in the task #%d' => 'Vous avez été mentionné dans la tâche n°%d', + 'You were mentioned in a comment on the task #%d' => 'Vous avez été mentionné dans un commentaire de la tâche n°%d', + 'Mentioned' => 'Mentionné', + 'Compare Estimated Time vs Actual Time' => 'Comparer le temps estimé et le temps actuel', + 'Estimated hours: ' => 'Heures estimées : ', + 'Actual hours: ' => 'Heures actuelles : ', + 'Hours Spent' => 'Heures passées', + 'Hours Estimated' => 'Heures estimées', + 'Estimated Time' => 'Temps estimé', + 'Actual Time' => 'Temps actuel', + 'Estimated vs actual time' => 'Temps estimé vs actuel', + 'RUB - Russian Ruble' => 'RUB - Rouble russe', + 'Assign the task to the person who does the action when the column is changed' => 'Assigner la tâche à la personne qui fait l\'action lorsque la colonne est changée', + 'Close a task in a specific column' => 'Fermer une tâche dans une colonne specifique', + 'Time-based One-time Password Algorithm' => 'Mot de passe à usage unique basé sur le temps', + 'Two-Factor Provider: ' => 'Fournisseur d\'authentification à deux facteurs : ', + 'Disable two-factor authentication' => 'Désactiver l\'authentification à deux-facteurs', + 'Enable two-factor authentication' => 'Activer l\'authentification à deux-facteurs', + 'There is no integration registered at the moment.' => 'Il n\'y a aucune intégration enregistrée pour le moment.', + 'Password Reset for Kanboard' => 'Réinitialisation du mot de passe pour Kanboard', + 'Forgot password?' => 'Mot de passe oublié ?', + 'Enable "Forget Password"' => 'Activer la fonctionnalité « Mot de passe oublié »', + 'Password Reset' => 'Réinitialisation du mot de passe', + 'New password' => 'Nouveau mot de passe', + 'Change Password' => 'Changer de mot de passe', + 'To reset your password click on this link:' => 'Pour réinitialiser votre mot de passe cliquer sur ce lien :', + 'Last Password Reset' => 'Dernières réinitialisation de mot de passe', + 'The password has never been reinitialized.' => 'Le mot de passe n\'a jamais été réinitialisé.', + 'Creation' => 'Création', + 'Expiration' => 'Expiration', + 'Password reset history' => 'Historique de la réinitialisation du mot de passe', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Toutes les tâches de la colonne « %s » et de la swimlane « %s » ont été fermées avec succès.', + 'Do you really want to close all tasks of this column?' => 'Voulez-vous vraiment fermer toutes les tâches de cette colonne ?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tâche(s) dans la colonne « %s » et la swimlane « %s » seront fermées.', + 'Close all tasks of this column' => 'Fermer toutes les tâches de cette colonne', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Aucun plugin n\'a enregistré une méthode de notification de projet. Vous pouvez toujours configurer les notifications individuelles dans votre profil d\'utilisateur.', + 'My dashboard' => 'Mon tableau de bord', + 'My profile' => 'Mon profile', ); diff --git a/sources/app/Locale/hu_HU/translations.php b/sources/app/Locale/hu_HU/translations.php index 6fb1801..8f93d1c 100644 --- a/sources/app/Locale/hu_HU/translations.php +++ b/sources/app/Locale/hu_HU/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s', 'New password for the user "%s"' => 'Felhasználó új jelszava: %s', 'Choose an event' => 'Válasszon eseményt', - 'Github commit received' => 'Github commit érkezett', - 'Github issue opened' => 'Github issue nyitás', - 'Github issue closed' => 'Github issue zárás', - 'Github issue reopened' => 'Github issue újranyitva', - 'Github issue assignee change' => 'Github issue felelős változás', - 'Github issue label change' => 'Github issue címke változás', 'Create a task from an external provider' => 'Feladat létrehozása külsős számára', 'Change the assignee based on an external username' => 'Felelős módosítása külső felhasználónév alapján', 'Change the category based on an external label' => 'Kategória módosítása külső címke alapján', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Mindenki elérheti a projektet', 'Webhooks' => 'Webhook', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Github Webhook súgó', 'Create a comment from an external provider' => 'Megjegyzés létrehozása külső felhasználótól', - 'Github issue comment created' => 'Github issue megjegyzés létrehozva', 'Project management' => 'Projekt menedzsment', 'My projects' => 'Projektjeim', 'Columns' => 'Oszlopok', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s', 'Clone this project' => 'Projekt másolása', 'Column removed successfully.' => 'Oszlop sikeresen törölve.', - 'Github Issue' => 'Github issue', 'Not enough data to show the graph.' => 'Nincs elég adat a grafikonhoz.', 'Previous' => 'Előző', 'The id must be an integer' => 'Az ID csak egész szám lehet', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'A folyamat sikeresen létrehozva.', 'Example: "Bug, Feature Request, Improvement"' => 'Például: Hiba, Új funkció, Fejlesztés', 'Default categories for new projects (Comma-separated)' => 'Alapértelmezett kategóriák az új projektekben (Vesszővel elválasztva)', - 'Gitlab commit received' => 'Gitlab commit érkezett', - 'Gitlab issue opened' => 'Gitlab issue nyitás', - 'Gitlab issue closed' => 'Gitlab issue zárás', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Gitlab webhooks súgó', 'Integrations' => 'Integráció', 'Integration with third-party services' => 'Integráció harmadik féllel', - 'Gitlab Issue' => 'Gitlab issue', 'Subtask Id' => 'Részfeladat id', 'Subtasks' => 'Részfeladatok', 'Subtasks Export' => 'Részfeladat exportálás', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Már van egy folyamatban levő részfeladata', 'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné másolni?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit érkezett', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Bitbucket webhooks súgó', 'Start' => 'Kezdet', 'End' => 'Vég', 'Task age in days' => 'Feladat életkora napokban', @@ -688,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/id_ID/translations.php b/sources/app/Locale/id_ID/translations.php index 686124f..170a5b7 100644 --- a/sources/app/Locale/id_ID/translations.php +++ b/sources/app/Locale/id_ID/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s', 'New password for the user "%s"' => 'Kata sandi baru untuk pengguna « %s »', 'Choose an event' => 'Pilih acara', - 'Github commit received' => 'Menerima komit dari Github', - 'Github issue opened' => 'Tiket Github dibuka', - 'Github issue closed' => 'Tiket Github ditutup', - 'Github issue reopened' => 'Tiket Github dibuka kembali', - 'Github issue assignee change' => 'Rubah penugasan tiket Github', - 'Github issue label change' => 'Perubahan label pada tiket Github', 'Create a task from an external provider' => 'Buat tugas dari pemasok eksternal', 'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal', 'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Semua orang mendapat akses untuk proyek ini.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Webhook Github', - 'Help on Github webhooks' => 'Bantuan pada webhook Github', 'Create a comment from an external provider' => 'Buat komentar dari pemasok eksternal', - 'Github issue comment created' => 'Komentar dibuat pada tiket Github', 'Project management' => 'Manajemen proyek', 'My projects' => 'Proyek saya', 'Columns' => 'Kolom', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Partisi ulang pengguna untuk « %s »', 'Clone this project' => 'Gandakan proyek ini', 'Column removed successfully.' => 'Kolom berhasil dihapus.', - 'Github Issue' => 'Tiket Github', 'Not enough data to show the graph.' => 'Tidak cukup data untuk menampilkan grafik.', 'Previous' => 'Sebelumnya', 'The id must be an integer' => 'Id harus integer', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.', 'Example: "Bug, Feature Request, Improvement"' => 'Contoh: « Insiden, Permintaan Fitur, Perbaikan »', 'Default categories for new projects (Comma-separated)' => 'Standar kategori untuk proyek baru (dipisahkan dengan koma)', - 'Gitlab commit received' => 'Menerima komit Gitlab', - 'Gitlab issue opened' => 'Tiket Gitlab dibuka', - 'Gitlab issue closed' => 'Tiket Gitlab ditutup', - 'Gitlab webhooks' => 'Webhook Gitlab', - 'Help on Gitlab webhooks' => 'Bantuan pada webhook Gitlab', 'Integrations' => 'Integrasi', 'Integration with third-party services' => 'Integrasi dengan layanan pihak ketiga', - 'Gitlab Issue' => 'Tiket Gitlab', 'Subtask Id' => 'Id Subtugas', 'Subtasks' => 'Subtugas', 'Subtasks Export' => 'Ekspor Subtugas', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Anda sudah ada satu subtugas dalam proses', 'Which parts of the project do you want to duplicate?' => 'Bagian dalam proyek mana yang ingin anda duplikasi?', 'Disallow login form' => 'Larang formulir masuk', - 'Bitbucket commit received' => 'Menerima komit Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Bantuan pada webhook Bitbucket', 'Start' => 'Mulai', 'End' => 'Selesai', 'Task age in days' => 'Usia tugas dalam hari', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Kode dua faktor kode otentifikasi valid.', 'Code' => 'Kode', 'Two factor authentication' => 'Dua faktor otentifikasi', - 'Enable/disable two factor authentication' => 'Matikan/hidupkan dua faktor otentifikasi', 'This QR code contains the key URI: ' => 'kode QR ini mengandung kunci URI : ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Menyimpan kunci rahasia ini dalam perangkat lunak TOTP anda(misalnya Googel Authenticator atau FreeOTP).', 'Check my code' => 'Memeriksa kode saya', 'Secret key: ' => 'Kunci rahasia : ', 'Test your device' => 'Menguji perangkat anda', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'Pengguna yang akan menerima email', 'Email subject' => 'Subjek Email', 'Date' => 'Tanggal', - 'By @%s on Bitbucket' => 'Oleh @%s pada Bitbucket', - 'Bitbucket Issue' => 'Tiket Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Komit dibuat oleh @%s pada Bitbucket', - 'Commit made by @%s on Github' => 'Komit dibuat oleh @%s pada Github', - 'By @%s on Github' => 'Oleh @%s pada Github', - 'Commit made by @%s on Gitlab' => 'Komit dibuat oleh @%s pada Gitlab', 'Add a comment log when moving the task between columns' => 'Menambahkan log komentar ketika memindahkan tugas antara kolom', 'Move the task to another column when the category is changed' => 'Pindahkan tugas ke kolom lain ketika kategori berubah', 'Send a task by email to someone' => 'Kirim tugas melalui email ke seseorang', 'Reopen a task' => 'Membuka kembali tugas', - 'Bitbucket issue opened' => 'Tiket Bitbucket dibuka', - 'Bitbucket issue closed' => 'Tiket Bitbucket ditutup', - 'Bitbucket issue reopened' => 'Tiket Bitbucket dibuka kembali', - 'Bitbucket issue assignee change' => 'Perubahan penugasan tiket Bitbucket', - 'Bitbucket issue comment created' => 'Komentar dibuat tiket Bitbucket', 'Column change' => 'Kolom berubah', 'Position change' => 'Posisi berubah', 'Swimlane change' => 'Swimlane berubah', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Pengguna jauh', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata sandi mereka dalam basis data Kanboard, contoh: akun LDAP, Google dan Github.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.', - 'By @%s on Gitlab' => 'Dengan @%s pada Gitlab', - 'Gitlab issue comment created' => 'Komentar dibuat pada tiket Gitlab', 'New remote user' => 'Pengguna baru jauh', 'New local user' => 'Pengguna baru lokal', 'Default task color' => 'Standar warna tugas', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', 'Project members' => 'Anggota proyek', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/it_IT/translations.php b/sources/app/Locale/it_IT/translations.php index 69975f4..cd86c98 100644 --- a/sources/app/Locale/it_IT/translations.php +++ b/sources/app/Locale/it_IT/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s ha cambiato l\'assegnatario del compito %s a %s', 'New password for the user "%s"' => 'Nuova password per l\'utente "%s"', 'Choose an event' => 'Scegli un evento', - 'Github commit received' => 'Commit di Github ricevuto', - 'Github issue opened' => 'Issue di Github ricevuto', - 'Github issue closed' => 'Issue di Github chiusa', - 'Github issue reopened' => 'Issue di Github riaperta', - 'Github issue assignee change' => 'Assegnatario dell\'issue di Github cambiato', - 'Github issue label change' => 'Etichetta dell\'issue di Github cambiata', 'Create a task from an external provider' => 'Crea un compito da un provider esterno', 'Change the assignee based on an external username' => 'Cambia l\'assegnatario basandosi su un username esterno', 'Change the category based on an external label' => 'Cambia la categoria basandosi su un\'etichetta esterna', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Tutti hanno accesso a questo progetto', // 'Webhooks' => '', // 'API' => '', - 'Github webhooks' => 'Webhooks di Github', - 'Help on Github webhooks' => 'Guida ai Webhooks di Github', 'Create a comment from an external provider' => 'Crea un commit da un provider esterno', - 'Github issue comment created' => 'Commento ad un Issue di Github creato', 'Project management' => 'Gestione del progetto', 'My projects' => 'I miei progetti', 'Columns' => 'Colonne', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Ripartizione utente per "%s"', 'Clone this project' => 'Clona questo progetto', 'Column removed successfully.' => 'Colonna rimossa con successo', - 'Github Issue' => 'Issue di Github', 'Not enough data to show the graph.' => 'Non ci sono abbastanza dati per visualizzare il grafico.', 'Previous' => 'Precendete', 'The id must be an integer' => 'L\'id deve essere un intero', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'La sua corsia è stata creata con successo', 'Example: "Bug, Feature Request, Improvement"' => 'Esempio: "Bug, Richiesta di Funzioni, Migliorie"', 'Default categories for new projects (Comma-separated)' => 'Categorie di default per i progetti (Separati da virgola)', - 'Gitlab commit received' => 'Commit ricevuto da Gitlab', - 'Gitlab issue opened' => 'Issue di Gitlab aperta', - 'Gitlab issue closed' => 'Issue di Gitlab chiusa', - 'Gitlab webhooks' => 'Webhooks di Gitlab', - 'Help on Gitlab webhooks' => 'Guida ai Webhooks di Gitlab', 'Integrations' => 'Integrazioni', 'Integration with third-party services' => 'Integrazione con servizi di terze parti', - 'Gitlab Issue' => 'Issue di Gitlab', 'Subtask Id' => 'Id del sotto-compito', 'Subtasks' => 'Sotto-compiti', 'Subtasks Export' => 'Esporta sotto-compiti', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Hai già un sotto-compito in progresso', 'Which parts of the project do you want to duplicate?' => 'Quali parti del progetto vuoi duplicare?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Commit ricevuto da Bitbucket', - 'Bitbucket webhooks' => 'Webhooks di Bitbucket', - 'Help on Bitbucket webhooks' => 'Guida ai Webhooks di Bitbucket', 'Start' => 'Inizio', 'End' => 'Fine', 'Task age in days' => 'Anzianità del compito in giorni', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Il codice di autenticazione a due fattori è valido', 'Code' => 'Codice', 'Two factor authentication' => 'Autenticazione a due fattori', - 'Enable/disable two factor authentication' => 'Abilita/disabilita autenticazione a due fattori', 'This QR code contains the key URI: ' => 'Questo QR code contiene l\'URI: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salva la chiave privata nel tuo software TOTP (per esempio Google Authenticator oppure FreeOTP).', 'Check my code' => 'Controlla il mio codice', 'Secret key: ' => 'Chiave privata:', 'Test your device' => 'Testa il tuo dispositivo', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/ja_JP/translations.php b/sources/app/Locale/ja_JP/translations.php index b4bcfac..e91a3bf 100644 --- a/sources/app/Locale/ja_JP/translations.php +++ b/sources/app/Locale/ja_JP/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s がタスク %s の担当を %s に変更しました', 'New password for the user "%s"' => 'ユーザ「%s」の新しいパスワード', 'Choose an event' => 'イベントの選択', - 'Github commit received' => 'Github のコミットを受け取った', - 'Github issue opened' => 'Github Issue がオープンされた', - 'Github issue closed' => 'Github Issue がクローズされた', - 'Github issue reopened' => 'Github Issue が再オープンされた', - 'Github issue assignee change' => 'Github Issue の担当が変更された', - 'Github issue label change' => 'Github のラベルが変更された', 'Create a task from an external provider' => 'タスクを外部サービスから作成する', 'Change the assignee based on an external username' => '担当者を外部サービスに基いて変更する', 'Change the category based on an external label' => 'カテゴリを外部サービスに基いて変更する', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => '誰でもこのプロジェクトにアクセスできます。', 'Webhooks' => 'Webhook', 'API' => 'API', - 'Github webhooks' => 'Github Webhook', - 'Help on Github webhooks' => 'Github webhook のヘルプ', 'Create a comment from an external provider' => '外部サービスからコメントを作成する', - 'Github issue comment created' => 'Github Issue コメントが作られました', 'Project management' => 'プロジェクト・マネジメント', 'My projects' => '自分のプロジェクト', 'Columns' => 'カラム', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => '「%s」の担当者分布', 'Clone this project' => 'このプロジェクトを複製する', 'Column removed successfully.' => 'カラムを削除しました', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'グラフを描画するには出たが足りません', 'Previous' => '戻る', 'The id must be an integer' => 'id は数字でなければなりません', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'スイムレーンが作成されました。', 'Example: "Bug, Feature Request, Improvement"' => '例: バグ, 機能, 改善', 'Default categories for new projects (Comma-separated)' => '新しいプロジェクトのデフォルトカテゴリー (コンマ区切り)', - 'Gitlab commit received' => 'Gitlab コミットを受診しました', - 'Gitlab issue opened' => 'Gitlab Issue がオープンされました', - 'Gitlab issue closed' => 'Gitlab Issue がクローズされました', - 'Gitlab webhooks' => 'Gitlab Webhooks', - 'Help on Gitlab webhooks' => 'Gitlab Webhooks のヘルプ', 'Integrations' => '連携', 'Integration with third-party services' => 'サードパーティサービスとの連携', - 'Gitlab Issue' => 'Gitlab Issue', 'Subtask Id' => 'サブタスク Id', 'Subtasks' => 'サブタスク', 'Subtasks Export' => 'サブタスクの出力', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'すでに進行中のサブタスクがあります。', 'Which parts of the project do you want to duplicate?' => 'プロジェクトの何を複製しますか?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket コミットを受信しました', - 'Bitbucket webhooks' => 'Bitbucket Webhooks', - 'Help on Bitbucket webhooks' => 'Bitbucket Webhooks のヘルプ', 'Start' => '開始', 'End' => '終了', 'Task age in days' => 'タスクの経過日数', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => '2 段認証コードは有効です。', 'Code' => 'コード', 'Two factor authentication' => '2 段認証', - 'Enable/disable two factor authentication' => '2 段認証の有効/無効', 'This QR code contains the key URI: ' => 'この QR コードが URI キーを含んでいます: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '秘密鍵を TOTP ソフトに保存 (Google Authenticator や FreeOTP など)', 'Check my code' => '自分のコードをチェック', 'Secret key: ' => '秘密鍵: ', 'Test your device' => 'デバイスをテストする', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/my_MY/translations.php b/sources/app/Locale/my_MY/translations.php new file mode 100644 index 0000000..e83f915 --- /dev/null +++ b/sources/app/Locale/my_MY/translations.php @@ -0,0 +1,1107 @@ + '.', + 'number.thousands_separator' => ',', + 'None' => 'Tiada', + 'edit' => 'sunting', + 'Edit' => 'Sunting', + 'remove' => 'hapus', + 'Remove' => 'Hapus', + 'Update' => 'Kemaskini', + 'Yes' => 'Ya', + 'No' => 'Tidak', + 'cancel' => 'batal', + 'or' => 'atau', + 'Yellow' => 'Kuning', + 'Blue' => 'Biru', + 'Green' => 'Hijau', + 'Purple' => 'Ungu', + 'Red' => 'Merah', + 'Orange' => 'Oren', + 'Grey' => 'Kelabu', + 'Brown' => 'Coklat', + 'Deep Orange' => 'Oren Gelap', + 'Dark Grey' => 'Kelabu Malap', + 'Pink' => 'Merah Jambu', + 'Teal' => 'Teal', + 'Cyan' => 'Sian', + 'Lime' => 'Lime', + 'Light Green' => 'Hijau Muda', + 'Amber' => 'Amber', + 'Save' => 'Simpan', + 'Login' => 'Masuk', + 'Official website:' => 'Laman rasmi :', + 'Unassigned' => 'Belum ditugaskan', + 'View this task' => 'Lihat tugas ini', + 'Remove user' => 'Hapus pengguna', + 'Do you really want to remove this user: "%s"?' => 'Anda yakin mahu menghapus pengguna ini : « %s » ?', + 'New user' => 'Pengguna baru', + 'All users' => 'Semua pengguna', + 'Username' => 'Nama pengguna', + 'Password' => 'Kata laluan', + 'Administrator' => 'Pentadbir', + 'Sign in' => 'Masuk', + 'Users' => 'Para Pengguna', + 'No user' => 'Tiada pengguna', + 'Forbidden' => 'Larangan', + 'Access Forbidden' => 'Akses Dilarang', + 'Edit user' => 'Ubah Pengguna', + 'Logout' => 'Keluar', + 'Bad username or password' => 'Nama pengguna atau kata laluan tidak sepadan', + 'Edit project' => 'Ubah projek', + 'Name' => 'Nama', + 'Projects' => 'Projek', + 'No project' => 'Tiada projek', + 'Project' => 'Projek', + 'Status' => 'Status', + 'Tasks' => 'Tugasan', + 'Board' => 'Papan', + 'Actions' => 'Tindakan', + 'Inactive' => 'Tidak Aktif', + 'Active' => 'Aktif', + 'Add this column' => 'Tambahkan kolom ini', + '%d tasks on the board' => '%d tugasan di papan', + '%d tasks in total' => 'Sejumlah %d tugasan', + 'Unable to update this board.' => 'Tidak berupaya mengemaskini papan ini', + 'Edit board' => 'ubah papan', + 'Disable' => 'Nyah-Upaya', + 'Enable' => 'Aktifkan', + 'New project' => 'Projek Baru', + 'Do you really want to remove this project: "%s"?' => 'Anda yakin mahu menghapus projek ini : « %s » ?', + 'Remove project' => 'Hapus projek', + 'Edit the board for "%s"' => 'Ubah papan untuk « %s »', + 'All projects' => 'Semua projek', + 'Change columns' => 'Ubah kolom', + 'Add a new column' => 'Tambah kolom baru', + 'Title' => 'Judul', + 'Nobody assigned' => 'Tidak ada yang ditugaskan', + 'Assigned to %s' => 'Ditugaskan ke %s', + 'Remove a column' => 'Hapus kolom', + 'Remove a column from a board' => 'Hapus kolom dari papan', + 'Unable to remove this column.' => 'Tidak dapat menghapus kolom ini.', + 'Do you really want to remove this column: "%s"?' => 'Apakah anda yakin akan menghapus kolom ini : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'tindakan ini akan MENGHAPUS SEMUA TUGAS yang terkait dengan kolom ini!', + 'Settings' => 'Penetapan', + 'Application settings' => 'Penetapan aplikasi', + 'Language' => 'Bahasa', + 'Webhook token:' => 'Token webhook :', + 'API token:' => 'Token API :', + 'Database size:' => 'Saiz pengkalan data:', + 'Download the database' => 'Muat turun pengkalan data', + 'Optimize the database' => 'Optimakan pengkalan data', + '(VACUUM command)' => '(perintah VACUUM)', + '(Gzip compressed Sqlite file)' => '(File Sqlite yang termampat Gzip)', + 'Close a task' => 'Tutup tugas', + 'Edit a task' => 'Sunting tugas', + 'Column' => 'Kolom', + 'Color' => 'Warna', + 'Assignee' => 'Orang yang ditugaskan', + 'Create another task' => 'Buat tugas lain', + 'New task' => 'Tugasan baru', + 'Open a task' => 'Buka tugas', + 'Do you really want to open this task: "%s"?' => 'Anda yakin untuk buka tugas ini : « %s » ?', + 'Back to the board' => 'Kembali ke papan', + 'Created on %B %e, %Y at %k:%M %p' => 'Dicipta pada tanggal %d/%m/%Y à %H:%M', + 'There is nobody assigned' => 'Tidak ada orang yand ditugaskan', + 'Column on the board:' => 'Kolom di dalam papan : ', + 'Status is open' => 'Status terbuka', + 'Status is closed' => 'Status ditutup', + 'Close this task' => 'Tutup tugas ini', + 'Open this task' => 'Buka tugas ini', + 'There is no description.' => 'Tidak ada keterangan.', + 'Add a new task' => 'Tambah tugas baru', + 'The username is required' => 'Nama pengguna adalah wajib', + 'The maximum length is %d characters' => 'Panjang maksimum adalah %d karakter', + 'The minimum length is %d characters' => 'Panjang minimum adalah %d karakter', + 'The password is required' => 'Kata laluan adalah wajib', + 'This value must be an integer' => 'Nilai ini harus integer', + 'The username must be unique' => 'Nama pengguna semestinya unik', + 'The user id is required' => 'Id Pengguna adalah wajib', + 'Passwords don\'t match' => 'Kata laluan tidak sepadan', + 'The confirmation is required' => 'Pengesahan diperlukan', + 'The project is required' => 'Projek diperlukan', + 'The id is required' => 'Id diperlukan', + 'The project id is required' => 'Id projek diperlukan', + 'The project name is required' => 'Nama projek diperlukan', + 'The title is required' => 'Judul diperlukan', + 'Settings saved successfully.' => 'Penetapan berjaya disimpan.', + 'Unable to save your settings.' => 'Tidak dapat menyimpan penetapan anda.', + 'Database optimization done.' => 'Optimasi pengkalan data selesai.', + 'Your project have been created successfully.' => 'Projek anda berhasil dibuat.', + 'Unable to create your project.' => 'Tidak dapat membuat projek anda.', + 'Project updated successfully.' => 'projek berhasil diperbaharui.', + 'Unable to update this project.' => 'Tidak dapat memperbaharui projek ini.', + 'Unable to remove this project.' => 'Tidak dapat menghapus projek ini.', + 'Project removed successfully.' => 'projek berhasil dihapus.', + 'Project activated successfully.' => 'projek berhasil diaktivasi.', + 'Unable to activate this project.' => 'Tidak dapat mengaktifkan projek ini.', + 'Project disabled successfully.' => 'projek berhasil dinonaktifkan.', + 'Unable to disable this project.' => 'Tidak dapat menonaktifkan projek ini.', + 'Unable to open this task.' => 'Tidak dapat membuka tugas ini.', + 'Task opened successfully.' => 'Tugas berhasil dibuka.', + 'Unable to close this task.' => 'Tidak dapat menutup tugas ini.', + 'Task closed successfully.' => 'Tugas berhasil ditutup.', + 'Unable to update your task.' => 'Tidak dapat memperbaharui tugas ini.', + 'Task updated successfully.' => 'Tugas berhasil diperbaharui.', + 'Unable to create your task.' => 'Tidak dapat membuat tugas anda.', + 'Task created successfully.' => 'Tugas berhasil dibuat.', + 'User created successfully.' => 'Pengguna berhasil dibuat.', + 'Unable to create your user.' => 'Tidak dapat membuat pengguna anda.', + 'User updated successfully.' => 'Pengguna berhasil diperbaharui.', + 'Unable to update your user.' => 'Tidak dapat memperbaharui pengguna anda.', + 'User removed successfully.' => 'pengguna berhasil dihapus.', + 'Unable to remove this user.' => 'Tidak dapat menghapus pengguna ini.', + 'Board updated successfully.' => 'Papan berhasil diperbaharui.', + 'Ready' => 'Siap', + 'Backlog' => 'Tertunda', + 'Work in progress' => 'Sedang dalam pengerjaan', + 'Done' => 'Selesai', + 'Application version:' => 'Versi aplikasi :', + 'Completed on %B %e, %Y at %k:%M %p' => 'Diselesaikan pada tanggal %d/%m/%Y à %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M', + 'Date created' => 'Tanggal dibuat', + 'Date completed' => 'Tanggal diselesaikan', + 'Id' => 'Id.', + '%d closed tasks' => '%d tugas yang ditutup', + 'No task for this project' => 'Tidak ada tugas dalam projek ini', + 'Public link' => 'Pautan publik', + 'Change assignee' => 'Mengubah orang yand ditugaskan', + 'Change assignee for the task "%s"' => 'Mengubah orang yang ditugaskan untuk tugas « %s »', + 'Timezone' => 'Zona waktu', + 'Sorry, I didn\'t find this information in my database!' => 'Maaf, saya tidak menemukan informasi ini dalam basis data saya !', + 'Page not found' => 'Halaman tidak ditemukan', + 'Complexity' => 'Kompleksitas', + 'Task limit' => 'Batas tugas.', + 'Task count' => 'Jumlah tugas', + 'User' => 'Pengguna', + 'Comments' => 'Komentar', + 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown', + 'Leave a comment' => 'Tinggalkan komentar', + 'Comment is required' => 'Komentar diperlukan', + 'Leave a description' => 'Tinggalkan deskripsi', + 'Comment added successfully.' => 'Komentar berhasil ditambahkan.', + 'Unable to create your comment.' => 'Tidak dapat menambahkan komentar anda.', + 'Edit this task' => 'Modifikasi tugas ini', + 'Due Date' => 'Batas Tanggal Terakhir', + 'Invalid date' => 'Tanggal tidak valid', + 'Must be done before %B %e, %Y' => 'Harus diselesaikan sebelum tanggal %d/%m/%Y', + '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d/%m/%Y', + 'Automatic actions' => 'Tindakan otomatis', + 'Your automatic action have been created successfully.' => 'Tindakan otomatis anda berhasil dibuat.', + 'Unable to create your automatic action.' => 'Tidak dapat membuat tindakan otomatis anda.', + 'Remove an action' => 'Hapus tindakan', + 'Unable to remove this action.' => 'Tidak dapat menghapus tindakan ini', + 'Action removed successfully.' => 'Tindakan berhasil dihapus.', + 'Automatic actions for the project "%s"' => 'Tindakan otomatis untuk projek ini « %s »', + 'Defined actions' => 'Tindakan didefinisikan', + 'Add an action' => 'Tambah tindakan', + 'Event name' => 'Nama acara', + 'Action name' => 'Nama tindakan', + 'Action parameters' => 'Parameter tindakan', + 'Action' => 'Tindakan', + 'Event' => 'Acara', + 'When the selected event occurs execute the corresponding action.' => 'Ketika acara yang dipilih terjadi, melakukan tindakan yang sesuai.', + 'Next step' => 'Langkah selanjutnya', + 'Define action parameters' => 'Definisi parameter tindakan', + 'Save this action' => 'Simpan tindakan ini', + 'Do you really want to remove this action: "%s"?' => 'Apakah anda yakin akan menghapus tindakan ini « %s » ?', + 'Remove an automatic action' => 'Hapus tindakan otomatis', + 'Assign the task to a specific user' => 'Menetapkan tugas untuk pengguna tertentu', + 'Assign the task to the person who does the action' => 'Memberikan tugas untuk orang yang melakukan tindakan', + 'Duplicate the task to another project' => 'Duplikasi tugas ke projek lain', + 'Move a task to another column' => 'Pindahkan tugas ke kolom lain', + 'Task modification' => 'Modifikasi tugas', + 'Task creation' => 'Membuat tugas', + 'Closing a task' => 'Menutup tugas', + 'Assign a color to a specific user' => 'Menetapkan warna untuk pengguna tertentu', + 'Column title' => 'Judul kolom', + 'Position' => 'Posisi', + 'Move Up' => 'Pindah ke atas', + 'Move Down' => 'Pindah ke bawah', + 'Duplicate to another project' => 'Duplikasi ke projek lain', + 'Duplicate' => 'Duplikasi', + 'link' => 'Pautan', + 'Comment updated successfully.' => 'Komentar berhasil diperbaharui.', + 'Unable to update your comment.' => 'Tidak dapat memperbaharui komentar anda.', + 'Remove a comment' => 'Hapus komentar', + 'Comment removed successfully.' => 'Komentar berhasil dihapus.', + 'Unable to remove this comment.' => 'Tidak dapat menghapus komentar ini.', + 'Do you really want to remove this comment?' => 'Apakah anda yakin akan menghapus komentar ini ?', + 'Only administrators or the creator of the comment can access to this page.' => 'Hanya administrator atau pembuat komentar yang dapat mengakses halaman ini.', + 'Current password for the user "%s"' => 'Kata laluan saat ini untuk pengguna « %s »', + 'The current password is required' => 'Kata laluan saat ini diperlukan', + 'Wrong password' => 'Kata laluan salah', + 'Unknown' => 'Tidak diketahui', + 'Last logins' => 'Masuk terakhir', + 'Login date' => 'Tanggal masuk', + 'Authentication method' => 'Metode otentifikasi', + 'IP address' => 'Alamat IP', + 'User agent' => 'Agen Pengguna', + 'Persistent connections' => 'Koneksi persisten', + 'No session.' => 'Tidak ada sesi.', + 'Expiration date' => 'Tanggal kadaluarsa', + 'Remember Me' => 'Ingat Saya', + 'Creation date' => 'Tanggal dibuat', + 'Everybody' => 'Semua orang', + 'Open' => 'Terbuka', + 'Closed' => 'Ditutup', + 'Search' => 'Cari', + 'Nothing found.' => 'Tidak ditemukan.', + 'Due date' => 'Batas tanggal terakhir', + 'Others formats accepted: %s and %s' => 'Format lain yang didukung : %s et %s', + 'Description' => 'Deskripsi', + '%d comments' => '%d komentar', + '%d comment' => '%d komentar', + 'Email address invalid' => 'Alamat email tidak valid', + 'Your external account is not linked anymore to your profile.' => 'Akaun eksternal anda tidak lagi terhubung ke profil anda.', + 'Unable to unlink your external account.' => 'Tidak dapat memutuskan Akaun eksternal anda.', + 'External authentication failed' => 'Otentifikasi eksternal gagal', + 'Your external account is linked to your profile successfully.' => 'Akaun eksternal anda berhasil dihubungkan ke profil anda.', + 'Email' => 'Email', + 'Link my Google Account' => 'Hubungkan Akaun Google saya', + 'Unlink my Google Account' => 'Putuskan Akaun Google saya', + 'Login with my Google Account' => 'Masuk menggunakan Akaun Google saya', + 'Project not found.' => 'projek tidak ditemukan.', + 'Task removed successfully.' => 'Tugas berhasil dihapus.', + 'Unable to remove this task.' => 'Tidak dapat menghapus tugas ini.', + 'Remove a task' => 'Hapus tugas', + 'Do you really want to remove this task: "%s"?' => 'Apakah anda yakin akan menghapus tugas ini « %s » ?', + 'Assign automatically a color based on a category' => 'Otomatis menetapkan warna berdasarkan kategori', + 'Assign automatically a category based on a color' => 'Otomatis menetapkan kategori berdasarkan warna', + 'Task creation or modification' => 'Tugas dibuat atau di mofifikasi', + 'Category' => 'Kategori', + 'Category:' => 'Kategori :', + 'Categories' => 'Kategori', + 'Category not found.' => 'Kategori tidak ditemukan', + 'Your category have been created successfully.' => 'Kategori anda berhasil dibuat.', + 'Unable to create your category.' => 'Tidak dapat membuat kategori anda.', + 'Your category have been updated successfully.' => 'Kategori anda berhasil diperbaharui.', + 'Unable to update your category.' => 'Tidak dapat memperbaharui kategori anda.', + 'Remove a category' => 'Hapus kategori', + 'Category removed successfully.' => 'Kategori berhasil dihapus.', + 'Unable to remove this category.' => 'Tidak dapat menghapus kategori ini.', + 'Category modification for the project "%s"' => 'Modifikasi kategori untuk projek « %s »', + 'Category Name' => 'Nama Kategori', + 'Add a new category' => 'Tambah kategori baru', + 'Do you really want to remove this category: "%s"?' => 'Apakah anda yakin akan menghapus kategori ini « %s » ?', + 'All categories' => 'Semua kategori', + 'No category' => 'Tidak ada kategori', + 'The name is required' => 'Nama diperlukan', + 'Remove a file' => 'Hapus berkas', + 'Unable to remove this file.' => 'Tidak dapat menghapus berkas ini.', + 'File removed successfully.' => 'Berkas berhasil dihapus.', + 'Attach a document' => 'Lampirkan dokumen', + 'Do you really want to remove this file: "%s"?' => 'Apakah anda yakin akan menghapus berkas ini « %s » ?', + 'Attachments' => 'Lampiran', + 'Edit the task' => 'Modifikasi tugas', + 'Edit the description' => 'Modifikasi deskripsi', + 'Add a comment' => 'Tambahkan komentar', + 'Edit a comment' => 'Modifikasi komentar', + 'Summary' => 'Ringkasan', + 'Time tracking' => 'Pelacakan waktu', + 'Estimate:' => 'Estimasi :', + 'Spent:' => 'Menghabiskan:', + 'Do you really want to remove this sub-task?' => 'Apakah anda yakin akan menghapus sub-tugas ini ?', + 'Remaining:' => 'Tersisa:', + 'hours' => 'jam', + 'spent' => 'menghabiskan', + 'estimated' => 'perkiraan', + 'Sub-Tasks' => 'Sub-tugas', + 'Add a sub-task' => 'Tambahkan sub-tugas', + 'Original estimate' => 'Perkiraan semula', + 'Create another sub-task' => 'Tambahkan sub-tugas lainnya', + 'Time spent' => 'Waktu yang dihabiskan', + 'Edit a sub-task' => 'Modifikasi sub-tugas', + 'Remove a sub-task' => 'Hapus sub-tugas', + 'The time must be a numeric value' => 'Waktu harus berisikan numerik', + 'Todo' => 'Yang harus dilakukan', + 'In progress' => 'Sedang proses', + 'Sub-task removed successfully.' => 'Sub-tugas berhasil dihapus.', + 'Unable to remove this sub-task.' => 'Tidak dapat menghapus sub-tugas.', + 'Sub-task updated successfully.' => 'Sub-tugas berhasil diperbaharui.', + 'Unable to update your sub-task.' => 'Tidak dapat memperbaharui sub-tugas anda.', + 'Unable to create your sub-task.' => 'Tidak dapat membuat sub-tugas anda.', + 'Sub-task added successfully.' => 'Sub-tugas berhasil dibuat.', + 'Maximum size: ' => 'Ukuran maksimum: ', + 'Unable to upload the file.' => 'Tidak dapat mengunggah berkas.', + 'Display another project' => 'Lihat projek lain', + 'Login with my Github Account' => 'Masuk menggunakan Akaun Github saya', + 'Link my Github Account' => 'Hubungkan Akaun Github saya ', + 'Unlink my Github Account' => 'Putuskan Akaun Github saya', + 'Created by %s' => 'Dibuat oleh %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifikasi terakhir pada tanggal %d/%m/%Y à %H:%M', + 'Tasks Export' => 'Ekspor Tugas', + 'Tasks exportation for "%s"' => 'Tugas di ekspor untuk « %s »', + 'Start Date' => 'Tanggal Mulai', + 'End Date' => 'Tanggal Berakhir', + 'Execute' => 'Eksekusi', + 'Task Id' => 'Id Tugas', + 'Creator' => 'Pembuat', + 'Modification date' => 'Tanggal modifikasi', + 'Completion date' => 'Tanggal penyelesaian', + 'Clone' => 'Klon', + 'Project cloned successfully.' => 'Kloning projek berhasil.', + 'Unable to clone this project.' => 'Tidak dapat mengkloning projek.', + 'Enable email notifications' => 'Aktifkan pemberitahuan dari email', + 'Task position:' => 'Posisi tugas :', + 'The task #%d have been opened.' => 'Tugas #%d telah dibuka.', + 'The task #%d have been closed.' => 'Tugas #%d telah ditutup.', + 'Sub-task updated' => 'Sub-tugas diperbaharui', + 'Title:' => 'Judul :', + 'Status:' => 'Status :', + 'Assignee:' => 'Ditugaskan ke :', + 'Time tracking:' => 'Pelacakan waktu :', + 'New sub-task' => 'Sub-tugas baru', + 'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »', + 'Comment updated' => 'Komentar diperbaharui', + 'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »', + 'New attachment' => 'Lampirkan baru', + 'New comment' => 'Komentar baru', + 'New subtask' => 'Sub-tugas baru', + 'Subtask updated' => 'Sub-tugas diperbaharui', + 'Task updated' => 'Tugas diperbaharui', + 'Task closed' => 'Tugas ditutup', + 'Task opened' => 'Tugas dibuka', + 'I want to receive notifications only for those projects:' => 'Saya ingin menerima pemberitahuan hanya untuk projek-projek yang dipilih :', + 'view the task on Kanboard' => 'lihat tugas di Kanboard', + 'Public access' => 'Akses awam', + 'User management' => 'Manajemen pengguna', + 'Active tasks' => 'Tugas aktif', + 'Disable public access' => 'Nyahaktifkan akses awam', + 'Enable public access' => 'Aktifkan akses awam', + 'Public access disabled' => 'Akses awam dinyahaktif', + 'Do you really want to disable this project: "%s"?' => 'Anda yakin menyah-aktifkan projek ini : « %s » ?', + 'Do you really want to enable this project: "%s"?' => 'Anda yakin untuk mengaktifkan projek ini : « %s » ?', + 'Project activation' => 'Aktifkan projek', + 'Move the task to another project' => 'Pindahkan tugas ke projek lain', + 'Move to another project' => 'Pindahkan ke projek lain', + 'Do you really want to duplicate this task?' => 'Anda yakin mengembarkan tugas ini ?', + 'Duplicate a task' => 'Kembarkan tugas', + 'External accounts' => 'Akaun luaran', + 'Account type' => 'Jenis Akaun', + 'Local' => 'Lokal', + 'Remote' => 'Jauh', + 'Enabled' => 'Aktif', + 'Disabled' => 'Tidak aktif', + 'Username:' => 'Nama pengguna :', + 'Name:' => 'Nama:', + 'Email:' => 'Emel:', + 'Notifications:' => 'Makluman:', + 'Notifications' => 'Makluman', + 'Account type:' => 'Jenis Akaun :', + 'Edit profile' => 'Sunting profil', + 'Change password' => 'Rubah kata sandri', + 'Password modification' => 'Modifikasi kata laluan', + 'External authentications' => 'Otentifikasi eksternal', + 'Google Account' => 'Akaun Google', + 'Github Account' => 'Akaun Github', + 'Never connected.' => 'Tidak pernah terhubung.', + 'No account linked.' => 'Tidak ada Akaun terhubung.', + 'Account linked.' => 'Akaun terhubung.', + 'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.', + 'Password modified successfully.' => 'Kata laluan telah berjaya ditukar.', + 'Unable to change the password.' => 'Tidak dapat merubah kata laluanr.', + 'Change category for the task "%s"' => 'Rubah kategori untuk tugas « %s »', + 'Change category' => 'Tukar kategori', + '%s updated the task %s' => '%s memperbaharui tugas %s', + '%s opened the task %s' => '%s membuka tugas %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s memindahkan tugas %s ke posisi n°%d dalam kolom « %s »', + '%s moved the task %s to the column "%s"' => '%s memindahkan tugas %s ke kolom « %s »', + '%s created the task %s' => '%s membuat tugas %s', + '%s closed the task %s' => '%s menutup tugas %s', + '%s created a subtask for the task %s' => '%s membuat subtugas untuk tugas %s', + '%s updated a subtask for the task %s' => '%s memperbaharui subtugas untuk tugas %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Ditugaskan untuk %s dengan perkiraan %s/%sh', + 'Not assigned, estimate of %sh' => 'Tiada yang ditugaskan, perkiraan %sh', + '%s updated a comment on the task %s' => '%s memperbaharui komentar pada tugas %s', + '%s commented the task %s' => '%s memberikan komentar pada tugas %s', + '%s\'s activity' => 'Aktifitas dari %s', + 'RSS feed' => 'RSS feed', + '%s updated a comment on the task #%d' => '%s memperbaharui komentar pada tugas n°%d', + '%s commented on the task #%d' => '%s memberikan komentar pada tugas n°%d', + '%s updated a subtask for the task #%d' => '%s memperbaharui subtugas untuk tugas n°%d', + '%s created a subtask for the task #%d' => '%s membuat subtugas untuk tugas n°%d', + '%s updated the task #%d' => '%s memperbaharui tugas n°%d', + '%s created the task #%d' => '%s membuat tugas n°%d', + '%s closed the task #%d' => '%s menutup tugas n°%d', + '%s open the task #%d' => '%s membuka tugas n°%d', + '%s moved the task #%d to the column "%s"' => '%s memindahkan tugas n°%d ke kolom « %s »', + '%s moved the task #%d to the position %d in the column "%s"' => '%s memindahkan tugas n°%d ke posisi n°%d dalam kolom « %s »', + 'Activity' => 'Aktifitas', + 'Default values are "%s"' => 'Standar nilai adalah« %s »', + 'Default columns for new projects (Comma-separated)' => 'Kolom default untuk projek baru (dipisahkan dengan koma)', + 'Task assignee change' => 'Mengubah orang ditugaskan untuk tugas', + '%s change the assignee of the task #%d to %s' => '%s rubah orang yang ditugaskan dari tugas n%d ke %s', + '%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s', + 'New password for the user "%s"' => 'Kata laluan baru untuk pengguna « %s »', + 'Choose an event' => 'Pilih sebuah acara', + 'Create a task from an external provider' => 'Buat tugas dari pemasok eksternal', + 'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal', + 'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal', + 'Reference' => 'Referensi', + 'Reference: %s' => 'Referensi : %s', + 'Label' => 'Label', + 'Database' => 'Pengkalan data', + 'About' => 'Tentang', + 'Database driver:' => 'Driver pengkalan data:', + 'Board settings' => 'Pengaturan papan', + 'URL and token' => 'URL dan token', + 'Webhook settings' => 'Penetapan webhook', + 'URL for task creation:' => 'URL untuk cipta tugas:', + 'Reset token' => 'Menetap semula token', + 'API endpoint:' => 'API endpoint :', + 'Refresh interval for private board' => 'Interval pembaruan untuk papan pribadi', + 'Refresh interval for public board' => 'Interval pembaruan untuk papan publik', + 'Task highlight period' => 'Periode puncak tugas', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periode (dalam detik) untuk mempertimbangkan tugas yang baru dimodifikasi (0 untuk menonaktifkan, standar 2 hari)', + 'Frequency in second (60 seconds by default)' => 'Frequensi dalam detik (standar 60 saat)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekuensi dalam detik (0 untuk menonaktifkan fitur ini, standar 10 detik)', + 'Application URL' => 'URL Aplikasi', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Contoh: http://example.kanboard.net/ (digunakan untuk pemberitahuan email)', + 'Token regenerated.' => 'Token diregenerasi.', + 'Date format' => 'Format tarikh', + 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalunya diterima, contoh: « %s » et « %s »', + 'New private project' => 'Projek peribadi baharu', + 'This project is private' => 'projek ini adalah peribadi', + 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru', + 'Add' => 'Tambah', + 'Estimated time: %s hours' => 'Anggaran waktu: %s jam', + 'Time spent: %s hours' => 'Waktu dihabiskan : %s jam', + 'Started on %B %e, %Y' => 'Dimulai pada %d/%m/%Y', + 'Start date' => 'Tarikh mula', + 'Time estimated' => 'Anggaran masa', + 'There is nothing assigned to you.' => 'Tidak ada yang diberikan kepada anda.', + 'My tasks' => 'Tugas saya', + 'Activity stream' => 'Arus aktifitas', + 'Dashboard' => 'Dasbor', + 'Confirmation' => 'Konfirmasi', + 'Allow everybody to access to this project' => 'Memungkinkan semua orang untuk mengakses projek ini', + 'Everybody have access to this project.' => 'Semua orang mendapat akses untuk projek ini.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Create a comment from an external provider' => 'Buat komentar dari pemasok eksternal', + 'Project management' => 'Manajemen projek', + 'My projects' => 'projek saya', + 'Columns' => 'Kolom', + 'Task' => 'Tugas', + 'Your are not member of any project.' => 'Anda bukan anggota dari setiap projek.', + 'Percentage' => 'Persentasi', + 'Number of tasks' => 'Jumlah dari tugas', + 'Task distribution' => 'Pembagian tugas', + 'Reportings' => 'Pelaporan', + 'Task repartition for "%s"' => 'Pembagian tugas untuk « %s »', + 'Analytics' => 'Analitis', + 'Subtask' => 'Subtugas', + 'My subtasks' => 'Subtugas saya', + 'User repartition' => 'Partisi ulang pengguna', + 'User repartition for "%s"' => 'Partisi ulang pengguna untuk « %s »', + 'Clone this project' => 'Gandakan projek ini', + 'Column removed successfully.' => 'Kolom berhasil dihapus.', + 'Not enough data to show the graph.' => 'Tidak cukup data untuk menampilkan grafik.', + 'Previous' => 'Sebelumnya', + 'The id must be an integer' => 'Id harus integer', + 'The project id must be an integer' => 'Id projek harus integer', + 'The status must be an integer' => 'Status harus integer', + 'The subtask id is required' => 'Id subtugas diperlukan', + 'The subtask id must be an integer' => 'Id subtugas harus integer', + 'The task id is required' => 'Id tugas diperlukan', + 'The task id must be an integer' => 'Id tugas harus integer', + 'The user id must be an integer' => 'Id user harus integer', + 'This value is required' => 'Nilai ini diperlukan', + 'This value must be numeric' => 'Nilai ini harus angka', + 'Unable to create this task.' => 'Tidak dapat membuat tugas ini', + 'Cumulative flow diagram' => 'Diagram alir kumulatif', + 'Cumulative flow diagram for "%s"' => 'Diagram alir kumulatif untuk « %s »', + 'Daily project summary' => 'Ringkasan projek harian', + 'Daily project summary export' => 'Ekspot ringkasan projek harian', + 'Daily project summary export for "%s"' => 'Ekspor ringkasan projek harian untuk « %s »', + 'Exports' => 'Ekspor', + 'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.', + 'Nothing to preview...' => 'Tiada yang dapat diintai...', + 'Preview' => 'Intai', + 'Write' => 'Tulis', + 'Active swimlanes' => 'Swimlanes aktif', + 'Add a new swimlane' => 'Tambah swimlane baharu', + 'Change default swimlane' => 'Tukar piawai swimlane', + 'Default swimlane' => 'Piawai swimlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Anda yakin untuk menghapus swimlane ini : « %s » ?', + 'Inactive swimlanes' => 'Swimlanes tidak aktif', + 'Remove a swimlane' => 'Padam swimlane', + 'Rename' => 'Namakan semula', + 'Show default swimlane' => 'Tampilkan piawai swimlane', + 'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk projek « %s »', + 'Swimlane not found.' => 'Swimlane tidak ditemui.', + 'Swimlane removed successfully.' => 'Swimlane telah dipadamkan.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane telah dikemaskini.', + 'The default swimlane have been updated successfully.' => 'Standar swimlane berhasil diperbaharui.', + 'Unable to create your swimlane.' => 'Tidak dapat membuat swimlane anda.', + 'Unable to remove this swimlane.' => 'Tidak dapat menghapus swimlane ini.', + 'Unable to update this swimlane.' => 'Tidak dapat memperbaharui swimlane ini.', + 'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.', + 'Example: "Bug, Feature Request, Improvement"' => 'Contoh: « Insiden, Permintaan Ciri, Pembaikan »', + 'Default categories for new projects (Comma-separated)' => 'Piawaian kategori untuk projek baru (asingkan guna koma)', + 'Integrations' => 'Integrasi', + 'Integration with third-party services' => 'Integrasi dengan khidmat pihak ketiga', + 'Subtask Id' => 'Id Subtugas', + 'Subtasks' => 'Subtugas', + 'Subtasks Export' => 'Ekspot Subtugas', + 'Subtasks exportation for "%s"' => 'Ekspor subtugas untuk « %s »', + 'Task Title' => 'Judul Tugas', + 'Untitled' => 'Tanpa nama', + 'Application default' => 'Aplikasi Piawaian', + 'Language:' => 'Bahasa:', + 'Timezone:' => 'Zon masa:', + 'All columns' => 'Semua kolom', + 'Calendar' => 'Kalender', + 'Next' => 'Selanjutnya', + '#%d' => 'n°%d', + 'All swimlanes' => 'Semua swimlane', + 'All colors' => 'Semua warna', + 'Moved to column %s' => 'Pindah ke kolom %s', + 'Change description' => 'Ubah keterangan', + 'User dashboard' => 'Papan Kenyataan pengguna', + 'Allow only one subtask in progress at the same time for a user' => 'Izinkan hanya satu subtugas dalam proses secara bersamaan untuk satu pengguna', + 'Edit column "%s"' => 'Modifikasi kolom « %s »', + 'Select the new status of the subtask: "%s"' => 'Pilih status baru untuk subtugas : « %s »', + 'Subtask timesheet' => 'Subtugas absen', + 'There is nothing to show.' => 'Tidak ada yang dapat diperlihatkan.', + 'Time Tracking' => 'Pelacakan waktu', + 'You already have one subtask in progress' => 'Anda sudah ada satu subtugas dalam proses', + 'Which parts of the project do you want to duplicate?' => 'Bagian dalam projek mana yang ingin anda duplikasi?', + 'Disallow login form' => 'Larang formulir masuk', + 'Start' => 'Mula', + 'End' => 'Selesai', + 'Task age in days' => 'Usia tugas dalam bentuk harian', + 'Days in this column' => 'Hari dalam kolom ini', + '%dd' => '%dj', + 'Add a link' => 'Menambahkan pautan', + 'Add a new link' => 'Tambah Pautan baru', + 'Do you really want to remove this link: "%s"?' => 'Anda yakin akan menghapus Pautan ini : « %s » ?', + 'Do you really want to remove this link with task #%d?' => 'Anda yakin akan menghapus Pautan ini dengan tugas n°%d ?', + 'Field required' => 'Medan diperlukan', + 'Link added successfully.' => 'Pautan berhasil ditambahkan.', + 'Link updated successfully.' => 'Pautan berhasil diperbaharui.', + 'Link removed successfully.' => 'Pautan berhasil dihapus.', + 'Link labels' => 'Label Pautan', + 'Link modification' => 'Modifikasi Pautan', + 'Links' => 'Pautan', + 'Link settings' => 'Pengaturan Pautan', + 'Opposite label' => 'Label berlawanan', + 'Remove a link' => 'Hapus Pautan', + 'Task\'s links' => 'Pautan tugas', + 'The labels must be different' => 'Label harus berbeda', + 'There is no link.' => 'Tidak ada Pautan.', + 'This label must be unique' => 'Label ini harus unik', + 'Unable to create your link.' => 'Tidak dapat membuat Pautan anda.', + 'Unable to update your link.' => 'Tidak dapat memperbaharui Pautan anda.', + 'Unable to remove this link.' => 'Tidak dapat menghapus Pautan ini.', + 'relates to' => 'berhubungan dengan', + 'blocks' => 'blok', + 'is blocked by' => 'diblokir oleh', + 'duplicates' => 'duplikat', + 'is duplicated by' => 'diduplikasi oleh', + 'is a child of' => 'anak dari', + 'is a parent of' => 'orant tua dari', + 'targets milestone' => 'milestone target', + 'is a milestone of' => 'adalah milestone dari', + 'fixes' => 'perbaikan', + 'is fixed by' => 'diperbaiki oleh', + 'This task' => 'Tugas ini', + '<1h' => '<1h', + '%dh' => '%dh', + '%b %e' => '%e %b', + 'Expand tasks' => 'Perluas tugas', + 'Collapse tasks' => 'Lipat tugas', + 'Expand/collapse tasks' => 'Perluas/lipat tugas', + 'Close dialog box' => 'Tutup kotak dialog', + 'Submit a form' => 'Submit formulir', + 'Board view' => 'Table halaman', + 'Keyboard shortcuts' => 'pintas keyboard', + 'Open board switcher' => 'Buka table switcher', + 'Application' => 'Aplikasi', + 'since %B %e, %Y at %k:%M %p' => 'sejak %d/%m/%Y à %H:%M', + 'Compact view' => 'Tampilan kompak', + 'Horizontal scrolling' => 'Horisontal bergulir', + 'Compact/wide view' => 'Beralih antara tampilan kompak dan diperluas', + 'No results match:' => 'Tidak ada hasil :', + 'Currency' => 'Mata uang', + 'Files' => 'Arsip', + 'Images' => 'Gambar', + 'Private project' => 'projek pribadi', + 'AUD - Australian Dollar' => 'AUD - Dollar Australia', + 'CAD - Canadian Dollar' => 'CAD - Dollar Kanada', + 'CHF - Swiss Francs' => 'CHF - Swiss Prancis', + 'Custom Stylesheet' => 'Kustomisasi Stylesheet', + 'download' => 'unduh', + 'EUR - Euro' => 'EUR - Euro', + 'GBP - British Pound' => 'GBP - Poundsterling inggris', + 'INR - Indian Rupee' => 'INR - Rupe India', + 'JPY - Japanese Yen' => 'JPY - Yen Jepang', + 'NZD - New Zealand Dollar' => 'NZD - Dollar Selandia baru', + 'RSD - Serbian dinar' => 'RSD - Dinar Serbia', + 'USD - US Dollar' => 'USD - Dollar Amerika', + 'Destination column' => 'Kolom tujuan', + 'Move the task to another column when assigned to a user' => 'Pindahkan tugas ke kolom lain ketika ditugaskan ke pengguna', + 'Move the task to another column when assignee is cleared' => 'Pindahkan tugas ke kolom lain ketika orang yang ditugaskan dibersihkan', + 'Source column' => 'Sumber kolom', + 'Transitions' => 'Transisi', + 'Executer' => 'Eksekusi', + 'Time spent in the column' => 'Waktu yang dihabiskan dalam kolom', + 'Task transitions' => 'Transisi tugas', + 'Task transitions export' => 'Ekspor transisi tugas', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Laporan ini berisi semua kolom yang pindah untuk setiap tugas dengan tanggal, pengguna dan waktu yang dihabiskan untuk setiap transisi.', + 'Currency rates' => 'Nilai tukar mata uang', + 'Rate' => 'Tarif', + 'Change reference currency' => 'Mengubah referensi mata uang', + 'Add a new currency rate' => 'Tambahkan nilai tukar mata uang baru', + 'Reference currency' => 'Referensi mata uang', + 'The currency rate have been added successfully.' => 'Nilai tukar mata uang berhasil ditambahkan.', + 'Unable to add this currency rate.' => 'Tidak dapat menambahkan nilai tukar mata uang', + 'Webhook URL' => 'URL webhook', + '%s remove the assignee of the task %s' => '%s menghapus penugasan dari tugas %s', + 'Enable Gravatar images' => 'Mengaktifkan gambar Gravatar', + 'Information' => 'Informasi', + 'Check two factor authentication code' => 'Cek dua faktor kode otentifikasi', + 'The two factor authentication code is not valid.' => 'Kode dua faktor kode otentifikasi tidak valid.', + 'The two factor authentication code is valid.' => 'Kode dua faktor kode otentifikasi valid.', + 'Code' => 'Kode', + 'Two factor authentication' => 'Dua faktor otentifikasi', + 'This QR code contains the key URI: ' => 'kode QR ini mengandung kunci URI : ', + 'Check my code' => 'Memeriksa kode saya', + 'Secret key: ' => 'Kunci rahasia : ', + 'Test your device' => 'Menguji perangkat anda', + 'Assign a color when the task is moved to a specific column' => 'Menetapkan warna ketika tugas tersebut dipindahkan ke kolom tertentu', + '%s via Kanboard' => '%s via Kanboard', + 'uploaded by: %s' => 'diunggah oleh %s', + 'uploaded on: %s' => 'diunggah pada %s', + 'size: %s' => 'ukuran : %s', + 'Burndown chart for "%s"' => 'Grafik Burndown untku « %s »', + 'Burndown chart' => 'Grafik Burndown', + 'This chart show the task complexity over the time (Work Remaining).' => 'Grafik ini menunjukkan kompleksitas tugas dari waktu ke waktu (Sisa Pekerjaan).', + 'Screenshot taken %s' => 'Screenshot diambil %s', + 'Add a screenshot' => 'Tambah screenshot', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + 'Screenshot uploaded successfully.' => 'Screenshot berhasil diunggah.', + 'SEK - Swedish Krona' => 'SEK - Krona Swedia', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identifier projek adalah kode alfanumerik opsional digunakan untuk mengidentifikasi projek Anda.', + 'Identifier' => 'Identifier', + 'Disable two factor authentication' => 'Matikan dua faktor otentifikasi', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Apakah anda yakin akan mematikan dua faktor otentifikasi untuk pengguna ini : « %s » ?', + 'Edit link' => 'Modifikasi Pautan', + 'Start to type task title...' => 'Mulai mengetik judul tugas...', + 'A task cannot be linked to itself' => 'Sebuah tugas tidak dapat dikaitkan dengan dirinya sendiri', + 'The exact same link already exists' => 'Pautan yang sama persis sudah ada', + 'Recurrent task is scheduled to be generated' => 'Tugas berulang dijadwalkan akan dihasilkan', + 'Recurring information' => 'Informasi berulang', + 'Score' => 'Skor', + 'The identifier must be unique' => 'Identifier harus unik', + 'This linked task id doesn\'t exists' => 'Id tugas terkait tidak ada', + 'This value must be alphanumeric' => 'Nilai harus alfanumerik', + 'Edit recurrence' => 'Modifikasi pengulangan', + 'Generate recurrent task' => 'Menghasilkan tugas berulang', + 'Trigger to generate recurrent task' => 'Memicu untuk menghasilkan tugas berulang', + 'Factor to calculate new due date' => 'Faktor untuk menghitung tanggal jatuh tempo baru', + 'Timeframe to calculate new due date' => 'Jangka waktu untuk menghitung tanggal jatuh tempo baru', + 'Base date to calculate new due date' => 'Tanggal dasar untuk menghitung tanggal jatuh tempo baru', + 'Action date' => 'Tanggal aksi', + 'Base date to calculate new due date: ' => 'Tanggal dasar untuk menghitung tanggal jatuh tempo baru: ', + 'This task has created this child task: ' => 'Tugas ini telah menciptakan tugas anak ini: ', + 'Day(s)' => 'Hari', + 'Existing due date' => 'Batas waktu yang ada', + 'Factor to calculate new due date: ' => 'Faktor untuk menghitung tanggal jatuh tempo baru: ', + 'Month(s)' => 'Bulan', + 'Recurrence' => 'Pengulangan', + 'This task has been created by: ' => 'Tugas ini telah dibuat oleh:', + 'Recurrent task has been generated:' => 'Tugas berulang telah dihasilkan:', + 'Timeframe to calculate new due date: ' => 'Jangka waktu untuk menghitung tanggal jatuh tempo baru: ', + 'Trigger to generate recurrent task: ' => 'Pemicu untuk menghasilkan tugas berulang: ', + 'When task is closed' => 'Ketika tugas ditutup', + 'When task is moved from first column' => 'Ketika tugas dipindahkan dari kolom pertama', + 'When task is moved to last column' => 'Ketika tugas dipindahkan ke kolom terakhir', + 'Year(s)' => 'Tahun', + 'Calendar settings' => 'Pengaturan kalender', + 'Project calendar view' => 'Tampilan kalender projek', + 'Project settings' => 'Pengaturan projek', + 'Show subtasks based on the time tracking' => 'Tampilkan subtugas berdasarkan pelacakan waktu', + 'Show tasks based on the creation date' => 'Tampilkan tugas berdasarkan tanggal pembuatan', + 'Show tasks based on the start date' => 'Tampilkan tugas berdasarkan tanggal mulai', + 'Subtasks time tracking' => 'Pelacakan waktu subtgas', + 'User calendar view' => 'Pengguna tampilan kalender', + 'Automatically update the start date' => 'Otomatikkan pengemaskinian tanggal', + 'iCal feed' => 'iCal feed', + 'Preferences' => 'Keutamaan', + 'Security' => 'Keamanan', + 'Two factor authentication disabled' => 'Otentifikasi dua faktor dimatikan', + 'Two factor authentication enabled' => 'Otentifikasi dua faktor dihidupkan', + 'Unable to update this user.' => 'Tidak dapat memperbarui pengguna ini.', + 'There is no user management for private projects.' => 'Tidak ada manajemen pengguna untuk projek-projek pribadi.', + 'User that will receive the email' => 'Pengguna yang akan menerima email', + 'Email subject' => 'Subjek Emel', + 'Date' => 'Tanggal', + 'Add a comment log when moving the task between columns' => 'Menambahkan log komentar ketika memindahkan tugas antara kolom', + 'Move the task to another column when the category is changed' => 'Pindahkan tugas ke kolom lain ketika kategori berubah', + 'Send a task by email to someone' => 'Kirim tugas melalui email ke seseorang', + 'Reopen a task' => 'Membuka kembali tugas', + 'Column change' => 'Kolom berubah', + 'Position change' => 'Posisi berubah', + 'Swimlane change' => 'Swimlane berubah', + 'Assignee change' => 'Penerima berubah', + '[%s] Overdue tasks' => '[%s] Tugas terlambat', + 'Notification' => 'Pemberitahuan', + '%s moved the task #%d to the first swimlane' => '%s memindahkan tugas n°%d ke swimlane pertama', + '%s moved the task #%d to the swimlane "%s"' => '%s memindahkan tugas n°%d ke swimlane « %s »', + 'Swimlane' => 'Swimlane', + 'Gravatar' => 'Gravatar', + '%s moved the task %s to the first swimlane' => '%s memindahkan tugas %s ke swimlane pertama', + '%s moved the task %s to the swimlane "%s"' => '%s memindahkan tugas %s ke swimlane « %s »', + 'This report contains all subtasks information for the given date range.' => 'Laporan ini berisi semua informasi subtugas untuk rentang tanggal tertentu.', + 'This report contains all tasks information for the given date range.' => 'Laporan ini berisi semua informasi tugas untuk rentang tanggal tertentu.', + 'Project activities for %s' => 'Aktifitas projek untuk « %s »', + 'view the board on Kanboard' => 'lihat papan di Kanboard', + 'The task have been moved to the first swimlane' => 'Tugas telah dipindahkan ke swimlane pertama', + 'The task have been moved to another swimlane:' => 'Tugas telah dipindahkan ke swimlane lain:', + 'Overdue tasks for the project "%s"' => 'Tugas terlambat untuk projek « %s »', + 'New title: %s' => 'Judul baru : %s', + 'The task is not assigned anymore' => 'Tugas tidak ditugaskan lagi', + 'New assignee: %s' => 'Penerima baru : %s', + 'There is no category now' => 'Tidak ada kategori untuk sekarang', + 'New category: %s' => 'Kategori baru : %s', + 'New color: %s' => 'Warna baru : %s', + 'New complexity: %d' => 'Kompleksitas baru : %d', + 'The due date have been removed' => 'Tanggal jatuh tempo telah dihapus', + 'There is no description anymore' => 'Tidak ada deskripsi lagi', + 'Recurrence settings have been modified' => 'Pengaturan pengulangan telah dimodifikasi', + 'Time spent changed: %sh' => 'Waktu yang dihabiskan berubah : %sh', + 'Time estimated changed: %sh' => 'Perkiraan waktu berubah : %sh', + 'The field "%s" have been updated' => 'Field « %s » telah diperbaharui', + 'The description have been modified' => 'Deskripsi telah dimodifikasi', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Apakah anda yakin akan menutup tugas « %s » beserta semua sub-tugasnya ?', + 'Swimlane: %s' => 'Swimlane : %s', + 'I want to receive notifications for:' => 'Saya ingin menerima pemberitahuan untuk :', + 'All tasks' => 'Semua tugas', + 'Only for tasks assigned to me' => 'Hanya untuk tugas yang ditugaskan ke saya', + 'Only for tasks created by me' => 'Hanya untuk tugas yang dibuat oleh saya', + 'Only for tasks created by me and assigned to me' => 'Hanya untuk tugas yang dibuat oleh saya dan ditugaskan ke saya', + '%A' => '%A', + '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', + 'New due date: %B %e, %Y' => 'Tanggal jatuh tempo baru : %d/%m/%Y', + 'Start date changed: %B %e, %Y' => 'Tanggal mulai berubah : %d/%m/%Y', + '%k:%M %p' => '%H:%M', + '%%Y-%%m-%%d' => '%%d/%%m/%%Y', + 'Total for all columns' => 'Total untuk semua kolom', + 'You need at least 2 days of data to show the chart.' => 'Anda memerlukan setidaknya 2 hari dari data yang menunjukkan grafik.', + '<15m' => '<15m', + '<30m' => '<30m', + 'Stop timer' => 'Hentikan timer', + 'Start timer' => 'Mulai timer', + 'Add project member' => 'Tambahkan anggota projek', + 'Enable notifications' => 'Aktifkan pemberitahuan', + 'My activity stream' => 'Aliran kegiatan saya', + 'My calendar' => 'Kalender saya', + 'Search tasks' => 'Cari tugas', + 'Back to the calendar' => 'Kembali ke kalender', + 'Filters' => 'Filter', + 'Reset filters' => 'Reset ulang filter', + 'My tasks due tomorrow' => 'Tugas saya yang berakhir besok', + 'Tasks due today' => 'Tugas yang berakhir hari ini', + 'Tasks due tomorrow' => 'Tugas yang berakhir besok', + 'Tasks due yesterday' => 'Tugas yang berakhir kemarin', + 'Closed tasks' => 'Tugas yang ditutup', + 'Open tasks' => 'Buka Tugas', + 'Not assigned' => 'Tidak ditugaskan', + 'View advanced search syntax' => 'Lihat sintaks pencarian lanjutan', + 'Overview' => 'Ikhtisar', + '%b %e %Y' => '%b %e %Y', + 'Board/Calendar/List view' => 'Tampilan Papan/Kalender/Daftar', + 'Switch to the board view' => 'Beralih ke tampilan papan', + 'Switch to the calendar view' => 'Beralih ke tampilan kalender', + 'Switch to the list view' => 'Beralih ke tampilan daftar', + 'Go to the search/filter box' => 'Pergi ke kotak pencarian/filter', + 'There is no activity yet.' => 'Tidak ada aktifitas saat ini.', + 'No tasks found.' => 'Tidak ada tugas yang ditemukan.', + 'Keyboard shortcut: "%s"' => 'Keyboard shortcut : « %s »', + 'List' => 'Daftar', + 'Filter' => 'Filter', + 'Advanced search' => 'Pencarian lanjutan', + 'Example of query: ' => 'Contoh dari query : ', + 'Search by project: ' => 'Pencarian berdasarkan projek : ', + 'Search by column: ' => 'Pencarian berdasarkan kolom : ', + 'Search by assignee: ' => 'Pencarian berdasarkan penerima : ', + 'Search by color: ' => 'Pencarian berdasarkan warna : ', + 'Search by category: ' => 'Pencarian berdasarkan kategori : ', + 'Search by description: ' => 'Pencarian berdasarkan deskripsi : ', + 'Search by due date: ' => 'Pencarian berdasarkan tanggal jatuh tempo : ', + 'Lead and Cycle time for "%s"' => 'Memimpin dan Siklus waktu untuk « %s »', + 'Average time spent into each column for "%s"' => 'Rata-rata waktu yang dihabiskan dalam setiap kolom untuk « %s »', + 'Average time spent into each column' => 'Rata-rata waktu yang dihabiskan dalam setiap kolom', + 'Average time spent' => 'Rata-rata waktu yang dihabiskan', + 'This chart show the average time spent into each column for the last %d tasks.' => 'Grafik ini menunjukkan rata-rata waktu yang dihabiskan dalam setiap kolom untuk %d tugas.', + 'Average Lead and Cycle time' => 'Rata-rata Memimpin dan Siklus waktu', + 'Average lead time: ' => 'Rata-rata waktu pimpinan : ', + 'Average cycle time: ' => 'Rata-rata siklus waktu : ', + 'Cycle Time' => 'Siklus Waktu', + 'Lead Time' => 'Lead Time', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Grafik ini menunjukkan memimpin rata-rata dan waktu siklus untuk %d tugas terakhir dari waktu ke waktu.', + 'Average time into each column' => 'Rata-rata waktu ke setiap kolom', + 'Lead and cycle time' => 'Lead dan siklus waktu', + 'Google Authentication' => 'Google Otentifikasi', + 'Help on Google authentication' => 'Bantuan pada otentifikasi Google', + 'Github Authentication' => 'Otentifikasi Github', + 'Help on Github authentication' => 'Bantuan pada otentifikasi Github', + 'Lead time: ' => 'Lead time : ', + 'Cycle time: ' => 'Siklus waktu : ', + 'Time spent into each column' => 'Waktu yang dihabiskan di setiap kolom', + 'The lead time is the duration between the task creation and the completion.' => 'Lead time adalah durasi antara pembuatan tugas dan penyelesaian.', + 'The cycle time is the duration between the start date and the completion.' => 'Siklus waktu adalah durasi antara tanggal mulai dan tanggal penyelesaian.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Jika tugas tidak ditutup waktu saat ini yang digunakan sebagai pengganti tanggal penyelesaian.', + 'Set automatically the start date' => 'Secara otomatis mengatur tanggal mulai', + 'Edit Authentication' => 'Modifikasi Otentifikasi', + 'Google Id' => 'Id Google', + 'Github Id' => 'Id Github', + 'Remote user' => 'Pengguna jauh', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata laluan mereka dalam basis data Kanboard, contoh: Akaun LDAP, Google dan Github.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.', + 'New remote user' => 'Pengguna baru jauh', + 'New local user' => 'Pengguna baru lokal', + 'Default task color' => 'Standar warna tugas', + 'Hide sidebar' => 'Sembunyikan sidebar', + 'Expand sidebar' => 'Perluas sidebar', + 'This feature does not work with all browsers.' => 'Ciri ini tidak dapat digunakan pada semua browsers', + 'There is no destination project available.' => 'Tiada destinasi projek yang tersedia.', + 'Trigger automatically subtask time tracking' => 'Picu pengesanan subtugas secara otomatik', + 'Include closed tasks in the cumulative flow diagram' => 'Termasuk tugas yang ditutup pada diagram aliran kumulatif', + 'Current swimlane: %s' => 'Swimlane saat ini : %s', + 'Current column: %s' => 'Kolom saat ini : %s', + 'Current category: %s' => 'Kategori saat ini : %s', + 'no category' => 'tiada kategori', + 'Current assignee: %s' => 'Saat ini ditugaskan pada: %s', + 'not assigned' => 'Belum ditugaskan', + 'Author:' => 'Penulis:', + 'contributors' => 'Penggiat', + 'License:' => 'Lesen:', + 'License' => 'Lesen', + 'Enter the text below' => 'Masukkan teks di bawah', + 'Gantt chart for %s' => 'Carta Gantt untuk %s', + 'Sort by position' => 'Urutkan berdasarkan posisi', + 'Sort by date' => 'Urutkan berdasarkan tanggal', + 'Add task' => 'Tambah tugas', + 'Start date:' => 'Tanggal mulai:', + 'Due date:' => 'Batas waktu:', + 'There is no start date or due date for this task.' => 'Tiada tanggal mulai dan batas waktu untuk tugas ini.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Memindahkan atau mengubah ukuran tugas anda akan mengubah tanggal mulai dan batas waktu dari tugas ini.', + 'There is no task in your project.' => 'Tiada tugas didalam projek anda.', + 'Gantt chart' => 'Carta Gantt', + 'People who are project managers' => 'Orang-orang yang menjadi pengurus projek', + 'People who are project members' => 'Orang-orang yang menjadi anggota projek', + 'NOK - Norwegian Krone' => 'NOK - Krone Norwegia', + 'Show this column' => 'Perlihatkan kolom ini', + 'Hide this column' => 'Sembunyikan kolom ini', + 'open file' => 'buka fail', + 'End date' => 'Waktu berakhir', + 'Users overview' => 'Ikhtisar pengguna', + 'Managers' => 'Pengurus', + 'Members' => 'Anggota', + 'Shared project' => 'projek bersama', + 'Project managers' => 'Pengurus projek', + 'Gantt chart for all projects' => 'Carta Gantt untuk kesemua projek', + 'Projects list' => 'Senarai projek', + 'Gantt chart for this project' => 'Carta Gantt untuk projek ini', + 'Project board' => 'Papan projek', + 'End date:' => 'Waktu berakhir :', + 'There is no start date or end date for this project.' => 'Tidak ada waktu mula atau waktu berakhir pada projek ini', + 'Projects Gantt chart' => 'projekkan carta Gantt', + 'Start date: %s' => 'Waktu mulai: %s', + 'End date: %s' => 'Waktu berakhir: %s', + 'Link type' => 'Jenis pautan', + 'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan Pautan tugas yang spesifik', + 'Task link creation or modification' => 'Pautan tugas pada penciptaan atau penyuntingan', + 'Login with my Gitlab Account' => 'Masuk menggunakan Akaun Gitlab saya', + 'Milestone' => 'Batu Tanda', + 'Gitlab Authentication' => 'Otentifikasi Gitlab', + 'Help on Gitlab authentication' => 'Bantuan pada otentifikasi Gitlab', + 'Gitlab Id' => 'Id Gitlab', + 'Gitlab Account' => 'Akaun Gitlab', + 'Link my Gitlab Account' => 'Hubungkan akaun Gitlab saya', + 'Unlink my Gitlab Account' => 'Putuskan akaun Gitlab saya', + 'Documentation: %s' => 'Dokumentasi : %s', + 'Switch to the Gantt chart view' => 'Beralih ke tampilan Carta Gantt', + 'Reset the search/filter box' => 'Tetap semula pencarian/saringan', + 'Documentation' => 'Dokumentasi', + 'Table of contents' => 'Isi kandungan', + 'Gantt' => 'Gantt', + // 'Author' => '', + // 'Version' => '', + // 'Plugins' => '', + // 'There is no plugin loaded.' => '', + // 'Set maximum column height' => '', + // 'Remove maximum column height' => '', + // 'My notifications' => '', + // 'Custom filters' => '', + // 'Your custom filter have been created successfully.' => '', + // 'Unable to create your custom filter.' => '', + // 'Custom filter removed successfully.' => '', + // 'Unable to remove this custom filter.' => '', + // 'Edit custom filter' => '', + // 'Your custom filter have been updated successfully.' => '', + // 'Unable to update custom filter.' => '', + // 'Web' => '', + // 'New attachment on task #%d: %s' => '', + // 'New comment on task #%d' => '', + // 'Comment updated on task #%d' => '', + // 'New subtask on task #%d' => '', + // 'Subtask updated on task #%d' => '', + // 'New task #%d: %s' => '', + // 'Task updated #%d' => '', + // 'Task #%d closed' => '', + // 'Task #%d opened' => '', + // 'Column changed for task #%d' => '', + // 'New position for task #%d' => '', + // 'Swimlane changed for task #%d' => '', + // 'Assignee changed on task #%d' => '', + // '%d overdue tasks' => '', + // 'Task #%d is overdue' => '', + // 'No new notifications.' => '', + // 'Mark all as read' => '', + // 'Mark as read' => '', + // 'Total number of tasks in this column across all swimlanes' => '', + // 'Collapse swimlane' => '', + // 'Expand swimlane' => '', + // 'Add a new filter' => '', + // 'Share with all project members' => '', + // 'Shared' => '', + // 'Owner' => '', + // 'Unread notifications' => '', + // 'My filters' => '', + // 'Notification methods:' => '', + // 'Import tasks from CSV file' => '', + // 'Unable to read your file' => '', + // '%d task(s) have been imported successfully.' => '', + // 'Nothing have been imported!' => '', + // 'Import users from CSV file' => '', + // '%d user(s) have been imported successfully.' => '', + // 'Comma' => '', + // 'Semi-colon' => '', + // 'Tab' => '', + // 'Vertical bar' => '', + // 'Double Quote' => '', + // 'Single Quote' => '', + // '%s attached a file to the task #%d' => '', + // 'There is no column or swimlane activated in your project!' => '', + // 'Append filter (instead of replacement)' => '', + // 'Append/Replace' => '', + // 'Append' => '', + // 'Replace' => '', + // 'Import' => '', + // 'change sorting' => '', + // 'Tasks Importation' => '', + // 'Delimiter' => '', + // 'Enclosure' => '', + // 'CSV File' => '', + // 'Instructions' => '', + // 'Your file must use the predefined CSV format' => '', + // 'Your file must be encoded in UTF-8' => '', + // 'The first row must be the header' => '', + // 'Duplicates are not verified for you' => '', + // 'The due date must use the ISO format: YYYY-MM-DD' => '', + // 'Download CSV template' => '', + // 'No external integration registered.' => '', + // 'Duplicates are not imported' => '', + // 'Usernames must be lowercase and unique' => '', + // 'Passwords will be encrypted if present' => '', + // '%s attached a new file to the task %s' => '', + // 'Assign automatically a category based on a link' => '', + // 'BAM - Konvertible Mark' => '', + // 'Assignee Username' => '', + // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + 'Role:' => 'Peranan', + 'Project members' => 'Anggota projek', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + 'Creation' => 'Ciptaan', + 'Expiration' => 'Jangka hayat', + 'Password reset history' => 'Sirah tetap semula kata laluan', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', +); diff --git a/sources/app/Locale/nb_NO/translations.php b/sources/app/Locale/nb_NO/translations.php index 5d03959..04f8e6a 100644 --- a/sources/app/Locale/nb_NO/translations.php +++ b/sources/app/Locale/nb_NO/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s endret ansvarlig for oppgaven %s til %s', 'New password for the user "%s"' => 'Nytt passord for brukeren "%s"', 'Choose an event' => 'Velg en hendelse', - 'Github commit received' => 'Github forpliktelse mottatt', - 'Github issue opened' => 'Github problem åpnet', - 'Github issue closed' => 'Github problem lukket', - 'Github issue reopened' => 'Github problem gjenåpnet', - 'Github issue assignee change' => 'Endre ansvarlig for Github problem', - 'Github issue label change' => 'Endre etikett for Github problem', 'Create a task from an external provider' => 'Oppret en oppgave fra en ekstern tilbyder', 'Change the assignee based on an external username' => 'Endre ansvarlige baseret på et eksternt brukernavn', 'Change the category based on an external label' => 'Endre kategorien basert på en ekstern etikett', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Alle har tilgang til dette prosjektet', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', 'Create a comment from an external provider' => 'Opprett en kommentar fra en ekstern tilbyder', - // 'Github issue comment created' => '', 'Project management' => 'Prosjektinnstillinger', 'My projects' => 'Mine prosjekter', 'Columns' => 'Kolonner', @@ -508,7 +499,6 @@ return array( // 'User repartition for "%s"' => '', 'Clone this project' => 'Kopier dette prosjektet', 'Column removed successfully.' => 'Kolonne flyttet', - // 'Github Issue' => '', // 'Not enough data to show the graph.' => '', 'Previous' => 'Forrige', // 'The id must be an integer' => '', @@ -553,14 +543,8 @@ return array( // 'Your swimlane have been created successfully.' => '', // 'Example: "Bug, Feature Request, Improvement"' => '', // 'Default categories for new projects (Comma-separated)' => '', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Integrasjoner', 'Integration with third-party services' => 'Integrasjoner med tredje-parts tjenester', - // 'Gitlab Issue' => '', 'Subtask Id' => 'Deloppgave ID', 'Subtasks' => 'Deloppgaver', 'Subtasks Export' => 'Eksporter deloppgaver', @@ -588,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', 'Which parts of the project do you want to duplicate?' => 'Hvilke deler av dette prosjektet ønsker du å kopiere?', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', 'Start' => 'Start', 'End' => 'Slutt', 'Task age in days' => 'Dager siden oppgaven ble opprettet', @@ -688,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', 'Two factor authentication' => 'Dobbel godkjenning', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', 'Date' => 'Dato', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', 'Add a comment log when moving the task between columns' => 'Legg til en kommentar i loggen når en oppgave flyttes mellom kolonnene', 'Move the task to another column when the category is changed' => 'Flytt oppgaven til en annen kolonne når kategorien endres', 'Send a task by email to someone' => 'Send en oppgave på epost til noen', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', 'Column change' => 'Endret kolonne', 'Position change' => 'Posisjonsendring', 'Swimlane change' => 'Endret svømmebane', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', 'New remote user' => 'Ny eksternbruker', 'New local user' => 'Ny internbruker', 'Default task color' => 'Standard oppgavefarge', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', 'Project members' => 'Prosjektmedlemmer', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/nl_NL/translations.php b/sources/app/Locale/nl_NL/translations.php index 1662165..420f835 100644 --- a/sources/app/Locale/nl_NL/translations.php +++ b/sources/app/Locale/nl_NL/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s heeft de toegewezene voor taak %s veranderd in %s', 'New password for the user "%s"' => 'Nieuw wachtwoord voor gebruiker « %s »', 'Choose an event' => 'Kies een gebeurtenis', - 'Github commit received' => 'Github commentaar ontvangen', - 'Github issue opened' => 'Github issue geopend', - 'Github issue closed' => 'Github issue gesloten', - 'Github issue reopened' => 'Github issue heropend', - 'Github issue assignee change' => 'Github toegewezen veranderd', - 'Github issue label change' => 'Github issue label verander', 'Create a task from an external provider' => 'Maak een taak aan vanuit een externe provider', 'Change the assignee based on an external username' => 'Verander de toegewezene aan de hand van de externe gebruikersnaam', 'Change the category based on an external label' => 'Verander de categorie aan de hand van een extern label', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Iedereen heeft toegang tot dit project.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Hulp bij Github webhooks', 'Create a comment from an external provider' => 'Voeg een commentaar toe van een externe provider', - 'Github issue comment created' => 'Github issue commentaar aangemaakt', 'Project management' => 'Project management', 'My projects' => 'Mijn projecten', 'Columns' => 'Kolommen', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Gebruikerverdeling voor « %s »', 'Clone this project' => 'Kloon dit project', 'Column removed successfully.' => 'Kolom succesvol verwijderd.', - 'Github Issue' => 'Github issue', 'Not enough data to show the graph.' => 'Niet genoeg data om de grafiek te laten zien.', // 'Previous' => '', 'The id must be an integer' => 'Het id moet een integer zijn', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Swimlane succesvol aangemaakt.', 'Example: "Bug, Feature Request, Improvement"' => 'Voorbeeld: « Bug, Feature Request, Improvement »', 'Default categories for new projects (Comma-separated)' => 'Standaard categorieën voor nieuwe projecten (komma gescheiden)', - 'Gitlab commit received' => 'Gitlab commir ontvangen', - 'Gitlab issue opened' => 'Gitlab issue geopend', - 'Gitlab issue closed' => 'Gitlab issue gesloten', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Hulp bij Gitlab webhooks', 'Integrations' => 'Integraties', 'Integration with third-party services' => 'Integratie met derde-partij-services', - 'Gitlab Issue' => 'Gitlab issue', 'Subtask Id' => 'Subtaak id', 'Subtasks' => 'Subtaken', 'Subtasks Export' => 'Subtaken exporteren', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'U heeft al een subtaak in behandeling', 'Which parts of the project do you want to duplicate?' => 'Welke onderdelen van het project wilt u dupliceren?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit ontvangen', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Help bij Bitbucket webhooks', 'Start' => 'Start', 'End' => 'Eind', 'Task age in days' => 'Leeftijd taak in dagen', @@ -688,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/pl_PL/translations.php b/sources/app/Locale/pl_PL/translations.php index 2ac6414..ecea090 100644 --- a/sources/app/Locale/pl_PL/translations.php +++ b/sources/app/Locale/pl_PL/translations.php @@ -20,15 +20,15 @@ return array( 'Red' => 'Czerwony', 'Orange' => 'Pomarańczowy', 'Grey' => 'Szary', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => 'Brąz', + 'Deep Orange' => 'Ciemnopomarańczowy', + 'Dark Grey' => 'Ciemnoszary', + 'Pink' => 'Różowy', + 'Teal' => 'Turkusowy', + 'Cyan' => 'Cyjan', + 'Lime' => 'Limonkowy', + 'Light Green' => 'Jasnozielony', + 'Amber' => 'Amber', 'Save' => 'Zapisz', 'Login' => 'Login', 'Official website:' => 'Oficjalna strona:', @@ -148,7 +148,7 @@ return array( 'Task created successfully.' => 'Zadanie zostało utworzone.', 'User created successfully.' => 'Użytkownik dodany', 'Unable to create your user.' => 'Nie udało się dodać użytkownika.', - 'User updated successfully.' => 'Użytkownik zaktualizowany.', + 'User updated successfully.' => 'Profil użytkownika został zaaktualizowany.', 'Unable to update your user.' => 'Nie udało się zaktualizować użytkownika.', 'User removed successfully.' => 'Użytkownik usunięty.', 'Unable to remove this user.' => 'Nie udało się usunąć użytkownika.', @@ -364,7 +364,7 @@ return array( 'Task updated' => 'Zaktualizowane zadanie', 'Task closed' => 'Zadanie zamknięte', 'Task opened' => 'Zadanie otwarte', - 'I want to receive notifications only for those projects:' => 'Chcę otrzymywać powiadiomienia tylko dla tych projektów:', + 'I want to receive notifications only for those projects:' => 'Chcę otrzymywać powiadomienia tylko dla poniższych projektów:', 'view the task on Kanboard' => 'Zobacz zadanie', 'Public access' => 'Dostęp publiczny', 'User management' => 'Zarządzanie użytkownikami', @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s zmienił osobę odpowiedzialną za zadanie %s na %s', 'New password for the user "%s"' => 'Nowe hasło użytkownika "%s"', 'Choose an event' => 'Wybierz zdarzenie', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', 'Create a task from an external provider' => 'Utwórz zadanie z dostawcy zewnętrznego', 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrznej etykiety', @@ -487,14 +481,11 @@ return array( 'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', 'Create a comment from an external provider' => 'Utwórz komentarz od zewnętrznego dostawcy', - // 'Github issue comment created' => '', 'Project management' => 'Menadżer projektu', 'My projects' => 'Moje projekty', 'Columns' => 'Kolumny', - 'Task' => 'zadania', + 'Task' => 'Zadanie', 'Your are not member of any project.' => 'Nie bierzesz udziału w żadnym projekcie', 'Percentage' => 'Procent', 'Number of tasks' => 'Liczba zadań', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Przydział użytkownika dla "%s"', 'Clone this project' => 'Sklonuj ten projekt', 'Column removed successfully.' => 'Kolumna usunięta pomyślnie.', - // 'Github Issue' => '', 'Not enough data to show the graph.' => 'Za mało danych do utworzenia wykresu.', 'Previous' => 'Poprzedni', 'The id must be an integer' => 'ID musi być liczbą całkowitą', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Proces tworzony pomyślnie.', 'Example: "Bug, Feature Request, Improvement"' => 'Przykład: "Błąd, Żądanie Funkcjonalności, Udoskonalenia"', 'Default categories for new projects (Comma-separated)' => 'Domyślne kategorie dla nowych projektów (oddzielone przecinkiem)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Integracje', 'Integration with third-party services' => 'Integracja z usługami firm trzecich', - // 'Gitlab Issue' => '', 'Subtask Id' => 'ID pod-zadania', 'Subtasks' => 'Pod-zadania', 'Subtasks Export' => 'Eksport pod-zadań', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Masz już zadanie o statusie "w trakcie"', 'Which parts of the project do you want to duplicate?' => 'Które elementy projektu chcesz zduplikować?', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', 'Start' => 'Początek', 'End' => 'Koniec', 'Task age in days' => 'Wiek zadania w dniach', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Kod weryfikujący poprawny', 'Code' => 'Kod', 'Two factor authentication' => 'Uwierzytelnianie dwustopniowe', - 'Enable/disable two factor authentication' => 'Włącz/Wyłącz uwierzytelnianie dwustopniowe', 'This QR code contains the key URI: ' => 'Ten kod QR zawiera URI klucza: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Zapisz sekretny klucz w swoim oprogramowaniu TOTP (na przykład FreeOTP lub Google Authenticator)', 'Check my code' => 'Sprawdź kod', 'Secret key: ' => 'Tajny kod: ', 'Test your device' => 'Przetestuj urządzenie', @@ -699,8 +678,8 @@ return array( 'uploaded by: %s' => 'Dodane przez: %s', 'uploaded on: %s' => 'Data dodania: %s', 'size: %s' => 'Rozmiar: %s', - // 'Burndown chart for "%s"' => '', - // 'Burndown chart' => '', + 'Burndown chart for "%s"' => 'Wykres Burndown dla "%s"', + 'Burndown chart' => 'Wykres Burndown', // 'This chart show the task complexity over the time (Work Remaining).' => '', 'Screenshot taken %s' => 'Zrzut ekranu zapisany %s', 'Add a screenshot' => 'Dodaj zrzut ekranu', @@ -762,27 +741,16 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', + 'Send a task by email to someone' => 'Wyślij zadanie mailem do kogokolwiek', + 'Reopen a task' => 'Otwórz ponownie zadanie', + 'Column change' => 'Zmiana kolumny', + 'Position change' => 'Zmiana pozycji', + 'Swimlane change' => 'Zmiana Swimlane', + 'Assignee change' => 'Zmiana przypisanego użytkownika', // '[%s] Overdue tasks' => '', - // 'Notification' => '', + 'Notification' => 'Powiadomienie', // '%s moved the task #%d to the first swimlane' => '', // '%s moved the task #%d to the swimlane "%s"' => '', // 'Swimlane' => '', @@ -796,12 +764,12 @@ return array( // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', + 'New title: %s' => 'Nowy tytuł: %s', + 'The task is not assigned anymore' => 'Brak osoby odpowiedzialnej za zadanie', + 'New assignee: %s' => 'Nowy odpowiedzialny: %s', + 'There is no category now' => 'Aktualnie zadanie nie posiada kategorii', + 'New category: %s' => 'Nowa kategoria: %s', + 'New color: %s' => 'Nowy kolor: %s', // 'New complexity: %d' => '', // 'The due date have been removed' => '', // 'There is no description anymore' => '', @@ -812,60 +780,60 @@ return array( // 'The description have been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', + 'I want to receive notifications for:' => 'Wysyłaj powiadomienia dla:', + 'All tasks' => 'Wszystkich zadań', + 'Only for tasks assigned to me' => 'Tylko zadań przypisanych do mnie', + 'Only for tasks created by me' => 'Tylko zadań utworzonych przeze mnie', + 'Only for tasks created by me and assigned to me' => 'Tylko zadań przypisanych lub utworzonych przeze mnie', // '%A' => '', // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', + 'New due date: %B %e, %Y' => 'Nowy termin: %B %e, %Y', + 'Start date changed: %B %e, %Y' => 'Zmiana daty rozpoczęcia: %B %e, %Y', // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', // '<15m' => '', // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', - // 'Reset filters' => '', - // 'My tasks due tomorrow' => '', - // 'Tasks due today' => '', - // 'Tasks due tomorrow' => '', - // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', - // 'Not assigned' => '', - // 'View advanced search syntax' => '', - // 'Overview' => '', + 'Stop timer' => 'Zatrzymaj pomiar czasu', + 'Start timer' => 'Uruchom pomiar czasu', + 'Add project member' => 'Dodaj członka projektu', + 'Enable notifications' => 'Włącz powiadomienia', + 'My activity stream' => 'Moja aktywność', + 'My calendar' => 'Mój kalendarz', + 'Search tasks' => 'Szukaj zadań', + 'Back to the calendar' => 'Wróć do kalendarza', + 'Filters' => 'Filtry', + 'Reset filters' => 'Resetuj zastosowane filtry', + 'My tasks due tomorrow' => 'Moje zadania do jutra', + 'Tasks due today' => 'Zadania do dzisiaj', + 'Tasks due tomorrow' => 'Zadania do jutra', + 'Tasks due yesterday' => 'Zadania na wczoraj', + 'Closed tasks' => 'Zamknięte zadania', + 'Open tasks' => 'Otwarte zadania', + 'Not assigned' => 'Nieprzypisane zadania', + 'View advanced search syntax' => 'Pomoc dotycząca budowania filtrów', + 'Overview' => 'Przegląd', // '%b %e %Y' => '', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', - // 'There is no activity yet.' => '', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', - // 'Example of query: ' => '', - // 'Search by project: ' => '', - // 'Search by column: ' => '', - // 'Search by assignee: ' => '', - // 'Search by color: ' => '', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', + 'Board/Calendar/List view' => 'Widok: Tablica/Kalendarz/Lista', + 'Switch to the board view' => 'Przełącz na tablicę', + 'Switch to the calendar view' => 'Przełącz na kalendarz', + 'Switch to the list view' => 'Przełącz na listę', + 'Go to the search/filter box' => 'Użyj pola wyszukiwania/filtrów', + 'There is no activity yet.' => 'Brak powiadomień', + 'No tasks found.' => 'Nie znaleziono zadań', + 'Keyboard shortcut: "%s"' => 'Skrót klawiaturowy: "%s"', + 'List' => 'Lista', + 'Filter' => 'Filtr', + 'Advanced search' => 'Zaawansowane wyszukiwanie', + 'Example of query: ' => 'Przykładowe zapytanie:', + 'Search by project: ' => 'Szukaj wg projektów:', + 'Search by column: ' => 'Szukaj wg kolumn:', + 'Search by assignee: ' => 'Szukaj wg użytkownika:', + 'Search by color: ' => 'Szukaj wg koloru:', + 'Search by category: ' => 'Szukaj wg kategorii:', + 'Search by description: ' => 'Szukaj wg opisu:', + 'Search by due date: ' => 'Szukaj wg terminu:', // 'Lead and Cycle time for "%s"' => '', // 'Average time spent into each column for "%s"' => '', // 'Average time spent into each column' => '', @@ -890,70 +858,68 @@ return array( // 'The cycle time is the duration between the start date and the completion.' => '', // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', - // 'Edit Authentication' => '', + 'Edit Authentication' => 'Edycja autoryzacji', // 'Google Id' => '', // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', - // 'New remote user' => '', - // 'New local user' => '', - // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', - // 'This feature does not work with all browsers.' => '', + 'New remote user' => 'Nowy użytkownik zdalny', + 'New local user' => 'Nowy użytkownik lokalny', + 'Default task color' => 'Domyślny kolor zadań', + 'Hide sidebar' => 'Ukryj menu boczne', + 'Expand sidebar' => 'Pokaż menu boczne', + 'This feature does not work with all browsers.' => 'Ta funkcja może nie działać z każdą przeglądarką', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', // 'Include closed tasks in the cumulative flow diagram' => '', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', - // 'no category' => '', + 'Current swimlane: %s' => 'Bieżący swimlane: %s', + 'Current column: %s' => 'Bieżąca kolumna: %s', + 'Current category: %s' => 'Bieżąca kategoria: %s', + 'no category' => 'brak kategorii', // 'Current assignee: %s' => '', - // 'not assigned' => '', - // 'Author:' => '', - // 'contributors' => '', - // 'License:' => '', - // 'License' => '', - // 'Enter the text below' => '', - // 'Gantt chart for %s' => '', - // 'Sort by position' => '', - // 'Sort by date' => '', - // 'Add task' => '', - // 'Start date:' => '', - // 'Due date:' => '', - // 'There is no start date or due date for this task.' => '', + 'not assigned' => 'Brak osoby odpowiedzialnej', + 'Author:' => 'Autor', + 'contributors' => 'współautorzy', + 'License:' => 'Licencja:', + 'License' => 'Licencja', + 'Enter the text below' => 'Wpisz tekst poniżej', + 'Gantt chart for %s' => 'Wykres Gantt dla %s', + 'Sort by position' => 'Sortuj wg pozycji', + 'Sort by date' => 'Sortuj wg daty', + 'Add task' => 'Dodaj zadanie', + 'Start date:' => 'Data rozpoczęcia:', + 'Due date:' => 'Termin', + 'There is no start date or due date for this task.' => 'Brak daty rozpoczęcia lub terminu zadania', // 'Moving or resizing a task will change the start and due date of the task.' => '', // 'There is no task in your project.' => '', - // 'Gantt chart' => '', - // 'People who are project managers' => '', - // 'People who are project members' => '', + 'Gantt chart' => 'Wykres Gantta', + 'People who are project managers' => 'Użytkownicy będący menedżerami projektu', + 'People who are project members' => 'Użytkownicy będący uczestnikami projektu', // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', - // 'End date' => '', - // 'Users overview' => '', - // 'Managers' => '', - // 'Members' => '', - // 'Shared project' => '', - // 'Project managers' => '', - // 'Gantt chart for all projects' => '', - // 'Projects list' => '', - // 'Gantt chart for this project' => '', - // 'Project board' => '', - // 'End date:' => '', - // 'There is no start date or end date for this project.' => '', - // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', - // 'Link type' => '', - // 'Change task color when using a specific task link' => '', - // 'Task link creation or modification' => '', + 'Show this column' => 'Pokaż tą kolumnę', + 'Hide this column' => 'Ukryj tą kolumnę', + 'open file' => 'otwórz plik', + 'End date' => 'Data zakończenia', + 'Users overview' => 'Przegląd użytkowników', + 'Managers' => 'Menedżerowie', + 'Members' => 'Uczestnicy', + 'Shared project' => 'Projekt udostępniony', + 'Project managers' => 'Menedżerowie projektu', + 'Gantt chart for all projects' => 'Wykres Gantta dla wszystkich projektów', + 'Projects list' => 'Lista projektów', + 'Gantt chart for this project' => 'Wykres Gantta dla bieżacego projektu', + 'Project board' => 'Talica projektu', + 'End date:' => 'Data zakończenia:', + 'There is no start date or end date for this project.' => 'Nie zdefiniowano czasu trwania projektu', + 'Projects Gantt chart' => 'Wykres Gantta dla projektów', + 'Start date: %s' => 'Data rozpoczęcia: %s', + 'End date: %s' => 'Data zakończenia: %s', + 'Link type' => 'Typ adresu URL', + 'Change task color when using a specific task link' => 'Zmień kolor zadania używając specjalnego adresu URL', + 'Task link creation or modification' => 'Adres URL do utworzenia zadania lub modyfikacji', // 'Login with my Gitlab Account' => '', - // 'Milestone' => '', + 'Milestone' => 'Kamień milowy', // 'Gitlab Authentication' => '', // 'Help on Gitlab authentication' => '', // 'Gitlab Id' => '', @@ -963,17 +929,17 @@ return array( // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', - // 'Documentation' => '', + 'Documentation' => 'Dokumentacja', // 'Table of contents' => '', // 'Gantt' => '', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', + 'Author' => 'Autor', + 'Version' => 'Wersja', + 'Plugins' => 'Wtyczki', + 'There is no plugin loaded.' => 'Nie wykryto żadnych wtyczek.', + 'Set maximum column height' => 'Ustaw maksymalną wysokość kolumn', + 'Remove maximum column height' => 'Usuń maksymalną wysokość kolumn', + 'My notifications' => 'Moje powiadomienia', + 'Custom filters' => 'Dostosuj filtry', // 'Your custom filter have been created successfully.' => '', // 'Unable to create your custom filter.' => '', // 'Custom filter removed successfully.' => '', @@ -982,68 +948,67 @@ return array( // 'Your custom filter have been updated successfully.' => '', // 'Unable to update custom filter.' => '', // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', + 'New attachment on task #%d: %s' => 'Nowy załącznik do zadania #%d: %s', + 'New comment on task #%d' => 'Nowy załącznik #%d', + 'Comment updated on task #%d' => 'Comment updated on task #%d', + 'New subtask on task #%d' => 'Nowe pod-zadanie dla zadania #%d', + 'Subtask updated on task #%d' => 'Aktualizacja pod-zadania w zadaniu #%d', + 'New task #%d: %s' => 'Nowe zadanie #%d: %s', + 'Task updated #%d' => 'Aktualizacja zadania #%d', + 'Task #%d closed' => 'Zamknięto zadanie #%d', + 'Task #%d opened' => 'Otwarto zadanie #%d', + 'Column changed for task #%d' => 'Zmieniono kolumnę zadania #%d', + 'New position for task #%d' => 'Ustalono nową pozycję zadania #%d', + 'Swimlane changed for task #%d' => 'Zmieniono swimlane dla zadania #%d', + 'Assignee changed on task #%d' => 'Zmieniono osobę odpowiedzialną dla zadania #%d', // '%d overdue tasks' => '', // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', + 'No new notifications.' => 'Brak nowych powiadomień.', + 'Mark all as read' => 'Oznacz wszystkie jako przeczytane', + 'Mark as read' => 'Oznacz jako przeczytane', // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', + 'Collapse swimlane' => 'Zwiń swimlane', + 'Expand swimlane' => 'Rozwiń swimlane', + 'Add a new filter' => 'Dodaj nowy filtr', + 'Share with all project members' => 'Udostępnij wszystkim uczestnikom projektu', // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', + 'Owner' => 'Właściciel', + 'Unread notifications' => 'Nieprzeczytane powiadomienia', + 'My filters' => 'Moje filtry', + 'Notification methods:' => 'Metody powiadomień:', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', // '%d task(s) have been imported successfully.' => '', // 'Nothing have been imported!' => '', // 'Import users from CSV file' => '', // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', + 'Comma' => 'Przecinek', + 'Semi-colon' => 'Średnik', + 'Tab' => 'Tabulacja', + 'Vertical bar' => 'Kreska pionowa', + 'Double Quote' => 'Cudzysłów', + 'Single Quote' => 'Apostrof', // '%s attached a file to the task #%d' => '', // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', + 'Append filter (instead of replacement)' => 'Dołączaj filtr do zastosowanego filtru(zamiast przełączać)', // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', + 'Tasks Importation' => 'Import zadań', + 'Delimiter' => 'Separator pola', + 'Enclosure' => 'Separator tekstu', + 'CSV File' => 'Plik CSV', + 'Instructions' => 'Instrukcje', + 'Your file must use the predefined CSV format' => 'Twój plik musi być zgodny z predefiniowanym formatem CSV (pobierz szablon)', + 'Your file must be encoded in UTF-8' => 'Twój plik musi być kodowany w UTF-8', + 'The first row must be the header' => 'Pierwszy wiersz pliku musi definiować nagłówki', + 'Duplicates are not verified for you' => 'Duplikaty nie będą weryfikowane', + 'The due date must use the ISO format: YYYY-MM-DD' => 'Data musi być w formacie ISO: YYYY-MM-DD', + 'Download CSV template' => 'Pobierz szablon pliku CSV', // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', + 'Duplicates are not imported' => 'Duplikaty nie zostaną zaimportowane', // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', @@ -1051,52 +1016,92 @@ return array( // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', - // 'Project members' => '', + 'Groups' => 'Grupy', + 'Members of %s' => 'Członkowie %s', + 'New group' => 'Nowa grupa', + 'Group created successfully.' => 'Grupa została utworzona.', + 'Unable to create your group.' => 'Nie można utworzyć grupy.', + 'Edit group' => 'Edytuj grupę', + 'Group updated successfully.' => 'Grupa została zaaktualizowana.', + 'Unable to update your group.' => 'Nie można zaaktualizować grupy.', + 'Add group member to "%s"' => 'Dodaj członka do grupy "%s"', + 'Group member added successfully.' => 'Użytkownik został dodany do grupy.', + 'Unable to add group member.' => 'Nie można dodać użytkownika do grupy.', + 'Remove user from group "%s"' => 'Usuń użytkownika z grupy "%s"', + 'User removed successfully from this group.' => 'Użytkownik został usunięty z grupy.', + 'Unable to remove this user from the group.' => 'Nie można usunąć użytkownika z grupy.', + 'Remove group' => 'Usuń grupę', + 'Group removed successfully.' => 'Grupa została usunięta.', + 'Unable to remove this group.' => 'Nie można usunąć grupy.', + 'Project Permissions' => 'Prawa dostępowe projektu', + 'Manager' => 'Menedżer', + 'Project Manager' => 'Menedżer projektu', + 'Project Member' => 'Uczestnik projektu', + 'Project Viewer' => 'Obserwator projektu', + 'Your account is locked for %d minutes' => 'Twoje konto zostało zablokowane na %d minut', + 'Invalid captcha' => 'Błędny kod z obrazka (captcha)', + 'The name must be unique' => 'Nazwa musi być unikatowa', + 'View all groups' => 'Wyświetl wszystkie grupy', + 'View group members' => 'Wyświetl wszystkich członków grupy', + 'There is no user available.' => 'Żaden użytkownik nie jest dostępny.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Czy napewno chcesz usunąć użytkownika "%s" z grupy "%s"?', + 'There is no group.' => 'Brak grup.', + 'External Id' => 'Zewnętrzny Id', + 'Add group member' => 'Dodaj członka grupy', + 'Do you really want to remove this group: "%s"?' => 'Czy napewno chcesz usunąć grupę "%s"?', + 'There is no user in this group.' => 'Wybrana grupa nie posiada członków.', + 'Remove this user' => 'Usuń użytkownika', + 'Permissions' => 'Prawa dostępu', + 'Allowed Users' => 'Użytkownicy z dostępem', + 'No user have been allowed specifically.' => 'Żaden użytkownik nie ma przyznanego dostępu.', + 'Role' => 'Rola', + 'Enter user name...' => 'Wprowadź nazwę użytkownika...', + 'Allowed Groups' => 'Dostępne grupy', + 'No group have been allowed specifically.' => 'Żadna grupa nie ma przyznanego dostępu.', + 'Group' => 'Grupa', + 'Group Name' => 'Nazwa grupy', + 'Enter group name...' => 'Wprowadź nazwę grupy...', + 'Role:' => 'Rola:', + 'Project members' => 'Uczestnicy projektu', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/pt_BR/translations.php b/sources/app/Locale/pt_BR/translations.php index f3e988e..e985f90 100644 --- a/sources/app/Locale/pt_BR/translations.php +++ b/sources/app/Locale/pt_BR/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s mudou a designação da tarefa %s para %s', 'New password for the user "%s"' => 'Nova senha para o usuário "%s"', 'Choose an event' => 'Escolher um evento', - 'Github commit received' => 'Github commit received', - 'Github issue opened' => 'Github issue opened', - 'Github issue closed' => 'Github issue closed', - 'Github issue reopened' => 'Github issue reopened', - 'Github issue assignee change' => 'Github issue assignee change', - 'Github issue label change' => 'Github issue label change', 'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo', 'Change the assignee based on an external username' => 'Alterar designação com base em um usuário externo', 'Change the category based on an external label' => 'Alterar categoria com base em um rótulo externo', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Todos possuem acesso a este projeto.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Ajuda sobre os webhooks do GitHub', 'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo', - 'Github issue comment created' => 'Github issue comment created', 'Project management' => 'Gerenciamento de projetos', 'My projects' => 'Meus projetos', 'Columns' => 'Colunas', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Redistribuição de usuário para "%s"', 'Clone this project' => 'Clonar este projeto', 'Column removed successfully.' => 'Coluna removida com sucesso.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.', 'Previous' => 'Anterior', 'The id must be an integer' => 'O ID deve ser um número inteiro', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Sua swimlane foi criada com sucesso.', 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Solicitação de Recurso, Melhoria"', 'Default categories for new projects (Comma-separated)' => 'Categorias padrões para novos projetos (separadas por vírgula)', - 'Gitlab commit received' => 'Gitlab commit received', - 'Gitlab issue opened' => 'Gitlab issue opened', - 'Gitlab issue closed' => 'Gitlab issue closed', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Ajuda sobre os webhooks do GitLab', 'Integrations' => 'Integrações', 'Integration with third-party services' => 'Integração com serviços de terceiros', - 'Gitlab Issue' => 'Gitlab Issue', 'Subtask Id' => 'ID da subtarefa', 'Subtasks' => 'Subtarefas', 'Subtasks Export' => 'Exportar subtarefas', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Você já tem um subtarefa em andamento', 'Which parts of the project do you want to duplicate?' => 'Quais partes do projeto você deseja duplicar?', 'Disallow login form' => 'Proibir o formulário de login', - 'Bitbucket commit received' => '"Commit" recebido via Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks do Bitbucket', 'Start' => 'Início', 'End' => 'Fim', 'Task age in days' => 'Idade da tarefa em dias', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'O código de autenticação em duas etapas é válido.', 'Code' => 'Código', 'Two factor authentication' => 'Autenticação em duas etapas', - 'Enable/disable two factor authentication' => 'Ativar/desativar autenticação em duas etapas', 'This QR code contains the key URI: ' => 'Este Código QR contém a chave URI:', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salve esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).', 'Check my code' => 'Verifique o meu código', 'Secret key: ' => 'Chave secreta:', 'Test your device' => 'Teste o seu dispositivo', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'O usuário que vai receber o e-mail', 'Email subject' => 'Assunto do e-mail', 'Date' => 'Data', - 'By @%s on Bitbucket' => 'Por @%s no Bitbucket', - 'Bitbucket Issue' => 'Bitbucket Issue', - 'Commit made by @%s on Bitbucket' => 'Commit feito por @%s no Bitbucket', - 'Commit made by @%s on Github' => 'Commit feito por @%s no Github', - 'By @%s on Github' => 'Por @%s no Github', - 'Commit made by @%s on Gitlab' => 'Commit feito por @%s no Gitlab', 'Add a comment log when moving the task between columns' => 'Adicionar um comentário de log quando uma tarefa é movida para uma outra coluna', 'Move the task to another column when the category is changed' => 'Mover uma tarefa para outra coluna quando a categoria mudou', 'Send a task by email to someone' => 'Enviar uma tarefa por e-mail a alguém', 'Reopen a task' => 'Reabrir uma tarefa', - 'Bitbucket issue opened' => 'Bitbucket issue opened', - 'Bitbucket issue closed' => 'Bitbucket issue closed', - 'Bitbucket issue reopened' => 'Bitbucket issue reopened', - 'Bitbucket issue assignee change' => 'Bitbucket issue assignee change', - 'Bitbucket issue comment created' => 'Bitbucket issue comment created', 'Column change' => 'Mudança de coluna', 'Position change' => 'Mudança de posição', 'Swimlane change' => 'Mudança de swimlane', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Usuário remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Os usuários remotos não conservam as suas senhas no banco de dados Kanboard, exemplos: contas LDAP, Github ou Google.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se você marcar "Interdir o formulário de autenticação", os identificadores entrados no formulário de login serão ignorado.', - 'By @%s on Gitlab' => 'Por @%s no Gitlab', - 'Gitlab issue comment created' => 'Comentário criado em um bilhete Gitlab', 'New remote user' => 'Criar um usuário remoto', 'New local user' => 'Criar um usuário local', 'Default task color' => 'Cor padrão para as tarefas', @@ -1028,7 +994,6 @@ return array( 'Append/Replace' => 'Adicionar/Substituir', 'Append' => 'Adicionar', 'Replace' => 'Substituir', - 'There is no notification method registered.' => 'Não há metodo de notificação registrado.', 'Import' => 'Importar', 'change sorting' => 'alterar ordenação', 'Tasks Importation' => 'Importação de Tarefas', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', 'Project members' => 'Membros de projeto', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/pt_PT/translations.php b/sources/app/Locale/pt_PT/translations.php index 5f5e9c8..bfe9803 100644 --- a/sources/app/Locale/pt_PT/translations.php +++ b/sources/app/Locale/pt_PT/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s mudou a assignação da tarefa %s para %s', 'New password for the user "%s"' => 'Nova senha para o utilizador "%s"', 'Choose an event' => 'Escolher um evento', - 'Github commit received' => 'Recebido commit do Github', - 'Github issue opened' => 'Problema aberto no Github', - 'Github issue closed' => 'Problema fechado no Github', - 'Github issue reopened' => 'Problema reaberto no Github', - 'Github issue assignee change' => 'Alterar assignação ao problema no Githubnge', - 'Github issue label change' => 'Alterar etiqueta do problema no Github', 'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo', 'Change the assignee based on an external username' => 'Alterar assignação com base num utilizador externo', 'Change the category based on an external label' => 'Alterar categoria com base num rótulo externo', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Todos possuem acesso a este projecto.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Ajuda para o Github webhooks', 'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo', - 'Github issue comment created' => 'Criado comentário ao problema no Github', 'Project management' => 'Gestão de projectos', 'My projects' => 'Os meus projectos', 'Columns' => 'Colunas', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Redistribuição de utilizador para "%s"', 'Clone this project' => 'Clonar este projecto', 'Column removed successfully.' => 'Coluna removida com sucesso.', - 'Github Issue' => 'Problema no Github', 'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.', 'Previous' => 'Anterior', 'The id must be an integer' => 'O ID deve ser um número inteiro', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Seu swimlane foi criado com sucesso.', 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Feature Request, Improvement"', 'Default categories for new projects (Comma-separated)' => 'Categorias padrão para novos projectos (Separadas por vírgula)', - 'Gitlab commit received' => 'Commit recebido do Gitlab', - 'Gitlab issue opened' => 'Problema aberto no Gitlab', - 'Gitlab issue closed' => 'Problema fechado no Gitlab', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Ajuda sobre Gitlab webhooks', 'Integrations' => 'Integrações', 'Integration with third-party services' => 'Integração com serviços de terceiros', - 'Gitlab Issue' => 'Problema Gitlab', 'Subtask Id' => 'ID da subtarefa', 'Subtasks' => 'Subtarefas', 'Subtasks Export' => 'Exportar subtarefas', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Já tem uma subtarefa em andamento', 'Which parts of the project do you want to duplicate?' => 'Quais as partes do projecto que deseja duplicar?', 'Disallow login form' => 'Desactivar login', - 'Bitbucket commit received' => '"Commit" recebido via Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks Bitbucket', 'Start' => 'Inicio', 'End' => 'Fim', 'Task age in days' => 'Idade da tarefa em dias', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'O código de autenticação com factor duplo é válido', 'Code' => 'Código', 'Two factor authentication' => 'Autenticação com factor duplo', - 'Enable/disable two factor authentication' => 'Activar/Desactivar autenticação com factor duplo', 'This QR code contains the key URI: ' => 'Este Código QR contém a chave URI: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Guarde esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).', 'Check my code' => 'Verificar o meu código', 'Secret key: ' => 'Chave secreta: ', 'Test your device' => 'Teste o seu dispositivo', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'O utilizador que vai receber o e-mail', 'Email subject' => 'Assunto do e-mail', 'Date' => 'Data', - 'By @%s on Bitbucket' => 'Por @%s no Bitbucket', - 'Bitbucket Issue' => 'Problema Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Commit feito por @%s no Bitbucket', - 'Commit made by @%s on Github' => 'Commit feito por @%s no Github', - 'By @%s on Github' => 'Por @%s no Github', - 'Commit made by @%s on Gitlab' => 'Commit feito por @%s no Gitlab', 'Add a comment log when moving the task between columns' => 'Adicionar um comentário de log quando uma tarefa é movida para uma outra coluna', 'Move the task to another column when the category is changed' => 'Mover uma tarefa para outra coluna quando a categoria mudar', 'Send a task by email to someone' => 'Enviar uma tarefa por e-mail a alguém', 'Reopen a task' => 'Reabrir uma tarefa', - 'Bitbucket issue opened' => 'Problema aberto no Bitbucket', - 'Bitbucket issue closed' => 'Problema fechado no Bitbucket', - 'Bitbucket issue reopened' => 'Problema reaberto no Bitbucket', - 'Bitbucket issue assignee change' => 'Alterar assignação do problema no Bitbucket', - 'Bitbucket issue comment created' => 'Comentário ao problema adicionado ao Bitbucket', 'Column change' => 'Mudança de coluna', 'Position change' => 'Mudança de posição', 'Swimlane change' => 'Mudança de swimlane', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Utilizador remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Utilizadores remotos não guardam a password na base de dados do Kanboard, por exemplo: LDAP, contas do Google e Github.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se activar a opção "Desactivar login", as credenciais digitadas no login serão ignoradas.', - 'By @%s on Gitlab' => 'Por @%s no Gitlab', - 'Gitlab issue comment created' => 'Comentário a problema no Gitlab adicionado', 'New remote user' => 'Novo utilizador remoto', 'New local user' => 'Novo utilizador local', 'Default task color' => 'Cor de tarefa por defeito', @@ -929,7 +895,7 @@ return array( 'There is no task in your project.' => 'Não existe tarefa no seu projecto.', 'Gantt chart' => 'Gráfico de Gantt', 'People who are project managers' => 'Pessoas que são gestores do projecto', - 'People who are project members' => 'Pessoas que são membors do projecto', + 'People who are project members' => 'Pessoas que são membros do projecto', 'NOK - Norwegian Krone' => 'NOK - Coroa Norueguesa', 'Show this column' => 'Mostrar esta coluna', 'Hide this column' => 'Esconder esta coluna', @@ -1028,7 +994,6 @@ return array( 'Append/Replace' => 'Acrescentar/Substituir', 'Append' => 'Acrescentar', 'Replace' => 'Substituir', - 'There is no notification method registered.' => 'Não existe método de notificação registrado.', 'Import' => 'Importar', 'change sorting' => 'alterar ordernação', 'Tasks Importation' => 'Importação de Tarefas', @@ -1051,52 +1016,92 @@ return array( 'BAM - Konvertible Mark' => 'BAM - Marca Conversível', 'Assignee Username' => 'Utilizador do Assignado', 'Assignee Name' => 'Nome do Assignado', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', + 'Groups' => 'Grupos', + 'Members of %s' => 'Membros de %s', + 'New group' => 'Novo grupo', + 'Group created successfully.' => 'Grupo criado com sucesso.', + 'Unable to create your group.' => 'Não foi possivel criar o seu grupo.', + 'Edit group' => 'Editar grupo', + 'Group updated successfully.' => 'Grupo actualizado com sucesso.', + 'Unable to update your group.' => 'Não foi possivel actualizar o seu grupo.', + 'Add group member to "%s"' => 'Adicionar membro do grupo a "%s"', + 'Group member added successfully.' => 'Membro de grupo adicionado com sucesso.', + 'Unable to add group member.' => 'Não foi possivel adicionar membro de grupo.', + 'Remove user from group "%s"' => 'Remover utilizador do grupo "%s"', + 'User removed successfully from this group.' => 'Utilizador removido com sucesso deste grupo.', + 'Unable to remove this user from the group.' => 'Não foi possivel remover este utilizador do grupo.', + 'Remove group' => 'Remover grupo.', + 'Group removed successfully.' => 'Grupo removido com sucesso.', + 'Unable to remove this group.' => 'Não foi possivel remover este grupo.', + 'Project Permissions' => 'Permissões de Projecto', + 'Manager' => 'Gestor', + 'Project Manager' => 'Gestor de Projecto', + 'Project Member' => 'Membro de Projecto', + 'Project Viewer' => 'Visualizador de Projecto', + 'Your account is locked for %d minutes' => 'A sua conta está bloqueada por %d minutos', + 'Invalid captcha' => 'Captcha inválido', + 'The name must be unique' => 'O nome deve ser único', + 'View all groups' => 'Ver todos os grupos', + 'View group members' => 'Ver membros do grupo', + 'There is no user available.' => 'Não existe utilizador disponivel.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Tem a certeza que quer remover o utilizador "%s" do grupo "%s"?', + 'There is no group.' => 'Não existe grupo.', + 'External Id' => 'Id externo', + 'Add group member' => 'Adicionar membro de grupo', + 'Do you really want to remove this group: "%s"?' => 'Tem a certeza que quer remover este grupo: "%s"?', + 'There is no user in this group.' => 'Não existe utilizadores neste grupo.', + 'Remove this user' => 'Remover este utilizador', + 'Permissions' => 'Permissões', + 'Allowed Users' => 'Utilizadores Permitidos', + 'No user have been allowed specifically.' => 'Nenhum utilizador foi especificamente permitido.', + 'Role' => 'Função', + 'Enter user name...' => 'Escreva o nome do utilizador...', + 'Allowed Groups' => 'Grupos Permitidos', + 'No group have been allowed specifically.' => 'Nenhum grupo foi especificamente permitido.', + 'Group' => 'Grupo', + 'Group Name' => 'Nome do Grupo', + 'Enter group name...' => 'Escreva o nome do Grupo', + 'Role:' => 'Função:', 'Project members' => 'Membros do projecto', + 'Compare hours for "%s"' => 'Comparar horas para "%s"', + '%s mentioned you in the task #%d' => '%s mencionou-te na tarefa #%d', + '%s mentioned you in a comment on the task #%d' => '%s mencionou-te num comentário na tarefa #%d', + 'You were mentioned in the task #%d' => 'Foi mencionado na tarefa #%d', + 'You were mentioned in a comment on the task #%d' => 'Foi mencionado num comentário na tarefa #%d', + 'Mentioned' => 'Mencionado', + 'Compare Estimated Time vs Actual Time' => 'Comparar Tempo Estimado vs Tempo Real', + 'Estimated hours: ' => 'Horas estimadas: ', + 'Actual hours: ' => 'Horas reais: ', + 'Hours Spent' => 'Horas Gastas', + 'Hours Estimated' => 'Horas Estimadas', + 'Estimated Time' => 'Tempo Estimado', + 'Actual Time' => 'Tempo Real', + 'Estimated vs actual time' => 'Tempo estimado vs real', + 'RUB - Russian Ruble' => 'RUB - Rublo Russo', + 'Assign the task to the person who does the action when the column is changed' => 'Assignar a tarefa à pessoa que realiza a acção quando a coluna é alterada', + 'Close a task in a specific column' => 'Fechar tarefa numa coluna especifica', + 'Time-based One-time Password Algorithm' => 'Algoritmo de password para uso único baseado em tempo', + 'Two-Factor Provider: ' => 'Provedor de Dois Passos: ', + 'Disable two-factor authentication' => 'Desactivar autenticação de dois passos', + 'Enable two-factor authentication' => 'Activar autenticação de dois passos', + 'There is no integration registered at the moment.' => 'Não existe nenhuma integração registrada até ao momento.', + 'Password Reset for Kanboard' => 'Redefinir Password para Kanboard', + 'Forgot password?' => 'Esqueceu a password?', + 'Enable "Forget Password"' => 'Activar "Esqueceu a password"', + 'Password Reset' => 'Redefinir a Password', + 'New password' => 'Nova Password', + 'Change Password' => 'Alterar Password', + 'To reset your password click on this link:' => 'Para redefinir a sua password click nesta ligação:', + 'Last Password Reset' => 'Última Redefinição da Password', + 'The password has never been reinitialized.' => 'A password nunca foi redefinida.', + 'Creation' => 'Criação', + 'Expiration' => 'Expiração', + 'Password reset history' => 'Histórico da redefinição da password', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas as tarefas na coluna "%s" e na swimlane "%s" foram fechadas com successo.', + 'Do you really want to close all tasks of this column?' => 'Tem a certeza que quer fechar todas as tarefas nesta coluna?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarefa(s) na coluna "%s" e na swimlane "%s" serão fechadas.', + 'Close all tasks of this column' => 'Fechar todas as tarefas nesta coluna', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/ru_RU/translations.php b/sources/app/Locale/ru_RU/translations.php index 06b3b9d..7e406c9 100644 --- a/sources/app/Locale/ru_RU/translations.php +++ b/sources/app/Locale/ru_RU/translations.php @@ -294,7 +294,7 @@ return array( 'File removed successfully.' => 'Файл удален.', 'Attach a document' => 'Прикрепить файл', 'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?', - 'Attachments' => 'Приложение', + 'Attachments' => 'Вложения', 'Edit the task' => 'Изменить задачу', 'Edit the description' => 'Изменить описание', 'Add a comment' => 'Добавить комментарий', @@ -310,7 +310,7 @@ return array( 'estimated' => 'расчетное', 'Sub-Tasks' => 'Подзадачи', 'Add a sub-task' => 'Добавить подзадачу', - 'Original estimate' => 'Запланировано', + 'Original estimate' => 'Заплан.', 'Create another sub-task' => 'Создать другую подзадачу', 'Time spent' => 'Времени затрачено', 'Edit a sub-task' => 'Изменить подзадачу', @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s сменил назначенного для задачи %s на %s', 'New password for the user "%s"' => 'Новый пароль для пользователя "%s"', 'Choose an event' => 'Выберите событие', - 'Github commit received' => 'Github: коммит получен', - 'Github issue opened' => 'Github: новая проблема', - 'Github issue closed' => 'Github: проблема закрыта', - 'Github issue reopened' => 'Github: проблема переоткрыта', - 'Github issue assignee change' => 'Github: сменить ответственного за проблему', - 'Github issue label change' => 'Github: ярлык проблемы изменен', 'Create a task from an external provider' => 'Создать задачу из внешнего источника', 'Change the assignee based on an external username' => 'Изменить назначенного основываясь на внешнем имени пользователя', 'Change the category based on an external label' => 'Изменить категорию основываясь на внешнем ярлыке', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Любой может получить доступ к этому проекту.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Помощь по Github webhooks', 'Create a comment from an external provider' => 'Создать комментарий из внешнего источника', - 'Github issue comment created' => 'Github issue комментарий создан', 'Project management' => 'Управление проектом', 'My projects' => 'Мои проекты', 'Columns' => 'Колонки', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Перераспределение пользователей для "%s"', 'Clone this project' => 'Клонировать проект', 'Column removed successfully.' => 'Колонка успешно удалена.', - 'Github Issue' => 'Вопрос на Github', 'Not enough data to show the graph.' => 'Недостаточно данных, чтобы показать график.', 'Previous' => 'Предыдущий', 'The id must be an integer' => 'Этот id должен быть целочисленным', @@ -532,7 +522,7 @@ return array( 'Nothing to preview...' => 'Нет данных для предпросмотра...', 'Preview' => 'Предпросмотр', 'Write' => 'Написание', - 'Active swimlanes' => 'Активные ', + 'Active swimlanes' => 'Активные дорожки', 'Add a new swimlane' => 'Добавить новую дорожку', 'Change default swimlane' => 'Сменить стандартную дорожку', 'Default swimlane' => 'Стандартная дорожка', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Ваша дорожка была успешно создан.', 'Example: "Bug, Feature Request, Improvement"' => 'Например: "Баг, Фича, Улучшение"', 'Default categories for new projects (Comma-separated)' => 'Стандартные категории для нового проекта (разделяются запятыми)', - // 'Gitlab commit received' => '', - 'Gitlab issue opened' => 'Gitlab вопрос открыт', - 'Gitlab issue closed' => 'Gitlab вопрос закрыт', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Помощь по Gitlab webhooks', 'Integrations' => 'Интеграции', 'Integration with third-party services' => 'Интеграция со сторонними сервисами', - 'Gitlab Issue' => 'Gitlab вопросы', 'Subtask Id' => 'Id подзадачи', 'Subtasks' => 'Подзадачи', 'Subtasks Export' => 'Экспортировать подзадачи', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'У вас уже есть одна задача в разработке', 'Which parts of the project do you want to duplicate?' => 'Какие части проекта должны быть дублированы?', 'Disallow login form' => 'Запретить форму входа', - // 'Bitbucket commit received' => '', - 'Bitbucket webhooks' => 'BitBucket webhooks', - 'Help on Bitbucket webhooks' => 'Помощь по BitBucket webhooks', 'Start' => 'Начало', 'End' => 'Конец', 'Task age in days' => 'Возраст задачи в днях', @@ -613,7 +594,7 @@ return array( 'Task\'s links' => 'Ссылки задачи', 'The labels must be different' => 'Ярлыки должны быть разными', 'There is no link.' => 'Это не ссылка', - 'This label must be unique' => 'Этот ярлык должна быть уникальной ', + 'This label must be unique' => 'Этот ярлык должен быть уникальным ', 'Unable to create your link.' => 'Не удается создать эту ссылку.', 'Unable to update your link.' => 'Не удается обновить эту ссылку.', 'Unable to remove this link.' => 'Не удается удалить эту ссылку.', @@ -641,7 +622,7 @@ return array( 'Keyboard shortcuts' => 'Горячие клавиши', 'Open board switcher' => 'Открыть переключатель доски', 'Application' => 'Приложение', - // 'since %B %e, %Y at %k:%M %p' => '', + 'since %B %e, %Y at %k:%M %p' => 'с %B %e, %Y - %k:%M %p', 'Compact view' => 'Компактный вид', 'Horizontal scrolling' => 'Широкий вид', 'Compact/wide view' => 'Компактный/широкий вид', @@ -652,7 +633,7 @@ return array( 'Private project' => 'Приватный проект', 'AUD - Australian Dollar' => 'AUD - Австралийский доллар', 'CAD - Canadian Dollar' => 'CAD - Канадский доллар', - 'CHF - Swiss Francs' => 'CHF - Швейцарский Франк', + 'CHF - Swiss Francs' => 'CHF - Швейцарский франк', 'Custom Stylesheet' => 'Пользовательский стиль', 'download' => 'загрузить', 'EUR - Euro' => 'EUR - Евро', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Код двухфакторной авторизации валиден', 'Code' => 'Код', 'Two factor authentication' => 'Двухфакторная авторизация', - 'Enable/disable two factor authentication' => 'Включить/выключить двухфакторную авторизацию', 'This QR code contains the key URI: ' => 'Это QR-код содержит ключевую URI:', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Сохраните Ваш секретный ключ в TOTP программе (например Google Autentificator или FreeOTP).', 'Check my code' => 'Проверить мой код', 'Secret key: ' => 'Секретный ключ: ', 'Test your device' => 'Проверьте свое устройство', @@ -754,7 +733,7 @@ return array( 'Automatically update the start date' => 'Автоматическое обновление даты начала', 'iCal feed' => 'iCal данные', 'Preferences' => 'Предпочтения', - 'Security' => 'Безопастность', + 'Security' => 'Безопасность', 'Two factor authentication disabled' => 'Двухфакторная аутентификация отключена', 'Two factor authentication enabled' => 'Включена двухфакторная аутентификация', 'Unable to update this user.' => 'Не удается обновить этого пользователя.', @@ -762,43 +741,32 @@ return array( 'User that will receive the email' => 'Пользователь, который будет получать e-mail', 'Email subject' => 'Тема e-mail', 'Date' => 'Дата', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', + 'Add a comment log when moving the task between columns' => 'Добавлять запись при перемещении задачи между колонками', + 'Move the task to another column when the category is changed' => 'Переносить задачи в другую колонку при изменении категории', + 'Send a task by email to someone' => 'Отправить задачу по email', + 'Reopen a task' => 'Переоткрыть задачу', 'Column change' => 'Изменение колонки', 'Position change' => 'Позиция изменена', 'Swimlane change' => 'Дорожка изменена', - // 'Assignee change' => '', + 'Assignee change' => 'Назначенный пользователь изменен', '[%s] Overdue tasks' => '[%s] просроченные задачи', 'Notification' => 'Уведомления', '%s moved the task #%d to the first swimlane' => '%s задач перемещено #%d в первой дорожке', '%s moved the task #%d to the swimlane "%s"' => '%s задач перемещено #%d в дорожке "%s"', 'Swimlane' => 'Дорожки', 'Gravatar' => 'Граватар', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', + '%s moved the task %s to the first swimlane' => '%s переместил задачу %s на первую дорожку', + '%s moved the task %s to the swimlane "%s"' => '%s переместил задачу %s на дорожку "%s"', 'This report contains all subtasks information for the given date range.' => 'Этот отчет содержит всю информацию подзадач в заданном диапазоне дат.', 'This report contains all tasks information for the given date range.' => 'Этот отчет содержит всю информацию для задачи в заданном диапазоне дат.', 'Project activities for %s' => 'Активность проекта для %s', - // 'view the board on Kanboard' => '', + 'view the board on Kanboard' => 'посмотреть доску на Kanboard', 'The task have been moved to the first swimlane' => 'Эта задача была перемещена в первую дорожку', 'The task have been moved to another swimlane:' => 'Эта задача была перемещена в другую дорожку:', 'Overdue tasks for the project "%s"' => 'Просроченные задачи для проекта "%s"', 'New title: %s' => 'Новый заголовок: %s', 'The task is not assigned anymore' => 'Задача больше не назначена', - // 'New assignee: %s' => '', + 'New assignee: %s' => 'Новый назначенный: %s', 'There is no category now' => 'В настоящее время здесь нет категорий', 'New category: %s' => 'Новая категория: %s', 'New color: %s' => 'Новый цвет: %s', @@ -888,16 +856,14 @@ return array( 'Time spent into each column' => 'Время, проведенное в каждой колонке', 'The lead time is the duration between the task creation and the completion.' => 'Время выполнения - период между созданием задачи и завершения.', 'The cycle time is the duration between the start date and the completion.' => 'Время цикла - период времени между датой начала и завершения.', - // 'If the task is not closed the current time is used instead of the completion date.' => '', + 'If the task is not closed the current time is used instead of the completion date.' => 'Если задача не закрыта, то текущая дата будет указана в дате завершения задачи.', 'Set automatically the start date' => 'Установить автоматическую дату начала', 'Edit Authentication' => 'Редактировать авторизацию', - 'Google Id' => 'Google I', + 'Google Id' => 'Google Id', 'Github Id' => 'Github Id', 'Remote user' => 'Удаленный пользователь', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Учетные данные для входа через LDAP, Google и Github не будут сохранены в Kanboard.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Если вы установите флажок "Запретить форму входа", учетные данные, введенные в форму входа будет игнорироваться.', - 'By @%s on Gitlab' => 'От @%s на Gitlab', - 'Gitlab issue comment created' => 'Был создан комментарий к задаче на Gitlab', 'New remote user' => 'Новый удаленный пользователь', 'New local user' => 'Новый локальный пользователь', 'Default task color' => 'Стандартные цвета задач', @@ -964,139 +930,178 @@ return array( 'Switch to the Gantt chart view' => 'Переключиться в режим диаграммы Гантта', 'Reset the search/filter box' => 'Сбросить поиск/фильтр', 'Documentation' => 'Документация', - 'Table of contents' => 'Сожержание', + 'Table of contents' => 'Содержание', 'Gantt' => 'Гантт', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', - // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', - // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'There is no notification method registered.' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', + 'Author' => 'Автор', + 'Version' => 'Версия', + 'Plugins' => 'Плагины', + 'There is no plugin loaded.' => 'Нет установленных плагинов.', + 'Set maximum column height' => 'Установить максимальную высоту колонки', + 'Remove maximum column height' => 'Сбросить максимальную высоту колонки', + 'My notifications' => 'Мои уведомления', + 'Custom filters' => 'Пользовательские фильтры', + 'Your custom filter have been created successfully.' => 'Фильтр был успешно создан.', + 'Unable to create your custom filter.' => 'Невозможно создать фильтр.', + 'Custom filter removed successfully.' => 'Пользовательский фильтр был успешно удален.', + 'Unable to remove this custom filter.' => 'Невозможно удалить фильтр.', + 'Edit custom filter' => 'Изменить пользовательский фильтр', + 'Your custom filter have been updated successfully.' => 'Пользовательский фильтр был успешно обновлен.', + 'Unable to update custom filter.' => 'Невозможно обновить фильтр.', + 'Web' => 'Интернет', + 'New attachment on task #%d: %s' => 'Новое вложение для задачи #%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', + 'New task #%d: %s' => 'Новая задача #%d: %s', + 'Task updated #%d' => 'Обновлена задача #%d', + 'Task #%d closed' => 'Задача #%d закрыта', + 'Task #%d opened' => 'Задача #%d открыта', + 'Column changed for task #%d' => 'Обновлена колонка у задачи #%d', + 'New position for task #%d' => 'Новая позиция для задачи #%d', + 'Swimlane changed for task #%d' => 'Изменена дорожка у задачи #%d', + 'Assignee changed on task #%d' => 'Изменен назначенный у задачи #%d', + '%d overdue tasks' => '%d просроченных задач', + 'Task #%d is overdue' => 'Задача #%d просрочена', + 'No new notifications.' => 'Нет новых уведомлений.', + 'Mark all as read' => 'Пометить все прочитанными', + 'Mark as read' => 'Пометить прочитанным', + 'Total number of tasks in this column across all swimlanes' => 'Общее число задач в этой колонке на всех дорожках', + 'Collapse swimlane' => 'Свернуть дорожку', + 'Expand swimlane' => 'Развернуть дорожку', + 'Add a new filter' => 'Добавить новый фильтр', + 'Share with all project members' => 'Сделать общим для всех участников проекта', + 'Shared' => 'Общие', + 'Owner' => 'Владелец', + 'Unread notifications' => 'Непрочитанные уведомления', + 'My filters' => 'Мои фильтры', + 'Notification methods:' => 'Способы уведомления:', + 'Import tasks from CSV file' => 'Импорт задач из CSV-файла', + 'Unable to read your file' => 'Невозможно прочитать файл', + '%d task(s) have been imported successfully.' => '%d задач было успешно импортировано.', + 'Nothing have been imported!' => 'Ничего не было импортировано!', + 'Import users from CSV file' => 'Импорт пользователей из CSV-файла', + '%d user(s) have been imported successfully.' => '%d пользователей было успешно импортировано.', + 'Comma' => 'Запятая', + 'Semi-colon' => 'Точка с запятой', + 'Tab' => 'Пробел (Tab)', + 'Vertical bar' => 'Вертикальная черта (|)', + 'Double Quote' => 'Одинарные кавычки', + 'Single Quote' => 'Двойные кавычки', + '%s attached a file to the task #%d' => '%s добавил файл к задаче #%d', + 'There is no column or swimlane activated in your project!' => 'В вашей задаче нет активных колонок или дорожек!', + 'Append filter (instead of replacement)' => 'Добавляющий фильтр (не заменяющий)', + 'Append/Replace' => 'Добавление/Замена', + 'Append' => 'Добавление', + 'Replace' => 'Замена', + 'Import' => 'Импорт', + 'change sorting' => 'изменить сортировку', + 'Tasks Importation' => 'Импортирование задач', + 'Delimiter' => 'Разделитель', + 'Enclosure' => 'Тип кавычек', + 'CSV File' => 'CSV-файл', + 'Instructions' => 'Инструкции', + 'Your file must use the predefined CSV format' => 'Ваш файл должен использовать структуру формата CSV', + 'Your file must be encoded in UTF-8' => 'Ваш файл должен иметь кодировку UTF-8', + 'The first row must be the header' => 'В первой строке должны быть заголовки столбцов', + 'Duplicates are not verified for you' => 'Проверка на дубликаты не осуществляется', + 'The due date must use the ISO format: YYYY-MM-DD' => 'Дата просрочки должна быть в формате ISO: ГГГГ-ММ-ДД', + 'Download CSV template' => 'Скачать шаблон CSV-файла', + 'No external integration registered.' => 'Нет зарегистрированных внешних интеграций.', + 'Duplicates are not imported' => 'Дубликаты не импортируются', + 'Usernames must be lowercase and unique' => 'Логины пользователей должны быть строчными и уникальными', + 'Passwords will be encrypted if present' => 'Пароли будут зашифрованы (если указаны)', + '%s attached a new file to the task %s' => '%s добавил новый файл к задаче %s', + 'Assign automatically a category based on a link' => 'Автоматически назначать категории на основе ссылки', + 'BAM - Konvertible Mark' => 'BAM - Конвертируемая марка', + 'Assignee Username' => 'Логин назначенного', + 'Assignee Name' => 'Имя назначенного', + 'Groups' => 'Группы', + 'Members of %s' => 'Участник группы %s', + 'New group' => 'Новая группа', + 'Group created successfully.' => 'Группа успешно создана.', + 'Unable to create your group.' => 'Невозможно создать группу.', + 'Edit group' => 'Именить группу', + 'Group updated successfully.' => 'Группы успешно обновлена.', + 'Unable to update your group.' => 'Невозможно обновить группу.', + 'Add group member to "%s"' => 'Добавить участника в "%s"', + 'Group member added successfully.' => 'Участник группы успешно добавлен.', + 'Unable to add group member.' => 'Невозможно добавить участника.', + 'Remove user from group "%s"' => 'Удалить пользователя из группы "%s"', + 'User removed successfully from this group.' => 'Пользователь успешно удален из группы.', + 'Unable to remove this user from the group.' => 'Невозможно удалить пользователя из группы.', + 'Remove group' => 'Удалить группу', + 'Group removed successfully.' => 'Группа успешно удалена.', + 'Unable to remove this group.' => 'Невозможно удалить группу.', + 'Project Permissions' => 'Разрешения проекта', + 'Manager' => 'Менеджер', + 'Project Manager' => 'Менеджер проекта', + 'Project Member' => 'Участник проекта', + 'Project Viewer' => 'Наблюдатель проекта', + 'Your account is locked for %d minutes' => 'Ваш аккаунт заблокирован на %d минут', + 'Invalid captcha' => 'Неверный код подтверждения', + 'The name must be unique' => 'Имя должно быть уникальным', + 'View all groups' => 'Просмотр всех группы', + 'View group members' => 'Просмотр всех группы участников группы', + 'There is no user available.' => 'Нет доступных пользователей.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Вы действительно хотите удалить пользователя "%s" из группы "%s"?', + 'There is no group.' => 'Нет созданных групп.', + 'External Id' => 'Внешний Id', + 'Add group member' => 'Добавить участника в группу', + 'Do you really want to remove this group: "%s"?' => 'Вы действительно хотите удалить группу "%s"?', + 'There is no user in this group.' => 'В этой группе нет участников.', + 'Remove this user' => 'Удалить пользователя.', + 'Permissions' => 'Разрешения', + 'Allowed Users' => 'Разрешенные пользователи', + 'No user have been allowed specifically.' => 'Нет заданных разрешений для пользователей.', + 'Role' => 'Роль', + 'Enter user name...' => 'Введите имя пользователя...', + 'Allowed Groups' => 'Разрешенные группы', + 'No group have been allowed specifically.' => 'Нет заданных разрешений для групп.', + 'Group' => 'Группа', + 'Group Name' => 'Имя группы', + 'Enter group name...' => 'Введите имя группы...', + 'Role:' => 'Роль:', 'Project members' => 'Участники проекта', + 'Compare hours for "%s"' => 'Сравнить часы для "%s"', + '%s mentioned you in the task #%d' => '%s упомянул вас в задаче #%d', + '%s mentioned you in a comment on the task #%d' => '%s упомянул вас в комментарии к задаче #%d', + 'You were mentioned in the task #%d' => 'Вы упомянуты в задаче #%d', + 'You were mentioned in a comment on the task #%d' => 'Вы упомянуты в комментарии к задаче #%d', + 'Mentioned' => 'Упоминания', + 'Compare Estimated Time vs Actual Time' => 'Сравнить запланированное время и реальное', + 'Estimated hours: ' => 'Запланировано часов: ', + 'Actual hours: ' => 'Реально затрачено часов: ', + 'Hours Spent' => 'Затрачено часов', + 'Hours Estimated' => 'Запланировано часов', + 'Estimated Time' => 'Запланировано времени', + 'Actual Time' => 'Затрачено времени', + 'Estimated vs actual time' => 'Запланировано и реально затрачено времени', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/sr_Latn_RS/translations.php b/sources/app/Locale/sr_Latn_RS/translations.php index 8ad8d00..995cd48 100644 --- a/sources/app/Locale/sr_Latn_RS/translations.php +++ b/sources/app/Locale/sr_Latn_RS/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s zamena dodele za zadatak %s na %s', 'New password for the user "%s"' => 'Nova lozinka za korisnika "%s"', 'Choose an event' => 'Izaberi događaj', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', 'Create a task from an external provider' => 'Kreiraj zadatak preko posrednika', 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrzenj etykiety', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Svima je dozvoljen pristup.', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', 'Project management' => 'Uređivanje projekata', 'My projects' => 'Moji projekti', 'Columns' => 'Kolone', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Zaduženja korisnika za "%s"', 'Clone this project' => 'Kopiraj projekat', 'Column removed successfully.' => 'Kolumna usunięta pomyslnie.', - // 'Github Issue' => '', 'Not enough data to show the graph.' => 'Nedovoljno podataka za grafikon.', 'Previous' => 'Prethodni', 'The id must be an integer' => 'ID musi być liczbą całkowitą', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Razdelnik je uspešno kreiran.', 'Example: "Bug, Feature Request, Improvement"' => 'Npr: "Greška, Zahtev za izmenama, Poboljšanje"', 'Default categories for new projects (Comma-separated)' => 'Osnovne kategorije za projekat', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Integracje', 'Integration with third-party services' => 'Integracja sa uslugama spoljnih servisa', - // 'Gitlab Issue' => '', 'Subtask Id' => 'ID pod-zadania', 'Subtasks' => 'Pod-zadataka', 'Subtasks Export' => 'Eksport pod-zadań', @@ -588,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', 'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želite da kopirate', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', // 'Start' => '', // 'End' => '', // 'Task age in days' => '', @@ -688,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/sv_SE/translations.php b/sources/app/Locale/sv_SE/translations.php index 015fbfc..af14409 100644 --- a/sources/app/Locale/sv_SE/translations.php +++ b/sources/app/Locale/sv_SE/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s byt tilldelning av uppgiften %s till %s', 'New password for the user "%s"' => 'Nytt lösenord för användaren "%s"', 'Choose an event' => 'Välj en händelse', - 'Github commit received' => 'Github-bidrag mottaget', - 'Github issue opened' => 'Github-fråga öppnad', - 'Github issue closed' => 'Github-fråga stängd', - 'Github issue reopened' => 'Github-fråga öppnad på nytt', - 'Github issue assignee change' => 'Github-fråga ny tilldelning', - 'Github issue label change' => 'Github-fråga etikettförändring', 'Create a task from an external provider' => 'Skapa en uppgift från en extern leverantör', 'Change the assignee based on an external username' => 'Ändra tilldelning baserat på ett externt användarnamn', 'Change the category based on an external label' => 'Ändra kategori baserat på en extern etikett', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Alla har tillgång till projektet', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Hjälp för Github webhooks', 'Create a comment from an external provider' => 'Skapa en kommentar från en extern leverantör', - 'Github issue comment created' => 'Github frågekommentar skapad', 'Project management' => 'Projekthantering', 'My projects' => 'Mina projekt', 'Columns' => 'Kolumner', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Användardeltagande för "%s"', 'Clone this project' => 'Klona projektet', 'Column removed successfully.' => 'Kolumnen togs bort', - 'Github Issue' => 'Github fråga', 'Not enough data to show the graph.' => 'Inte tillräckligt med data för att visa graf', 'Previous' => 'Föregående', 'The id must be an integer' => 'ID måste vara ett heltal', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Din swimlane har skapats', 'Example: "Bug, Feature Request, Improvement"' => 'Exempel: "Bug, ny funktionalitet, förbättringar"', 'Default categories for new projects (Comma-separated)' => 'Standardkategorier för nya projekt (komma-separerade)', - 'Gitlab commit received' => 'Gitlab bidrag mottaget', - 'Gitlab issue opened' => 'Gitlab fråga öppnad', - 'Gitlab issue closed' => 'Gitlab fråga stängd', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Hjälp för Gitlab webhooks', 'Integrations' => 'Integrationer', 'Integration with third-party services' => 'Integration med tjänst från tredjepart', - 'Gitlab Issue' => 'Gitlab fråga', 'Subtask Id' => 'Deluppgifts-ID', 'Subtasks' => 'Deluppgift', 'Subtasks Export' => 'Export av deluppgifter', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Du har redan en deluppgift igång', 'Which parts of the project do you want to duplicate?' => 'Vilka delar av projektet vill du duplicera?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket bidrag mottaget', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Hjälp för Bitbucket webhooks', 'Start' => 'Start', 'End' => 'Slut', 'Task age in days' => 'Uppgiftsålder i dagar', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Tvåfaktorsverifieringskoden är giltig.', 'Code' => 'Kod', 'Two factor authentication' => 'Tvåfaktorsverifiering', - 'Enable/disable two factor authentication' => 'Aktivera/avaktivera tvåfaktorsverifiering', 'This QR code contains the key URI: ' => 'Denna QR-kod innehåller nyckel-URI:n', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Spara säkerhetsnyckeln i din TOTP mjukvara (med exempelvis Google Authenticator eller FreeOTP).', 'Check my code' => 'Kolla min kod', 'Secret key: ' => 'Säkerhetsnyckel:', 'Test your device' => 'Testa din enhet', @@ -762,21 +741,10 @@ return array( 'User that will receive the email' => 'Användare som kommer att ta emot mailet', 'Email subject' => 'E-post ämne', 'Date' => 'Datum', - 'By @%s on Bitbucket' => 'Av @%s på Bitbucket', - 'Bitbucket Issue' => 'Bitbucket fråga', - 'Commit made by @%s on Bitbucket' => 'Bidrag gjort av @%s på Bitbucket', - 'Commit made by @%s on Github' => 'Bidrag gjort av @%s på Github', - 'By @%s on Github' => 'Av @%s på Github', - 'Commit made by @%s on Gitlab' => 'Bidrag gjort av @%s på Gitlab', 'Add a comment log when moving the task between columns' => 'Lägg till en kommentarslogg när en uppgift flyttas mellan kolumner', 'Move the task to another column when the category is changed' => 'Flyttas uppgiften till en annan kolumn när kategorin ändras', 'Send a task by email to someone' => 'Skicka en uppgift med e-post till någon', 'Reopen a task' => 'Återöppna en uppgift', - 'Bitbucket issue opened' => 'Bitbucketfråga öppnad', - 'Bitbucket issue closed' => 'Bitbucketfråga stängd', - 'Bitbucket issue reopened' => 'Bitbucketfråga återöppnad', - 'Bitbucket issue assignee change' => 'Bitbucketfråga tilldelningsändring', - 'Bitbucket issue comment created' => 'Bitbucketfråga kommentar skapad', 'Column change' => 'Kolumnändring', 'Position change' => 'Positionsändring', 'Swimlane change' => 'Swimlaneändring', @@ -896,8 +864,6 @@ return array( 'Remote user' => 'Extern användare', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Externa användares lösenord lagras inte i Kanboard-databasen, exempel: LDAP, Google och Github-konton.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Om du aktiverar boxen "Tillåt inte loginformulär" kommer inloggningsuppgifter i formuläret att ignoreras.', - 'By @%s on Gitlab' => 'Av @%s på Gitlab', - 'Gitlab issue comment created' => 'Gitlab frågekommentar skapad', 'New remote user' => 'Ny extern användare', 'New local user' => 'Ny lokal användare', 'Default task color' => 'Standardfärg för uppgifter', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/th_TH/translations.php b/sources/app/Locale/th_TH/translations.php index 7660641..07733f2 100644 --- a/sources/app/Locale/th_TH/translations.php +++ b/sources/app/Locale/th_TH/translations.php @@ -437,12 +437,6 @@ return array( // '%s changed the assignee of the task %s to %s' => '', 'New password for the user "%s"' => 'รหัสผ่านใหม่สำหรับผู้ใช้ "%s"', 'Choose an event' => 'เลือกเหตุการณ์', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', // 'Create a task from an external provider' => '', // 'Change the assignee based on an external username' => '', // 'Change the category based on an external label' => '', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'ทุกคนสามารถเข้าถึงโปรเจคนี้', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', 'Project management' => 'การจัดการโปรเจค', 'My projects' => 'โปรเจคของฉัน', 'Columns' => 'คอลัมน์', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => 'การแบ่งงานของผู้ใช้ "%s"', 'Clone this project' => 'เลียนแบบโปรเจคนี้', 'Column removed successfully.' => 'ลบคอลัมน์สำเร็จ', - // 'Github Issue' => '', 'Not enough data to show the graph.' => 'ไม่มีข้อมูลแสดงเป็นกราฟ', 'Previous' => 'ก่อนหน้า', 'The id must be an integer' => 'ไอดีต้องเป็นตัวเลขจำนวนเต็ม', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'สวิมเลนของคุณถูกสร้างเรียบร้อยแล้ว', 'Example: "Bug, Feature Request, Improvement"' => 'ตัวอย่าง: "Bug, Feature Request, Improvement"', 'Default categories for new projects (Comma-separated)' => 'ค่าเริ่มต้นกลุ่มสำหรับโปรเจคใหม่ (Comma-separated)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'การใช้ร่วมกัน', 'Integration with third-party services' => 'การใช้งานร่วมกับบริการ third-party', - // 'Gitlab Issue' => '', 'Subtask Id' => 'รหัสงานย่อย', 'Subtasks' => 'งานย่อย', 'Subtasks Export' => 'ส่งออก งานย่อย', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'คุณมีหนึ่งงานย่อยที่กำลังทำงาน', // 'Which parts of the project do you want to duplicate?' => '', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', 'Start' => 'เริ่ม', 'End' => 'จบ', 'Task age in days' => 'อายุงาน', @@ -688,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', 'Code' => 'รหัส', // 'Two factor authentication' => '', - 'Enable/disable two factor authentication' => 'เปิด/ปิด การยืนยันตัวตนสองชั้น', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', 'Check my code' => 'ตรวจสอบรหัสของฉัน', 'Secret key: ' => 'กุญแจลับ', 'Test your device' => 'ทดสอบอุปกรณ์ของคุณ', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/tr_TR/translations.php b/sources/app/Locale/tr_TR/translations.php index 6d6f1dc..8fb2483 100644 --- a/sources/app/Locale/tr_TR/translations.php +++ b/sources/app/Locale/tr_TR/translations.php @@ -20,15 +20,15 @@ return array( 'Red' => 'Kırmızı', 'Orange' => 'Turuncu', 'Grey' => 'Gri', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => 'Kahverengi', + 'Deep Orange' => 'Koyu Turuncu', + 'Dark Grey' => 'Koyu Gri', + 'Pink' => 'Pembe', + 'Teal' => 'Turkuaz', + 'Cyan' => 'Cam Göbeği', + 'Lime' => 'Limon rengi', + 'Light Green' => 'Açık Yeşil', + 'Amber' => 'Koyu sarı', 'Save' => 'Kaydet', 'Login' => 'Giriş', 'Official website:' => 'Resmi internet sitesi:', @@ -86,7 +86,7 @@ return array( 'Application settings' => 'Uygulama ayarları', 'Language' => 'Dil', // 'Webhook token:' => '', - 'API token:' => 'API Token:', + 'API token:' => 'API belirteci:', 'Database size:' => 'Veritabanı boyutu :', 'Download the database' => 'Veritabanını indir', 'Optimize the database' => 'Veritabanını optimize et', @@ -98,11 +98,11 @@ return array( 'Color' => 'Renk', 'Assignee' => 'Atanan', 'Create another task' => 'Başka bir görev oluştur', - 'New task' => 'Nouvelle tâche', + 'New task' => 'Yeni görev', 'Open a task' => 'Bir görevi aç', 'Do you really want to open this task: "%s"?' => 'Bu görevi gerçekten açmak istiyor musunuz: "%s"?', 'Back to the board' => 'Tabloya dön', - // 'Created on %B %e, %Y at %k:%M %p' => '', + 'Created on %B %e, %Y at %k:%M %p' => '%B %e, %Y, saat %k:%M %p da oluşturuldu', 'There is nobody assigned' => 'Kimse atanmamış', 'Column on the board:' => 'Tablodaki sütun:', 'Status is open' => 'Açık durumda', @@ -158,18 +158,18 @@ return array( 'Work in progress' => 'İşlemde', 'Done' => 'Tamamlandı', 'Application version:' => 'Uygulama versiyonu:', - // 'Completed on %B %e, %Y at %k:%M %p' => '', - // '%B %e, %Y at %k:%M %p' => '', + 'Completed on %B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p da tamamlandı', + '%B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p', 'Date created' => 'Oluşturulma tarihi', 'Date completed' => 'Tamamlanma tarihi', 'Id' => 'Kod', '%d closed tasks' => '%d kapatılmış görevler', - // 'No task for this project' => '', + 'No task for this project' => 'Bu proje için görev yok', 'Public link' => 'Dışa açık link', 'Change assignee' => 'Atanmış Kullanıcıyı değiştir', 'Change assignee for the task "%s"' => '"%s" görevi için atanmış kullanıcıyı değiştir', 'Timezone' => 'Saat dilimi', - // 'Sorry, I didn\'t find this information in my database!' => '', + 'Sorry, I didn\'t find this information in my database!' => 'Üzgünüm, bu bilgiyi veri tabanımda bulamadım.', 'Page not found' => 'Sayfa bulunamadı', 'Complexity' => 'Zorluk seviyesi', 'Task limit' => 'Görev limiti', @@ -183,9 +183,9 @@ return array( 'Comment added successfully.' => 'Yorum eklendi', 'Unable to create your comment.' => 'Yorumunuz oluşturulamadı', 'Edit this task' => 'Bu görevi değiştir', - 'Due Date' => 'Termin', + 'Due Date' => 'Bitiş Tarihi', 'Invalid date' => 'Geçersiz tarihi', - // 'Must be done before %B %e, %Y' => '', + 'Must be done before %B %e, %Y' => '%B %e, %Y tarihinden önce yapılmalı', '%B %e, %Y' => '%d %B %Y', '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Otomatik işlemler', @@ -222,7 +222,7 @@ return array( 'Move Down' => 'Aşağı taşı', 'Duplicate to another project' => 'Başka bir projeye kopyala', 'Duplicate' => 'Kopya oluştur', - 'link' => 'link', + 'link' => 'bağlantı', 'Comment updated successfully.' => 'Yorum güncellendi.', 'Unable to update your comment.' => 'Yorum güncellenemedi.', 'Remove a comment' => 'Bir yorumu sil', @@ -232,7 +232,7 @@ return array( 'Only administrators or the creator of the comment can access to this page.' => 'Bu sayfaya yalnızca yorum sahibi ve yöneticiler erişebilir.', 'Current password for the user "%s"' => 'Kullanıcı için mevcut şifre "%s"', 'The current password is required' => 'Mevcut şifre gerekli', - 'Wrong password' => 'Yanlış Şifre', + 'Wrong password' => 'Yanlış şifre', 'Unknown' => 'Bilinmeyen', 'Last logins' => 'Son kullanıcı girişleri', 'Login date' => 'Giriş tarihi', @@ -240,7 +240,7 @@ return array( 'IP address' => 'IP adresi', 'User agent' => 'Kullanıcı sistemi', 'Persistent connections' => 'Kalıcı bağlantılar', - // 'No session.' => '', + 'No session.' => 'Oturum yok.', 'Expiration date' => 'Geçerlilik sonu', 'Remember Me' => 'Beni hatırla', 'Creation date' => 'Oluşturulma tarihi', @@ -248,41 +248,41 @@ return array( 'Open' => 'Açık', 'Closed' => 'Kapalı', 'Search' => 'Ara', - 'Nothing found.' => 'Hiçbir şey bulunamadı', - 'Due date' => 'Termin', + 'Nothing found.' => 'Hiçbir şey bulunamadı.', + 'Due date' => 'Bitiş tarihi', 'Others formats accepted: %s and %s' => 'Diğer kabul edilen formatlar: %s ve %s', 'Description' => 'Açıklama', - '%d comments' => '%d yorumlar', + '%d comments' => '%d yorum', '%d comment' => '%d yorum', - 'Email address invalid' => 'E-Posta adresi geçersiz', - // 'Your external account is not linked anymore to your profile.' => '', - // 'Unable to unlink your external account.' => '', - // 'External authentication failed' => '', - // 'Your external account is linked to your profile successfully.' => '', - 'Email' => 'E-Posta', + 'Email address invalid' => 'E-posta adresi geçersiz', + 'Your external account is not linked anymore to your profile.' => 'Harici hesabınız artık profilinize bağlı değil', + 'Unable to unlink your external account.' => 'Harici hesabınızla bağlantı koparılamadı', + 'External authentication failed' => 'Harici hesap doğrulaması başarısız', + 'Your external account is linked to your profile successfully.' => 'Harici hesabınız profilinizle başarıyla bağlandı.', + 'Email' => 'E-posta', 'Link my Google Account' => 'Google hesabımla bağ oluştur', 'Unlink my Google Account' => 'Google hesabımla bağı kaldır', 'Login with my Google Account' => 'Google hesabımla giriş yap', 'Project not found.' => 'Proje bulunamadı', - 'Task removed successfully.' => 'Görev silindi', - 'Unable to remove this task.' => 'Görev silinemiyor', + 'Task removed successfully.' => 'Görev başarıyla silindi.', + 'Unable to remove this task.' => 'Görev silinemiyor.', 'Remove a task' => 'Bir görevi sil', 'Do you really want to remove this task: "%s"?' => 'Bu görevi silmek istediğinize emin misiniz: "%s"?', 'Assign automatically a color based on a category' => 'Kategoriye göre otomatik renk ata', 'Assign automatically a category based on a color' => 'Rengine göre otomatik kategori ata', - 'Task creation or modification' => 'Görev oluşturma veya değiştirme', + 'Task creation or modification' => 'Görev oluşturma veya düzenleme', 'Category' => 'Kategori', 'Category:' => 'Kategori:', 'Categories' => 'Kategoriler', - 'Category not found.' => 'Kategori bulunamadı', - 'Your category have been created successfully.' => 'Kategori oluşturuldu', - 'Unable to create your category.' => 'Kategori oluşturulamadı', - 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi', - 'Unable to update your category.' => 'Kategori güncellenemedi', + 'Category not found.' => 'Kategori bulunamadı.', + 'Your category have been created successfully.' => 'Kategori başarıyla oluşturuldu.', + 'Unable to create your category.' => 'Kategori oluşturulamadı.', + 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi.', + 'Unable to update your category.' => 'Kategori güncellenemedi.', 'Remove a category' => 'Bir kategoriyi sil', - 'Category removed successfully.' => 'Kategori silindi', - 'Unable to remove this category.' => 'Bu kategori silinemedi', - 'Category modification for the project "%s"' => '"%s" projesi için kategori değiştirme', + 'Category removed successfully.' => 'Kategori başarıyla silindi.', + 'Unable to remove this category.' => 'Bu kategori silinemedi.', + 'Category modification for the project "%s"' => '"%s" projesi için kategori düzenleme', 'Category Name' => 'Kategori adı', 'Add a new category' => 'Yeni kategori ekle', 'Do you really want to remove this category: "%s"?' => 'Bu kategoriyi silmek istediğinize emin misiniz: "%s"?', @@ -310,26 +310,26 @@ return array( 'estimated' => 'tahmini', 'Sub-Tasks' => 'Alt Görev', 'Add a sub-task' => 'Alt görev ekle', - // 'Original estimate' => '', + 'Original estimate' => 'Orjinal tahmin', 'Create another sub-task' => 'Başka bir alt görev daha oluştur', - // 'Time spent' => '', + 'Time spent' => 'Harcanan zaman', 'Edit a sub-task' => 'Alt görev düzenle', 'Remove a sub-task' => 'Alt görev sil', - 'The time must be a numeric value' => 'Zaman alfanumerik bir değer olmalı', + 'The time must be a numeric value' => 'Zaman alfanümerik bir değer olmalı', 'Todo' => 'Yapılacaklar', - 'In progress' => 'İşlemde', - 'Sub-task removed successfully.' => 'Alt görev silindi', - 'Unable to remove this sub-task.' => 'Alt görev silinemedi', - 'Sub-task updated successfully.' => 'Alt görev güncellendi', - 'Unable to update your sub-task.' => 'Alt görev güncellenemiyor', - 'Unable to create your sub-task.' => 'Alt görev oluşturulamadı', - 'Sub-task added successfully.' => 'Alt görev başarıyla eklendii', + 'In progress' => 'Devam etmekte', + 'Sub-task removed successfully.' => 'Alt görev başarıyla silindi.', + 'Unable to remove this sub-task.' => 'Alt görev silinemedi.', + 'Sub-task updated successfully.' => 'Alt görev güncellendi.', + 'Unable to update your sub-task.' => 'Alt görev güncellenemiyor.', + 'Unable to create your sub-task.' => 'Alt görev oluşturulamadı.', + 'Sub-task added successfully.' => 'Alt görev başarıyla eklendi.', 'Maximum size: ' => 'Maksimum boyutu', - 'Unable to upload the file.' => 'Karşıya yükleme başarısız', + 'Unable to upload the file.' => 'Dosya yüklenemiyor.', 'Display another project' => 'Başka bir proje göster', - // 'Login with my Github Account' => '', - // 'Link my Github Account' => '', - // 'Unlink my Github Account' => '', + 'Login with my Github Account' => 'Github hesabımla giriş yap', + 'Link my Github Account' => 'Github hesabını ilişkilendir', + 'Unlink my Github Account' => 'Github hesabıyla bağlantıyı kopar', 'Created by %s' => '%s tarafından oluşturuldu', 'Last modified on %B %e, %Y at %k:%M %p' => 'Son değişiklik tarihi %d.%m.%Y, saati %H:%M', 'Tasks Export' => 'Görevleri dışa aktar', @@ -337,14 +337,14 @@ return array( 'Start Date' => 'Başlangıç tarihi', 'End Date' => 'Bitiş tarihi', 'Execute' => 'Gerçekleştir', - 'Task Id' => 'Görev No', + 'Task Id' => 'Görev Kimliği', 'Creator' => 'Oluşturan', 'Modification date' => 'Değişiklik tarihi', 'Completion date' => 'Tamamlanma tarihi', 'Clone' => 'Kopya oluştur', 'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.', - 'Unable to clone this project.' => 'Proje kopyası oluşturulamadı.', - 'Enable email notifications' => 'E-Posta bilgilendirmesini aç', + 'Unable to clone this project.' => 'Proje kopyası oluşturulamıyor.', + 'Enable email notifications' => 'E-posta bilgilendirmesini aç', 'Task position:' => 'Görev pozisyonu', 'The task #%d have been opened.' => '#%d numaralı görev açıldı.', 'The task #%d have been closed.' => '#%d numaralı görev kapatıldı.', @@ -365,7 +365,7 @@ return array( 'Task closed' => 'Görev kapatıldı', 'Task opened' => 'Görev açıldı', 'I want to receive notifications only for those projects:' => 'Yalnızca bu projelerle ilgili bildirim almak istiyorum:', - 'view the task on Kanboard' => 'bu görevi Kanboard\'da göster', + 'view the task on Kanboard' => 'bu görevi Kanboard üzerinde göster', 'Public access' => 'Dışa açık erişim', 'User management' => 'Kullanıcı yönetimi', 'Active tasks' => 'Aktif görevler', @@ -387,7 +387,7 @@ return array( 'Disabled' => 'Devre dışı bırakıldı', 'Username:' => 'Kullanıcı adı', 'Name:' => 'Ad', - 'Email:' => 'E-Posta', + 'Email:' => 'E-posta', 'Notifications:' => 'Bildirimler:', 'Notifications' => 'Bildirimler', 'Account type:' => 'Hesap türü:', @@ -399,10 +399,10 @@ return array( 'Github Account' => 'Github hesabı', 'Never connected.' => 'Hiç bağlanmamış.', 'No account linked.' => 'Bağlanmış hesap yok.', - 'Account linked.' => 'Hesap bağlandı', + 'Account linked.' => 'Hesap bağlandı.', 'No external authentication enabled.' => 'Dış kimlik doğrulama kapalı.', 'Password modified successfully.' => 'Şifre başarıyla değiştirildi.', - 'Unable to change the password.' => 'Şifre değiştirilemedi.', + 'Unable to change the password.' => 'Şifre değiştirilemiyor.', 'Change category for the task "%s"' => '"%s" görevi için kategori değiştirme', 'Change category' => 'Kategori değiştirme', '%s updated the task %s' => '%s kullanıcısı %s görevini güncelledi', @@ -437,27 +437,21 @@ return array( '%s changed the assignee of the task %s to %s' => '%s kullanıcısı %s görevinin sorumlusunu %s olarak değiştirdi', 'New password for the user "%s"' => '"%s" kullanıcısı için yeni şifre', 'Choose an event' => 'Bir durum seçin', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', 'Create a task from an external provider' => 'Dış sağlayıcı ile bir görev oluştur', 'Change the assignee based on an external username' => 'Dış kaynaklı kullanıcı adı ile göreve atananı değiştir', 'Change the category based on an external label' => 'Dış kaynaklı bir etiket ile kategori değiştir', 'Reference' => 'Referans', 'Reference: %s' => 'Referans: %s', 'Label' => 'Etiket', - 'Database' => 'Veri bankası', + 'Database' => 'Veritabanı', 'About' => 'Hakkında', - 'Database driver:' => 'Veri bankası sürücüsü', + 'Database driver:' => 'Veritabanı sürücüsü:', 'Board settings' => 'Tablo ayarları', - 'URL and token' => 'URL veya Token', + 'URL and token' => 'URL veya Belirteç', 'Webhook settings' => 'Webhook ayarları', 'URL for task creation:' => 'Görev oluşturma için URL', - 'Reset token' => 'Reset Token', - 'API endpoint:' => 'API endpoint', + 'Reset token' => 'Belirteci sıfırla', + 'API endpoint:' => 'API bitiş noktası:', 'Refresh interval for private board' => 'Özel tablolar için yenileme sıklığı', 'Refresh interval for public board' => 'Dışa açık tablolar için yenileme sıklığı', 'Task highlight period' => 'Görevi öne çıkarma süresi', @@ -466,7 +460,7 @@ return array( 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Saniye olarak frekans (Bu özelliği iptal etmek için 0, varsayılan değer 10 saniye)', 'Application URL' => 'Uygulama URL', 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Örneğin: http://example.kanboard.net/ (E-posta bildirimleri için kullanılıyor)', - 'Token regenerated.' => 'Token yeniden oluşturuldu.', + 'Token regenerated.' => 'Beliteç yeniden oluşturuldu.', 'Date format' => 'Tarih formatı', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"', 'New private project' => 'Yeni özel proje', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Bu projeye herkesin erişimi var.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github Webhook', - 'Help on Github webhooks' => 'Github Webhooks hakkında yardım', 'Create a comment from an external provider' => 'Dış sağlayıcı ile bir yorum oluştur', - 'Github issue comment created' => 'Github hata yorumu oluşturuldu', 'Project management' => 'Proje yönetimi', 'My projects' => 'Projelerim', 'Columns' => 'Sütunlar', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => '"%s" için kullanıcı dağılımı', 'Clone this project' => 'Projenin kopyasını oluştur', 'Column removed successfully.' => 'Sütun başarıyla kaldırıldı.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Grafik gösterimi için yeterli veri yok.', 'Previous' => 'Önceki', 'The id must be an integer' => 'ID bir tamsayı olmalı', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Kulvar başarıyla oluşturuldu.', 'Example: "Bug, Feature Request, Improvement"' => 'Örnek: "Sorun, Özellik talebi, İyileştirme"', 'Default categories for new projects (Comma-separated)' => 'Yeni projeler için varsayılan kategoriler (Virgül ile ayrılmış)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Entegrasyon', 'Integration with third-party services' => 'Dış servislerle entegrasyon', - // 'Gitlab Issue' => '', 'Subtask Id' => 'Alt görev No:', 'Subtasks' => 'Alt görevler', 'Subtasks Export' => 'Alt görevleri dışa aktar', @@ -587,10 +571,7 @@ return array( 'Time Tracking' => 'Zaman takibi', 'You already have one subtask in progress' => 'Zaten işlemde olan bir alt görev var', 'Which parts of the project do you want to duplicate?' => 'Projenin hangi kısımlarının kopyasını oluşturmak istiyorsunuz?', - // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit alındı', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Bitbucket webhooks için yardım', + 'Disallow login form' => 'Giriş formu erişimini engelle', 'Start' => 'Başlangıç', 'End' => 'Son', 'Task age in days' => 'Görev yaşı gün olarak', @@ -645,16 +626,16 @@ return array( 'Compact view' => 'Ekrana sığdır', 'Horizontal scrolling' => 'Geniş görünüm', 'Compact/wide view' => 'Ekrana sığdır / Geniş görünüm', - // 'No results match:' => '', - // 'Currency' => '', - // 'Files' => '', - // 'Images' => '', - // 'Private project' => '', + 'No results match:' => 'Uygun sonuç bulunamadı', + 'Currency' => 'Para birimi', + 'Files' => 'Dosyalar', + 'Images' => 'Resimler', + 'Private project' => 'Özel proje', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', // 'CHF - Swiss Francs' => '', // 'Custom Stylesheet' => '', - // 'download' => '', + 'download' => 'indir', // 'EUR - Euro' => '', // 'GBP - British Pound' => '', // 'INR - Indian Rupee' => '', @@ -662,441 +643,465 @@ return array( // 'NZD - New Zealand Dollar' => '', // 'RSD - Serbian dinar' => '', // 'USD - US Dollar' => '', - // 'Destination column' => '', - // 'Move the task to another column when assigned to a user' => '', - // 'Move the task to another column when assignee is cleared' => '', - // 'Source column' => '', - // 'Transitions' => '', - // 'Executer' => '', - // 'Time spent in the column' => '', - // 'Task transitions' => '', - // 'Task transitions export' => '', - // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', - // 'Currency rates' => '', - // 'Rate' => '', - // 'Change reference currency' => '', - // 'Add a new currency rate' => '', - // 'Reference currency' => '', - // 'The currency rate have been added successfully.' => '', - // 'Unable to add this currency rate.' => '', + 'Destination column' => 'Hedef Sütun', + 'Move the task to another column when assigned to a user' => 'Bir kullanıcıya atandığında görevi başka bir sütuna taşı', + 'Move the task to another column when assignee is cleared' => 'Atanmış kullanıcı kaldırıldığında görevi başka bir sütuna taşı', + 'Source column' => 'Kaynak sütun', + 'Transitions' => 'Geçişler', + 'Executer' => 'Uygulayıcı', + 'Time spent in the column' => 'Sütunda harcanan süre', + 'Task transitions' => 'Görev geçişleri', + 'Task transitions export' => 'Görev geçişlerini dışa aktar', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Bu rapor her bir görevin sütunlar arası geçişlerini tarih, kullanıcı ve sütunda harcanan zaman detaylarıyla içerir.', + 'Currency rates' => 'Döviz kurları', + 'Rate' => 'Kurlar', + 'Change reference currency' => 'Referans kur değiştir', + 'Add a new currency rate' => 'Yeni bir kur ekle', + 'Reference currency' => 'Referans kur', + 'The currency rate have been added successfully.' => 'Kur başarıyla eklendi', + 'Unable to add this currency rate.' => 'Bu kur eklenemedi', // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', - // 'Enable Gravatar images' => '', - // 'Information' => '', - // 'Check two factor authentication code' => '', - // 'The two factor authentication code is not valid.' => '', - // 'The two factor authentication code is valid.' => '', - // 'Code' => '', - // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', - // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', - // 'Check my code' => '', - // 'Secret key: ' => '', - // 'Test your device' => '', - // 'Assign a color when the task is moved to a specific column' => '', - // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', - // 'Burndown chart for "%s"' => '', - // 'Burndown chart' => '', - // 'This chart show the task complexity over the time (Work Remaining).' => '', - // 'Screenshot taken %s' => '', - // 'Add a screenshot' => '', - // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', - // 'Screenshot uploaded successfully.' => '', + '%s remove the assignee of the task %s' => '%s, %s görevinin atanan bilgisini kaldırdı', + 'Enable Gravatar images' => 'Gravatar resimlerini kullanıma aç', + 'Information' => 'Bilgi', + 'Check two factor authentication code' => 'İki kademeli doğrulama kodunu kontrol et', + 'The two factor authentication code is not valid.' => 'İki kademeli doğrulama kodu geçersiz', + 'The two factor authentication code is valid.' => 'İki kademeli doğrulama kodu onaylandı', + 'Code' => 'Kod', + 'Two factor authentication' => 'İki kademeli doğrulama', + 'This QR code contains the key URI: ' => 'Bu QR kodu anahtar URI içerir', + 'Check my code' => 'Kodu kontrol et', + 'Secret key: ' => 'Gizli anahtar', + 'Test your device' => 'Cihazınızı test edin', + 'Assign a color when the task is moved to a specific column' => 'Görev belirli bir sütuna taşındığında rengini değiştir', + '%s via Kanboard' => '%s Kanboard ile', + 'uploaded by: %s' => '%s tarafından yüklendi', + 'uploaded on: %s' => '%s tarihinda yüklendi', + 'size: %s' => 'Boyut: %s', + 'Burndown chart for "%s"' => '%s icin kalan iş grafiği', + 'Burndown chart' => 'Kalan iş grafiği', + 'This chart show the task complexity over the time (Work Remaining).' => 'Bu grafik zorluk seviyesini zamana göre gösterir (kalan iş)', + 'Screenshot taken %s' => 'Ekran görüntüsü alındı %s', + 'Add a screenshot' => 'Bir ekran görüntüsü ekle', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Bir ekran görüntüsü alın ve buraya yapıştırmak için CTRL+V veya ⌘+V tuşlarına basın.', + 'Screenshot uploaded successfully.' => 'Ekran görüntüsü başarıyla yüklendi', // 'SEK - Swedish Krona' => '', - // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', - // 'Identifier' => '', - // 'Disable two factor authentication' => '', - // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', - // 'Edit link' => '', - // 'Start to type task title...' => '', - // 'A task cannot be linked to itself' => '', - // 'The exact same link already exists' => '', - // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', - // 'Score' => '', - // 'The identifier must be unique' => '', - // 'This linked task id doesn\'t exists' => '', - // 'This value must be alphanumeric' => '', - // 'Edit recurrence' => '', - // 'Generate recurrent task' => '', - // 'Trigger to generate recurrent task' => '', - // 'Factor to calculate new due date' => '', - // 'Timeframe to calculate new due date' => '', - // 'Base date to calculate new due date' => '', - // 'Action date' => '', - // 'Base date to calculate new due date: ' => '', - // 'This task has created this child task: ' => '', - // 'Day(s)' => '', - // 'Existing due date' => '', - // 'Factor to calculate new due date: ' => '', - // 'Month(s)' => '', - // 'Recurrence' => '', - // 'This task has been created by: ' => '', - // 'Recurrent task has been generated:' => '', - // 'Timeframe to calculate new due date: ' => '', - // 'Trigger to generate recurrent task: ' => '', - // 'When task is closed' => '', - // 'When task is moved from first column' => '', - // 'When task is moved to last column' => '', - // 'Year(s)' => '', - // 'Calendar settings' => '', - // 'Project calendar view' => '', - // 'Project settings' => '', - // 'Show subtasks based on the time tracking' => '', - // 'Show tasks based on the creation date' => '', - // 'Show tasks based on the start date' => '', - // 'Subtasks time tracking' => '', - // 'User calendar view' => '', - // 'Automatically update the start date' => '', - // 'iCal feed' => '', - // 'Preferences' => '', - // 'Security' => '', - // 'Two factor authentication disabled' => '', - // 'Two factor authentication enabled' => '', - // 'Unable to update this user.' => '', - // 'There is no user management for private projects.' => '', - // 'User that will receive the email' => '', - // 'Email subject' => '', - // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', - // '[%s] Overdue tasks' => '', - // 'Notification' => '', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', - // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', - // 'New complexity: %d' => '', - // 'The due date have been removed' => '', - // 'There is no description anymore' => '', - // 'Recurrence settings have been modified' => '', - // 'Time spent changed: %sh' => '', - // 'Time estimated changed: %sh' => '', - // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', - // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', - // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', - // '<15m' => '', - // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', - // 'Reset filters' => '', - // 'My tasks due tomorrow' => '', - // 'Tasks due today' => '', - // 'Tasks due tomorrow' => '', - // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', - // 'Not assigned' => '', - // 'View advanced search syntax' => '', - // 'Overview' => '', - // '%b %e %Y' => '', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', - // 'There is no activity yet.' => '', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', - // 'Example of query: ' => '', - // 'Search by project: ' => '', - // 'Search by column: ' => '', - // 'Search by assignee: ' => '', - // 'Search by color: ' => '', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', - // 'Lead and Cycle time for "%s"' => '', - // 'Average time spent into each column for "%s"' => '', - // 'Average time spent into each column' => '', - // 'Average time spent' => '', - // 'This chart show the average time spent into each column for the last %d tasks.' => '', - // 'Average Lead and Cycle time' => '', - // 'Average lead time: ' => '', - // 'Average cycle time: ' => '', - // 'Cycle Time' => '', - // 'Lead Time' => '', - // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', - // 'Average time into each column' => '', - // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', - // 'Lead time: ' => '', - // 'Cycle time: ' => '', - // 'Time spent into each column' => '', - // 'The lead time is the duration between the task creation and the completion.' => '', - // 'The cycle time is the duration between the start date and the completion.' => '', - // 'If the task is not closed the current time is used instead of the completion date.' => '', - // 'Set automatically the start date' => '', - // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', - // 'Remote user' => '', - // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', - // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', - // 'New remote user' => '', - // 'New local user' => '', - // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', - // 'This feature does not work with all browsers.' => '', - // 'There is no destination project available.' => '', - // 'Trigger automatically subtask time tracking' => '', - // 'Include closed tasks in the cumulative flow diagram' => '', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', - // 'no category' => '', - // 'Current assignee: %s' => '', - // 'not assigned' => '', - // 'Author:' => '', - // 'contributors' => '', - // 'License:' => '', - // 'License' => '', - // 'Enter the text below' => '', - // 'Gantt chart for %s' => '', - // 'Sort by position' => '', - // 'Sort by date' => '', - // 'Add task' => '', - // 'Start date:' => '', - // 'Due date:' => '', - // 'There is no start date or due date for this task.' => '', - // 'Moving or resizing a task will change the start and due date of the task.' => '', - // 'There is no task in your project.' => '', - // 'Gantt chart' => '', - // 'People who are project managers' => '', - // 'People who are project members' => '', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Proje kimliği, projeyi tanımlamak için kullanılan opsiyonel bir alfanumerik koddur.', + 'Identifier' => 'Kimlik', + 'Disable two factor authentication' => 'İki kademeli doğrulamayı iptal et', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Bu kullanıcı için iki kademeli doğrulamayı iptal etmek istediğinize emin misiniz: "%s"?', + 'Edit link' => 'Linki düzenle', + 'Start to type task title...' => 'Görev başlığını yazmaya başlayın...', + 'A task cannot be linked to itself' => 'Bir görevden kendine link atanamaz', + 'The exact same link already exists' => 'Birebir aynı link zaten var', + 'Recurrent task is scheduled to be generated' => 'Tekrarlanan görev oluşturulması zamanlandı', + 'Recurring information' => 'Tekrarlanan bilgi', + 'Score' => 'Skor', + 'The identifier must be unique' => 'Kimlik özgün olmalı', + 'This linked task id doesn\'t exists' => 'Link oluşturulan görec kodu mevcut değil', + 'This value must be alphanumeric' => 'Bu değer alfanumerik olmalı', + 'Edit recurrence' => 'Tekrarlanmayı düzenle', + 'Generate recurrent task' => 'Tekrarlanan görev oluştur', + 'Trigger to generate recurrent task' => 'Tekrarlanan görev oluşturmak için tetikleyici', + 'Factor to calculate new due date' => 'Yeni tamamlanma tarihi için hesaplama faktörü', + 'Timeframe to calculate new due date' => 'Yeni tamamlanma tarihinin hesaplanması için zaman dilimi', + 'Base date to calculate new due date' => 'Yeni tamamlanma tarihinin hesaplanması için baz alınacak tarih', + 'Action date' => 'Hareket tarihi', + 'Base date to calculate new due date: ' => 'Yeni tamamlanma tarihinin hesaplanması için baz alınacak tarih', + 'This task has created this child task: ' => 'Bu görev şu alt görevi oluşturdu:', + 'Day(s)' => 'Gün(ler)', + 'Existing due date' => 'Mevcut tamamlanma tarihi', + 'Factor to calculate new due date: ' => 'Yeni tamamlanma tarihi için hesaplama faktörü', + 'Month(s)' => 'Ay(lar)', + 'Recurrence' => 'Tekrar', + 'This task has been created by: ' => 'Bu görev şunun tarafından oluşturuldu:', + 'Recurrent task has been generated:' => 'Tekrarlanan görev oluşturuldu:', + 'Timeframe to calculate new due date: ' => 'Yeni tamamlanma tarihinin hesaplanması için zaman dilimi', + 'Trigger to generate recurrent task: ' => 'Tekrarlanan görev oluşturmak için tetikleyici', + 'When task is closed' => 'Görev kapatıldığı zaman', + 'When task is moved from first column' => 'Görev ilk sütundan taşındığı zaman', + 'When task is moved to last column' => 'Görev son sütuna taşındığı zaman', + 'Year(s)' => 'Yıl(lar)', + 'Calendar settings' => 'Takvim ayarları', + 'Project calendar view' => 'Proje takvim görünümü', + 'Project settings' => 'Proje ayarları', + 'Show subtasks based on the time tracking' => 'Zaman takibi bazında alt görevleri göster', + 'Show tasks based on the creation date' => 'Oluşturulma zamanına göre görevleri göster', + 'Show tasks based on the start date' => 'Başlangıç zamanına göre görevleri göster', + 'Subtasks time tracking' => 'Alt görevler zaman takibi', + 'User calendar view' => 'Kullanıcı takvim görünümü', + 'Automatically update the start date' => 'Başlangıç tarihini otomatik olarak güncelle', + 'iCal feed' => 'iCal akışı', + 'Preferences' => 'Ayarlar', + 'Security' => 'Güvenlik', + 'Two factor authentication disabled' => 'İki kademeli doğrulamayı devre dışı bırak', + 'Two factor authentication enabled' => 'İki kademeli doğrulamayı etkinleştir', + 'Unable to update this user.' => 'Bu kullanıcı güncellenemiyor', + 'There is no user management for private projects.' => 'Özel projeler için kullanıcı yönetimi yoktur.', + 'User that will receive the email' => 'Email alacak kullanıcı', + 'Email subject' => 'Email başlığı', + 'Date' => 'Tarih', + 'Add a comment log when moving the task between columns' => 'Görevi sütunlar arasında taşırken yorum kaydı ekle', + 'Move the task to another column when the category is changed' => 'Kategori değiştirildiğinde görevi başka sütuna taşı', + 'Send a task by email to someone' => 'Bir görevi email ile birine gönder', + 'Reopen a task' => 'Bir görevi tekrar aç', + 'Column change' => 'Sütun değişikliği', + 'Position change' => 'Konum değişikliği', + 'Swimlane change' => 'Kulvar değişikliği', + 'Assignee change' => 'Atanan değişikliği', + '[%s] Overdue tasks' => '[%s] Gecikmiş görevler', + 'Notification' => 'Uyarılar', + '%s moved the task #%d to the first swimlane' => '%s, #%d görevini birinci kulvara taşıdı', + '%s moved the task #%d to the swimlane "%s"' => '%s, #%d görevini "%s" kulvarına taşıdı', + 'Swimlane' => 'Kulvar', + 'Gravatar' => 'Gravatar', + '%s moved the task %s to the first swimlane' => '%s, %s görevini ilk kulvara taşıdı', + '%s moved the task %s to the swimlane "%s"' => '%s, %s görevini "%s" kulvarına taşıdı', + 'This report contains all subtasks information for the given date range.' => 'Bu rapor belirtilen tarih aralığında tüm alt görev bilgilerini içerir.', + 'This report contains all tasks information for the given date range.' => 'Bu rapor belirtilen tarih aralığında tüm görev bilgilerini içerir.', + 'Project activities for %s' => '%s için proje aktiviteleri', + 'view the board on Kanboard' => 'Tabloyu Kanboard\'da görüntüle', + 'The task have been moved to the first swimlane' => 'Görev birinci kulvara taşındı', + 'The task have been moved to another swimlane:' => 'Görev başka bir kulvara taşındı:', + 'Overdue tasks for the project "%s"' => '"%s" projesi için gecikmiş görevler', + 'New title: %s' => 'Yeni başlık: %s', + 'The task is not assigned anymore' => 'Görev artık atanmamış', + 'New assignee: %s' => 'Yeni atanan: %s', + 'There is no category now' => 'Şu anda kategori yok', + 'New category: %s' => 'Yeni kategori: %s', + 'New color: %s' => 'Yeni renk: %s', + 'New complexity: %d' => 'Yeni zorluk seviyesi: %d', + 'The due date have been removed' => 'Tamamlanma tarihi silindi', + 'There is no description anymore' => 'Artık açıklama yok', + 'Recurrence settings have been modified' => 'Tekrarlanma ayarları değiştirildi', + 'Time spent changed: %sh' => 'Harcanan zaman değiştirildi: %sh', + 'Time estimated changed: %sh' => 'Tahmini süre değiştirildi: %sh', + 'The field "%s" have been updated' => '"%s" hanesi değiştirildi', + 'The description have been modified' => 'Açıklama değiştirildi', + 'Do you really want to close the task "%s" as well as all subtasks?' => '"%s" görevini ve tüm alt görevlerini kapatmak istediğinize emin misiniz?', + 'Swimlane: %s' => 'Kulvar: %s', + 'I want to receive notifications for:' => 'Bununla ilgili bildirimler almak istiyorum:', + 'All tasks' => 'Tüm görevler', + 'Only for tasks assigned to me' => 'Yalnızca bana atanmış görevler için', + 'Only for tasks created by me' => 'Yalnızca benim oluşturduğum görevler için', + 'Only for tasks created by me and assigned to me' => 'Yalnızca benim oluşturduğum ve bana atanmış görevler için', + '%A' => '%A', + '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', + 'New due date: %B %e, %Y' => 'Yeni tamamlanma tarihi: %B %e, %Y', + 'Start date changed: %B %e, %Y' => 'Başlangıç tarihi değiştirildi: %B %e, %Y', + '%k:%M %p' => '%k:%M %p', + '%%Y-%%m-%%d' => '%%Y-%%m-%%d', + 'Total for all columns' => 'Tüm sütunlar için toplam', + 'You need at least 2 days of data to show the chart.' => 'Grafiği göstermek için en az iki günlük veriye ihtiyaç var.', + '<15m' => '<15dk', + '<30m' => '<30dk', + 'Stop timer' => 'Zamanlayıcıyı durdur', + 'Start timer' => 'Zamanlayıcıyı başlat', + 'Add project member' => 'Proje üyesi ekle', + 'Enable notifications' => 'Bildirimleri etkinleştir', + 'My activity stream' => 'Olay akışım', + 'My calendar' => 'Takvimim', + 'Search tasks' => 'Görevleri ara', + 'Back to the calendar' => 'Takvime geri dön', + 'Filters' => 'Filtreler', + 'Reset filters' => 'Filtreleri sıfırla', + 'My tasks due tomorrow' => 'Yarına tamamlanması gereken görevlerim', + 'Tasks due today' => 'Bugün tamamlanması gereken görevler', + 'Tasks due tomorrow' => 'Yarına tamamlanması gereken görevler', + 'Tasks due yesterday' => 'Dün tamamlanmış olması gereken görevler', + 'Closed tasks' => 'Kapatılmış görevler', + 'Open tasks' => 'Açık görevler', + 'Not assigned' => 'Atanmamış', + 'View advanced search syntax' => 'Gelişmiş arama kodlarını göster', + 'Overview' => 'Genel bakış', + '%b %e %Y' => '%b %e %Y', + 'Board/Calendar/List view' => 'Tablo/Takvim/Liste görünümü', + 'Switch to the board view' => 'Tablo görünümüne geç', + 'Switch to the calendar view' => 'Takvim görünümüne geç', + 'Switch to the list view' => 'Liste görünümüne geç', + 'Go to the search/filter box' => 'Arama/Filtreleme kutusuna git', + 'There is no activity yet.' => 'Henüz bir aktivite yok.', + 'No tasks found.' => 'Hiç görev bulunamadı.', + 'Keyboard shortcut: "%s"' => 'Klavye kısayolu: "%s"', + 'List' => 'Liste', + 'Filter' => 'Filtre', + 'Advanced search' => 'Gelişmiş arama', + 'Example of query: ' => 'Sorgu örneği', + 'Search by project: ' => 'Projeye göre ara', + 'Search by column: ' => 'Sütuna göre ara', + 'Search by assignee: ' => 'Atanana göre ara', + 'Search by color: ' => 'Renge göre ara', + 'Search by category: ' => 'Kategoriye göre ara', + 'Search by description: ' => 'Açıklamaya göre ara', + 'Search by due date: ' => 'Tamamlanma tarihine göre ara', + 'Lead and Cycle time for "%s"' => '"%s" için teslim ve çevrim süresi', + 'Average time spent into each column for "%s"' => '"%s" için her bir sütunda geçirilen ortalama zaman', + 'Average time spent into each column' => 'Her bir sütunda geçirilen ortalama zaman', + 'Average time spent' => 'Harcanan ortalama zaman', + 'This chart show the average time spent into each column for the last %d tasks.' => 'Bu grafik son %d görev için her bir sütunda geçirilen ortalama zamanı gösterir.', + 'Average Lead and Cycle time' => 'Ortalama teslim ve çevrim süresi', + 'Average lead time: ' => 'Ortalama teslim süresi', + 'Average cycle time: ' => 'Ortalama çevrim süresi', + 'Cycle Time' => 'Çevrim süresi', + 'Lead Time' => 'Teslim süresi', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Bu grafik son %d görev için zaman içinde gerçekleşen ortalama teslim ve çevrim sürelerini gösterir.', + 'Average time into each column' => 'Her bir sütunda ortalama zaman', + 'Lead and cycle time' => 'Teslim ve çevrim süresi', + 'Google Authentication' => 'Google doğrulaması', + 'Help on Google authentication' => 'Google doğrulaması hakkında yardım', + 'Github Authentication' => 'Github doğrulaması', + 'Help on Github authentication' => 'Github doğrulaması hakkında yardım', + 'Lead time: ' => 'Teslim süresi: ', + 'Cycle time: ' => 'Çevrim süresi: ', + 'Time spent into each column' => 'Her sütunda harcanan zaman', + 'The lead time is the duration between the task creation and the completion.' => 'Teslim süresi, görevin oluşturulması ile tamamlanması arasında geçen süredir.', + 'The cycle time is the duration between the start date and the completion.' => 'Çevrim süresi, görevin başlangıç tarihi ile tamamlanması arasında geçen süredir.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Eğer görev henüz kapatılmamışsa, tamamlanma tarihi yerine şu anki tarih kullanılır.', + 'Set automatically the start date' => 'Başlangıç tarihini otomatik olarak belirle', + 'Edit Authentication' => 'Doğrulamayı düzenle', + 'Google Id' => 'Google kimliği', + 'Github Id' => 'Github Kimliği', + 'Remote user' => 'Uzak kullanıcı', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Uzak kullanıcıların şifreleri Kanboard veritabanında saklanmaz, örnek: LDAP, Google ve Github hesapları', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Eğer giriş formuna erişimi engelleyi seçerseniz, giriş formuna girilen bilgiler gözardı edilir.', + 'New remote user' => 'Yeni uzak kullanıcı', + 'New local user' => 'Yeni yerel kullanıcı', + 'Default task color' => 'Varsayılan görev rengi', + 'Hide sidebar' => 'Yan menüyü gizle', + 'Expand sidebar' => 'Yan menüyü genişlet', + 'This feature does not work with all browsers.' => 'Bu özellik tüm tarayıcılarla çalışmaz', + 'There is no destination project available.' => 'Seçilebilecek hedef proje yok.', + 'Trigger automatically subtask time tracking' => 'Altgörev zamanlayıcıyı otomatik olarak başlat', + 'Include closed tasks in the cumulative flow diagram' => 'Kümülatif akış diyagramında kapatılmış görevleri de dahil et', + 'Current swimlane: %s' => 'Geçerli kulvar: %s', + 'Current column: %s' => 'Geçerli sütun: %s', + 'Current category: %s' => 'Geçerli kategori: %s', + 'no category' => 'kategori yok', + 'Current assignee: %s' => 'Geçerli atanan: %s', + 'not assigned' => 'atanmamış', + 'Author:' => 'Yazar', + 'contributors' => 'Katkı sağlayanlar', + 'License:' => 'Lisans:', + 'License' => 'Lisans', + 'Enter the text below' => 'Aşağıdaki metni girin', + 'Gantt chart for %s' => '%s için Gantt diyagramı', + 'Sort by position' => 'Pozisyona göre sırala', + 'Sort by date' => 'Tarihe göre sırala', + 'Add task' => 'Görev ekle', + 'Start date:' => 'Başlangıç tarihi:', + 'Due date:' => 'Tamamlanması gereken tarih:', + 'There is no start date or due date for this task.' => 'Bu görev için başlangıç veya tamamlanması gereken tarih yok.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Bir görevin boyutunu değiştirmek, görevin başlangıç ve tamamlanması gereken tarihlerini değiştirir.', + 'There is no task in your project.' => 'Projenizde hiç görev yok.', + 'Gantt chart' => 'Gantt diyagramı', + 'People who are project managers' => 'Proje yöneticisi olan kişiler', + 'People who are project members' => 'Proje üyesi olan kişiler', // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', - // 'End date' => '', - // 'Users overview' => '', - // 'Managers' => '', - // 'Members' => '', - // 'Shared project' => '', - // 'Project managers' => '', - // 'Gantt chart for all projects' => '', - // 'Projects list' => '', - // 'Gantt chart for this project' => '', - // 'Project board' => '', - // 'End date:' => '', - // 'There is no start date or end date for this project.' => '', - // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', - // 'Link type' => '', - // 'Change task color when using a specific task link' => '', - // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', - // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', - // 'Documentation: %s' => '', - // 'Switch to the Gantt chart view' => '', - // 'Reset the search/filter box' => '', - // 'Documentation' => '', - // 'Table of contents' => '', - // 'Gantt' => '', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', - // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', - // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'There is no notification method registered.' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', - // 'Project members' => '', + 'Show this column' => 'Bu sütunu göster', + 'Hide this column' => 'Bu sütunu gizle', + 'open file' => 'dosyayı aç', + 'End date' => 'Bitiş tarihi', + 'Users overview' => 'Kullanıcılara genel bakış', + 'Managers' => 'Yöneticiler', + 'Members' => 'Üyeler', + 'Shared project' => 'Paylaşılan proje', + 'Project managers' => 'Proje yöneticileri', + 'Gantt chart for all projects' => 'Tüm projeler için Gantt diyagramı', + 'Projects list' => 'Proje listesi', + 'Gantt chart for this project' => 'Bu proje için Gantt diyagramı', + 'Project board' => 'Proje tablosu', + 'End date:' => 'Bitiş tarihi:', + 'There is no start date or end date for this project.' => 'Bu proje için başlangıç veya bitiş tarihi yok.', + 'Projects Gantt chart' => 'Projeler Gantt diyagramı', + 'Start date: %s' => 'Başlangıç tarihi: %s', + 'End date: %s' => 'Bitiş tarihi: %s', + 'Link type' => 'Bağlantı türü', + 'Change task color when using a specific task link' => 'Belirli bir görev bağlantısı kullanıldığında görevin rengini değiştir', + 'Task link creation or modification' => 'Görev bağlantısı oluşturulması veya değiştirilmesi', + 'Login with my Gitlab Account' => 'Gitlab hesabımla giriş yap', + 'Milestone' => 'Kilometre taşı', + 'Gitlab Authentication' => 'Gitlab doğrulaması', + 'Help on Gitlab authentication' => 'Gitlab doğrulaması hakkında yardım', + 'Gitlab Id' => 'Gitlab kimliği', + 'Gitlab Account' => 'Gitlab hesabı', + 'Link my Gitlab Account' => 'Gitlab hesabını ilişkilendir', + 'Unlink my Gitlab Account' => 'Gitlab hesabımla bağlantıyı kopar', + 'Documentation: %s' => 'Dokümantasyon: %s', + 'Switch to the Gantt chart view' => 'Gantt diyagramı görünümüne geç', + 'Reset the search/filter box' => 'Arama/Filtre kutusunu sıfırla', + 'Documentation' => 'Dokümantasyon', + 'Table of contents' => 'İçindekiler', + 'Gantt' => 'Gantt', + 'Author' => 'Yazar', + 'Version' => 'Versiyon', + 'Plugins' => 'Eklentiler', + 'There is no plugin loaded.' => 'Yüklenmiş bir eklendi yok', + 'Set maximum column height' => 'Maksimum sütun yüksekliğini belirle', + 'Remove maximum column height' => 'Maksimum sütun yüksekliğini iptal et', + 'My notifications' => 'Bildirimlerim', + 'Custom filters' => 'Özel filtreler', + 'Your custom filter have been created successfully.' => 'Özel filtreleriniz başarıyla oluşturuldu.', + 'Unable to create your custom filter.' => 'Özel filtreniz oluşturulamadı.', + 'Custom filter removed successfully.' => 'Özel filtreniz başarıyla silindi.', + 'Unable to remove this custom filter.' => 'Bu özel filtre silinemiyor.', + 'Edit custom filter' => 'Özel filtreyi düzenle', + 'Your custom filter have been updated successfully.' => 'Özel filtreleriniz başarıyla güncellendi.', + 'Unable to update custom filter.' => 'Özel filtre güncellenemiyor.', + 'Web' => 'İnternet', + 'New attachment on task #%d: %s' => '#%d görevinde yeni dosya: %s', + 'New comment on task #%d' => '#%d görevinde yeni yorum', + 'Comment updated on task #%d' => '#%d görevinde yorum güncellendi', + 'New subtask on task #%d' => '#%d görevinde yeni alt görev', + 'Subtask updated on task #%d' => '#%d görevinde alt görev güncellendi', + 'New task #%d: %s' => 'Yeni #%d görevi: %s', + 'Task updated #%d' => '#%d görevi güncellendi', + 'Task #%d closed' => '#%d görevi kapatıldı', + 'Task #%d opened' => '#%d görevi oluşturuldu', + 'Column changed for task #%d' => '#%d görevinin sütunu değişti', + 'New position for task #%d' => '#%d görevinin konumu değişti', + 'Swimlane changed for task #%d' => '#%d görevinin kulvarı değişti', + 'Assignee changed on task #%d' => '#%d görevine atanan değişti', + '%d overdue tasks' => '%d gecikmiş görev', + 'Task #%d is overdue' => '#%d görevi gecikti', + 'No new notifications.' => 'Yeni bildirim yok.', + 'Mark all as read' => 'Tümünü okunmuş olarak işaretle', + 'Mark as read' => 'Okunmuş olarak işaretle', + 'Total number of tasks in this column across all swimlanes' => 'Bu sutündaki görev sayısının tüm kulvarlardaki toplamı', + 'Collapse swimlane' => 'Kulvarı daralt', + 'Expand swimlane' => 'Kulvarı genişlet', + 'Add a new filter' => 'Yeni bir filtre ekle', + 'Share with all project members' => 'Tüm proje üyeleriyle paylaş', + 'Shared' => 'Paylaşılan', + 'Owner' => 'Sahibi', + 'Unread notifications' => 'Okunmamış bildirimler', + 'My filters' => 'Filtrelerim', + 'Notification methods:' => 'Bildirim yöntemleri:', + 'Import tasks from CSV file' => 'CSV dosyasından görevleri içeri aktar', + 'Unable to read your file' => 'Dosya okunamıyor', + '%d task(s) have been imported successfully.' => '%d görev başarıyla içeri aktarıldı.', + 'Nothing have been imported!' => 'Hiçbir şey içeri aktarılamadı!', + 'Import users from CSV file' => 'CSV dosyasından kullanıcıları içeri aktar', + '%d user(s) have been imported successfully.' => '%d kullanıcı başarıyla içeri aktarıldı.', + 'Comma' => 'Virgül', + 'Semi-colon' => 'Noktalı virgül', + 'Tab' => 'Tab', + 'Vertical bar' => 'Dikey çizgi', + 'Double Quote' => 'Tırnak işareti', + 'Single Quote' => 'Kesme işareti', + '%s attached a file to the task #%d' => '%s, #%d görevine bir dosya ekledi.', + 'There is no column or swimlane activated in your project!' => 'Projenizde etkinleştirilmiş hiç bir sütun veya kulvar yok!', + 'Append filter (instead of replacement)' => 'Fıltreye ekle (üzerine yazmak yerine)', + 'Append/Replace' => 'Ekle/Üzerine yaz', + 'Append' => 'Ekle', + 'Replace' => 'Üzerine yaz', + 'Import' => 'İçeri aktar', + 'change sorting' => 'sıralamayı değiştir', + 'Tasks Importation' => 'Görevleri içeri aktar', + 'Delimiter' => 'Ayırıcı', + 'Enclosure' => 'Enclosure', + 'CSV File' => 'CSV Dosyası', + 'Instructions' => 'Yönergeler', + 'Your file must use the predefined CSV format' => 'Dosyanız önceden belirlenmiş CSV formatını kullanmalı', + 'Your file must be encoded in UTF-8' => 'Dosyanız UTF-8 kodlamasında olmalı', + 'The first row must be the header' => 'İlk satır başlık olmalı', + 'Duplicates are not verified for you' => 'Çift girişler sizin için onaylanmamış', + 'The due date must use the ISO format: YYYY-MM-DD' => 'Tamamlanma tarihi ISO formatını kullanmalı: YYYY-MM-DD', + 'Download CSV template' => 'CSV taslağını indir', + 'No external integration registered.' => 'Hiç dış entegrasyon kaydedilmemiş.', + 'Duplicates are not imported' => 'Çift girişler içeri aktarılmaz', + 'Usernames must be lowercase and unique' => 'Kullanıcı adları küçük harf ve tekil olmalı', + 'Passwords will be encrypted if present' => 'Şifreler (eğer varsa) kriptolanır', + '%s attached a new file to the task %s' => '%s, %s görevine yeni dosya ekledi', + 'Assign automatically a category based on a link' => 'Bir bağlantıya göre otomatik olarak kategori ata', + 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark', + 'Assignee Username' => 'Atanan kullanıcı adı', + 'Assignee Name' => 'Atanan İsmi', + 'Groups' => 'Gruplar', + 'Members of %s' => '%s in üyeleri', + 'New group' => 'Yeni grup', + 'Group created successfully.' => 'Grup başarıyla oluşturuldu.', + 'Unable to create your group.' => 'Grup oluşturulamadı.', + 'Edit group' => 'Grubu düzenle', + 'Group updated successfully.' => 'Grup başarıyla güncellendi.', + 'Unable to update your group.' => 'Grup güncellenemedi.', + 'Add group member to "%s"' => '"%s" e grup üyesi ekle', + 'Group member added successfully.' => 'Grup üyesi başarıyla eklendi.', + 'Unable to add group member.' => 'Grup üyesi eklenemedi.', + 'Remove user from group "%s"' => '"%s" grubundan kullanıcı çıkar', + 'User removed successfully from this group.' => 'Kullanıcı bu gruptan başarıyla çıkarıldı.', + 'Unable to remove this user from the group.' => 'Bu kullanıcı bu grubtan çıkarılamadı', + 'Remove group' => 'Grubu sil', + 'Group removed successfully.' => 'Grup başarıyla silindi.', + 'Unable to remove this group.' => 'Grup silinemedi.', + 'Project Permissions' => 'Proje izimleri', + 'Manager' => 'Yönetici', + 'Project Manager' => 'Proje yöneticisi', + 'Project Member' => 'Proje üyesi', + 'Project Viewer' => 'Proje izleyicisi', + 'Your account is locked for %d minutes' => 'Hesabınız %d dakika boyunca kilitlendi', + 'Invalid captcha' => 'Geçersiz captcha', + 'The name must be unique' => 'İsim tekil olmalı', + 'View all groups' => 'Tüm grupları görüntüle', + 'View group members' => 'Grup üyelerini görüntüle', + 'There is no user available.' => 'Uygun üye yok', + 'Do you really want to remove the user "%s" from the group "%s"?' => '"%s" kullanıcısını "%s" grubundan çıkarmak istediğinize emin misiniz?', + 'There is no group.' => 'Hiç grup yok.', + 'External Id' => 'Harici Kimlik', + 'Add group member' => 'Grup üyesi ekle', + 'Do you really want to remove this group: "%s"?' => '"%s" grubunu silmek istediğinize emin misiniz?', + 'There is no user in this group.' => 'Bu grupta hiç kullanıcı yok.', + 'Remove this user' => 'Bu kullanıcıyı sil', + 'Permissions' => 'İzinler', + 'Allowed Users' => 'İzin verilen kullanıcı', + 'No user have been allowed specifically.' => 'Hiç bir kullanıcıya özel olarak izin verilmemiş.', + 'Role' => 'Rol', + 'Enter user name...' => 'Kullanıcı adını girin...', + 'Allowed Groups' => 'İzinli gruplar', + 'No group have been allowed specifically.' => 'Hiç bir gruba özel olarak izin verilmemiş', + 'Group' => 'Grup', + 'Group Name' => 'Grup adı', + 'Enter group name...' => 'Grup adını girin...', + 'Role:' => 'Rol:', + 'Project members' => 'Proje üyeleri', + 'Compare hours for "%s"' => '"%s" için saatleri karşılaştır', + '%s mentioned you in the task #%d' => '%s sizden #%d görevinde bahsetti', + '%s mentioned you in a comment on the task #%d' => '%s sizden #%d görevindeki bir yorumda bahsetti', + 'You were mentioned in the task #%d' => '#%d görevinde sizden bahsedildi', + 'You were mentioned in a comment on the task #%d' => '#%d görevindeki bir yorumda sizden bahsedildi', + 'Mentioned' => 'Bahsedilmiş', + 'Compare Estimated Time vs Actual Time' => 'Tahmini süre ile gerçekleşen süreyi karşılaştır', + 'Estimated hours: ' => 'Tahmini saat:', + 'Actual hours: ' => 'Gerçekleşen saat:', + 'Hours Spent' => 'Harcanan saat', + 'Hours Estimated' => 'Tahmini saat', + 'Estimated Time' => 'Tahmini süre', + 'Actual Time' => 'Gerçekleşen süre', + 'Estimated vs actual time' => 'Tahmini vs gerçekleşen süre', + // 'RUB - Russian Ruble' => '', + 'Assign the task to the person who does the action when the column is changed' => 'Sütun değiştirildiği zaman görevi eylemi gerçekleştiren kişiye ata', + 'Close a task in a specific column' => 'Belirli bir sütundaki görevi kapat', + 'Time-based One-time Password Algorithm' => 'Zamana bağlı tek kullanımlık şifre algoritması', + 'Two-Factor Provider: ' => 'Çift kademeli doğrulama sağlayıcısı', + 'Disable two-factor authentication' => 'Çift kademeli doğrulamayı devre dışı bırak', + 'Enable two-factor authentication' => 'Çift kademeli doğrulamayı etkinleştir', + 'There is no integration registered at the moment.' => 'Şu anda kayıtlı bir entegrasyon bulunmuyor.', + 'Password Reset for Kanboard' => 'Kanboard için şifre sıfırlama', + 'Forgot password?' => 'Şifrenizi mi unuttunuz?', + 'Enable "Forget Password"' => '"Şifremi unuttum" komutunu etkinleştir', + 'Password Reset' => 'Şifre sıfırlama', + 'New password' => 'Yeni şifre', + 'Change Password' => 'Şifreyi değiştir', + 'To reset your password click on this link:' => 'Şifrenizi sıfırlamak için bu linke tıklayın:', + 'Last Password Reset' => 'Son şifre sıfırlama', + 'The password has never been reinitialized.' => 'Şifre hiç bir zaman tekrar başlatılmamış.', + 'Creation' => 'Oluşturulma', + 'Expiration' => 'Sona erme', + 'Password reset history' => 'Şifre sıfırlama geçmişi', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '"%s" sütunu ve "%s" kulvarındaki tüm görevler başarıyla kapatıldı.', + 'Do you really want to close all tasks of this column?' => 'Bu sütundaki tüm görevleri kapatmak istediğinize emin misiniz?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '"%s" sütunu ve "%s" kulvarındaki %d görev kapatılacak.', + 'Close all tasks of this column' => 'Bu sütundaki tüm görevleri kapat', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/zh_CN/translations.php b/sources/app/Locale/zh_CN/translations.php index 0355b1f..c591fa6 100644 --- a/sources/app/Locale/zh_CN/translations.php +++ b/sources/app/Locale/zh_CN/translations.php @@ -437,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s 将任务 %s 分配给 %s', 'New password for the user "%s"' => '用户"%s"的新密码', 'Choose an event' => '选择一个事件', - 'Github commit received' => '收到了Github提交', - 'Github issue opened' => '开启了Github问题报告', - 'Github issue closed' => '关闭了Github问题报告', - 'Github issue reopened' => '重新开启Github问题', - 'Github issue assignee change' => 'Github问题负责人已经变更', - 'Github issue label change' => 'Github任务标签修改', 'Create a task from an external provider' => '从外部创建任务', 'Change the assignee based on an external username' => '根据外部用户名修改任务分配', 'Change the category based on an external label' => '根据外部标签修改分类', @@ -487,10 +481,7 @@ return array( 'Everybody have access to this project.' => '所有人都可以访问此项目', 'Webhooks' => '网络钩子', 'API' => '应用程序接口', - 'Github webhooks' => 'Github 网络钩子', - 'Help on Github webhooks' => 'Github 网络钩子帮助', 'Create a comment from an external provider' => '从外部创建一个评论', - 'Github issue comment created' => '已经创建了Github问题评论', 'Project management' => '项目管理', 'My projects' => '我的项目', 'Columns' => '栏目', @@ -508,7 +499,6 @@ return array( 'User repartition for "%s"' => '"%s"的用户分析', 'Clone this project' => '复制此项目', 'Column removed successfully.' => '成功删除了栏目。', - 'Github Issue' => 'Github 任务报告', 'Not enough data to show the graph.' => '数据不足,无法绘图。', 'Previous' => '后退', 'The id must be an integer' => '编号必须为整数', @@ -553,14 +543,8 @@ return array( 'Your swimlane have been created successfully.' => '已经成功创建泳道。', 'Example: "Bug, Feature Request, Improvement"' => '示例:“缺陷,功能需求,提升', 'Default categories for new projects (Comma-separated)' => '新项目的默认分类(用逗号分隔)', - 'Gitlab commit received' => '收到 Gitlab 提交', - 'Gitlab issue opened' => '开启 Gitlab 问题', - 'Gitlab issue closed' => '关闭 Gitlab 问题', - 'Gitlab webhooks' => 'Gitlab 网络钩子', - 'Help on Gitlab webhooks' => 'Gitlab 网络钩子帮助', 'Integrations' => '整合', 'Integration with third-party services' => '与第三方服务进行整合', - 'Gitlab Issue' => 'Gitlab 问题', 'Subtask Id' => '子任务 Id', 'Subtasks' => '子任务', 'Subtasks Export' => '子任务导出', @@ -588,9 +572,6 @@ return array( 'You already have one subtask in progress' => '你已经有了一个进行中的子任务', 'Which parts of the project do you want to duplicate?' => '要复制项目的哪些内容?', // 'Disallow login form' => '', - 'Bitbucket commit received' => '收到Bitbucket提交', - 'Bitbucket webhooks' => 'Bitbucket网络钩子', - 'Help on Bitbucket webhooks' => 'Bitbucket网络钩子帮助', 'Start' => '开始', 'End' => '结束', 'Task age in days' => '任务存在天数', @@ -688,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => '双重认证码正确。', 'Code' => '认证码', 'Two factor authentication' => '双重认证', - 'Enable/disable two factor authentication' => '启用/禁用双重认证', 'This QR code contains the key URI: ' => '此二维码包含密码 URI:', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '将密码保存到 TOTP 软件(例如Google 认证或 FreeOTP)', 'Check my code' => '检查我的认证码', 'Secret key: ' => '密码:', 'Test your device' => '测试设备', @@ -762,21 +741,10 @@ return array( // 'User that will receive the email' => '', 'Email subject' => '邮件主题', 'Date' => '日期', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', 'Reopen a task' => '重新开始一个任务', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -896,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', 'Default task color' => '默认任务颜色', @@ -1028,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1073,7 +1038,6 @@ return array( // 'Project Manager' => '', // 'Project Member' => '', // 'Project Viewer' => '', - // 'Gitlab issue reopened' => '', // 'Your account is locked for %d minutes' => '', // 'Invalid captcha' => '', // 'The name must be unique' => '', @@ -1099,4 +1063,45 @@ return array( // 'Enter group name...' => '', // 'Role:' => '', // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Model/Action.php b/sources/app/Model/Action.php index 289471f..4da2fb8 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/Action.php @@ -2,14 +2,8 @@ namespace Kanboard\Model; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** - * Action model + * Action Model * * @package model * @author Frederic Guillot @@ -24,154 +18,38 @@ class Action extends Base const TABLE = 'actions'; /** - * SQL table name for action parameters - * - * @var string - */ - const TABLE_PARAMS = 'action_has_params'; - - /** - * Extended actions - * - * @access private - * @var array - */ - private $actions = array(); - - /** - * Extend the list of default actions - * - * @access public - * @param string $className - * @param string $description - * @return Action - */ - public function extendActions($className, $description) - { - $this->actions[$className] = $description; - return $this; - } - - /** - * Return the name and description of available actions + * Return actions and parameters for a given user * * @access public + * @param integer $user_id * @return array */ - public function getAvailableActions() + public function getAllByUser($user_id) { - $values = array( - 'TaskClose' => t('Close a task'), - 'TaskOpen' => t('Open a task'), - 'TaskAssignSpecificUser' => t('Assign the task to a specific user'), - 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), - 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), - 'TaskMoveAnotherProject' => t('Move the task to another project'), - 'TaskMoveColumnAssigned' => t('Move the task to another column when assigned to a user'), - 'TaskMoveColumnUnAssigned' => t('Move the task to another column when assignee is cleared'), - 'TaskAssignColorColumn' => t('Assign a color when the task is moved to a specific column'), - 'TaskAssignColorUser' => t('Assign a color to a specific user'), - 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), - 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), - 'TaskAssignCategoryLink' => t('Assign automatically a category based on a link'), - 'CommentCreation' => t('Create a comment from an external provider'), - 'TaskCreation' => t('Create a task from an external provider'), - 'TaskLogMoveAnotherColumn' => t('Add a comment log when moving the task between columns'), - 'TaskAssignUser' => t('Change the assignee based on an external username'), - 'TaskAssignCategoryLabel' => t('Change the category based on an external label'), - 'TaskUpdateStartDate' => t('Automatically update the start date'), - 'TaskMoveColumnCategoryChange' => t('Move the task to another column when the category is changed'), - 'TaskEmail' => t('Send a task by email to someone'), - 'TaskAssignColorLink' => t('Change task color when using a specific task link'), - ); + $project_ids = $this->projectPermission->getActiveProjectIds($user_id); + $actions = array(); - $values = array_merge($values, $this->actions); - - asort($values); - - return $values; - } - - /** - * Return the name and description of available actions - * - * @access public - * @return array - */ - public function getAvailableEvents() - { - $values = array( - TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'), - Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), - Task::EVENT_UPDATE => t('Task modification'), - Task::EVENT_CREATE => t('Task creation'), - Task::EVENT_OPEN => t('Reopen a task'), - Task::EVENT_CLOSE => t('Closing a task'), - Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), - Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), - GithubWebhook::EVENT_COMMIT => t('Github commit received'), - GithubWebhook::EVENT_ISSUE_OPENED => t('Github issue opened'), - GithubWebhook::EVENT_ISSUE_CLOSED => t('Github issue closed'), - GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'), - GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'), - GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'), - GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'), - GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'), - GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'), - GitlabWebhook::EVENT_ISSUE_REOPENED => t('Gitlab issue reopened'), - GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), - GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'), - BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'), - BitbucketWebhook::EVENT_ISSUE_OPENED => t('Bitbucket issue opened'), - BitbucketWebhook::EVENT_ISSUE_CLOSED => t('Bitbucket issue closed'), - BitbucketWebhook::EVENT_ISSUE_REOPENED => t('Bitbucket issue reopened'), - BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Bitbucket issue assignee change'), - BitbucketWebhook::EVENT_ISSUE_COMMENT => t('Bitbucket issue comment created'), - ); - - asort($values); - - return $values; - } - - /** - * Return the name and description of compatible actions - * - * @access public - * @param string $action_name Action name - * @return array - */ - public function getCompatibleEvents($action_name) - { - $action = $this->load($action_name, 0, ''); - $compatible_events = $action->getCompatibleEvents(); - $events = array(); - - foreach ($this->getAvailableEvents() as $event_name => $event_description) { - if (in_array($event_name, $compatible_events)) { - $events[$event_name] = $event_description; - } + if (! empty($project_ids)) { + $actions = $this->db->table(self::TABLE)->in('project_id', $project_ids)->findAll(); + $params = $this->actionParameter->getAllByActions(array_column($actions, 'id')); + $this->attachParamsToActions($actions, $params); } - return $events; + return $actions; } /** * Return actions and parameters for a given project * * @access public - * @param $project_id + * @param integer $project_id * @return array */ public function getAllByProject($project_id) { $actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll(); - - foreach ($actions as &$action) { - $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); - } - - return $actions; + $params = $this->actionParameter->getAllByActions(array_column($actions, 'id')); + return $this->attachParamsToActions($actions, $params); } /** @@ -183,63 +61,51 @@ class Action extends Base public function getAll() { $actions = $this->db->table(self::TABLE)->findAll(); - $params = $this->db->table(self::TABLE_PARAMS)->findAll(); - - foreach ($actions as &$action) { - $action['params'] = array(); - - foreach ($params as $param) { - if ($param['action_id'] === $action['id']) { - $action['params'][] = $param; - } - } - } - - return $actions; - } - - /** - * Get all required action parameters for all registered actions - * - * @access public - * @return array All required parameters for all actions - */ - public function getAllActionParameters() - { - $params = array(); - - foreach ($this->getAll() as $action) { - $action = $this->load($action['action_name'], $action['project_id'], $action['event_name']); - $params += $action->getActionRequiredParameters(); - } - - return $params; + $params = $this->actionParameter->getAll(); + return $this->attachParamsToActions($actions, $params); } /** * Fetch an action * * @access public - * @param integer $action_id Action id - * @return array Action data + * @param integer $action_id + * @return array */ public function getById($action_id) { $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne(); if (! empty($action)) { - $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll(); + $action['params'] = $this->actionParameter->getAllByAction($action_id); } return $action; } + /** + * Attach parameters to actions + * + * @access private + * @param array &$actions + * @param array &$params + * @return array + */ + private function attachParamsToActions(array &$actions, array &$params) + { + foreach ($actions as &$action) { + $action['params'] = isset($params[$action['id']]) ? $params[$action['id']] : array(); + } + + return $actions; + } + /** * Remove an action * * @access public - * @param integer $action_id Action id - * @return bool Success or not + * @param integer $action_id + * @return bool */ public function remove($action_id) { @@ -263,24 +129,16 @@ class Action extends Base 'action_name' => $values['action_name'], ); - if (! $this->db->table(self::TABLE)->save($action)) { + if (! $this->db->table(self::TABLE)->insert($action)) { $this->db->cancelTransaction(); return false; } $action_id = $this->db->getLastId(); - foreach ($values['params'] as $param_name => $param_value) { - $action_param = array( - 'action_id' => $action_id, - 'name' => $param_name, - 'value' => $param_value, - ); - - if (! $this->db->table(self::TABLE_PARAMS)->save($action_param)) { - $this->db->cancelTransaction(); - return false; - } + if (! $this->actionParameter->create($action_id, $values)) { + $this->db->cancelTransaction(); + return false; } $this->db->closeTransaction(); @@ -288,42 +146,6 @@ class Action extends Base return $action_id; } - /** - * Load all actions and attach events - * - * @access public - */ - public function attachEvents() - { - $actions = $this->getAll(); - - foreach ($actions as $action) { - $listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']); - - foreach ($action['params'] as $param) { - $listener->setParam($param['name'], $param['value']); - } - - $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute')); - } - } - - /** - * Load an action - * - * @access public - * @param string $name Action class name - * @param integer $project_id Project id - * @param string $event Event name - * @return \Action\Base - */ - public function load($name, $project_id, $event) - { - $className = $name{0} - !== '\\' ? '\Kanboard\Action\\'.$name : $name; - return new $className($this->container, $project_id, $event); - } - /** * Copy actions from a project to another one (skip actions that cannot resolve parameters) * @@ -346,15 +168,14 @@ class Action extends Base ); if (! $this->db->table(self::TABLE)->insert($values)) { - $this->container['logger']->debug('Action::duplicate => unable to create '.$action['action_name']); $this->db->cancelTransaction(); continue; } $action_id = $this->db->getLastId(); - if (! $this->duplicateParameters($dst_project_id, $action_id, $action['params'])) { - $this->container['logger']->debug('Action::duplicate => unable to copy parameters for '.$action['action_name']); + if (! $this->actionParameter->duplicateParameters($dst_project_id, $action_id, $action['params'])) { + $this->logger->error('Action::duplicate => skip action '.$action['action_name'].' '.$action['id']); $this->db->cancelTransaction(); continue; } @@ -364,95 +185,4 @@ class Action extends Base return true; } - - /** - * Duplicate action parameters - * - * @access public - * @param integer $project_id - * @param integer $action_id - * @param array $params - * @return boolean - */ - public function duplicateParameters($project_id, $action_id, array $params) - { - foreach ($params as $param) { - $value = $this->resolveParameters($param, $project_id); - - if ($value === false) { - $this->container['logger']->debug('Action::duplicateParameters => unable to resolve '.$param['name'].'='.$param['value']); - return false; - } - - $values = array( - 'action_id' => $action_id, - 'name' => $param['name'], - 'value' => $value, - ); - - if (! $this->db->table(self::TABLE_PARAMS)->insert($values)) { - return false; - } - } - - return true; - } - - /** - * Resolve action parameter values according to another project - * - * @author Antonio Rabelo - * @access public - * @param array $param Action parameter - * @param integer $project_id Project to find the corresponding values - * @return mixed - */ - public function resolveParameters(array $param, $project_id) - { - switch ($param['name']) { - case 'project_id': - return $project_id; - case 'category_id': - return $this->category->getIdByName($project_id, $this->category->getNameById($param['value'])) ?: false; - case 'src_column_id': - case 'dest_column_id': - case 'dst_column_id': - case 'column_id': - $column = $this->board->getColumn($param['value']); - - if (empty($column)) { - return false; - } - - return $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; - case 'user_id': - case 'owner_id': - return $this->projectPermission->isMember($project_id, $param['value']) ? $param['value'] : false; - default: - return $param['value']; - } - } - - /** - * Validate action creation - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('event_name', t('This value is required')), - new Validators\Required('action_name', t('This value is required')), - new Validators\Required('params', t('This value is required')), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/ActionParameter.php b/sources/app/Model/ActionParameter.php new file mode 100644 index 0000000..62b0314 --- /dev/null +++ b/sources/app/Model/ActionParameter.php @@ -0,0 +1,162 @@ +db->table(self::TABLE)->findAll(); + return $this->toDictionary($params); + } + + /** + * Get all params for a list of actions + * + * @access public + * @param array $action_ids + * @return array + */ + public function getAllByActions(array $action_ids) + { + $params = $this->db->table(self::TABLE)->in('action_id', $action_ids)->findAll(); + return $this->toDictionary($params); + } + + /** + * Build params dictionary + * + * @access private + * @param array $params + * @return array + */ + private function toDictionary(array $params) + { + $result = array(); + + foreach ($params as $param) { + $result[$param['action_id']][$param['name']] = $param['value']; + } + + return $result; + } + + /** + * Get all action params for a given action + * + * @access public + * @param integer $action_id + * @return array + */ + public function getAllByAction($action_id) + { + return $this->db->hashtable(self::TABLE)->eq('action_id', $action_id)->getAll('name', 'value'); + } + + /** + * Insert new parameters for an action + * + * @access public + * @param integer $action_id + * @param array $values + * @return boolean + */ + public function create($action_id, array $values) + { + foreach ($values['params'] as $name => $value) { + $param = array( + 'action_id' => $action_id, + 'name' => $name, + 'value' => $value, + ); + + if (! $this->db->table(self::TABLE)->save($param)) { + return false; + } + } + + return true; + } + + /** + * Duplicate action parameters + * + * @access public + * @param integer $project_id + * @param integer $action_id + * @param array $params + * @return boolean + */ + public function duplicateParameters($project_id, $action_id, array $params) + { + foreach ($params as $name => $value) { + $value = $this->resolveParameter($project_id, $name, $value); + + if ($value === false) { + $this->logger->error('ActionParameter::duplicateParameters => unable to resolve '.$name.'='.$value); + return false; + } + + $values = array( + 'action_id' => $action_id, + 'name' => $name, + 'value' => $value, + ); + + if (! $this->db->table(self::TABLE)->insert($values)) { + return false; + } + } + + return true; + } + + /** + * Resolve action parameter values according to another project + * + * @access private + * @param integer $project_id + * @param string $name + * @param string $value + * @return mixed + */ + private function resolveParameter($project_id, $name, $value) + { + switch ($name) { + case 'project_id': + return $value != $project_id ? $value : false; + case 'category_id': + return $this->category->getIdByName($project_id, $this->category->getNameById($value)) ?: false; + case 'src_column_id': + case 'dest_column_id': + case 'dst_column_id': + case 'column_id': + $column = $this->board->getColumn($value); + return empty($column) ? false : $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; + case 'user_id': + case 'owner_id': + return $this->projectPermission->isAssignable($project_id, $value) ? $value : false; + default: + return $value; + } + } +} diff --git a/sources/app/Model/Board.php b/sources/app/Model/Board.php index 79a1a92..0f980f6 100644 --- a/sources/app/Model/Board.php +++ b/sources/app/Model/Board.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use PicoDb\Database; /** @@ -436,47 +434,4 @@ class Board extends Base { return $this->db->table(self::TABLE)->eq('id', $column_id)->remove(); } - - /** - * Validate column modification - * - * @access public - * @param array $values Required parameters to update a column - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $v = new Validator($values, array( - new Validators\Integer('task_limit', t('This value must be an integer')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate column creation - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/Category.php b/sources/app/Model/Category.php index bf40c60..58cee73 100644 --- a/sources/app/Model/Category.php +++ b/sources/app/Model/Category.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Category model * @@ -212,63 +209,4 @@ class Category extends Base return true; } - - /** - * Validate category creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate category modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('The id must be an integer')), - new Validators\Integer('project_id', t('The project id must be an integer')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) - ); - } } diff --git a/sources/app/Model/Comment.php b/sources/app/Model/Comment.php index c7125a2..6eb4a1e 100644 --- a/sources/app/Model/Comment.php +++ b/sources/app/Model/Comment.php @@ -3,8 +3,6 @@ namespace Kanboard\Model; use Kanboard\Event\CommentEvent; -use SimpleValidator\Validator; -use SimpleValidator\Validators; /** * Comment model @@ -26,8 +24,9 @@ class Comment extends Base * * @var string */ - const EVENT_UPDATE = 'comment.update'; - const EVENT_CREATE = 'comment.create'; + const EVENT_UPDATE = 'comment.update'; + const EVENT_CREATE = 'comment.create'; + const EVENT_USER_MENTION = 'comment.user.mention'; /** * Get all comments for a given task @@ -74,6 +73,7 @@ class Comment extends Base self::TABLE.'.user_id', self::TABLE.'.date_creation', self::TABLE.'.comment', + self::TABLE.'.reference', User::TABLE.'.username', User::TABLE.'.name' ) @@ -110,7 +110,9 @@ class Comment extends Base $comment_id = $this->persist(self::TABLE, $values); if ($comment_id) { - $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values)); + $event = new CommentEvent(array('id' => $comment_id) + $values); + $this->dispatcher->dispatch(self::EVENT_CREATE, $event); + $this->userMention->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); } return $comment_id; @@ -148,62 +150,4 @@ class Comment extends Base { return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove(); } - - /** - * Validate comment creation - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('task_id', t('This value is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate comment modification - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('This value is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('This value must be an integer')), - new Validators\Integer('task_id', t('This value must be an integer')), - new Validators\Integer('user_id', t('This value must be an integer')), - new Validators\Required('comment', t('Comment is required')) - ); - } } diff --git a/sources/app/Model/Config.php b/sources/app/Model/Config.php index be9d4ae..5599931 100644 --- a/sources/app/Model/Config.php +++ b/sources/app/Model/Config.php @@ -4,7 +4,6 @@ namespace Kanboard\Model; use Kanboard\Core\Translator; use Kanboard\Core\Security\Token; -use Kanboard\Core\Session\SessionManager; /** * Config model @@ -14,31 +13,6 @@ use Kanboard\Core\Session\SessionManager; */ class Config extends Setting { - /** - * Get available currencies - * - * @access public - * @return array - */ - public function getCurrencies() - { - return array( - 'USD' => t('USD - US Dollar'), - 'EUR' => t('EUR - Euro'), - 'GBP' => t('GBP - British Pound'), - 'CHF' => t('CHF - Swiss Francs'), - 'CAD' => t('CAD - Canadian Dollar'), - 'AUD' => t('AUD - Australian Dollar'), - 'NZD' => t('NZD - New Zealand Dollar'), - 'INR' => t('INR - Indian Rupee'), - 'JPY' => t('JPY - Japanese Yen'), - 'RSD' => t('RSD - Serbian dinar'), - 'SEK' => t('SEK - Swedish Krona'), - 'NOK' => t('NOK - Norwegian Krone'), - 'BAM' => t('BAM - Konvertible Mark'), - ); - } - /** * Get available timezones * @@ -58,6 +32,31 @@ class Config extends Setting return $listing; } + /** + * Get current timezone + * + * @access public + * @return string + */ + public function getCurrentTimezone() + { + if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) { + return $this->sessionStorage->user['timezone']; + } + + return $this->get('application_timezone', 'UTC'); + } + + /** + * Set timezone + * + * @access public + */ + public function setupTimezone() + { + date_default_timezone_set($this->getCurrentTimezone()); + } + /** * Get available languages * @@ -79,6 +78,7 @@ class Config extends Setting 'fr_FR' => 'Français', 'it_IT' => 'Italiano', 'hu_HU' => 'Magyar', + 'my_MY' => 'Melayu', 'nl_NL' => 'Nederlands', 'nb_NO' => 'Norsk', 'pl_PL' => 'Polski', @@ -154,43 +154,6 @@ class Config extends Setting return $this->get('application_language', 'en_US'); } - /** - * Get a config variable from the session or the database - * - * @access public - * @param string $name Parameter name - * @param string $default_value Default value of the parameter - * @return string - */ - public function get($name, $default_value = '') - { - if (! SessionManager::isOpen()) { - return $this->getOption($name, $default_value); - } - - // Cache config in session - if (! isset($this->sessionStorage->config[$name])) { - $this->sessionStorage->config = $this->getAll(); - } - - if (! empty($this->sessionStorage->config[$name])) { - return $this->sessionStorage->config[$name]; - } - - return $default_value; - } - - /** - * Reload settings in the session and the translations - * - * @access public - */ - public function reload() - { - $this->sessionStorage->config = $this->getAll(); - $this->setupTranslations(); - } - /** * Load translations * @@ -202,28 +165,27 @@ class Config extends Setting } /** - * Get current timezone + * Get a config variable from the session or the database * * @access public + * @param string $name Parameter name + * @param string $default_value Default value of the parameter * @return string */ - public function getCurrentTimezone() + public function get($name, $default_value = '') { - if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) { - return $this->sessionStorage->user['timezone']; - } - - return $this->get('application_timezone', 'UTC'); + $options = $this->memoryCache->proxy($this, 'getAll'); + return isset($options[$name]) && $options[$name] !== '' ? $options[$name] : $default_value; } /** - * Set timezone + * Reload settings in the session and the translations * * @access public */ - public function setupTimezone() + public function reload() { - date_default_timezone_set($this->getCurrentTimezone()); + $this->setupTranslations(); } /** @@ -234,7 +196,7 @@ class Config extends Setting */ public function optimizeDatabase() { - return $this->db->getconnection()->exec("VACUUM"); + return $this->db->getconnection()->exec('VACUUM'); } /** @@ -264,10 +226,11 @@ class Config extends Setting * * @access public * @param string $option Parameter name + * @return boolean */ public function regenerateToken($option) { - $this->save(array($option => Token::getToken())); + return $this->save(array($option => Token::getToken())); } /** diff --git a/sources/app/Model/Currency.php b/sources/app/Model/Currency.php index c115661..abcce2f 100644 --- a/sources/app/Model/Currency.php +++ b/sources/app/Model/Currency.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Currency * @@ -20,6 +17,32 @@ class Currency extends Base */ const TABLE = 'currencies'; + /** + * Get available application currencies + * + * @access public + * @return array + */ + public function getCurrencies() + { + return array( + 'USD' => t('USD - US Dollar'), + 'EUR' => t('EUR - Euro'), + 'GBP' => t('GBP - British Pound'), + 'CHF' => t('CHF - Swiss Francs'), + 'CAD' => t('CAD - Canadian Dollar'), + 'AUD' => t('AUD - Australian Dollar'), + 'NZD' => t('NZD - New Zealand Dollar'), + 'INR' => t('INR - Indian Rupee'), + 'JPY' => t('JPY - Japanese Yen'), + 'RSD' => t('RSD - Serbian dinar'), + 'SEK' => t('SEK - Swedish Krona'), + 'NOK' => t('NOK - Norwegian Krone'), + 'BAM' => t('BAM - Konvertible Mark'), + 'RUB' => t('RUB - Russian Ruble'), + ); + } + /** * Get all currency rates * @@ -45,7 +68,7 @@ class Currency extends Base $reference = $this->config->get('application_currency', 'USD'); if ($reference !== $currency) { - $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array(); + $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : $rates; $rate = isset($rates[$currency]) ? $rates[$currency] : 1; return $rate * $price; @@ -68,7 +91,7 @@ class Currency extends Base return $this->update($currency, $rate); } - return $this->persist(self::TABLE, compact('currency', 'rate')); + return $this->db->table(self::TABLE)->insert(array('currency' => $currency, 'rate' => $rate)); } /** @@ -83,24 +106,4 @@ class Currency extends Base { return $this->db->table(self::TABLE)->eq('currency', $currency)->update(array('rate' => $rate)); } - - /** - * Validate - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validate(array $values) - { - $v = new Validator($values, array( - new Validators\Required('currency', t('Field required')), - new Validators\Required('rate', t('Field required')), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/CustomFilter.php b/sources/app/Model/CustomFilter.php index 6550b4a..3a6a1a3 100644 --- a/sources/app/Model/CustomFilter.php +++ b/sources/app/Model/CustomFilter.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Custom Filter model * @@ -102,63 +99,4 @@ class CustomFilter extends Base { return $this->db->table(self::TABLE)->eq('id', $filter_id)->remove(); } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Required('project_id', t('Field required')), - new Validators\Required('user_id', t('Field required')), - new Validators\Required('name', t('Field required')), - new Validators\Required('filter', t('Field required')), - new Validators\Integer('user_id', t('This value must be an integer')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100), - new Validators\MaxLength('filter', t('The maximum length is %d characters', 100), 100) - ); - } - - /** - * Validate filter creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, $this->commonValidationRules()); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate filter modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('Field required')), - new Validators\Integer('id', t('This value must be an integer')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/File.php b/sources/app/Model/File.php index daade51..be62cdb 100644 --- a/sources/app/Model/File.php +++ b/sources/app/Model/File.php @@ -263,14 +263,16 @@ class File extends Base public function uploadFiles($project_id, $task_id, $form_name) { try { - if (empty($_FILES[$form_name])) { + $file = $this->request->getFileInfo($form_name); + + if (empty($file)) { return false; } - foreach ($_FILES[$form_name]['error'] as $key => $error) { - if ($error == UPLOAD_ERR_OK && $_FILES[$form_name]['size'][$key] > 0) { - $original_filename = $_FILES[$form_name]['name'][$key]; - $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key]; + foreach ($file['error'] as $key => $error) { + if ($error == UPLOAD_ERR_OK && $file['size'][$key] > 0) { + $original_filename = $file['name'][$key]; + $uploaded_filename = $file['tmp_name'][$key]; $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); if ($this->isImage($original_filename)) { @@ -283,7 +285,7 @@ class File extends Base $task_id, $original_filename, $destination_filename, - $_FILES[$form_name]['size'][$key] + $file['size'][$key] ); } } diff --git a/sources/app/Model/Group.php b/sources/app/Model/Group.php index a086fe9..6789950 100644 --- a/sources/app/Model/Group.php +++ b/sources/app/Model/Group.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Group Model * @@ -117,59 +114,4 @@ class Group extends Base { return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, $this->commonValidationRules()); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The id is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Required('name', t('The name is required')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100), - new Validators\Unique('name', t('The name must be unique'), $this->db->getConnection(), self::TABLE, 'id'), - new Validators\MaxLength('external_id', t('The maximum length is %d characters', 255), 255), - new Validators\Integer('id', t('This value must be an integer')), - ); - } } diff --git a/sources/app/Model/LastLogin.php b/sources/app/Model/LastLogin.php index 0f148ea..f5be020 100644 --- a/sources/app/Model/LastLogin.php +++ b/sources/app/Model/LastLogin.php @@ -36,11 +36,31 @@ class LastLogin extends Base */ public function create($auth_type, $user_id, $ip, $user_agent) { - // Cleanup old sessions if necessary + $this->cleanup($user_id); + + return $this->db + ->table(self::TABLE) + ->insert(array( + 'auth_type' => $auth_type, + 'user_id' => $user_id, + 'ip' => $ip, + 'user_agent' => substr($user_agent, 0, 255), + 'date_creation' => time(), + )); + } + + /** + * Cleanup login history + * + * @access public + * @param integer $user_id + */ + public function cleanup($user_id) + { $connections = $this->db ->table(self::TABLE) ->eq('user_id', $user_id) - ->desc('date_creation') + ->desc('id') ->findAllByColumn('id'); if (count($connections) >= self::NB_LOGINS) { @@ -49,16 +69,6 @@ class LastLogin extends Base ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) ->remove(); } - - return $this->db - ->table(self::TABLE) - ->insert(array( - 'auth_type' => $auth_type, - 'user_id' => $user_id, - 'ip' => $ip, - 'user_agent' => $user_agent, - 'date_creation' => time(), - )); } /** @@ -73,7 +83,7 @@ class LastLogin extends Base return $this->db ->table(self::TABLE) ->eq('user_id', $user_id) - ->desc('date_creation') + ->desc('id') ->columns('id', 'auth_type', 'ip', 'user_agent', 'date_creation') ->findAll(); } diff --git a/sources/app/Model/Link.php b/sources/app/Model/Link.php index 00b6dfc..7b81a23 100644 --- a/sources/app/Model/Link.php +++ b/sources/app/Model/Link.php @@ -3,8 +3,6 @@ namespace Kanboard\Model; use PDO; -use SimpleValidator\Validator; -use SimpleValidator\Validators; /** * Link model @@ -176,47 +174,4 @@ class Link extends Base $this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0)); return $this->db->table(self::TABLE)->eq('id', $link_id)->remove(); } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\Required('label', t('Field required')), - new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), - new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $v = new Validator($values, array( - new Validators\Required('id', t('Field required')), - new Validators\Required('opposite_id', t('Field required')), - new Validators\Required('label', t('Field required')), - new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/Metadata.php b/sources/app/Model/Metadata.php index 83c8f49..690b226 100644 --- a/sources/app/Model/Metadata.php +++ b/sources/app/Model/Metadata.php @@ -95,4 +95,17 @@ abstract class Metadata extends Base return ! in_array(false, $results, true); } + + /** + * Remove a metadata + * + * @access public + * @param integer $entity_id + * @param string $name + * @return bool + */ + public function remove($entity_id, $name) + { + return $this->db->table(static::TABLE)->eq($this->getEntityKey(), $entity_id)->eq('name', $name)->remove(); + } } diff --git a/sources/app/Model/Notification.php b/sources/app/Model/Notification.php index f112299..87c1a79 100644 --- a/sources/app/Model/Notification.php +++ b/sources/app/Model/Notification.php @@ -74,6 +74,10 @@ class Notification extends Base return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); case File::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); + case Task::EVENT_USER_MENTION: + return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); + case Comment::EVENT_USER_MENTION: + return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']); default: return e('Notification'); } @@ -91,52 +95,40 @@ class Notification extends Base { switch ($event_name) { case File::EVENT_CREATE: - $title = e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); - break; + return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); case Comment::EVENT_CREATE: - $title = e('New comment on task #%d', $event_data['comment']['task_id']); - break; + return e('New comment on task #%d', $event_data['comment']['task_id']); case Comment::EVENT_UPDATE: - $title = e('Comment updated on task #%d', $event_data['comment']['task_id']); - break; + return e('Comment updated on task #%d', $event_data['comment']['task_id']); case Subtask::EVENT_CREATE: - $title = e('New subtask on task #%d', $event_data['subtask']['task_id']); - break; + return e('New subtask on task #%d', $event_data['subtask']['task_id']); case Subtask::EVENT_UPDATE: - $title = e('Subtask updated on task #%d', $event_data['subtask']['task_id']); - break; + return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); case Task::EVENT_CREATE: - $title = e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); - break; + return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); case Task::EVENT_UPDATE: - $title = e('Task updated #%d', $event_data['task']['id']); - break; + return e('Task updated #%d', $event_data['task']['id']); case Task::EVENT_CLOSE: - $title = e('Task #%d closed', $event_data['task']['id']); - break; + return e('Task #%d closed', $event_data['task']['id']); case Task::EVENT_OPEN: - $title = e('Task #%d opened', $event_data['task']['id']); - break; + return e('Task #%d opened', $event_data['task']['id']); case Task::EVENT_MOVE_COLUMN: - $title = e('Column changed for task #%d', $event_data['task']['id']); - break; + return e('Column changed for task #%d', $event_data['task']['id']); case Task::EVENT_MOVE_POSITION: - $title = e('New position for task #%d', $event_data['task']['id']); - break; + return e('New position for task #%d', $event_data['task']['id']); case Task::EVENT_MOVE_SWIMLANE: - $title = e('Swimlane changed for task #%d', $event_data['task']['id']); - break; + return e('Swimlane changed for task #%d', $event_data['task']['id']); case Task::EVENT_ASSIGNEE_CHANGE: - $title = e('Assignee changed on task #%d', $event_data['task']['id']); - break; + return e('Assignee changed on task #%d', $event_data['task']['id']); case Task::EVENT_OVERDUE: $nb = count($event_data['tasks']); - $title = $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); - break; + return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); + case Task::EVENT_USER_MENTION: + return e('You were mentioned in the task #%d', $event_data['task']['id']); + case Comment::EVENT_USER_MENTION: + return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']); default: - $title = e('Notification'); + return e('Notification'); } - - return $title; } } diff --git a/sources/app/Model/PasswordReset.php b/sources/app/Model/PasswordReset.php new file mode 100644 index 0000000..c2d7dde --- /dev/null +++ b/sources/app/Model/PasswordReset.php @@ -0,0 +1,93 @@ +db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_creation')->limit(100)->findAll(); + } + + /** + * Generate a new reset token for a user + * + * @access public + * @param string $username + * @param integer $expiration + * @return boolean|string + */ + public function create($username, $expiration = 0) + { + $user_id = $this->db->table(User::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id'); + + if (! $user_id) { + return false; + } + + $token = $this->token->getToken(); + + $result = $this->db->table(self::TABLE)->insert(array( + 'token' => $token, + 'user_id' => $user_id, + 'date_expiration' => $expiration ?: time() + self::DURATION, + 'date_creation' => time(), + 'ip' => $this->request->getIpAddress(), + 'user_agent' => $this->request->getUserAgent(), + 'is_active' => 1, + )); + + return $result ? $token : false; + } + + /** + * Get user id from the token + * + * @access public + * @param string $token + * @return integer + */ + public function getUserIdByToken($token) + { + return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_active', 1)->gte('date_expiration', time())->findOneColumn('user_id'); + } + + /** + * Disable all tokens for a user + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function disable($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->update(array('is_active' => 0)); + } +} diff --git a/sources/app/Model/Project.php b/sources/app/Model/Project.php index 8a949ba..6307734 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/Project.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; @@ -510,71 +508,4 @@ class Project extends Base ->eq('id', $project_id) ->save(array('is_public' => 0, 'token' => '')); } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('This value must be an integer')), - new Validators\Integer('is_active', t('This value must be an integer')), - new Validators\Required('name', t('The project name is required')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), - new Validators\MaxLength('identifier', t('The maximum length is %d characters', 50), 50), - new Validators\MaxLength('start_date', t('The maximum length is %d characters', 10), 10), - new Validators\MaxLength('end_date', t('The maximum length is %d characters', 10), 10), - new Validators\AlphaNumeric('identifier', t('This value must be alphanumeric')) , - new Validators\Unique('identifier', t('The identifier must be unique'), $this->db->getConnection(), self::TABLE), - ); - } - - /** - * Validate project creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - if (! empty($values['identifier'])) { - $values['identifier'] = strtoupper($values['identifier']); - } - - $v = new Validator($values, $this->commonValidationRules()); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate project modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - if (! empty($values['identifier'])) { - $values['identifier'] = strtoupper($values['identifier']); - } - - $rules = array( - new Validators\Required('id', t('This value is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/ProjectActivity.php b/sources/app/Model/ProjectActivity.php index 309bab9..74df26a 100644 --- a/sources/app/Model/ProjectActivity.php +++ b/sources/app/Model/ProjectActivity.php @@ -168,15 +168,11 @@ class ProjectActivity extends Base */ public function cleanup($max) { - if ($this->db->table(self::TABLE)->count() > $max) { - $this->db->execute(' - DELETE FROM '.self::TABLE.' - WHERE id <= ( - SELECT id FROM ( - SELECT id FROM '.self::TABLE.' ORDER BY id DESC LIMIT 1 OFFSET '.$max.' - ) foo - )' - ); + $total = $this->db->table(self::TABLE)->count(); + + if ($total > $max) { + $ids = $this->db->table(self::TABLE)->asc('id')->limit($total - $max)->findAllByColumn('id'); + $this->db->table(self::TABLE)->in('id', $ids)->remove(); } } diff --git a/sources/app/Model/ProjectAnalytic.php b/sources/app/Model/ProjectAnalytic.php deleted file mode 100644 index e77a036..0000000 --- a/sources/app/Model/ProjectAnalytic.php +++ /dev/null @@ -1,182 +0,0 @@ -board->getColumns($project_id); - - foreach ($columns as $column) { - $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']); - $total += $nb_tasks; - - $metrics[] = array( - 'column_title' => $column['title'], - 'nb_tasks' => $nb_tasks, - ); - } - - if ($total === 0) { - return array(); - } - - foreach ($metrics as &$metric) { - $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); - } - - return $metrics; - } - - /** - * Get users repartition - * - * @access public - * @param integer $project_id - * @return array - */ - public function getUserRepartition($project_id) - { - $metrics = array(); - $total = 0; - $tasks = $this->taskFinder->getAll($project_id); - $users = $this->projectUserRole->getAssignableUsersList($project_id); - - foreach ($tasks as $task) { - $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0]; - $total++; - - if (! isset($metrics[$user])) { - $metrics[$user] = array( - 'nb_tasks' => 0, - 'percentage' => 0, - 'user' => $user, - ); - } - - $metrics[$user]['nb_tasks']++; - } - - if ($total === 0) { - return array(); - } - - foreach ($metrics as &$metric) { - $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); - } - - ksort($metrics); - - return array_values($metrics); - } - - /** - * Get the average lead and cycle time - * - * @access public - * @param integer $project_id - * @return array - */ - public function getAverageLeadAndCycleTime($project_id) - { - $stats = array( - 'count' => 0, - 'total_lead_time' => 0, - 'total_cycle_time' => 0, - 'avg_lead_time' => 0, - 'avg_cycle_time' => 0, - ); - - $tasks = $this->db - ->table(Task::TABLE) - ->columns('date_completed', 'date_creation', 'date_started') - ->eq('project_id', $project_id) - ->desc('id') - ->limit(1000) - ->findAll(); - - foreach ($tasks as &$task) { - $stats['count']++; - $stats['total_lead_time'] += ($task['date_completed'] ?: time()) - $task['date_creation']; - $stats['total_cycle_time'] += empty($task['date_started']) ? 0 : ($task['date_completed'] ?: time()) - $task['date_started']; - } - - $stats['avg_lead_time'] = $stats['count'] > 0 ? (int) ($stats['total_lead_time'] / $stats['count']) : 0; - $stats['avg_cycle_time'] = $stats['count'] > 0 ? (int) ($stats['total_cycle_time'] / $stats['count']) : 0; - - return $stats; - } - - /** - * Get the average time spent into each column - * - * @access public - * @param integer $project_id - * @return array - */ - public function getAverageTimeSpentByColumn($project_id) - { - $stats = array(); - $columns = $this->board->getColumnsList($project_id); - - // Get the time spent of the last move for each tasks - $tasks = $this->db - ->table(Task::TABLE) - ->columns('id', 'date_completed', 'date_moved', 'column_id') - ->eq('project_id', $project_id) - ->desc('id') - ->limit(1000) - ->findAll(); - - // Init values - foreach ($columns as $column_id => $column_title) { - $stats[$column_id] = array( - 'count' => 0, - 'time_spent' => 0, - 'average' => 0, - 'title' => $column_title, - ); - } - - // Get time spent foreach task/column and take into account the last move - foreach ($tasks as &$task) { - $sums = $this->transition->getTimeSpentByTask($task['id']); - - if (! isset($sums[$task['column_id']])) { - $sums[$task['column_id']] = 0; - } - - $sums[$task['column_id']] += ($task['date_completed'] ?: time()) - $task['date_moved']; - - foreach ($sums as $column_id => $time_spent) { - if (isset($stats[$column_id])) { - $stats[$column_id]['count']++; - $stats[$column_id]['time_spent'] += $time_spent; - } - } - } - - // Calculate average for each column - foreach ($columns as $column_id => $column_title) { - $stats[$column_id]['average'] = $stats[$column_id]['count'] > 0 ? (int) ($stats[$column_id]['time_spent'] / $stats[$column_id]['count']) : 0; - } - - return $stats; - } -} diff --git a/sources/app/Model/ProjectDailyColumnStats.php b/sources/app/Model/ProjectDailyColumnStats.php index 7c89283..cf79be8 100644 --- a/sources/app/Model/ProjectDailyColumnStats.php +++ b/sources/app/Model/ProjectDailyColumnStats.php @@ -18,7 +18,7 @@ class ProjectDailyColumnStats extends Base const TABLE = 'project_daily_column_stats'; /** - * Update daily totals for the project and foreach column + * Update daily totals for the project and for each column * * "total" is the number open of tasks in the column * "score" is the sum of tasks score in the column @@ -30,48 +30,17 @@ class ProjectDailyColumnStats extends Base */ public function updateTotals($project_id, $date) { - $status = $this->config->get('cfd_include_closed_tasks') == 1 ? array(Task::STATUS_OPEN, Task::STATUS_CLOSED) : array(Task::STATUS_OPEN); - $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('day', $date)->remove(); - $column_ids = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id'); - - foreach ($column_ids as $column_id) { - - $exists = $this->db->table(ProjectDailyColumnStats::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->eq('day', $date) - ->exists(); - - $score = $this->db->table(Task::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->eq('is_active', Task::STATUS_OPEN) - ->sum('score'); - - $total = $this->db->table(Task::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->in('is_active', $status) - ->count(); - - if ($exists) { - $this->db->table(ProjectDailyColumnStats::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->eq('day', $date) - ->update(array('score' => $score, 'total' => $total)); - - } else { - $this->db->table(ProjectDailyColumnStats::TABLE)->insert(array( - 'day' => $date, - 'project_id' => $project_id, - 'column_id' => $column_id, - 'total' => $total, - 'score' => $score, - )); - } + foreach ($this->getStatsByColumns($project_id) as $column_id => $column) { + $this->db->table(self::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'column_id' => $column_id, + 'total' => $column['total'], + 'score' => $column['score'], + )); } $this->db->closeTransaction(); @@ -90,64 +59,11 @@ class ProjectDailyColumnStats extends Base */ public function countDays($project_id, $from, $to) { - $rq = $this->db->execute( - 'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?', - array($from, $to, $project_id) - ); - - return $rq !== false ? $rq->fetchColumn(0) : 0; - } - - /** - * Get raw metrics for the project within a data range - * - * @access public - * @param integer $project_id Project id - * @param string $from Start date (ISO format YYYY-MM-DD) - * @param string $to End date - * @return array - */ - public function getRawMetrics($project_id, $from, $to) - { - return $this->db->table(ProjectDailyColumnStats::TABLE) - ->columns( - ProjectDailyColumnStats::TABLE.'.column_id', - ProjectDailyColumnStats::TABLE.'.day', - ProjectDailyColumnStats::TABLE.'.total', - ProjectDailyColumnStats::TABLE.'.score', - Board::TABLE.'.title AS column_title' - ) - ->join(Board::TABLE, 'id', 'column_id') - ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->asc(ProjectDailyColumnStats::TABLE.'.day') - ->findAll(); - } - - /** - * Get raw metrics for the project within a data range grouped by day - * - * @access public - * @param integer $project_id Project id - * @param string $from Start date (ISO format YYYY-MM-DD) - * @param string $to End date - * @return array - */ - public function getRawMetricsByDay($project_id, $from, $to) - { - return $this->db->table(ProjectDailyColumnStats::TABLE) - ->columns( - ProjectDailyColumnStats::TABLE.'.day', - 'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total', - 'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score' - ) - ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->asc(ProjectDailyColumnStats::TABLE.'.day') - ->groupBy(ProjectDailyColumnStats::TABLE.'.day') - ->findAll(); + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->findOneColumn('COUNT(DISTINCT day)'); } /** @@ -163,43 +79,174 @@ class ProjectDailyColumnStats extends Base * @param integer $project_id Project id * @param string $from Start date (ISO format YYYY-MM-DD) * @param string $to End date - * @param string $column Column to aggregate + * @param string $field Column to aggregate * @return array */ - public function getAggregatedMetrics($project_id, $from, $to, $column = 'total') + public function getAggregatedMetrics($project_id, $from, $to, $field = 'total') { $columns = $this->board->getColumnsList($project_id); + $metrics = $this->getMetrics($project_id, $from, $to); + return $this->buildAggregate($metrics, $columns, $field); + } + + /** + * Fetch metrics + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return array + */ + public function getMetrics($project_id, $from, $to) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(self::TABLE.'.day') + ->findAll(); + } + + /** + * Build aggregate + * + * @access private + * @param array $metrics + * @param array $columns + * @param string $field + * @return array + */ + private function buildAggregate(array &$metrics, array &$columns, $field) + { $column_ids = array_keys($columns); - $metrics = array(array_merge(array(e('Date')), array_values($columns))); - $aggregates = array(); + $days = array_unique(array_column($metrics, 'day')); + $rows = array(array_merge(array(e('Date')), array_values($columns))); - // Fetch metrics for the project - $records = $this->db->table(ProjectDailyColumnStats::TABLE) - ->eq('project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->findAll(); - - // Aggregate by day - foreach ($records as $record) { - if (! isset($aggregates[$record['day']])) { - $aggregates[$record['day']] = array($record['day']); - } - - $aggregates[$record['day']][$record['column_id']] = $record[$column]; + foreach ($days as $day) { + $rows[] = $this->buildRowAggregate($metrics, $column_ids, $day, $field); } - // Aggregate by row - foreach ($aggregates as $aggregate) { - $row = array($aggregate[0]); + return $rows; + } - foreach ($column_ids as $column_id) { - $row[] = (int) $aggregate[$column_id]; - } + /** + * Build one row of the aggregate + * + * @access private + * @param array $metrics + * @param array $column_ids + * @param string $day + * @param string $field + * @return array + */ + private function buildRowAggregate(array &$metrics, array &$column_ids, $day, $field) + { + $row = array($day); - $metrics[] = $row; + foreach ($column_ids as $column_id) { + $row[] = $this->findValueInMetrics($metrics, $day, $column_id, $field); } - return $metrics; + return $row; + } + + /** + * Find the value in the metrics + * + * @access private + * @param array $metrics + * @param string $day + * @param string $column_id + * @param string $field + * @return integer + */ + private function findValueInMetrics(array &$metrics, $day, $column_id, $field) + { + foreach ($metrics as $metric) { + if ($metric['day'] === $day && $metric['column_id'] == $column_id) { + return $metric[$field]; + } + } + + return 0; + } + + /** + * Get number of tasks and score by columns + * + * @access private + * @param integer $project_id + * @return array + */ + private function getStatsByColumns($project_id) + { + $totals = $this->getTotalByColumns($project_id); + $scores = $this->getScoreByColumns($project_id); + $columns = array(); + + foreach ($totals as $column_id => $total) { + $columns[$column_id] = array('total' => $total, 'score' => 0); + } + + foreach ($scores as $column_id => $score) { + $columns[$column_id]['score'] = (int) $score; + } + + return $columns; + } + + /** + * Get number of tasks and score by columns + * + * @access private + * @param integer $project_id + * @return array + */ + private function getScoreByColumns($project_id) + { + $stats = $this->db->table(Task::TABLE) + ->columns('column_id', 'SUM(score) AS score') + ->eq('project_id', $project_id) + ->eq('is_active', Task::STATUS_OPEN) + ->notNull('score') + ->groupBy('column_id') + ->findAll(); + + return array_column($stats, 'score', 'column_id'); + } + + /** + * Get number of tasks and score by columns + * + * @access private + * @param integer $project_id + * @return array + */ + private function getTotalByColumns($project_id) + { + $stats = $this->db->table(Task::TABLE) + ->columns('column_id', 'COUNT(*) AS total') + ->eq('project_id', $project_id) + ->in('is_active', $this->getTaskStatusConfig()) + ->groupBy('column_id') + ->findAll(); + + return array_column($stats, 'total', 'column_id'); + } + + /** + * Get task status to use for total calculation + * + * @access private + * @return array + */ + private function getTaskStatusConfig() + { + if ($this->config->get('cfd_include_closed_tasks') == 1) { + return array(Task::STATUS_OPEN, Task::STATUS_CLOSED); + } + + return array(Task::STATUS_OPEN); } } diff --git a/sources/app/Model/ProjectDailyStats.php b/sources/app/Model/ProjectDailyStats.php index e79af37..957ad51 100644 --- a/sources/app/Model/ProjectDailyStats.php +++ b/sources/app/Model/ProjectDailyStats.php @@ -29,29 +29,16 @@ class ProjectDailyStats extends Base { $this->db->startTransaction(); - $lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id); + $lead_cycle_time = $this->averageLeadCycleTimeAnalytic->build($project_id); - $exists = $this->db->table(ProjectDailyStats::TABLE) - ->eq('day', $date) - ->eq('project_id', $project_id) - ->exists(); + $this->db->table(self::TABLE)->eq('day', $date)->eq('project_id', $project_id)->remove(); - if ($exists) { - $this->db->table(ProjectDailyStats::TABLE) - ->eq('project_id', $project_id) - ->eq('day', $date) - ->update(array( - 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], - 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], - )); - } else { - $this->db->table(ProjectDailyStats::TABLE)->insert(array( - 'day' => $date, - 'project_id' => $project_id, - 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], - 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], - )); - } + $this->db->table(self::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], + 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], + )); $this->db->closeTransaction(); @@ -70,11 +57,11 @@ class ProjectDailyStats extends Base public function getRawMetrics($project_id, $from, $to) { return $this->db->table(self::TABLE) - ->columns('day', 'avg_lead_time', 'avg_cycle_time') - ->eq(self::TABLE.'.project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->asc(self::TABLE.'.day') - ->findAll(); + ->columns('day', 'avg_lead_time', 'avg_cycle_time') + ->eq('project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc('day') + ->findAll(); } } diff --git a/sources/app/Model/ProjectGroupRole.php b/sources/app/Model/ProjectGroupRole.php index 2fe22ca..591b28c 100644 --- a/sources/app/Model/ProjectGroupRole.php +++ b/sources/app/Model/ProjectGroupRole.php @@ -48,11 +48,13 @@ class ProjectGroupRole extends Base */ public function getUserRole($project_id, $user_id) { - return $this->db->table(self::TABLE) + $roles = $this->db->table(self::TABLE) ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) ->eq(GroupMember::TABLE.'.user_id', $user_id) ->eq(self::TABLE.'.project_id', $project_id) - ->findOneColumn('role'); + ->findAllByColumn('role'); + + return $this->projectAccessMap->getHighestRole($roles); } /** @@ -99,10 +101,10 @@ class ProjectGroupRole extends Base */ public function getAssignableUsers($project_id) { - return $this->db->table(self::TABLE) + return $this->db->table(User::TABLE) ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') - ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) - ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->join(GroupMember::TABLE, 'user_id', 'id', User::TABLE) + ->join(self::TABLE, 'group_id', 'group_id', GroupMember::TABLE) ->eq(self::TABLE.'.project_id', $project_id) ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) ->asc(User::TABLE.'.username') diff --git a/sources/app/Model/ProjectGroupRoleFilter.php b/sources/app/Model/ProjectGroupRoleFilter.php new file mode 100644 index 0000000..989d307 --- /dev/null +++ b/sources/app/Model/ProjectGroupRoleFilter.php @@ -0,0 +1,89 @@ +query = $this->db->table(ProjectGroupRole::TABLE); + return $this; + } + + /** + * Get all results of the filter + * + * @access public + * @param string $column + * @return array + */ + public function findAll($column = '') + { + if ($column !== '') { + return $this->query->asc($column)->findAllByColumn($column); + } + + return $this->query->findAll(); + } + + /** + * Get the PicoDb query + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Filter by project id + * + * @access public + * @param integer $project_id + * @return ProjectUserRoleFilter + */ + public function filterByProjectId($project_id) + { + $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id); + return $this; + } + + /** + * Filter by username + * + * @access public + * @param string $input + * @return ProjectUserRoleFilter + */ + public function startWithUsername($input) + { + $this->query + ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->ilike(User::TABLE.'.username', $input.'%'); + + return $this; + } +} diff --git a/sources/app/Model/ProjectPermission.php b/sources/app/Model/ProjectPermission.php index b311c10..cea62e1 100644 --- a/sources/app/Model/ProjectPermission.php +++ b/sources/app/Model/ProjectPermission.php @@ -28,10 +28,10 @@ class ProjectPermission extends Base return $this ->db - ->table(self::TABLE) + ->table(ProjectUserRole::TABLE) ->join(User::TABLE, 'id', 'user_id') ->join(Project::TABLE, 'id', 'project_id') - ->eq(self::TABLE.'.role', $role) + ->eq(ProjectUserRole::TABLE.'.role', $role) ->eq(Project::TABLE.'.is_private', 0) ->in(Project::TABLE.'.id', $project_ids) ->columns( @@ -43,6 +43,25 @@ class ProjectPermission extends Base ); } + /** + * Get all usernames (fetch users from groups) + * + * @access public + * @param integer $project_id + * @param string $input + * @return array + */ + public function findUsernames($project_id, $input) + { + $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $members = array_unique(array_merge($userMembers, $groupMembers)); + + sort($members); + + return $members; + } + /** * Return true if everybody is allowed for the project * @@ -86,9 +105,22 @@ class ProjectPermission extends Base * @param integer $user_id * @return boolean */ + public function isAssignable($project_id, $user_id) + { + return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); + } + + /** + * Return true if the user is member + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @return boolean + */ public function isMember($project_id, $user_id) { - return in_array($this->projectUserRole->getUSerRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); + return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER, Role::PROJECT_VIEWER)); } /** @@ -100,7 +132,7 @@ class ProjectPermission extends Base */ public function getActiveProjectIds($user_id) { - return array_keys($this->projectUserRole->getProjectsByUser($user_id, array(Project::ACTIVE))); + return array_keys($this->projectUserRole->getActiveProjectsByUser($user_id)); } /** diff --git a/sources/app/Model/ProjectUserRole.php b/sources/app/Model/ProjectUserRole.php index 28e6c8c..8149a25 100644 --- a/sources/app/Model/ProjectUserRole.php +++ b/sources/app/Model/ProjectUserRole.php @@ -20,7 +20,19 @@ class ProjectUserRole extends Base const TABLE = 'project_has_users'; /** - * Get the list of project visible by the given user + * Get the list of active project for the given user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getActiveProjectsByUser($user_id) + { + return $this->getProjectsByUser($user_id, array(Project::ACTIVE)); + } + + /** + * Get the list of project visible for the given user * * @access public * @param integer $user_id @@ -40,11 +52,11 @@ class ProjectUserRole extends Base ->getAll(Project::TABLE.'.id', Project::TABLE.'.name'); $groupProjects = $this->projectGroupRole->getProjectsByUser($user_id, $status); - $groups = $userProjects + $groupProjects; + $projects = $userProjects + $groupProjects; - asort($groups); + asort($projects); - return $groups; + return $projects; } /** diff --git a/sources/app/Model/ProjectUserRoleFilter.php b/sources/app/Model/ProjectUserRoleFilter.php new file mode 100644 index 0000000..6440364 --- /dev/null +++ b/sources/app/Model/ProjectUserRoleFilter.php @@ -0,0 +1,88 @@ +query = $this->db->table(ProjectUserRole::TABLE); + return $this; + } + + /** + * Get all results of the filter + * + * @access public + * @param string $column + * @return array + */ + public function findAll($column = '') + { + if ($column !== '') { + return $this->query->asc($column)->findAllByColumn($column); + } + + return $this->query->findAll(); + } + + /** + * Get the PicoDb query + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Filter by project id + * + * @access public + * @param integer $project_id + * @return ProjectUserRoleFilter + */ + public function filterByProjectId($project_id) + { + $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id); + return $this; + } + + /** + * Filter by username + * + * @access public + * @param string $input + * @return ProjectUserRoleFilter + */ + public function startWithUsername($input) + { + $this->query + ->join(User::TABLE, 'id', 'user_id') + ->ilike(User::TABLE.'.username', $input.'%'); + + return $this; + } +} diff --git a/sources/app/Model/Setting.php b/sources/app/Model/Setting.php index 3507d42..44e6c06 100644 --- a/sources/app/Model/Setting.php +++ b/sources/app/Model/Setting.php @@ -47,10 +47,12 @@ abstract class Setting extends Base */ public function getOption($name, $default = '') { - return $this->db + $value = $this->db ->table(self::TABLE) ->eq('option', $name) - ->findOneColumn('value') ?: $default; + ->findOneColumn('value'); + + return $value === null || $value === false || $value === '' ? $default : $value; } /** diff --git a/sources/app/Model/Subtask.php b/sources/app/Model/Subtask.php index 664e41e..0e039bb 100644 --- a/sources/app/Model/Subtask.php +++ b/sources/app/Model/Subtask.php @@ -4,11 +4,9 @@ namespace Kanboard\Model; use PicoDb\Database; use Kanboard\Event\SubtaskEvent; -use SimpleValidator\Validator; -use SimpleValidator\Validators; /** - * Subtask model + * Subtask Model * * @package model * @author Frederic Guillot @@ -451,90 +449,4 @@ class Subtask extends Base } }); } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('task_id', t('The task id is required')), - new Validators\Required('title', t('The title is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The subtask id is required')), - new Validators\Required('task_id', t('The task id is required')), - new Validators\Required('title', t('The title is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate API modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateApiModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The subtask id is required')), - new Validators\Required('task_id', t('The task id is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('The subtask id must be an integer')), - new Validators\Integer('task_id', t('The task id must be an integer')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255), - new Validators\Integer('user_id', t('The user id must be an integer')), - new Validators\Integer('status', t('The status must be an integer')), - new Validators\Numeric('time_estimated', t('The time must be a numeric value')), - new Validators\Numeric('time_spent', t('The time must be a numeric value')), - ); - } } diff --git a/sources/app/Model/Swimlane.php b/sources/app/Model/Swimlane.php index df44985..e5124e8 100644 --- a/sources/app/Model/Swimlane.php +++ b/sources/app/Model/Swimlane.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Swimlanes * @@ -470,85 +467,4 @@ class Swimlane extends Base return true; } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate default swimlane modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateDefaultModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The id is required')), - new Validators\Required('default_swimlane', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('The id must be an integer')), - new Validators\Integer('project_id', t('The project id must be an integer')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) - ); - } } diff --git a/sources/app/Model/Task.php b/sources/app/Model/Task.php index f1cd094..7aa9e31 100644 --- a/sources/app/Model/Task.php +++ b/sources/app/Model/Task.php @@ -41,6 +41,7 @@ class Task extends Base const EVENT_CREATE_UPDATE = 'task.create_update'; const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; const EVENT_OVERDUE = 'task.overdue'; + const EVENT_USER_MENTION = 'task.user.mention'; /** * Recurrence: status diff --git a/sources/app/Model/TaskCreation.php b/sources/app/Model/TaskCreation.php index 5ef1a04..975cc7a 100644 --- a/sources/app/Model/TaskCreation.php +++ b/sources/app/Model/TaskCreation.php @@ -86,8 +86,16 @@ class TaskCreation extends Base */ private function fireEvents($task_id, array $values) { - $values['task_id'] = $task_id; - $this->container['dispatcher']->dispatch(Task::EVENT_CREATE_UPDATE, new TaskEvent($values)); - $this->container['dispatcher']->dispatch(Task::EVENT_CREATE, new TaskEvent($values)); + $event = new TaskEvent(array('task_id' => $task_id) + $values); + + $this->logger->debug('Event fired: '.Task::EVENT_CREATE_UPDATE); + $this->logger->debug('Event fired: '.Task::EVENT_CREATE); + + $this->dispatcher->dispatch(Task::EVENT_CREATE_UPDATE, $event); + $this->dispatcher->dispatch(Task::EVENT_CREATE, $event); + + if (! empty($values['description'])) { + $this->userMention->fireEvents($values['description'], Task::EVENT_USER_MENTION, $event); + } } } diff --git a/sources/app/Model/TaskFinder.php b/sources/app/Model/TaskFinder.php index 9514fe4..836fbe4 100644 --- a/sources/app/Model/TaskFinder.php +++ b/sources/app/Model/TaskFinder.php @@ -122,6 +122,7 @@ class TaskFinder extends Base 'tasks.recurrence_parent', 'tasks.recurrence_child', 'tasks.time_estimated', + 'tasks.time_spent', User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name', Category::TABLE.'.name AS category_name', diff --git a/sources/app/Model/TaskLink.php b/sources/app/Model/TaskLink.php index 1ac5920..87aae55 100644 --- a/sources/app/Model/TaskLink.php +++ b/sources/app/Model/TaskLink.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use Kanboard\Event\TaskLinkEvent; /** @@ -261,59 +259,4 @@ class TaskLink extends Base return true; } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Required('task_id', t('Field required')), - new Validators\Required('opposite_task_id', t('Field required')), - new Validators\Required('link_id', t('Field required')), - new Validators\NotEquals('opposite_task_id', 'task_id', t('A task cannot be linked to itself')), - new Validators\Exists('opposite_task_id', t('This linked task id doesn\'t exists'), $this->db->getConnection(), Task::TABLE, 'id') - ); - } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, $this->commonValidationRules()); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('Field required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/TaskModification.php b/sources/app/Model/TaskModification.php index 781646b..a0ad292 100644 --- a/sources/app/Model/TaskModification.php +++ b/sources/app/Model/TaskModification.php @@ -17,16 +17,17 @@ class TaskModification extends Base * * @access public * @param array $values + * @param boolean $fire_events * @return boolean */ - public function update(array $values) + public function update(array $values, $fire_events = true) { $original_task = $this->taskFinder->getById($values['id']); $this->prepare($values); $result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values); - if ($result) { + if ($fire_events && $result) { $this->fireEvents($original_task, $values); } @@ -51,13 +52,14 @@ class TaskModification extends Base if ($this->isFieldModified('owner_id', $event_data['changes'])) { $events[] = Task::EVENT_ASSIGNEE_CHANGE; - } else { + } elseif (! empty($event_data['changes'])) { $events[] = Task::EVENT_CREATE_UPDATE; $events[] = Task::EVENT_UPDATE; } foreach ($events as $event) { - $this->container['dispatcher']->dispatch($event, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.$event); + $this->dispatcher->dispatch($event, new TaskEvent($event_data)); } } diff --git a/sources/app/Model/TaskPosition.php b/sources/app/Model/TaskPosition.php index da363cb..4c9928d 100644 --- a/sources/app/Model/TaskPosition.php +++ b/sources/app/Model/TaskPosition.php @@ -32,7 +32,6 @@ class TaskPosition extends Base $task = $this->taskFinder->getById($task_id); - // Ignore closed tasks if ($task['is_active'] == Task::STATUS_CLOSED) { return true; } @@ -167,7 +166,12 @@ class TaskPosition extends Base return false; } - return true; + $now = time(); + + return $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array( + 'date_moved' => $now, + 'date_modification' => $now, + )); } /** @@ -221,11 +225,14 @@ class TaskPosition extends Base ); if ($task['swimlane_id'] != $new_swimlane_id) { - $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.Task::EVENT_MOVE_SWIMLANE); + $this->dispatcher->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); } elseif ($task['column_id'] != $new_column_id) { - $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.Task::EVENT_MOVE_COLUMN); + $this->dispatcher->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); } elseif ($task['position'] != $new_position) { - $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.Task::EVENT_MOVE_POSITION); + $this->dispatcher->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); } } } diff --git a/sources/app/Model/TaskStatus.php b/sources/app/Model/TaskStatus.php index a5199ed..2b90281 100644 --- a/sources/app/Model/TaskStatus.php +++ b/sources/app/Model/TaskStatus.php @@ -61,6 +61,32 @@ class TaskStatus extends Base return $this->changeStatus($task_id, Task::STATUS_OPEN, 0, Task::EVENT_OPEN); } + /** + * Close multiple tasks + * + * @access public + * @param array $task_ids + */ + public function closeMultipleTasks(array $task_ids) + { + foreach ($task_ids as $task_id) { + $this->close($task_id); + } + } + + /** + * Close all tasks within a column/swimlane + * + * @access public + * @param integer $swimlane_id + * @param integer $column_id + */ + public function closeTasksBySwimlaneAndColumn($swimlane_id, $column_id) + { + $task_ids = $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->eq('column_id', $column_id)->findAllByColumn('id'); + $this->closeMultipleTasks($task_ids); + } + /** * Common method to change the status of task * @@ -87,10 +113,8 @@ class TaskStatus extends Base )); if ($result) { - $this->container['dispatcher']->dispatch( - $event, - new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id)) - ); + $this->logger->debug('Event fired: '.$event); + $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id))); } return $result; diff --git a/sources/app/Model/User.php b/sources/app/Model/User.php index 50e9b31..ac0e749 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/User.php @@ -3,9 +3,6 @@ namespace Kanboard\Model; use PicoDb\Database; -use SimpleValidator\Validator; -use SimpleValidator\Validators; -use Kanboard\Core\Session\SessionManager; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; @@ -369,132 +366,4 @@ class User extends Base ->eq('id', $user_id) ->save(array('token' => '')); } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\MaxLength('role', t('The maximum length is %d characters', 25), 25), - new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), - new Validators\Email('email', t('Email address invalid')), - new Validators\Integer('is_ldap_user', t('This value must be an integer')), - ); - } - - /** - * Common password validation rules - * - * @access private - * @return array - */ - private function commonPasswordValidationRules() - { - return array( - new Validators\Required('password', t('The password is required')), - new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6), - new Validators\Required('confirmation', t('The confirmation is required')), - new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), - ); - } - - /** - * Validate user creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('username', t('The username is required')), - ); - - if (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) { - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - } else { - $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules())); - } - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate user modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The user id is required')), - new Validators\Required('username', t('The username is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate user API modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateApiModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The user id is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate password modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validatePasswordModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The user id is required')), - new Validators\Required('current_password', t('The current password is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules())); - - if ($v->execute()) { - if ($this->authenticationManager->passwordAuthentication($this->userSession->getUsername(), $values['current_password'], false)) { - return array(true, array()); - } else { - return array(false, array('current_password' => array(t('Wrong password')))); - } - } - - return array(false, $v->getErrors()); - } } diff --git a/sources/app/Model/UserMention.php b/sources/app/Model/UserMention.php new file mode 100644 index 0000000..97a4e41 --- /dev/null +++ b/sources/app/Model/UserMention.php @@ -0,0 +1,61 @@ +db->table(User::TABLE) + ->columns('id', 'username', 'name', 'email', 'language') + ->eq('notifications_enabled', 1) + ->neq('id', $this->userSession->getId()) + ->in('username', array_unique($matches[1])) + ->findAll(); + } + + return $users; + } + + /** + * Fire events for user mentions + * + * @access public + * @param string $content + * @param string $eventName + * @param GenericEvent $event + */ + public function fireEvents($content, $eventName, GenericEvent $event) + { + if (empty($event['project_id'])) { + $event['project_id'] = $this->taskFinder->getProjectId($event['task_id']); + } + + $users = $this->getMentionedUsers($content); + + foreach ($users as $user) { + if ($this->projectPermission->isMember($event['project_id'], $user['id'])) { + $event['mention'] = $user; + $this->dispatcher->dispatch($eventName, $event); + } + } + } +} diff --git a/sources/app/Model/UserNotification.php b/sources/app/Model/UserNotification.php index e00f23c..e8a967a 100644 --- a/sources/app/Model/UserNotification.php +++ b/sources/app/Model/UserNotification.php @@ -21,18 +21,12 @@ class UserNotification extends Base */ public function sendNotifications($event_name, array $event_data) { - $logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0; - $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id); + $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $this->userSession->getId()); - if (! empty($users)) { - foreach ($users as $user) { - if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { - $this->sendUserNotification($user, $event_name, $event_data); - } + foreach ($users as $user) { + if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { + $this->sendUserNotification($user, $event_name, $event_data); } - - // Restore locales - $this->config->setupTranslations(); } } @@ -58,6 +52,9 @@ class UserNotification extends Base foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) { $this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data); } + + // Restore locales + $this->config->setupTranslations(); } /** @@ -74,7 +71,17 @@ class UserNotification extends Base return $this->getEverybodyWithNotificationEnabled($exclude_user_id); } - return $this->getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id); + $users = array(); + $members = $this->getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id); + $groups = $this->getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id); + + foreach (array_merge($members, $groups) as $user) { + if (! isset($users[$user['id']])) { + $users[$user['id']] = $user; + } + } + + return array_values($users); } /** @@ -145,14 +152,14 @@ class UserNotification extends Base } /** - * Get a list of project members with notification enabled + * Get a list of group members with notification enabled * * @access private * @param integer $project_id Project id * @param integer $exclude_user_id User id to exclude * @return array */ - private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id) + private function getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id) { return $this->db ->table(ProjectUserRole::TABLE) @@ -164,6 +171,19 @@ class UserNotification extends Base ->findAll(); } + private function getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id) + { + return $this->db + ->table(ProjectGroupRole::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') + ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->eq(ProjectGroupRole::TABLE.'.project_id', $project_id) + ->eq(User::TABLE.'.notifications_enabled', '1') + ->neq(User::TABLE.'.id', $exclude_user_id) + ->findAll(); + } + /** * Get a list of project members with notification enabled * diff --git a/sources/app/Notification/Mail.php b/sources/app/Notification/Mail.php index 98bffef..d05dbdf 100644 --- a/sources/app/Notification/Mail.php +++ b/sources/app/Notification/Mail.php @@ -121,6 +121,10 @@ class Mail extends Base implements NotificationInterface case Task::EVENT_ASSIGNEE_CHANGE: $subject = $this->getStandardMailSubject(e('Assignee change'), $event_data); break; + case Task::EVENT_USER_MENTION: + case Comment::EVENT_USER_MENTION: + $subject = $this->getStandardMailSubject(e('Mentioned'), $event_data); + break; case Task::EVENT_OVERDUE: $subject = e('[%s] Overdue tasks', $event_data['project_name']); break; diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index 3bdcc5e..c98e083 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -6,7 +6,53 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 98; +const VERSION = 101; + +function version_101(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE password_reset ( + token VARCHAR(80) PRIMARY KEY, + user_id INT NOT NULL, + date_expiration INT NOT NULL, + date_creation INT NOT NULL, + ip VARCHAR(45) NOT NULL, + user_agent VARCHAR(255) NOT NULL, + is_active TINYINT(1) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')"); +} + +function version_100(PDO $pdo) +{ + $pdo->exec('ALTER TABLE `actions` MODIFY `action_name` VARCHAR(255)'); +} + +function version_99(PDO $pdo) +{ + $rq = $pdo->prepare('SELECT * FROM actions'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?'); + + foreach ($rows as $row) { + if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn'; + } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskCloseColumn'; + } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') { + $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn'; + } elseif ($row['action_name']{0} !== '\\') { + $row['action_name'] = '\Kanboard\Action\\'.$row['action_name']; + } + + $rq->execute(array($row['action_name'], $row['id'])); + } +} function version_98(PDO $pdo) { @@ -1035,7 +1081,7 @@ function version_12(PDO $pdo) CREATE TABLE remember_me ( id INT NOT NULL AUTO_INCREMENT, user_id INT, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), token VARCHAR(255), sequence VARCHAR(255), @@ -1051,7 +1097,7 @@ function version_12(PDO $pdo) id INT NOT NULL AUTO_INCREMENT, auth_type VARCHAR(25), user_id INT, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), date_creation INT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index 23ed840..961d8f4 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -6,7 +6,53 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 78; +const VERSION = 81; + +function version_81(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE password_reset ( + token VARCHAR(80) PRIMARY KEY, + user_id INTEGER NOT NULL, + date_expiration INTEGER NOT NULL, + date_creation INTEGER NOT NULL, + ip VARCHAR(45) NOT NULL, + user_agent VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + "); + + $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')"); +} + +function version_80(PDO $pdo) +{ + $pdo->exec('ALTER TABLE "actions" ALTER COLUMN "action_name" TYPE VARCHAR(255)'); +} + +function version_79(PDO $pdo) +{ + $rq = $pdo->prepare('SELECT * FROM actions'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?'); + + foreach ($rows as $row) { + if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn'; + } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskCloseColumn'; + } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') { + $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn'; + } elseif ($row['action_name']{0} !== '\\') { + $row['action_name'] = '\Kanboard\Action\\'.$row['action_name']; + } + + $rq->execute(array($row['action_name'], $row['id'])); + } +} function version_78(PDO $pdo) { @@ -955,7 +1001,7 @@ function version_1(PDO $pdo) CREATE TABLE remember_me ( id SERIAL PRIMARY KEY, user_id INTEGER, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), token VARCHAR(255), sequence VARCHAR(255), @@ -968,7 +1014,7 @@ function version_1(PDO $pdo) id SERIAL PRIMARY KEY, auth_type VARCHAR(25), user_id INTEGER, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), date_creation INTEGER, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index 534c3f3..f1be0cf 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -6,7 +6,48 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 91; +const VERSION = 93; + +function version_93(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE password_reset ( + token TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + date_expiration INTEGER NOT NULL, + date_creation INTEGER NOT NULL, + ip TEXT NOT NULL, + user_agent TEXT NOT NULL, + is_active INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + "); + + $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')"); +} + +function version_92(PDO $pdo) +{ + $rq = $pdo->prepare('SELECT * FROM actions'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?'); + + foreach ($rows as $row) { + if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn'; + } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskCloseColumn'; + } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') { + $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn'; + } elseif ($row['action_name']{0} !== '\\') { + $row['action_name'] = '\Kanboard\Action\\'.$row['action_name']; + } + + $rq->execute(array($row['action_name'], $row['id'])); + } +} function version_91(PDO $pdo) { diff --git a/sources/app/ServiceProvider/ActionProvider.php b/sources/app/ServiceProvider/ActionProvider.php new file mode 100644 index 0000000..0aba29f --- /dev/null +++ b/sources/app/ServiceProvider/ActionProvider.php @@ -0,0 +1,78 @@ +register(new CommentCreation($container)); + $container['actionManager']->register(new CommentCreationMoveTaskColumn($container)); + $container['actionManager']->register(new TaskAssignCategoryColor($container)); + $container['actionManager']->register(new TaskAssignCategoryLabel($container)); + $container['actionManager']->register(new TaskAssignCategoryLink($container)); + $container['actionManager']->register(new TaskAssignColorCategory($container)); + $container['actionManager']->register(new TaskAssignColorColumn($container)); + $container['actionManager']->register(new TaskAssignColorLink($container)); + $container['actionManager']->register(new TaskAssignColorUser($container)); + $container['actionManager']->register(new TaskAssignCurrentUser($container)); + $container['actionManager']->register(new TaskAssignCurrentUserColumn($container)); + $container['actionManager']->register(new TaskAssignSpecificUser($container)); + $container['actionManager']->register(new TaskAssignUser($container)); + $container['actionManager']->register(new TaskClose($container)); + $container['actionManager']->register(new TaskCloseColumn($container)); + $container['actionManager']->register(new TaskCreation($container)); + $container['actionManager']->register(new TaskDuplicateAnotherProject($container)); + $container['actionManager']->register(new TaskEmail($container)); + $container['actionManager']->register(new TaskMoveAnotherProject($container)); + $container['actionManager']->register(new TaskMoveColumnAssigned($container)); + $container['actionManager']->register(new TaskMoveColumnCategoryChange($container)); + $container['actionManager']->register(new TaskMoveColumnUnAssigned($container)); + $container['actionManager']->register(new TaskOpen($container)); + $container['actionManager']->register(new TaskUpdateStartDate($container)); + + return $container; + } +} diff --git a/sources/app/ServiceProvider/AuthenticationProvider.php b/sources/app/ServiceProvider/AuthenticationProvider.php index 46ba5be..7617ba9 100644 --- a/sources/app/ServiceProvider/AuthenticationProvider.php +++ b/sources/app/ServiceProvider/AuthenticationProvider.php @@ -106,6 +106,7 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('Taskmodification', '*', Role::PROJECT_MEMBER); $acl->add('Taskstatus', '*', Role::PROJECT_MEMBER); $acl->add('Timer', '*', Role::PROJECT_MEMBER); + $acl->add('UserHelper', array('mention'), Role::PROJECT_MEMBER); return $acl; } @@ -125,7 +126,9 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC)); $acl->add('Oauth', array('google', 'github', 'gitlab'), Role::APP_PUBLIC); - $acl->add('Auth', array('login', 'check', 'captcha'), Role::APP_PUBLIC); + $acl->add('Auth', array('login', 'check'), Role::APP_PUBLIC); + $acl->add('Captcha', '*', Role::APP_PUBLIC); + $acl->add('PasswordReset', '*', Role::APP_PUBLIC); $acl->add('Webhook', '*', Role::APP_PUBLIC); $acl->add('Task', 'readonly', Role::APP_PUBLIC); $acl->add('Board', 'readonly', Role::APP_PUBLIC); diff --git a/sources/app/ServiceProvider/ClassProvider.php b/sources/app/ServiceProvider/ClassProvider.php index fad7304..df4e183 100644 --- a/sources/app/ServiceProvider/ClassProvider.php +++ b/sources/app/ServiceProvider/ClassProvider.php @@ -4,7 +4,6 @@ namespace Kanboard\ServiceProvider; use Pimple\Container; use Pimple\ServiceProviderInterface; -use League\HTMLToMarkdown\HtmlConverter; use Kanboard\Core\Mail\Client as EmailClient; use Kanboard\Core\ObjectStorage\FileStorage; use Kanboard\Core\Paginator; @@ -15,9 +14,16 @@ use Kanboard\Core\Http\Client as HttpClient; class ClassProvider implements ServiceProviderInterface { private $classes = array( + 'Analytic' => array( + 'TaskDistributionAnalytic', + 'UserDistributionAnalytic', + 'EstimatedTimeComparisonAnalytic', + 'AverageLeadCycleTimeAnalytic', + 'AverageTimeSpentColumnAnalytic', + ), 'Model' => array( 'Action', - 'Authentication', + 'ActionParameter', 'Board', 'Category', 'Color', @@ -32,9 +38,9 @@ class ClassProvider implements ServiceProviderInterface 'Link', 'Notification', 'OverdueNotification', + 'PasswordReset', 'Project', 'ProjectActivity', - 'ProjectAnalytic', 'ProjectDuplication', 'ProjectDailyColumnStats', 'ProjectDailyStats', @@ -42,7 +48,9 @@ class ClassProvider implements ServiceProviderInterface 'ProjectNotification', 'ProjectMetadata', 'ProjectGroupRole', + 'ProjectGroupRoleFilter', 'ProjectUserRole', + 'ProjectUserRoleFilter', 'RememberMeSession', 'Subtask', 'SubtaskExport', @@ -60,13 +68,13 @@ class ClassProvider implements ServiceProviderInterface 'TaskPermission', 'TaskPosition', 'TaskStatus', - 'TaskValidator', 'TaskImport', 'TaskMetadata', 'Transition', 'User', 'UserImport', 'UserLocking', + 'UserMention', 'UserNotification', 'UserNotificationFilter', 'UserUnreadNotification', @@ -81,12 +89,33 @@ class ClassProvider implements ServiceProviderInterface 'UserFilterAutoCompleteFormatter', 'GroupAutoCompleteFormatter', ), + 'Validator' => array( + 'ActionValidator', + 'AuthValidator', + 'CategoryValidator', + 'ColumnValidator', + 'CommentValidator', + 'CurrencyValidator', + 'CustomFilterValidator', + 'GroupValidator', + 'LinkValidator', + 'PasswordResetValidator', + 'ProjectValidator', + 'SubtaskValidator', + 'SwimlaneValidator', + 'TaskValidator', + 'TaskLinkValidator', + 'UserValidator', + ), 'Core' => array( 'DateParser', 'Helper', 'Lexer', 'Template', ), + 'Core\Event' => array( + 'EventManager', + ), 'Core\Http' => array( 'Request', 'Response', @@ -107,11 +136,6 @@ class ClassProvider implements ServiceProviderInterface 'UserSync', 'UserSession', 'UserProfile', - ), - 'Integration' => array( - 'BitbucketWebhook', - 'GithubWebhook', - 'GitlabWebhook', ) ); @@ -131,10 +155,6 @@ class ClassProvider implements ServiceProviderInterface return new HttpClient($c); }; - $container['htmlConverter'] = function () { - return new HtmlConverter(array('strip_tags' => true)); - }; - $container['objectStorage'] = function () { return new FileStorage(FILES_DIR); }; @@ -147,7 +167,11 @@ class ClassProvider implements ServiceProviderInterface return $mailer; }; - $container['cspRules'] = array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '* data:'); + $container['cspRules'] = array( + 'default-src' => "'self'", + 'style-src' => "'self' 'unsafe-inline'", + 'img-src' => '* data:', + ); return $container; } diff --git a/sources/app/ServiceProvider/EventDispatcherProvider.php b/sources/app/ServiceProvider/EventDispatcherProvider.php index 17141fd..880caa4 100644 --- a/sources/app/ServiceProvider/EventDispatcherProvider.php +++ b/sources/app/ServiceProvider/EventDispatcherProvider.php @@ -11,7 +11,6 @@ use Kanboard\Subscriber\NotificationSubscriber; use Kanboard\Subscriber\ProjectDailySummarySubscriber; use Kanboard\Subscriber\ProjectModificationDateSubscriber; use Kanboard\Subscriber\SubtaskTimeTrackingSubscriber; -use Kanboard\Subscriber\TaskMovedDateSubscriber; use Kanboard\Subscriber\TransitionSubscriber; use Kanboard\Subscriber\RecurringTaskSubscriber; @@ -26,13 +25,9 @@ class EventDispatcherProvider implements ServiceProviderInterface $container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container)); $container['dispatcher']->addSubscriber(new NotificationSubscriber($container)); $container['dispatcher']->addSubscriber(new SubtaskTimeTrackingSubscriber($container)); - $container['dispatcher']->addSubscriber(new TaskMovedDateSubscriber($container)); $container['dispatcher']->addSubscriber(new TransitionSubscriber($container)); $container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container)); - // Automatic actions - $container['action']->attachEvents(); - return $container; } } diff --git a/sources/app/ServiceProvider/RouteProvider.php b/sources/app/ServiceProvider/RouteProvider.php index 6b1d0d8..ce66090 100644 --- a/sources/app/ServiceProvider/RouteProvider.php +++ b/sources/app/ServiceProvider/RouteProvider.php @@ -4,6 +4,7 @@ namespace Kanboard\ServiceProvider; use Pimple\Container; use Pimple\ServiceProviderInterface; +use Kanboard\Core\Http\Route; use Kanboard\Core\Http\Router; /** @@ -24,175 +25,190 @@ class RouteProvider implements ServiceProviderInterface public function register(Container $container) { $container['router'] = new Router($container); + $container['route'] = new Route($container); if (ENABLE_URL_REWRITE) { + $container['route']->enable(); + // Dashboard - $container['router']->addRoute('dashboard', 'app', 'index'); - $container['router']->addRoute('dashboard/:user_id', 'app', 'index', array('user_id')); - $container['router']->addRoute('dashboard/:user_id/projects', 'app', 'projects', array('user_id')); - $container['router']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks', array('user_id')); - $container['router']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks', array('user_id')); - $container['router']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar', array('user_id')); - $container['router']->addRoute('dashboard/:user_id/activity', 'app', 'activity', array('user_id')); + $container['route']->addRoute('dashboard', 'app', 'index'); + $container['route']->addRoute('dashboard/:user_id', 'app', 'index'); + $container['route']->addRoute('dashboard/:user_id/projects', 'app', 'projects'); + $container['route']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks'); + $container['route']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks'); + $container['route']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar'); + $container['route']->addRoute('dashboard/:user_id/activity', 'app', 'activity'); // Search routes - $container['router']->addRoute('search', 'search', 'index'); - $container['router']->addRoute('search/:search', 'search', 'index', array('search')); + $container['route']->addRoute('search', 'search', 'index'); + $container['route']->addRoute('search/:search', 'search', 'index'); // Project routes - $container['router']->addRoute('projects', 'project', 'index'); - $container['router']->addRoute('project/create', 'project', 'create'); - $container['router']->addRoute('project/create/private', 'project', 'createPrivate'); - $container['router']->addRoute('project/:project_id', 'project', 'show', array('project_id')); - $container['router']->addRoute('p/:project_id', 'project', 'show', array('project_id')); - $container['router']->addRoute('project/:project_id/customer-filter', 'customfilter', 'index', array('project_id')); - $container['router']->addRoute('project/:project_id/share', 'project', 'share', array('project_id')); - $container['router']->addRoute('project/:project_id/notifications', 'project', 'notifications', array('project_id')); - $container['router']->addRoute('project/:project_id/edit', 'project', 'edit', array('project_id')); - $container['router']->addRoute('project/:project_id/integrations', 'project', 'integrations', array('project_id')); - $container['router']->addRoute('project/:project_id/duplicate', 'project', 'duplicate', array('project_id')); - $container['router']->addRoute('project/:project_id/remove', 'project', 'remove', array('project_id')); - $container['router']->addRoute('project/:project_id/disable', 'project', 'disable', array('project_id')); - $container['router']->addRoute('project/:project_id/enable', 'project', 'enable', array('project_id')); - $container['router']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index', array('project_id')); - $container['router']->addRoute('project/:project_id/import', 'taskImport', 'step1', array('project_id')); + $container['route']->addRoute('projects', 'project', 'index'); + $container['route']->addRoute('project/create', 'project', 'create'); + $container['route']->addRoute('project/create/private', 'project', 'createPrivate'); + $container['route']->addRoute('project/:project_id', 'project', 'show'); + $container['route']->addRoute('p/:project_id', 'project', 'show'); + $container['route']->addRoute('project/:project_id/customer-filter', 'customfilter', 'index'); + $container['route']->addRoute('project/:project_id/share', 'project', 'share'); + $container['route']->addRoute('project/:project_id/notifications', 'project', 'notifications'); + $container['route']->addRoute('project/:project_id/edit', 'project', 'edit'); + $container['route']->addRoute('project/:project_id/integrations', 'project', 'integrations'); + $container['route']->addRoute('project/:project_id/duplicate', 'project', 'duplicate'); + $container['route']->addRoute('project/:project_id/remove', 'project', 'remove'); + $container['route']->addRoute('project/:project_id/disable', 'project', 'disable'); + $container['route']->addRoute('project/:project_id/enable', 'project', 'enable'); + $container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index'); + $container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1'); + + // ProjectUser routes + $container['route']->addRoute('projects/managers/:user_id', 'projectuser', 'managers'); + $container['route']->addRoute('projects/members/:user_id', 'projectuser', 'members'); + $container['route']->addRoute('projects/tasks/:user_id/opens', 'projectuser', 'opens'); + $container['route']->addRoute('projects/tasks/:user_id/closed', 'projectuser', 'closed'); + $container['route']->addRoute('projects/managers', 'projectuser', 'managers'); // Action routes - $container['router']->addRoute('project/:project_id/actions', 'action', 'index', array('project_id')); - $container['router']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm', array('project_id', 'action_id')); + $container['route']->addRoute('project/:project_id/actions', 'action', 'index'); + $container['route']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm'); // Column routes - $container['router']->addRoute('project/:project_id/columns', 'column', 'index', array('project_id')); - $container['router']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit', array('project_id', 'column_id')); - $container['router']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm', array('project_id', 'column_id')); - $container['router']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move', array('project_id', 'column_id', 'direction')); + $container['route']->addRoute('project/:project_id/columns', 'column', 'index'); + $container['route']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit'); + $container['route']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm'); + $container['route']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move'); // Swimlane routes - $container['router']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index', array('project_id')); - $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit', array('project_id', 'swimlane_id')); - $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm', array('project_id', 'swimlane_id')); - $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable', array('project_id', 'swimlane_id')); - $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable', array('project_id', 'swimlane_id')); - $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup', array('project_id', 'swimlane_id')); - $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown', array('project_id', 'swimlane_id')); + $container['route']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown'); // Category routes - $container['router']->addRoute('project/:project_id/categories', 'category', 'index', array('project_id')); - $container['router']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit', array('project_id', 'category_id')); - $container['router']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm', array('project_id', 'category_id')); + $container['route']->addRoute('project/:project_id/categories', 'category', 'index'); + $container['route']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit'); + $container['route']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm'); // Task routes - $container['router']->addRoute('project/:project_id/task/:task_id', 'task', 'show', array('project_id', 'task_id')); - $container['router']->addRoute('t/:task_id', 'task', 'show', array('task_id')); - $container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token')); + $container['route']->addRoute('project/:project_id/task/:task_id', 'task', 'show'); + $container['route']->addRoute('t/:task_id', 'task', 'show'); + $container['route']->addRoute('public/task/:task_id/:token', 'task', 'readonly'); - $container['router']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove', array('project_id', 'task_id')); + $container['route']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task'); + $container['route']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot'); + $container['route']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions'); + $container['route']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics'); + $container['route']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove'); - $container['router']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence', array('project_id', 'task_id')); + $container['route']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit'); + $container['route']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description'); + $container['route']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence'); - $container['router']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close', array('task_id', 'project_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open', array('task_id', 'project_id')); + $container['route']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close'); + $container['route']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open'); - $container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id')); + $container['route']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate'); + $container['route']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy'); + $container['route']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy'); + $container['route']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move'); + $container['route']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move'); // Exports - $container['router']->addRoute('export/tasks/:project_id', 'export', 'tasks', array('project_id')); - $container['router']->addRoute('export/subtasks/:project_id', 'export', 'subtasks', array('project_id')); - $container['router']->addRoute('export/transitions/:project_id', 'export', 'transitions', array('project_id')); - $container['router']->addRoute('export/summary/:project_id', 'export', 'summary', array('project_id')); + $container['route']->addRoute('export/tasks/:project_id', 'export', 'tasks'); + $container['route']->addRoute('export/subtasks/:project_id', 'export', 'subtasks'); + $container['route']->addRoute('export/transitions/:project_id', 'export', 'transitions'); + $container['route']->addRoute('export/summary/:project_id', 'export', 'summary'); // Board routes - $container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id')); - $container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id')); - $container['router']->addRoute('public/board/:token', 'board', 'readonly', array('token')); + $container['route']->addRoute('board/:project_id', 'board', 'show'); + $container['route']->addRoute('b/:project_id', 'board', 'show'); + $container['route']->addRoute('public/board/:token', 'board', 'readonly'); // Calendar routes - $container['router']->addRoute('calendar/:project_id', 'calendar', 'show', array('project_id')); - $container['router']->addRoute('c/:project_id', 'calendar', 'show', array('project_id')); + $container['route']->addRoute('calendar/:project_id', 'calendar', 'show'); + $container['route']->addRoute('c/:project_id', 'calendar', 'show'); // Listing routes - $container['router']->addRoute('list/:project_id', 'listing', 'show', array('project_id')); - $container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id')); + $container['route']->addRoute('list/:project_id', 'listing', 'show'); + $container['route']->addRoute('l/:project_id', 'listing', 'show'); // Gantt routes - $container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id')); - $container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting')); + $container['route']->addRoute('gantt/:project_id', 'gantt', 'project'); + $container['route']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project'); // Subtask routes - $container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id')); - $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit', array('project_id', 'task_id', 'subtask_id')); + $container['route']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm'); + $container['route']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit'); // Feed routes - $container['router']->addRoute('feed/project/:token', 'feed', 'project', array('token')); - $container['router']->addRoute('feed/user/:token', 'feed', 'user', array('token')); + $container['route']->addRoute('feed/project/:token', 'feed', 'project'); + $container['route']->addRoute('feed/user/:token', 'feed', 'user'); // Ical routes - $container['router']->addRoute('ical/project/:token', 'ical', 'project', array('token')); - $container['router']->addRoute('ical/user/:token', 'ical', 'user', array('token')); + $container['route']->addRoute('ical/project/:token', 'ical', 'project'); + $container['route']->addRoute('ical/user/:token', 'ical', 'user'); // Users - $container['router']->addRoute('users', 'user', 'index'); - $container['router']->addRoute('user/show/:user_id', 'user', 'show', array('user_id')); - $container['router']->addRoute('user/show/:user_id/timesheet', 'user', 'timesheet', array('user_id')); - $container['router']->addRoute('user/show/:user_id/last-logins', 'user', 'last', array('user_id')); - $container['router']->addRoute('user/show/:user_id/sessions', 'user', 'sessions', array('user_id')); - $container['router']->addRoute('user/:user_id/edit', 'user', 'edit', array('user_id')); - $container['router']->addRoute('user/:user_id/password', 'user', 'password', array('user_id')); - $container['router']->addRoute('user/:user_id/share', 'user', 'share', array('user_id')); - $container['router']->addRoute('user/:user_id/notifications', 'user', 'notifications', array('user_id')); - $container['router']->addRoute('user/:user_id/accounts', 'user', 'external', array('user_id')); - $container['router']->addRoute('user/:user_id/integrations', 'user', 'integrations', array('user_id')); - $container['router']->addRoute('user/:user_id/authentication', 'user', 'authentication', array('user_id')); - $container['router']->addRoute('user/:user_id/remove', 'user', 'remove', array('user_id')); - $container['router']->addRoute('user/:user_id/2fa', 'twofactor', 'index', array('user_id')); + $container['route']->addRoute('users', 'user', 'index'); + $container['route']->addRoute('user/profile/:user_id', 'user', 'profile'); + $container['route']->addRoute('user/show/:user_id', 'user', 'show'); + $container['route']->addRoute('user/show/:user_id/timesheet', 'user', 'timesheet'); + $container['route']->addRoute('user/show/:user_id/last-logins', 'user', 'last'); + $container['route']->addRoute('user/show/:user_id/sessions', 'user', 'sessions'); + $container['route']->addRoute('user/:user_id/edit', 'user', 'edit'); + $container['route']->addRoute('user/:user_id/password', 'user', 'password'); + $container['route']->addRoute('user/:user_id/share', 'user', 'share'); + $container['route']->addRoute('user/:user_id/notifications', 'user', 'notifications'); + $container['route']->addRoute('user/:user_id/accounts', 'user', 'external'); + $container['route']->addRoute('user/:user_id/integrations', 'user', 'integrations'); + $container['route']->addRoute('user/:user_id/authentication', 'user', 'authentication'); + $container['route']->addRoute('user/:user_id/remove', 'user', 'remove'); + $container['route']->addRoute('user/:user_id/2fa', 'twofactor', 'index'); // Groups - $container['router']->addRoute('groups', 'group', 'index'); - $container['router']->addRoute('groups/create', 'group', 'create'); - $container['router']->addRoute('group/:group_id/associate', 'group', 'associate', array('group_id')); - $container['router']->addRoute('group/:group_id/dissociate/:user_id', 'group', 'dissociate', array('group_id', 'user_id')); - $container['router']->addRoute('group/:group_id/edit', 'group', 'edit', array('group_id')); - $container['router']->addRoute('group/:group_id/members', 'group', 'users', array('group_id')); - $container['router']->addRoute('group/:group_id/remove', 'group', 'confirm', array('group_id')); + $container['route']->addRoute('groups', 'group', 'index'); + $container['route']->addRoute('groups/create', 'group', 'create'); + $container['route']->addRoute('group/:group_id/associate', 'group', 'associate'); + $container['route']->addRoute('group/:group_id/dissociate/:user_id', 'group', 'dissociate'); + $container['route']->addRoute('group/:group_id/edit', 'group', 'edit'); + $container['route']->addRoute('group/:group_id/members', 'group', 'users'); + $container['route']->addRoute('group/:group_id/remove', 'group', 'confirm'); // Config - $container['router']->addRoute('settings', 'config', 'index'); - $container['router']->addRoute('settings/plugins', 'config', 'plugins'); - $container['router']->addRoute('settings/application', 'config', 'application'); - $container['router']->addRoute('settings/project', 'config', 'project'); - $container['router']->addRoute('settings/project', 'config', 'project'); - $container['router']->addRoute('settings/board', 'config', 'board'); - $container['router']->addRoute('settings/calendar', 'config', 'calendar'); - $container['router']->addRoute('settings/integrations', 'config', 'integrations'); - $container['router']->addRoute('settings/webhook', 'config', 'webhook'); - $container['router']->addRoute('settings/api', 'config', 'api'); - $container['router']->addRoute('settings/links', 'link', 'index'); - $container['router']->addRoute('settings/currencies', 'currency', 'index'); + $container['route']->addRoute('settings', 'config', 'index'); + $container['route']->addRoute('settings/plugins', 'config', 'plugins'); + $container['route']->addRoute('settings/application', 'config', 'application'); + $container['route']->addRoute('settings/project', 'config', 'project'); + $container['route']->addRoute('settings/project', 'config', 'project'); + $container['route']->addRoute('settings/board', 'config', 'board'); + $container['route']->addRoute('settings/calendar', 'config', 'calendar'); + $container['route']->addRoute('settings/integrations', 'config', 'integrations'); + $container['route']->addRoute('settings/webhook', 'config', 'webhook'); + $container['route']->addRoute('settings/api', 'config', 'api'); + $container['route']->addRoute('settings/links', 'link', 'index'); + $container['route']->addRoute('settings/currencies', 'currency', 'index'); // Doc - $container['router']->addRoute('documentation/:file', 'doc', 'show', array('file')); - $container['router']->addRoute('documentation', 'doc', 'show'); + $container['route']->addRoute('documentation/:file', 'doc', 'show'); + $container['route']->addRoute('documentation', 'doc', 'show'); // Auth routes - $container['router']->addRoute('oauth/google', 'oauth', 'google'); - $container['router']->addRoute('oauth/github', 'oauth', 'github'); - $container['router']->addRoute('oauth/gitlab', 'oauth', 'gitlab'); - $container['router']->addRoute('login', 'auth', 'login'); - $container['router']->addRoute('logout', 'auth', 'logout'); + $container['route']->addRoute('oauth/google', 'oauth', 'google'); + $container['route']->addRoute('oauth/github', 'oauth', 'github'); + $container['route']->addRoute('oauth/gitlab', 'oauth', 'gitlab'); + $container['route']->addRoute('login', 'auth', 'login'); + $container['route']->addRoute('logout', 'auth', 'logout'); + + // PasswordReset + $container['route']->addRoute('forgot-password', 'PasswordReset', 'create'); + $container['route']->addRoute('forgot-password/change/:token', 'PasswordReset', 'change'); } return $container; diff --git a/sources/app/Subscriber/AuthSubscriber.php b/sources/app/Subscriber/AuthSubscriber.php index f834afe..e839385 100644 --- a/sources/app/Subscriber/AuthSubscriber.php +++ b/sources/app/Subscriber/AuthSubscriber.php @@ -3,7 +3,6 @@ namespace Kanboard\Subscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Kanboard\Core\Base; use Kanboard\Core\Security\AuthenticationManager; use Kanboard\Core\Session\SessionManager; use Kanboard\Event\AuthSuccessEvent; @@ -15,7 +14,7 @@ use Kanboard\Event\AuthFailureEvent; * @package subscriber * @author Frederic Guillot */ -class AuthSubscriber extends Base implements EventSubscriberInterface +class AuthSubscriber extends BaseSubscriber implements EventSubscriberInterface { /** * Get event listeners @@ -41,6 +40,8 @@ class AuthSubscriber extends Base implements EventSubscriberInterface */ public function afterLogin(AuthSuccessEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + $userAgent = $this->request->getUserAgent(); $ipAddress = $this->request->getIpAddress(); @@ -70,6 +71,7 @@ class AuthSubscriber extends Base implements EventSubscriberInterface */ public function afterLogout() { + $this->logger->debug('Subscriber executed: '.__METHOD__); $credentials = $this->rememberMeCookie->read(); if ($credentials !== false) { @@ -90,6 +92,7 @@ class AuthSubscriber extends Base implements EventSubscriberInterface */ public function onLoginFailure(AuthFailureEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $username = $event->getUsername(); if (! empty($username)) { diff --git a/sources/app/Subscriber/BaseSubscriber.php b/sources/app/Subscriber/BaseSubscriber.php new file mode 100644 index 0000000..2e41da7 --- /dev/null +++ b/sources/app/Subscriber/BaseSubscriber.php @@ -0,0 +1,40 @@ +called[$key])) { + return true; + } + + $this->called[$key] = true; + + return false; + } +} diff --git a/sources/app/Subscriber/BootstrapSubscriber.php b/sources/app/Subscriber/BootstrapSubscriber.php index e399f68..ef0215f 100644 --- a/sources/app/Subscriber/BootstrapSubscriber.php +++ b/sources/app/Subscriber/BootstrapSubscriber.php @@ -4,20 +4,25 @@ namespace Kanboard\Subscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class BootstrapSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - 'app.bootstrap' => array('setup', 0), + 'app.bootstrap' => 'execute', ); } - public function setup() + public function execute() { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->config->setupTranslations(); $this->config->setupTimezone(); - $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + $this->actionManager->attachEvents(); + + if ($this->userSession->isLogged()) { + $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + } } public function __destruct() diff --git a/sources/app/Subscriber/NotificationSubscriber.php b/sources/app/Subscriber/NotificationSubscriber.php index 394573e..0766005 100644 --- a/sources/app/Subscriber/NotificationSubscriber.php +++ b/sources/app/Subscriber/NotificationSubscriber.php @@ -9,34 +9,43 @@ use Kanboard\Model\Subtask; use Kanboard\Model\File; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class NotificationSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class NotificationSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE => array('execute', 0), - Task::EVENT_UPDATE => array('execute', 0), - Task::EVENT_CLOSE => array('execute', 0), - Task::EVENT_OPEN => array('execute', 0), - Task::EVENT_MOVE_COLUMN => array('execute', 0), - Task::EVENT_MOVE_POSITION => array('execute', 0), - Task::EVENT_MOVE_SWIMLANE => array('execute', 0), - Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), - Subtask::EVENT_CREATE => array('execute', 0), - Subtask::EVENT_UPDATE => array('execute', 0), - Comment::EVENT_CREATE => array('execute', 0), - Comment::EVENT_UPDATE => array('execute', 0), - File::EVENT_CREATE => array('execute', 0), + Task::EVENT_USER_MENTION => 'handleEvent', + Task::EVENT_CREATE => 'handleEvent', + Task::EVENT_UPDATE => 'handleEvent', + Task::EVENT_CLOSE => 'handleEvent', + Task::EVENT_OPEN => 'handleEvent', + Task::EVENT_MOVE_COLUMN => 'handleEvent', + Task::EVENT_MOVE_POSITION => 'handleEvent', + Task::EVENT_MOVE_SWIMLANE => 'handleEvent', + Task::EVENT_ASSIGNEE_CHANGE => 'handleEvent', + Subtask::EVENT_CREATE => 'handleEvent', + Subtask::EVENT_UPDATE => 'handleEvent', + Comment::EVENT_CREATE => 'handleEvent', + Comment::EVENT_UPDATE => 'handleEvent', + Comment::EVENT_USER_MENTION => 'handleEvent', + File::EVENT_CREATE => 'handleEvent', ); } - public function execute(GenericEvent $event, $event_name) + public function handleEvent(GenericEvent $event, $event_name) { - $event_data = $this->getEventData($event); + if (! $this->isExecuted($event_name)) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + $event_data = $this->getEventData($event); - if (! empty($event_data)) { - $this->userNotification->sendNotifications($event_name, $event_data); - $this->projectNotification->sendNotifications($event_data['task']['project_id'], $event_name, $event_data); + if (! empty($event_data)) { + if (! empty($event['mention'])) { + $this->userNotification->sendUserNotification($event['mention'], $event_name, $event_data); + } else { + $this->userNotification->sendNotifications($event_name, $event_data); + $this->projectNotification->sendNotifications($event_data['task']['project_id'], $event_name, $event_data); + } + } } } diff --git a/sources/app/Subscriber/ProjectDailySummarySubscriber.php b/sources/app/Subscriber/ProjectDailySummarySubscriber.php index bfa6cd4..44138f4 100644 --- a/sources/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/sources/app/Subscriber/ProjectDailySummarySubscriber.php @@ -6,22 +6,23 @@ use Kanboard\Event\TaskEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class ProjectDailySummarySubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE => array('execute', 0), - Task::EVENT_UPDATE => array('execute', 0), - Task::EVENT_CLOSE => array('execute', 0), - Task::EVENT_OPEN => array('execute', 0), - Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_CREATE_UPDATE => 'execute', + Task::EVENT_CLOSE => 'execute', + Task::EVENT_OPEN => 'execute', + Task::EVENT_MOVE_COLUMN => 'execute', + Task::EVENT_MOVE_SWIMLANE => 'execute', ); } public function execute(TaskEvent $event) { - if (isset($event['project_id'])) { + if (isset($event['project_id']) && !$this->isExecuted()) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->projectDailyColumnStats->updateTotals($event['project_id'], date('Y-m-d')); $this->projectDailyStats->updateTotals($event['project_id'], date('Y-m-d')); } diff --git a/sources/app/Subscriber/ProjectModificationDateSubscriber.php b/sources/app/Subscriber/ProjectModificationDateSubscriber.php index 1f99840..62804a8 100644 --- a/sources/app/Subscriber/ProjectModificationDateSubscriber.php +++ b/sources/app/Subscriber/ProjectModificationDateSubscriber.php @@ -6,25 +6,26 @@ use Kanboard\Event\GenericEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class ProjectModificationDateSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class ProjectModificationDateSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE_UPDATE => array('execute', 0), - Task::EVENT_CLOSE => array('execute', 0), - Task::EVENT_OPEN => array('execute', 0), - Task::EVENT_MOVE_SWIMLANE => array('execute', 0), - Task::EVENT_MOVE_COLUMN => array('execute', 0), - Task::EVENT_MOVE_POSITION => array('execute', 0), - Task::EVENT_MOVE_PROJECT => array('execute', 0), - Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), + Task::EVENT_CREATE_UPDATE => 'execute', + Task::EVENT_CLOSE => 'execute', + Task::EVENT_OPEN => 'execute', + Task::EVENT_MOVE_SWIMLANE => 'execute', + Task::EVENT_MOVE_COLUMN => 'execute', + Task::EVENT_MOVE_POSITION => 'execute', + Task::EVENT_MOVE_PROJECT => 'execute', + Task::EVENT_ASSIGNEE_CHANGE => 'execute', ); } public function execute(GenericEvent $event) { - if (isset($event['project_id'])) { + if (isset($event['project_id']) && !$this->isExecuted()) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->project->updateModificationDate($event['project_id']); } } diff --git a/sources/app/Subscriber/RecurringTaskSubscriber.php b/sources/app/Subscriber/RecurringTaskSubscriber.php index b03ffcf..6d5aee8 100644 --- a/sources/app/Subscriber/RecurringTaskSubscriber.php +++ b/sources/app/Subscriber/RecurringTaskSubscriber.php @@ -6,18 +6,20 @@ use Kanboard\Event\TaskEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class RecurringTaskSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_MOVE_COLUMN => array('onMove', 0), - Task::EVENT_CLOSE => array('onClose', 0), + Task::EVENT_MOVE_COLUMN => 'onMove', + Task::EVENT_CLOSE => 'onClose', ); } public function onMove(TaskEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) { $this->taskDuplication->duplicateRecurringTask($event['task_id']); @@ -29,6 +31,8 @@ class RecurringTaskSubscriber extends \Kanboard\Core\Base implements EventSubscr public function onClose(TaskEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == Task::RECURRING_TRIGGER_CLOSE) { $this->taskDuplication->duplicateRecurringTask($event['task_id']); } diff --git a/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php index b5e0354..c0852bc 100644 --- a/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php +++ b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php @@ -6,13 +6,13 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Kanboard\Model\Subtask; use Kanboard\Event\SubtaskEvent; -class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class SubtaskTimeTrackingSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Subtask::EVENT_CREATE => array('updateTaskTime', 0), - Subtask::EVENT_DELETE => array('updateTaskTime', 0), + Subtask::EVENT_CREATE => 'updateTaskTime', + Subtask::EVENT_DELETE => 'updateTaskTime', Subtask::EVENT_UPDATE => array( array('logStartEnd', 10), array('updateTaskTime', 0), @@ -23,6 +23,7 @@ class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements Event public function updateTaskTime(SubtaskEvent $event) { if (isset($event['task_id'])) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']); } } @@ -30,6 +31,7 @@ class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements Event public function logStartEnd(SubtaskEvent $event) { if (isset($event['status']) && $this->config->get('subtask_time_tracking') == 1) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $subtask = $this->subtask->getById($event['id']); if (empty($subtask['user_id'])) { diff --git a/sources/app/Subscriber/TaskMovedDateSubscriber.php b/sources/app/Subscriber/TaskMovedDateSubscriber.php deleted file mode 100644 index 9857f4b..0000000 --- a/sources/app/Subscriber/TaskMovedDateSubscriber.php +++ /dev/null @@ -1,25 +0,0 @@ - array('execute', 0), - Task::EVENT_MOVE_SWIMLANE => array('execute', 0), - ); - } - - public function execute(TaskEvent $event) - { - if (isset($event['task_id'])) { - $this->container['db']->table(Task::TABLE)->eq('id', $event['task_id'])->update(array('date_moved' => time())); - } - } -} diff --git a/sources/app/Subscriber/TransitionSubscriber.php b/sources/app/Subscriber/TransitionSubscriber.php index 35352dc..bd53748 100644 --- a/sources/app/Subscriber/TransitionSubscriber.php +++ b/sources/app/Subscriber/TransitionSubscriber.php @@ -6,17 +6,19 @@ use Kanboard\Event\TaskEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class TransitionSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class TransitionSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => 'execute', ); } public function execute(TaskEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + $user_id = $this->userSession->getId(); if (! empty($user_id)) { diff --git a/sources/app/Template/action/index.php b/sources/app/Template/action/index.php index bf2f747..8275f08 100644 --- a/sources/app/Template/action/index.php +++ b/sources/app/Template/action/index.php @@ -28,24 +28,24 @@
= t('Not enough data to show the graph.') ?>
+ += t('No tasks found.') ?>
+ isEmpty()): ?> += $paginator->order(t('Id'), 'tasks.id') ?> | += $paginator->order(t('Title'), 'tasks.title') ?> | += $paginator->order(t('Status'), 'tasks.is_active') ?> | += $paginator->order(t('Estimated Time'), 'tasks.time_estimated') ?> | += $paginator->order(t('Actual Time'), 'tasks.time_spent') ?> | +
---|---|---|---|---|
+ = $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + | ++ = $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + | ++ + = t('Open') ?> + + = t('Closed') ?> + + | ++ = $this->e($task['time_estimated']) ?> + | ++ = $this->e($task['time_spent']) ?> + | +
= t('Your are not member of any project.') ?>
diff --git a/sources/app/Template/app/sidebar.php b/sources/app/Template/app/sidebar.php index 552a9c2..b5e14aa 100644 --- a/sources/app/Template/app/sidebar.php +++ b/sources/app/Template/app/sidebar.php @@ -1,25 +1,25 @@