diff options
268 files changed, 13051 insertions, 936 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2ddc7146..0453d535 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -11,8 +11,10 @@ Contributors: - [Ally Raza](https://github.com/alirz23) - [Angystardust](https://github.com/angystardust) - [Anjar Febrianto](https://github.com/Lasut) +- [Anton](https://github.com/tester22) - [Ashbike](https://github.com/ashbike) - [Ashish Kulkarni](https://github.com/ashkulz) +- [Biniou180](https://github.com/Biniou180) - [Bitcoin 333](https://github.com/bitcoin333) - [Busfreak](https://github.com/Busfreak) - [Christian González](https://github.com/nerdoc) @@ -114,6 +116,7 @@ Contributors: - [StavrosKa](https://github.com/StavrosKa) - [Sylvain Veyrié](https://github.com/turb) - [Thomas Lutz](https://github.com/phoen1x) +- [Thomas Stinner](https://github.com/stinnux) - [Timo](https://github.com/BlueTeck) - [Timotheus Pokorra](https://github.com/tpokorra) - [Tomáš Votruba](https://github.com/TomasVotruba) @@ -127,6 +130,7 @@ Contributors: - [Vitaliy S. Orlov](https://github.com/orlov0562) - [Vladimir Babin](https://github.com/Chiliec) - [Yannick Ihmels](https://github.com/ihmels) +- [Yakovenkov](https://github.com/yakovenkov) - [Ybarc](https://github.com/ybarc) - [Yu Yongwoo](https://github.com/uyu423) - [Yuichi Murata](https://github.com/yuichi1004) @@ -1,3 +1,27 @@ +Version 1.0.32 (unreleased) +-------------- + +New features: + +* New automated action to close tasks without activity in a specific column +* Added new event for removed comments +* Added search filter for task priority +* Added the possibility to hide tasks in dashboard for a specific column + +Improvements: + +* Handle header X-Real-IP to get IP address +* Display project name for task auto-complete fields +* Make search attributes not case sensitive +* Display TOTP issuer for 2FA +* Make sure that the table schema_version use InnoDB for Mysql + +Bug fixes: + +* Fixed search query with multiple assignees (nested OR conditions) +* Fixed Markdown editor auto-grow on the task form (Safari) +* Fixed compatibility issue with PHP 5.3 for OAuthUserProvider class + Version 1.0.31 -------------- @@ -32,6 +56,10 @@ Bug fixes: * Take default swimlane into consideration for SwimlaneModel::getFirstActiveSwimlane() * Fixed "due today" highlighting +Breaking changes: + +* Docker volume paths are changed to /var/www/app/{data,plugins} + Version 1.0.30 -------------- @@ -4,6 +4,7 @@ COPY . /var/www/app COPY docker/kanboard/config.php /var/www/app/config.php COPY docker/crontab/cronjob.alpine /var/spool/cron/crontabs/nginx COPY docker/services.d/cron /etc/services.d/cron +COPY docker/php/env.conf /etc/php7/php-fpm.d/env.conf RUN cd /var/www/app && composer --prefer-dist --no-dev --optimize-autoloader --quiet install RUN chown -R nginx:nginx /var/www/app/data /var/www/app/plugins @@ -9,7 +9,7 @@ static: archive: @ echo "Build archive: version=${version}, destination=${dst}" @ rm -rf ${BUILD_DIR}/kanboard ${BUILD_DIR}/kanboard-*.zip - @ cd ${BUILD_DIR} && git clone --depth 1 -q https://github.com/fguillot/kanboard.git + @ cd ${BUILD_DIR} && git clone --depth 1 -q https://github.com/kanboard/kanboard.git @ cd ${BUILD_DIR}/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install @ rm -rf ${BUILD_DIR}/kanboard/data/*.sqlite @ rm -rf ${BUILD_DIR}/kanboard/data/*.log @@ -1,10 +1,8 @@ Kanboard ======== -[](https://travis-ci.org/fguillot/kanboard) -[](https://scrutinizer-ci.com/g/fguillot/kanboard/) -[](https://insight.sensiolabs.com/projects/5e50750e-fc62-4a1f-b02a-71991123a2a7) -[](https://gitter.im/kanboard/kanboard) +[](https://travis-ci.org/kanboard/kanboard) +[](https://scrutinizer-ci.com/g/kanboard/kanboard/?branch=master) Kanboard is a project management software that focus on the Kanban methodology. @@ -15,16 +13,17 @@ Official website: <https://kanboard.net> - Open source and self-hosted - Super simple installation - Translated in many languages -- Distributed under [MIT License](https://github.com/fguillot/kanboard/blob/master/LICENSE) +- Distributed under [MIT License](https://github.com/kanboard/kanboard/blob/master/LICENSE) - The complete [list of features are available on the website](https://kanboard.net/features) -- [Change Log](https://github.com/fguillot/kanboard/blob/master/ChangeLog) -- [Documentation](https://github.com/fguillot/kanboard/blob/master/doc/index.markdown) +- [Change Log](https://github.com/kanboard/kanboard/blob/master/ChangeLog) +- [Documentation](https://github.com/kanboard/kanboard/blob/master/doc/index.markdown) +- IRC channel: [#kanboard](ircs://chat.freenode.net:6697/#kanboard) (Freenode) Authors ------- - Main developer: [Frédéric Guillot](https://github.com/fguillot) -- [List of contributors](https://github.com/fguillot/kanboard/blob/master/CONTRIBUTORS.md) +- [List of contributors](https://github.com/kanboard/kanboard/blob/master/CONTRIBUTORS.md) Installation and Upgrade ------------------------ @@ -1,7 +1,7 @@ { "name": "Kanboard", "description": "Kanboard is a simple visual task board", - "repository": "https://github.com/fguillot/kanboard", + "repository": "https://github.com/kanboard/kanboard", "logo": "https://kanboard.net/assets/img/icon.svg", "keywords": ["kanboard", "kanban", "php", "agile"], "addons": ["heroku-postgresql:hobby-dev"] diff --git a/app/Action/Base.php b/app/Action/Base.php index e5c65a17..e0ed8bde 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -216,7 +216,8 @@ abstract class Base extends \Kanboard\Core\Base */ public function hasRequiredProject(array $data) { - return isset($data['project_id']) && $data['project_id'] == $this->getProjectId(); + return (isset($data['project_id']) && $data['project_id'] == $this->getProjectId()) || + (isset($data['task']['project_id']) && $data['task']['project_id'] == $this->getProjectId()); } /** @@ -226,10 +227,14 @@ abstract class Base extends \Kanboard\Core\Base * @param array $data Event data dictionary * @return bool True if all keys are there */ - public function hasRequiredParameters(array $data) + public function hasRequiredParameters(array $data, array $parameters = array()) { - foreach ($this->getEventRequiredParameters() as $parameter) { - if (! isset($data[$parameter])) { + $parameters = $parameters ?: $this->getEventRequiredParameters(); + + foreach ($parameters as $key => $value) { + if (is_array($value)) { + return isset($data[$key]) && $this->hasRequiredParameters($data[$key], $value); + } else if (! isset($data[$value])) { return false; } } diff --git a/app/Action/CommentCreationMoveTaskColumn.php b/app/Action/CommentCreationMoveTaskColumn.php index 1b16f481..8ab792ad 100644 --- a/app/Action/CommentCreationMoveTaskColumn.php +++ b/app/Action/CommentCreationMoveTaskColumn.php @@ -55,7 +55,13 @@ class CommentCreationMoveTaskColumn extends Base */ public function getEventRequiredParameters() { - return array('task_id', 'column_id'); + return array( + 'task_id', + 'task' => array( + 'column_id', + 'project_id', + ), + ); } /** @@ -71,7 +77,7 @@ class CommentCreationMoveTaskColumn extends Base return false; } - $column = $this->columnModel->getById($data['column_id']); + $column = $this->columnModel->getById($data['task']['column_id']); return (bool) $this->commentModel->create(array( 'comment' => t('Moved to column %s', $column['title']), @@ -89,6 +95,6 @@ class CommentCreationMoveTaskColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php index fc486870..2df90b2c 100644 --- a/app/Action/TaskAssignCategoryColor.php +++ b/app/Action/TaskAssignCategoryColor.php @@ -60,7 +60,10 @@ class TaskAssignCategoryColor extends Base { return array( 'task_id', - 'color_id', + 'task' => array( + 'project_id', + 'color_id', + ), ); } @@ -90,6 +93,6 @@ class TaskAssignCategoryColor extends Base */ public function hasRequiredCondition(array $data) { - return $data['color_id'] == $this->getParam('color_id'); + return $data['task']['color_id'] == $this->getParam('color_id'); } } diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php index 284b8f40..91860be4 100644 --- a/app/Action/TaskAssignColorCategory.php +++ b/app/Action/TaskAssignColorCategory.php @@ -60,7 +60,10 @@ class TaskAssignColorCategory extends Base { return array( 'task_id', - 'category_id', + 'task' => array( + 'project_id', + 'category_id', + ), ); } @@ -90,6 +93,6 @@ class TaskAssignColorCategory extends Base */ public function hasRequiredCondition(array $data) { - return $data['category_id'] == $this->getParam('category_id'); + return $data['task']['category_id'] == $this->getParam('category_id'); } } diff --git a/app/Action/TaskAssignColorColumn.php b/app/Action/TaskAssignColorColumn.php index 57fd6f44..6c674b1f 100644 --- a/app/Action/TaskAssignColorColumn.php +++ b/app/Action/TaskAssignColorColumn.php @@ -61,7 +61,10 @@ class TaskAssignColorColumn extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -91,6 +94,6 @@ class TaskAssignColorColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskAssignColorPriority.php b/app/Action/TaskAssignColorPriority.php index eae1b771..57000ba8 100644 --- a/app/Action/TaskAssignColorPriority.php +++ b/app/Action/TaskAssignColorPriority.php @@ -60,7 +60,10 @@ class TaskAssignColorPriority extends Base { return array( 'task_id', - 'priority', + 'task' => array( + 'project_id', + 'priority', + ), ); } @@ -90,6 +93,6 @@ class TaskAssignColorPriority extends Base */ public function hasRequiredCondition(array $data) { - return $data['priority'] == $this->getParam('priority'); + return $data['task']['priority'] == $this->getParam('priority'); } } diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php index 4bcf7a5c..385db793 100644 --- a/app/Action/TaskAssignColorUser.php +++ b/app/Action/TaskAssignColorUser.php @@ -61,7 +61,10 @@ class TaskAssignColorUser extends Base { return array( 'task_id', - 'owner_id', + 'task' => array( + 'project_id', + 'owner_id', + ), ); } @@ -91,6 +94,6 @@ class TaskAssignColorUser extends Base */ public function hasRequiredCondition(array $data) { - return $data['owner_id'] == $this->getParam('user_id'); + return $data['task']['owner_id'] == $this->getParam('user_id'); } } diff --git a/app/Action/TaskAssignCurrentUserColumn.php b/app/Action/TaskAssignCurrentUserColumn.php index bc28a90b..e4eade33 100644 --- a/app/Action/TaskAssignCurrentUserColumn.php +++ b/app/Action/TaskAssignCurrentUserColumn.php @@ -59,7 +59,10 @@ class TaskAssignCurrentUserColumn extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -93,6 +96,6 @@ class TaskAssignCurrentUserColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php index 50a2b2ae..2c7dcacd 100644 --- a/app/Action/TaskAssignSpecificUser.php +++ b/app/Action/TaskAssignSpecificUser.php @@ -61,7 +61,10 @@ class TaskAssignSpecificUser extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -91,6 +94,6 @@ class TaskAssignSpecificUser extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskCloseColumn.php b/app/Action/TaskCloseColumn.php index 1edce8fa..4f1ffc92 100644 --- a/app/Action/TaskCloseColumn.php +++ b/app/Action/TaskCloseColumn.php @@ -55,7 +55,13 @@ class TaskCloseColumn extends Base */ public function getEventRequiredParameters() { - return array('task_id', 'column_id'); + return array( + 'task_id', + 'task' => array( + 'project_id', + 'column_id', + ) + ); } /** @@ -79,6 +85,6 @@ class TaskCloseColumn extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskCloseNoActivityColumn.php b/app/Action/TaskCloseNoActivityColumn.php new file mode 100644 index 00000000..7af0b7fc --- /dev/null +++ b/app/Action/TaskCloseNoActivityColumn.php @@ -0,0 +1,96 @@ +<?php + +namespace Kanboard\Action; + +use Kanboard\Model\TaskModel; + +/** + * Close automatically a task after inactive and in an defined column + * + * @package action + * @author Frederic Guillot + */ +class TaskCloseNoActivityColumn extends Base +{ + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Close a task when there is no activity in an specific column'); + } + + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array(TaskModel::EVENT_DAILY_CRONJOB); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'duration' => t('Duration in days'), + 'column_id' => t('Column') + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('tasks'); + } + + /** + * Execute the action (close the task) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + $results = array(); + $max = $this->getParam('duration') * 86400; + + foreach ($data['tasks'] as $task) { + $duration = time() - $task['date_modification']; + + if ($duration > $max && $task['column_id'] == $this->getParam('column_id')) { + $results[] = $this->taskStatusModel->close($task['id']); + } + } + + return in_array(true, $results, true); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return count($data['tasks']) > 0; + } +} diff --git a/app/Action/TaskCreation.php b/app/Action/TaskCreation.php index e9e5c5f3..0620afd3 100644 --- a/app/Action/TaskCreation.php +++ b/app/Action/TaskCreation.php @@ -52,6 +52,7 @@ class TaskCreation extends Base public function getEventRequiredParameters() { return array( + 'project_id', 'reference', 'title', ); diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php index d70d2ee8..d6d8d51f 100644 --- a/app/Action/TaskDuplicateAnotherProject.php +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -62,7 +62,10 @@ class TaskDuplicateAnotherProject extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ) ); } @@ -76,7 +79,12 @@ class TaskDuplicateAnotherProject extends Base public function doAction(array $data) { $destination_column_id = $this->columnModel->getFirstColumnId($this->getParam('project_id')); - return (bool) $this->taskProjectDuplicationModel->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); + return (bool) $this->taskProjectDuplicationModel->duplicateToProject( + $data['task_id'], + $this->getParam('project_id'), + null, + $destination_column_id + ); } /** @@ -88,6 +96,6 @@ class TaskDuplicateAnotherProject extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id'); + return $data['task']['column_id'] == $this->getParam('column_id') && $data['task']['project_id'] != $this->getParam('project_id'); } } diff --git a/app/Action/TaskEmail.php b/app/Action/TaskEmail.php index 7f9ba416..526e9aa8 100644 --- a/app/Action/TaskEmail.php +++ b/app/Action/TaskEmail.php @@ -62,7 +62,10 @@ class TaskEmail extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -78,13 +81,14 @@ class TaskEmail extends Base $user = $this->userModel->getById($this->getParam('user_id')); if (! empty($user['email'])) { - $task = $this->taskFinderModel->getDetails($data['task_id']); - $this->emailClient->send( $user['email'], $user['name'] ?: $user['username'], $this->getParam('subject'), - $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->configModel->get('application_url'))) + $this->template->render('notification/task_create', array( + 'task' => $data['task'], + 'application_url' => $this->configModel->get('application_url'), + )) ); return true; @@ -102,6 +106,6 @@ class TaskEmail extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php index 66635a63..148b6b0c 100644 --- a/app/Action/TaskMoveAnotherProject.php +++ b/app/Action/TaskMoveAnotherProject.php @@ -61,8 +61,10 @@ class TaskMoveAnotherProject extends Base { return array( 'task_id', - 'column_id', - 'project_id', + 'task' => array( + 'project_id', + 'column_id', + ) ); } @@ -87,6 +89,6 @@ class TaskMoveAnotherProject extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id'); + return $data['task']['column_id'] == $this->getParam('column_id') && $data['task']['project_id'] != $this->getParam('project_id'); } } diff --git a/app/Action/TaskMoveColumnAssigned.php b/app/Action/TaskMoveColumnAssigned.php index 7e3db9c5..1c1f657a 100644 --- a/app/Action/TaskMoveColumnAssigned.php +++ b/app/Action/TaskMoveColumnAssigned.php @@ -61,8 +61,13 @@ class TaskMoveColumnAssigned extends Base { return array( 'task_id', - 'column_id', - 'owner_id' + 'task' => array( + 'project_id', + 'column_id', + 'owner_id', + 'position', + 'swimlane_id', + ) ); } @@ -75,14 +80,12 @@ class TaskMoveColumnAssigned extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPositionModel->movePosition( - $data['project_id'], + $data['task']['project_id'], $data['task_id'], $this->getParam('dest_column_id'), - $original_task['position'], - $original_task['swimlane_id'], + $data['task']['position'], + $data['task']['swimlane_id'], false ); } @@ -96,6 +99,6 @@ class TaskMoveColumnAssigned extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('src_column_id') && $data['owner_id'] > 0; + return $data['task']['column_id'] == $this->getParam('src_column_id') && $data['task']['owner_id'] > 0; } } diff --git a/app/Action/TaskMoveColumnCategoryChange.php b/app/Action/TaskMoveColumnCategoryChange.php index e4f88760..4c2b289a 100644 --- a/app/Action/TaskMoveColumnCategoryChange.php +++ b/app/Action/TaskMoveColumnCategoryChange.php @@ -60,8 +60,13 @@ class TaskMoveColumnCategoryChange extends Base { return array( 'task_id', - 'column_id', - 'category_id', + 'task' => array( + 'project_id', + 'column_id', + 'category_id', + 'position', + 'swimlane_id', + ) ); } @@ -74,14 +79,12 @@ class TaskMoveColumnCategoryChange extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPositionModel->movePosition( - $data['project_id'], + $data['task']['project_id'], $data['task_id'], $this->getParam('dest_column_id'), - $original_task['position'], - $original_task['swimlane_id'], + $data['task']['position'], + $data['task']['swimlane_id'], false ); } @@ -95,6 +98,6 @@ class TaskMoveColumnCategoryChange extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] != $this->getParam('dest_column_id') && $data['category_id'] == $this->getParam('category_id'); + return $data['task']['column_id'] != $this->getParam('dest_column_id') && $data['task']['category_id'] == $this->getParam('category_id'); } } diff --git a/app/Action/TaskMoveColumnUnAssigned.php b/app/Action/TaskMoveColumnUnAssigned.php index c3ae9e1d..0e9a8a16 100644 --- a/app/Action/TaskMoveColumnUnAssigned.php +++ b/app/Action/TaskMoveColumnUnAssigned.php @@ -61,8 +61,13 @@ class TaskMoveColumnUnAssigned extends Base { return array( 'task_id', - 'column_id', - 'owner_id' + 'task' => array( + 'project_id', + 'column_id', + 'owner_id', + 'position', + 'swimlane_id', + ) ); } @@ -75,14 +80,12 @@ class TaskMoveColumnUnAssigned extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPositionModel->movePosition( - $data['project_id'], + $data['task']['project_id'], $data['task_id'], $this->getParam('dest_column_id'), - $original_task['position'], - $original_task['swimlane_id'], + $data['task']['position'], + $data['task']['swimlane_id'], false ); } @@ -96,6 +99,6 @@ class TaskMoveColumnUnAssigned extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('src_column_id') && $data['owner_id'] == 0; + return $data['task']['column_id'] == $this->getParam('src_column_id') && $data['task']['owner_id'] == 0; } } diff --git a/app/Action/TaskUpdateStartDate.php b/app/Action/TaskUpdateStartDate.php index e5410a87..cc016da1 100644 --- a/app/Action/TaskUpdateStartDate.php +++ b/app/Action/TaskUpdateStartDate.php @@ -59,7 +59,10 @@ class TaskUpdateStartDate extends Base { return array( 'task_id', - 'column_id', + 'task' => array( + 'project_id', + 'column_id', + ), ); } @@ -89,6 +92,6 @@ class TaskUpdateStartDate extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return $data['task']['column_id'] == $this->getParam('column_id'); } } diff --git a/app/Auth/TotpAuth.php b/app/Auth/TotpAuth.php index f4304930..8e1ebe35 100644 --- a/app/Auth/TotpAuth.php +++ b/app/Auth/TotpAuth.php @@ -123,7 +123,8 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface return ''; } - return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret); + $options = array('issuer' => TOTP_ISSUER); + return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret, null, $options); } /** @@ -139,6 +140,7 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface return ''; } - return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret); + $options = array('issuer' => TOTP_ISSUER); + return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret, null, $options); } } diff --git a/app/Controller/ColumnController.php b/app/Controller/ColumnController.php index e3f9bfff..d3f0e36e 100644 --- a/app/Controller/ColumnController.php +++ b/app/Controller/ColumnController.php @@ -66,7 +66,15 @@ class ColumnController extends BaseController list($valid, $errors) = $this->columnValidator->validateCreation($values); if ($valid) { - if ($this->columnModel->create($project['id'], $values['title'], $values['task_limit'], $values['description'], $values['hide_in_dashboard']) !== false) { + $result = $this->columnModel->create( + $project['id'], + $values['title'], + $values['task_limit'], + $values['description'], + isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0 + ); + + if ($result !== false) { $this->flash->success(t('Column created successfully.')); return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id'])), true); } else { @@ -111,7 +119,15 @@ class ColumnController extends BaseController list($valid, $errors) = $this->columnValidator->validateModification($values); if ($valid) { - if ($this->columnModel->update($values['id'], $values['title'], $values['task_limit'], $values['description'], $values['hide_in_dashboard']) !== false) { + $result = $this->columnModel->update( + $values['id'], + $values['title'], + $values['task_limit'], + $values['description'], + isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0 + ); + + if ($result) { $this->flash->success(t('Board updated successfully.')); return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id']))); } else { diff --git a/app/Controller/ProjectPermissionController.php b/app/Controller/ProjectPermissionController.php index f3ca6ed9..99f556e8 100644 --- a/app/Controller/ProjectPermissionController.php +++ b/app/Controller/ProjectPermissionController.php @@ -147,7 +147,7 @@ class ProjectPermissionController extends BaseController $values = $this->request->getValues(); if (empty($values['group_id']) && ! empty($values['external_id'])) { - $values['group_id'] = $this->groupModel->create($values['name'], $values['external_id']); + $values['group_id'] = $this->groupModel->getOrCreateExternalGroupId($values['name'], $values['external_id']); } if ($this->projectGroupRoleModel->addGroup($project['id'], $values['group_id'], $values['role'])) { diff --git a/app/Core/Base.php b/app/Core/Base.php index 8103ec14..098bd880 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -150,6 +150,12 @@ use Pimple\Container; * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer + * @property \Kanboard\Job\CommentEventJob $commentEventJob + * @property \Kanboard\Job\SubtaskEventJob $subtaskEventJob + * @property \Kanboard\Job\TaskEventJob $taskEventJob + * @property \Kanboard\Job\TaskFileEventJob $taskFileEventJob + * @property \Kanboard\Job\ProjectFileEventJob $projectFileEventJob + * @property \Kanboard\Job\NotificationJob $notificationJob * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php index 7a9a714f..626d7614 100644 --- a/app/Core/Filter/LexerBuilder.php +++ b/app/Core/Filter/LexerBuilder.php @@ -69,7 +69,7 @@ class LexerBuilder foreach ($attributes as $attribute) { $this->filters[$attribute] = $filter; - $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute); + $this->lexer->addToken(sprintf("/^(%s:)/i", $attribute), $attribute); if ($default) { $this->lexer->setDefaultToken($attribute); diff --git a/app/Core/Http/Request.php b/app/Core/Http/Request.php index e0df2d3c..2e84958d 100644 --- a/app/Core/Http/Request.php +++ b/app/Core/Http/Request.php @@ -301,6 +301,7 @@ class Request extends Base public function getIpAddress() { $keys = array( + 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', diff --git a/app/Core/Queue/JobHandler.php b/app/Core/Queue/JobHandler.php index 7ca36328..326f3cef 100644 --- a/app/Core/Queue/JobHandler.php +++ b/app/Core/Queue/JobHandler.php @@ -2,6 +2,7 @@ namespace Kanboard\Core\Queue; +use Exception; use Kanboard\Core\Base; use Kanboard\Job\BaseJob; use SimpleQueue\Job; @@ -39,16 +40,23 @@ class JobHandler extends Base public function executeJob(Job $job) { $payload = $job->getBody(); - $className = $payload['class']; - $this->memoryCache->flush(); - $this->prepareJobSession($payload['user_id']); - if (DEBUG) { - $this->logger->debug(__METHOD__.' Received job => '.$className.' ('.getmypid().')'); - } + try { + $className = $payload['class']; + $this->memoryCache->flush(); + $this->prepareJobSession($payload['user_id']); + + if (DEBUG) { + $this->logger->debug(__METHOD__.' Received job => '.$className.' ('.getmypid().')'); + $this->logger->debug(__METHOD__.' => '.json_encode($payload)); + } - $worker = new $className($this->container); - call_user_func_array(array($worker, 'execute'), $payload['params']); + $worker = new $className($this->container); + call_user_func_array(array($worker, 'execute'), $payload['params']); + } catch (Exception $e) { + $this->logger->error(__METHOD__.': Error during job execution: '.$e->getMessage()); + $this->logger->error(__METHOD__ .' => '.json_encode($payload)); + } } /** diff --git a/app/Core/Queue/QueueManager.php b/app/Core/Queue/QueueManager.php index f34cb220..dcf0ebf5 100644 --- a/app/Core/Queue/QueueManager.php +++ b/app/Core/Queue/QueueManager.php @@ -42,9 +42,13 @@ class QueueManager extends Base */ public function push(BaseJob $job) { + $jobClassName = get_class($job); + if ($this->queue !== null) { + $this->logger->debug(__METHOD__.': Job pushed in queue: '.$jobClassName); $this->queue->push(JobHandler::getInstance($this->container)->serializeJob($job)); } else { + $this->logger->debug(__METHOD__.': Job executed synchronously: '.$jobClassName); call_user_func_array(array($job, 'execute'), $job->getJobParams()); } @@ -60,7 +64,7 @@ class QueueManager extends Base public function listen() { if ($this->queue === null) { - throw new LogicException('No Queue Driver defined!'); + throw new LogicException('No queue driver defined!'); } while ($job = $this->queue->pull()) { diff --git a/app/Event/FileEvent.php b/app/Event/FileEvent.php deleted file mode 100644 index 482a4eab..00000000 --- a/app/Event/FileEvent.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php - -namespace Kanboard\Event; - -class FileEvent extends GenericEvent -{ -} diff --git a/app/Event/ProjectFileEvent.php b/app/Event/ProjectFileEvent.php new file mode 100644 index 00000000..5d57e463 --- /dev/null +++ b/app/Event/ProjectFileEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Kanboard\Event; + +class ProjectFileEvent extends GenericEvent +{ +} diff --git a/app/Event/TaskFileEvent.php b/app/Event/TaskFileEvent.php new file mode 100644 index 00000000..fa3bdde9 --- /dev/null +++ b/app/Event/TaskFileEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Kanboard\Event; + +class TaskFileEvent extends GenericEvent +{ +} diff --git a/app/EventBuilder/BaseEventBuilder.php b/app/EventBuilder/BaseEventBuilder.php new file mode 100644 index 00000000..c677563e --- /dev/null +++ b/app/EventBuilder/BaseEventBuilder.php @@ -0,0 +1,23 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Core\Base; +use Kanboard\Event\GenericEvent; + +/** + * Class BaseEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +abstract class BaseEventBuilder extends Base +{ + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + abstract public function build(); +} diff --git a/app/EventBuilder/CommentEventBuilder.php b/app/EventBuilder/CommentEventBuilder.php new file mode 100644 index 00000000..7b4060e4 --- /dev/null +++ b/app/EventBuilder/CommentEventBuilder.php @@ -0,0 +1,48 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\CommentEvent; + +/** + * Class CommentEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class CommentEventBuilder extends BaseEventBuilder +{ + protected $commentId = 0; + + /** + * Set commentId + * + * @param int $commentId + * @return $this + */ + public function withCommentId($commentId) + { + $this->commentId = $commentId; + return $this; + } + + /** + * Build event data + * + * @access public + * @return CommentEvent|null + */ + public function build() + { + $comment = $this->commentModel->getById($this->commentId); + + if (empty($comment)) { + return null; + } + + return new CommentEvent(array( + 'comment' => $comment, + 'task' => $this->taskFinderModel->getDetails($comment['task_id']), + )); + } +} diff --git a/app/EventBuilder/ProjectFileEventBuilder.php b/app/EventBuilder/ProjectFileEventBuilder.php new file mode 100644 index 00000000..70514a99 --- /dev/null +++ b/app/EventBuilder/ProjectFileEventBuilder.php @@ -0,0 +1,50 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\ProjectFileEvent; +use Kanboard\Event\GenericEvent; + +/** + * Class ProjectFileEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class ProjectFileEventBuilder extends BaseEventBuilder +{ + protected $fileId = 0; + + /** + * Set fileId + * + * @param int $fileId + * @return $this + */ + public function withFileId($fileId) + { + $this->fileId = $fileId; + return $this; + } + + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + public function build() + { + $file = $this->projectFileModel->getById($this->fileId); + + if (empty($file)) { + $this->logger->debug(__METHOD__.': File not found'); + return null; + } + + return new ProjectFileEvent(array( + 'file' => $file, + 'project' => $this->projectModel->getById($file['project_id']), + )); + } +} diff --git a/app/EventBuilder/SubtaskEventBuilder.php b/app/EventBuilder/SubtaskEventBuilder.php new file mode 100644 index 00000000..f0271257 --- /dev/null +++ b/app/EventBuilder/SubtaskEventBuilder.php @@ -0,0 +1,79 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\SubtaskEvent; +use Kanboard\Event\GenericEvent; + +/** + * Class SubtaskEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class SubtaskEventBuilder extends BaseEventBuilder +{ + /** + * SubtaskId + * + * @access protected + * @var int + */ + protected $subtaskId = 0; + + /** + * Changed values + * + * @access protected + * @var array + */ + protected $values = array(); + + /** + * Set SubtaskId + * + * @param int $subtaskId + * @return $this + */ + public function withSubtaskId($subtaskId) + { + $this->subtaskId = $subtaskId; + return $this; + } + + /** + * Set values + * + * @param array $values + * @return $this + */ + public function withValues(array $values) + { + $this->values = $values; + return $this; + } + + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + public function build() + { + $eventData = array(); + $eventData['subtask'] = $this->subtaskModel->getById($this->subtaskId, true); + + if (empty($eventData['subtask'])) { + $this->logger->debug(__METHOD__.': Subtask not found'); + return null; + } + + if (! empty($this->values)) { + $eventData['changes'] = array_diff_assoc($this->values, $eventData['subtask']); + } + + $eventData['task'] = $this->taskFinderModel->getDetails($eventData['subtask']['task_id']); + return new SubtaskEvent($eventData); + } +} diff --git a/app/EventBuilder/TaskEventBuilder.php b/app/EventBuilder/TaskEventBuilder.php new file mode 100644 index 00000000..e7a5653d --- /dev/null +++ b/app/EventBuilder/TaskEventBuilder.php @@ -0,0 +1,123 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\TaskEvent; + +/** + * Class TaskEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class TaskEventBuilder extends BaseEventBuilder +{ + /** + * TaskId + * + * @access protected + * @var int + */ + protected $taskId = 0; + + /** + * Task + * + * @access protected + * @var array + */ + protected $task = array(); + + /** + * Extra values + * + * @access protected + * @var array + */ + protected $values = array(); + + /** + * Changed values + * + * @access protected + * @var array + */ + protected $changes = array(); + + /** + * Set TaskId + * + * @param int $taskId + * @return $this + */ + public function withTaskId($taskId) + { + $this->taskId = $taskId; + return $this; + } + + /** + * Set task + * + * @param array $task + * @return $this + */ + public function withTask(array $task) + { + $this->task = $task; + return $this; + } + + /** + * Set values + * + * @param array $values + * @return $this + */ + public function withValues(array $values) + { + $this->values = $values; + return $this; + } + + /** + * Set changes + * + * @param array $changes + * @return $this + */ + public function withChanges(array $changes) + { + $this->changes = $changes; + return $this; + } + + /** + * Build event data + * + * @access public + * @return TaskEvent|null + */ + public function build() + { + $eventData = array(); + $eventData['task_id'] = $this->taskId; + $eventData['task'] = $this->taskFinderModel->getDetails($this->taskId); + + if (empty($eventData['task'])) { + $this->logger->debug(__METHOD__.': Task not found'); + return null; + } + + if (! empty($this->changes)) { + if (empty($this->task)) { + $this->task = $eventData['task']; + } + + $eventData['changes'] = array_diff_assoc($this->changes, $this->task); + unset($eventData['changes']['date_modification']); + } + + return new TaskEvent(array_merge($eventData, $this->values)); + } +} diff --git a/app/EventBuilder/TaskFileEventBuilder.php b/app/EventBuilder/TaskFileEventBuilder.php new file mode 100644 index 00000000..7f1ce3b3 --- /dev/null +++ b/app/EventBuilder/TaskFileEventBuilder.php @@ -0,0 +1,50 @@ +<?php + +namespace Kanboard\EventBuilder; + +use Kanboard\Event\TaskFileEvent; +use Kanboard\Event\GenericEvent; + +/** + * Class TaskFileEventBuilder + * + * @package Kanboard\EventBuilder + * @author Frederic Guillot + */ +class TaskFileEventBuilder extends BaseEventBuilder +{ + protected $fileId = 0; + + /** + * Set fileId + * + * @param int $fileId + * @return $this + */ + public function withFileId($fileId) + { + $this->fileId = $fileId; + return $this; + } + + /** + * Build event data + * + * @access public + * @return GenericEvent|null + */ + public function build() + { + $file = $this->taskFileModel->getById($this->fileId); + + if (empty($file)) { + $this->logger->debug(__METHOD__.': File not found'); + return null; + } + + return new TaskFileEvent(array( + 'file' => $file, + 'task' => $this->taskFinderModel->getDetails($file['task_id']), + )); + } +} diff --git a/app/Filter/TaskPriorityFilter.php b/app/Filter/TaskPriorityFilter.php new file mode 100644 index 00000000..75f6ae3d --- /dev/null +++ b/app/Filter/TaskPriorityFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\TaskModel; + +/** + * Class TaskPriorityFilter + * + * @package Kanboard\Filter + * @author Frederic Guillot + */ +class TaskPriorityFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('priority'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(TaskModel::TABLE.'.priority', $this->value); + return $this; + } +} diff --git a/app/Formatter/TaskAutoCompleteFormatter.php b/app/Formatter/TaskAutoCompleteFormatter.php index 4f1c4c69..2d9f7341 100644 --- a/app/Formatter/TaskAutoCompleteFormatter.php +++ b/app/Formatter/TaskAutoCompleteFormatter.php @@ -3,6 +3,7 @@ namespace Kanboard\Formatter; use Kanboard\Core\Filter\FormatterInterface; +use Kanboard\Model\ProjectModel; use Kanboard\Model\TaskModel; /** @@ -21,11 +22,15 @@ class TaskAutoCompleteFormatter extends BaseFormatter implements FormatterInterf */ public function format() { - $tasks = $this->query->columns(TaskModel::TABLE.'.id', TaskModel::TABLE.'.title')->findAll(); + $tasks = $this->query->columns( + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.title', + ProjectModel::TABLE.'.name AS project_name' + )->asc(TaskModel::TABLE.'.id')->findAll(); foreach ($tasks as &$task) { $task['value'] = $task['title']; - $task['label'] = '#'.$task['id'].' - '.$task['title']; + $task['label'] = $task['project_name'].' > #'.$task['id'].' '.$task['title']; } return $tasks; diff --git a/app/Helper/TaskHelper.php b/app/Helper/TaskHelper.php index e1d65cca..481a5efb 100644 --- a/app/Helper/TaskHelper.php +++ b/app/Helper/TaskHelper.php @@ -50,6 +50,7 @@ class TaskHelper extends Base public function selectDescription(array $values, array $errors) { $html = $this->helper->form->label(t('Description'), 'description'); + $html .= '<div class="markdown-editor-container">'; $html .= $this->helper->form->textarea( 'description', $values, @@ -62,6 +63,7 @@ class TaskHelper extends Base 'markdown-editor' ); + $html .= '</div>'; return $html; } diff --git a/app/Job/CommentEventJob.php b/app/Job/CommentEventJob.php new file mode 100644 index 00000000..c89350ed --- /dev/null +++ b/app/Job/CommentEventJob.php @@ -0,0 +1,50 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\CommentEventBuilder; +use Kanboard\Model\CommentModel; + +/** + * Class CommentEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class CommentEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $commentId + * @param string $eventName + * @return $this + */ + public function withParams($commentId, $eventName) + { + $this->jobParams = array($commentId, $eventName); + return $this; + } + + /** + * Execute job + * + * @param int $commentId + * @param string $eventName + * @return $this + */ + public function execute($commentId, $eventName) + { + $event = CommentEventBuilder::getInstance($this->container) + ->withCommentId($commentId) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + + if ($eventName === CommentModel::EVENT_CREATE) { + $this->userMentionModel->fireEvents($event['comment']['comment'], CommentModel::EVENT_USER_MENTION, $event); + } + } + } +} diff --git a/app/Job/NotificationJob.php b/app/Job/NotificationJob.php index 904a9273..8fb260e8 100644 --- a/app/Job/NotificationJob.php +++ b/app/Job/NotificationJob.php @@ -17,69 +17,27 @@ class NotificationJob extends BaseJob * * @param GenericEvent $event * @param string $eventName - * @param string $eventObjectName * @return $this */ - public function withParams(GenericEvent $event, $eventName, $eventObjectName) + public function withParams(GenericEvent $event, $eventName) { - $this->jobParams = array($event->getAll(), $eventName, $eventObjectName); + $this->jobParams = array($event->getAll(), $eventName); return $this; } /** * Execute job * - * @param array $event + * @param array $eventData * @param string $eventName - * @param string $eventObjectName */ - public function execute(array $event, $eventName, $eventObjectName) + public function execute(array $eventData, $eventName) { - $eventData = $this->getEventData($event, $eventObjectName); - - if (! empty($eventData)) { - if (! empty($event['mention'])) { - $this->userNotificationModel->sendUserNotification($event['mention'], $eventName, $eventData); - } else { - $this->userNotificationModel->sendNotifications($eventName, $eventData); - $this->projectNotificationModel->sendNotifications($eventData['task']['project_id'], $eventName, $eventData); - } - } - } - - /** - * Get event data - * - * @param array $event - * @param string $eventObjectName - * @return array - */ - public function getEventData(array $event, $eventObjectName) - { - $values = array(); - - if (! empty($event['changes'])) { - $values['changes'] = $event['changes']; + if (! empty($eventData['mention'])) { + $this->userNotificationModel->sendUserNotification($eventData['mention'], $eventName, $eventData); + } else { + $this->userNotificationModel->sendNotifications($eventName, $eventData); + $this->projectNotificationModel->sendNotifications($eventData['task']['project_id'], $eventName, $eventData); } - - switch ($eventObjectName) { - case 'Kanboard\Event\TaskEvent': - $values['task'] = $this->taskFinderModel->getDetails($event['task_id']); - break; - case 'Kanboard\Event\SubtaskEvent': - $values['subtask'] = $this->subtaskModel->getById($event['id'], true); - $values['task'] = $this->taskFinderModel->getDetails($values['subtask']['task_id']); - break; - case 'Kanboard\Event\FileEvent': - $values['file'] = $event; - $values['task'] = $this->taskFinderModel->getDetails($values['file']['task_id']); - break; - case 'Kanboard\Event\CommentEvent': - $values['comment'] = $this->commentModel->getById($event['id']); - $values['task'] = $this->taskFinderModel->getDetails($values['comment']['task_id']); - break; - } - - return $values; } } diff --git a/app/Job/ProjectFileEventJob.php b/app/Job/ProjectFileEventJob.php new file mode 100644 index 00000000..d68949c5 --- /dev/null +++ b/app/Job/ProjectFileEventJob.php @@ -0,0 +1,45 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\ProjectFileEventBuilder; + +/** + * Class ProjectFileEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class ProjectFileEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function withParams($fileId, $eventName) + { + $this->jobParams = array($fileId, $eventName); + return $this; + } + + /** + * Execute job + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function execute($fileId, $eventName) + { + $event = ProjectFileEventBuilder::getInstance($this->container) + ->withFileId($fileId) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + } + } +} diff --git a/app/Job/SubtaskEventJob.php b/app/Job/SubtaskEventJob.php new file mode 100644 index 00000000..1dc243ef --- /dev/null +++ b/app/Job/SubtaskEventJob.php @@ -0,0 +1,48 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\SubtaskEventBuilder; + +/** + * Class SubtaskEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class SubtaskEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $subtaskId + * @param string $eventName + * @param array $values + * @return $this + */ + public function withParams($subtaskId, $eventName, array $values = array()) + { + $this->jobParams = array($subtaskId, $eventName, $values); + return $this; + } + + /** + * Execute job + * + * @param int $subtaskId + * @param string $eventName + * @param array $values + * @return $this + */ + public function execute($subtaskId, $eventName, array $values = array()) + { + $event = SubtaskEventBuilder::getInstance($this->container) + ->withSubtaskId($subtaskId) + ->withValues($values) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + } + } +} diff --git a/app/Job/TaskEventJob.php b/app/Job/TaskEventJob.php new file mode 100644 index 00000000..46f7a16c --- /dev/null +++ b/app/Job/TaskEventJob.php @@ -0,0 +1,75 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\Event\TaskEvent; +use Kanboard\EventBuilder\TaskEventBuilder; +use Kanboard\Model\TaskModel; + +/** + * Class TaskEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class TaskEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $taskId + * @param array $eventNames + * @param array $changes + * @param array $values + * @param array $task + * @return $this + */ + public function withParams($taskId, array $eventNames, array $changes = array(), array $values = array(), array $task = array()) + { + $this->jobParams = array($taskId, $eventNames, $changes, $values, $task); + return $this; + } + + /** + * Execute job + * + * @param int $taskId + * @param array $eventNames + * @param array $changes + * @param array $values + * @param array $task + * @return $this + */ + public function execute($taskId, array $eventNames, array $changes = array(), array $values = array(), array $task = array()) + { + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId($taskId) + ->withChanges($changes) + ->withValues($values) + ->withTask($task) + ->build(); + + if ($event !== null) { + foreach ($eventNames as $eventName) { + $this->fireEvent($eventName, $event); + } + } + } + + /** + * Trigger event + * + * @access protected + * @param string $eventName + * @param TaskEvent $event + */ + protected function fireEvent($eventName, TaskEvent $event) + { + $this->logger->debug(__METHOD__.' Event fired: '.$eventName); + $this->dispatcher->dispatch($eventName, $event); + + if ($eventName === TaskModel::EVENT_CREATE) { + $this->userMentionModel->fireEvents($event['task']['description'], TaskModel::EVENT_USER_MENTION, $event); + } + } +} diff --git a/app/Job/TaskFileEventJob.php b/app/Job/TaskFileEventJob.php new file mode 100644 index 00000000..de2c40db --- /dev/null +++ b/app/Job/TaskFileEventJob.php @@ -0,0 +1,45 @@ +<?php + +namespace Kanboard\Job; + +use Kanboard\EventBuilder\TaskFileEventBuilder; + +/** + * Class TaskFileEventJob + * + * @package Kanboard\Job + * @author Frederic Guillot + */ +class TaskFileEventJob extends BaseJob +{ + /** + * Set job params + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function withParams($fileId, $eventName) + { + $this->jobParams = array($fileId, $eventName); + return $this; + } + + /** + * Execute job + * + * @param int $fileId + * @param string $eventName + * @return $this + */ + public function execute($fileId, $eventName) + { + $event = TaskFileEventBuilder::getInstance($this->container) + ->withFileId($fileId) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + } + } +} diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index e3c29261..6a062068 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index ce66eee5..b9a4de6e 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index f9bc0031..050a37d9 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index d3192f46..d6c8bf60 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1216,5 +1216,14 @@ return array( 'Global tags' => 'Globale Schlagwörter', 'There is no global tag at the moment.' => 'Es gibt zur Zeit kein globales Schlagwort', 'This field cannot be empty' => 'Dieses Feld kann nicht leer sein', - 'Hide tasks in this column in the Dashboard' => 'Aufgaben in dieser Spalte im Dashboard ausblenden', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + 'Hide tasks in this column in the dashboard' => 'Aufgaben in dieser Spalte im Dashboard ausblenden', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index 745acaea..87ea68b0 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 604a5e3e..1a4bae82 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 73b65463..5d37cb82 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index e3ee8a4b..c8f7d343 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1217,5 +1217,14 @@ return array( 'Global tags' => 'Libellés globaux', 'There is no global tag at the moment.' => 'Il n\'y a aucun libellé global pour le moment.', 'This field cannot be empty' => 'Ce champ ne peut être vide', - // 'Hide tasks in this column in the Dashboard' => '', + 'Close a task when there is no activity in an specific column' => 'Fermer une tâche lorsqu\'il n\'y a aucune activité dans une colonne spécifique', + '%s removed a subtask for the task #%d' => '%s a supprimé une sous-tâche de la tâche n°%d', + '%s removed a comment on the task #%d' => '%s a supprimé un commentaire de la tâche n°%d', + 'Comment removed on task #%d' => 'Commentaire supprimé sur la tâche n°%d', + 'Subtask removed on task #%d' => 'Sous-tâche supprimée sur la tâche n°%d', + 'Hide tasks in this column in the dashboard' => 'Cacher les tâches de cette colonne dans le tableau de bord', + '%s removed a comment on the task %s' => '%s a supprimé un commentaire de la tâche %s', + '%s removed a subtask for the task %s' => '%s a supprimé une sous-tâche de la tâche %s', + 'Comment removed' => 'Commentaire supprimé', + 'Subtask removed' => 'Sous-tâche supprimée', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 9acdbd1a..febf8bc0 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index d8c42cf4..18a7a72d 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 9d2af814..f6c63076 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -1216,5 +1216,14 @@ return array( 'Global tags' => 'Tag globali', 'There is no global tag at the moment.' => 'Non sono definiti tag globali al momento.', 'This field cannot be empty' => 'Questo campo non può essere vuoto', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 9f6ab88f..dab731d2 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php index f48b7486..0b6007b1 100644 --- a/app/Locale/ko_KR/translations.php +++ b/app/Locale/ko_KR/translations.php @@ -100,9 +100,9 @@ return array( 'There is nobody assigned' => '담당자가 없습니다', 'Column on the board:' => '칼럼:', 'Close this task' => '할일 마치기', - 'Open this task' => '할일 열기', - 'There is no description.' => '설명이 없습니다', - 'Add a new task' => '할일 추가 ', + 'Open this task' => '할일을 열다', + 'There is no description.' => '설명이 없다', + 'Add a new task' => '할일을 추가하는 ', 'The username is required' => '사용자 이름이 필요합니다', 'The maximum length is %d characters' => '최대 길이는 "%d" 글자 입니다', 'The minimum length is %d characters' => '최소 길이는 "%d" 글자 입니다', @@ -145,7 +145,7 @@ return array( 'User removed successfully.' => '사용자를 삭제했습니다.', 'Unable to remove this user.' => '사용자 삭제에 실패했습니다.', 'Board updated successfully.' => '보드를 갱신했습니다.', - 'Ready' => '준비완료', + 'Ready' => '준비중', 'Backlog' => '요구사항', 'Work in progress' => '진행중', 'Done' => '완료', @@ -189,15 +189,15 @@ return array( 'Remove an automatic action' => '자동 액션의 삭제', 'Assign the task to a specific user' => '할일 담당자를 할당', 'Assign the task to the person who does the action' => '액션을 일으킨 사용자를 담당자이자', - 'Duplicate the task to another project' => '할일을 다른 프로젝트로 복사', - 'Move a task to another column' => '할일을 다른 칼럼으로 이동', + 'Duplicate the task to another project' => ' 다른 프로젝트에 할일을 복제하는 ', + 'Move a task to another column' => '할일을 다른 칼럼에 이동하는 ', 'Task modification' => '할일 변경', - 'Task creation' => '할일 만들기', - 'Closing a task' => '할일 종료', - 'Assign a color to a specific user' => '사용자 색 할당', + 'Task creation' => '할일을 만들', + 'Closing a task' => '할일을 닫혔다', + 'Assign a color to a specific user' => '색을 사용자에 할당', 'Column title' => '칼럼의 제목', 'Position' => '위치', - 'Duplicate to another project' => '다른 프로젝트로 복사', + 'Duplicate to another project' => '다른 프로젝트에 복사', 'Duplicate' => '복사', 'link' => '링크', 'Comment updated successfully.' => '댓글을 갱신했습니다.', @@ -327,7 +327,7 @@ return array( 'Comment updated' => '댓글가 갱신되었습니다', 'New subtask' => ' 새로운 서브 할일', 'Subtask updated' => '서브 할일 갱신', - 'Task updated' => '할일 업데이트', + 'Task updated' => '할일 갱신', 'Task closed' => '할일 마침', 'Task opened' => '할일 시작', 'I want to receive notifications only for those projects:' => '다음 프로젝트의 알림만 받겠습니다:', @@ -365,7 +365,7 @@ return array( 'Password modified successfully.' => '패스워드를 변경했습니다.', 'Unable to change the password.' => '비밀 번호가 변경할 수 없었습니다.', 'Change category' => '카테고리 수정', - '%s updated the task %s' => '%s이 할일 %s을 업데이트했습니다', + '%s updated the task %s' => '%s이 할일 %s을 갱신 하였습니다', '%s opened the task %s' => '%s이 할일 %s을 시작시켰습니다', '%s moved the task %s to the position #%d in the column "%s"' => '%s이 할일%s을 위치#%d컬럼%s로 옮겼습니다', '%s moved the task %s to the column "%s"' => '%s이 할일 %s을 칼럼 "%s" 로 옮겼습니다', @@ -665,13 +665,13 @@ return array( 'Show tasks based on the start date' => '시작 날짜로 할일 보기', 'Subtasks time tracking' => '서브 할일 시간 트래킹', 'User calendar view' => '담당자 달력 보기', - 'Automatically update the start date' => '시작일에 자동 업데이트', + 'Automatically update the start date' => '시작일에 자동 갱신', // 'iCal feed' => '', 'Preferences' => '우선권', 'Security' => '보안', 'Two factor authentication disabled' => '이중 인증이 비활성화 되었습니다', 'Two factor authentication enabled' => '이중 인증이 활성화 되었습니다', - 'Unable to update this user.' => '담당자의 업데이트가 가능합니다', + 'Unable to update this user.' => '담당자 갱신이 가능합니다', 'There is no user management for private projects.' => '비밀 프로젝트의 관리 담당자가 없습니다', 'User that will receive the email' => '그 담당자가 이메일을 수신할 것입니다', 'Email subject' => '이메일 제목', @@ -710,7 +710,7 @@ return array( 'Recurrence settings have been modified' => '반복할일 설정 수정', 'Time spent changed: %sh' => '경과시간 변경: %s시간', 'Time estimated changed: %sh' => '%s시간으로 예상시간 변경', - 'The field "%s" have been updated' => '%s 필드가 업데이트 되어있습니다', + 'The field "%s" have been updated' => '%s 필드가 갱신되어 있습니다', 'The description has been modified:' => '설명이 수정되어 있습니다: ', 'Do you really want to close the task "%s" as well as all subtasks?' => '할일 "%s"과 서브 할일을 모두 마치시겠습니까?', 'I want to receive notifications for:' => '다음의 알림을 받기를 원합니다:', @@ -855,11 +855,11 @@ return array( 'Web' => '웹', // 'New attachment on task #%d: %s' => '', 'New comment on task #%d' => '할일 #%d에 새로운 댓글이 달렸습니다', - 'Comment updated on task #%d' => '할일 #%d에 댓글이 업데이트되었습니다', - 'New subtask on task #%d' => '서브 할일 #%d이 업데이트되었습니다', - 'Subtask updated on task #%d' => '서브 할일 #%d가 업데이트되었습니다', + 'Comment updated on task #%d' => '할일 #%d에 댓글이 갱신 되었습니다', + 'New subtask on task #%d' => '서브 할일 #%d이 갱신 되었습니다', + 'Subtask updated on task #%d' => '서브 할일 #%d가 갱신 되었습니다', 'New task #%d: %s' => '할일 #%d: %s이 추가되었습니다', - 'Task updated #%d' => '할일 #%d이 업데이트되었습니다', + 'Task updated #%d' => '할일 #%d이 갱신 되었습니다', 'Task #%d closed' => '할일 #%d를 마쳤습니다', 'Task #%d opened' => '할일 #%d가 시작되었습니다', 'Column changed for task #%d' => '할일 #%d의 칼럼이 변경되었습니다', @@ -1159,40 +1159,40 @@ return array( 'Upload files' => '파일 올리기', 'Installed Plugins' => '설치된 플러그인', 'Plugin Directory' => '플러그인 폴더', - 'Plugin installed successfully.' => '플러그인이 성공적으로 설치 되었습니다', - 'Plugin updated successfully.' => '플러그인이 성공적으로 업데이트 되었습니다', - 'Plugin removed successfully.' => '플러그인이 성공적으로 삭제 되었습니다', - 'Subtask converted to task successfully.' => '서브 할일이 성공적으로 할일로 변경 되었습니다', - 'Unable to convert the subtask.' => '서브할일 변경 비활성화', - 'Unable to extract plugin archive.' => '플러그인 보관소 비활성화', - 'Plugin not found.' => '플러그인을 찾을 수 없습니다', - 'You don\'t have the permission to remove this plugin.' => '플러그인 삭제 권한이 없습니다', - 'Unable to download plugin archive.' => '플러그인 보관소 다운로드 비활성화', - 'Unable to write temporary file for plugin.' => '플러그인의 임시 파일 기록 비활성화', - 'Unable to open plugin archive.' => '플러그인 보관소 열기 비활성화', - 'There is no file in the plugin archive.' => '플러그인 보관소에 파일이 없습니다', - 'Create tasks in bulk' => '벌크에 할일 생성', + 'Plugin installed successfully.' => '플러그인이 성공적으로 설치 되었습니다.', + 'Plugin updated successfully.' => '플러그인이 성공적으로 갱신 되었습니다.', + 'Plugin removed successfully.' => '플러그인이 성공적으로 삭제 되었습니다.', + 'Subtask converted to task successfully.' => '서브 할일이 할일로 성공적으로 변경 되었습니다.', + 'Unable to convert the subtask.' => '서브 할일로 변경할 수 없습니다.', + 'Unable to extract plugin archive.' => '플러그인 아카이브를 추출할 수 없습니다.', + 'Plugin not found.' => '플러그인을 찾을 수 없습니다.', + 'You don\'t have the permission to remove this plugin.' => '이 플러그인의 삭제 권한이 없습니다.', + 'Unable to download plugin archive.' => '플러그인 아카이브를 다운로드할 수 없습니다.', + 'Unable to write temporary file for plugin.' => '플러그인의 임시 파일을 기록할 수 없습니다.', + 'Unable to open plugin archive.' => '플러그인 아카이브를 열수 없습니다.', + 'There is no file in the plugin archive.' => '플러그인 아카이브에 파일이 존재하지 않습니다.', + 'Create tasks in bulk' => '대량의 할일 만들기', // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', - 'There is no plugin available.' => '사용할 수 있는 플러그인이 없습니다', + 'There is no plugin available.' => '사용 가능한 플러그인이 없습니다.', 'Install' => '설치', - 'Update' => '업데이트', - 'Up to date' => '최신의', + 'Update' => '갱신', + 'Up to date' => '날짜 갱신', 'Not available' => '사용 불가능', 'Remove plugin' => '플러그인 삭제', - 'Do you really want to remove this plugin: "%s"?' => '플러그인을 삭제 하시겠습니까: "%s"?', - 'Uninstall' => '제거', + 'Do you really want to remove this plugin: "%s"?' => '정말로 플러그인을 삭제하시겠습니까: "%s"?', + 'Uninstall' => '삭제', 'Listing' => '목록', - 'Metadata' => '메타 데이터', + 'Metadata' => '메타데이터', 'Manage projects' => '프로젝트 관리', - 'Convert to task' => '할일로 변경', - 'Convert sub-task to task' => '서브 할일을 할일로 변경', - 'Do you really want to convert this sub-task to a task?' => '서브 할일을 일로 변경하시겠습니까?', - 'My task title' => '할일 제목', - // 'Enter one task by line.' => '', - 'Number of failed login:' => '로그인 실패 횟수', - 'Account locked until:' => '계정이 잠겼습니다:', + 'Convert to task' => '할일로 변경하기', + 'Convert sub-task to task' => '서브 할일을 할일로 변경하기', + 'Do you really want to convert this sub-task to a task?' => '정말로 서브 할일을 할일로 변경하시겠습니까?', + 'My task title' => '나의 할일 제목', + 'Enter one task by line.' => '정확히 하나의 할일로 진입', + 'Number of failed login:' => '로그인 실패 횟수:', + 'Account locked until:' => '다음동안 계정이 잠겼습니다:', 'Email settings' => '이메일 설정', - 'Email sender address' => '이메일 송신자 주소', + 'Email sender address' => '이메일 보낸이 주소', 'Email transport' => '이메일 전송', // 'Webhook token' => '', 'Imports' => '가져오기', @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index bad6d919..3d66b0bb 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 19372419..14e260cb 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 8ba0d394..8b47d514 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - //' Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 09c247d8..e72649e6 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 7659ba2b..7b64f0e7 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 1f9a7030..5267b03b 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -1215,6 +1215,15 @@ return array( 'Do you really want to remove this tag: "%s"?' => 'Tem a certeza que pretende remover esta etiqueta: "%s"?', 'Global tags' => 'Etiquetas globais', 'There is no global tag at the moment.' => 'De momento não existe nenhuma etiqueta global.', - // 'This field cannot be empty' => '', - //'Hide tasks in this column in the Dashboard' => '', + 'This field cannot be empty' => 'Este campo não pode ficar vazio', + 'Close a task when there is no activity in an specific column' => 'Fechar tarefa quando não houver actividade numa coluna especifica', + '%s removed a subtask for the task #%d' => '%s removeu uma sub-tarefa da tarefa #%d', + '%s removed a comment on the task #%d' => '%s removeu um comentário da tarefa #%d ', + 'Comment removed on task #%d' => 'Comentário removido da tarefa #%d', + 'Subtask removed on task #%d' => 'Sub-tarefa removida da tarefa #%d', + 'Hide tasks in this column in the dashboard' => 'Esconder do meu painel tarefas nesta coluna', + '%s removed a comment on the task %s' => '%s removeu um comentário da tarefa %s', + '%s removed a subtask for the task %s' => '%s removeu uma sub-tarefa da tarefa %s', + 'Comment removed' => 'Comentário removido', + 'Subtask removed' => 'Sub-tarefa removida', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index fe950172..b3682f03 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -1216,5 +1216,14 @@ return array( 'Global tags' => 'Глобальные метка', 'There is no global tag at the moment.' => 'Нет глобальных меток.', 'This field cannot be empty' => 'Это поле не может быть пустым', - 'Hide tasks in this column in the Dashboard' => 'Не показывать задачи из этой колонки в кабинете', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + 'Hide tasks in this column in the dashboard' => 'Не показывать задачи из этой колонки в кабинете', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 7aec7142..157d9e2d 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index bce1ccfb..e42a801d 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 48a0b3de..56adbdb8 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index b25ef122..4f4c84cd 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index d000b706..01eaff17 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1216,5 +1216,14 @@ return array( // 'Global tags' => '', // 'There is no global tag at the moment.' => '', // 'This field cannot be empty' => '', - // 'Hide tasks in this column in the Dashboard' => '', + // 'Close a task when there is no activity in an specific column' => '', + // '%s removed a subtask for the task #%d' => '', + // '%s removed a comment on the task #%d' => '', + // 'Comment removed on task #%d' => '', + // 'Subtask removed on task #%d' => '', + // 'Hide tasks in this column in the dashboard' => '', + // '%s removed a comment on the task %s' => '', + // '%s removed a subtask for the task %s' => '', + // 'Comment removed' => '', + // 'Subtask removed' => '', ); diff --git a/app/Model/ColumnModel.php b/app/Model/ColumnModel.php index 0a9c55a8..5498ef54 100644 --- a/app/Model/ColumnModel.php +++ b/app/Model/ColumnModel.php @@ -138,11 +138,12 @@ class ColumnModel extends Base * Add a new column to the board * * @access public - * @param integer $project_id Project id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Column description - * @return boolean|integer + * @param integer $project_id Project id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Column description + * @param integer $hide_in_dashboard + * @return bool|int */ public function create($project_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) { @@ -166,6 +167,7 @@ class ColumnModel extends Base * @param string $title Column title * @param integer $task_limit Task limit * @param string $description Optional description + * @param integer $hide_in_dashboard * @return boolean */ public function update($column_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) diff --git a/app/Model/CommentModel.php b/app/Model/CommentModel.php index 4231f29d..a9e48bd3 100644 --- a/app/Model/CommentModel.php +++ b/app/Model/CommentModel.php @@ -2,7 +2,6 @@ namespace Kanboard\Model; -use Kanboard\Event\CommentEvent; use Kanboard\Core\Base; /** @@ -27,6 +26,7 @@ class CommentModel extends Base */ const EVENT_UPDATE = 'comment.update'; const EVENT_CREATE = 'comment.create'; + const EVENT_DELETE = 'comment.delete'; const EVENT_USER_MENTION = 'comment.user.mention'; /** @@ -130,9 +130,7 @@ class CommentModel extends Base $comment_id = $this->db->table(self::TABLE)->persist($values); if ($comment_id !== false) { - $event = new CommentEvent(array('id' => $comment_id) + $values); - $this->dispatcher->dispatch(self::EVENT_CREATE, $event); - $this->userMentionModel->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); + $this->queueManager->push($this->commentEventJob->withParams($comment_id, self::EVENT_CREATE)); } return $comment_id; @@ -153,7 +151,7 @@ class CommentModel extends Base ->update(array('comment' => $values['comment'])); if ($result) { - $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values)); + $this->queueManager->push($this->commentEventJob->withParams($values['id'], self::EVENT_UPDATE)); } return $result; @@ -168,6 +166,7 @@ class CommentModel extends Base */ public function remove($comment_id) { + $this->commentEventJob->execute($comment_id, self::EVENT_DELETE); return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove(); } } diff --git a/app/Model/FileModel.php b/app/Model/FileModel.php index 8cdea9a0..98032f9d 100644 --- a/app/Model/FileModel.php +++ b/app/Model/FileModel.php @@ -5,7 +5,6 @@ namespace Kanboard\Model; use Exception; use Kanboard\Core\Base; use Kanboard\Core\Thumbnail; -use Kanboard\Event\FileEvent; use Kanboard\Core\ObjectStorage\ObjectStorageException; /** @@ -44,13 +43,13 @@ abstract class FileModel extends Base abstract protected function getPathPrefix(); /** - * Get event name + * Fire file creation event * * @abstract * @access protected - * @return string + * @param integer $file_id */ - abstract protected function getEventName(); + abstract protected function fireCreationEvent($file_id); /** * Get PicoDb query to get all files @@ -130,16 +129,16 @@ abstract class FileModel extends Base * Create a file entry in the database * * @access public - * @param integer $id Foreign key - * @param string $name Filename - * @param string $path Path on the disk - * @param integer $size File size + * @param integer $foreign_key_id Foreign key + * @param string $name Filename + * @param string $path Path on the disk + * @param integer $size File size * @return bool|integer */ - public function create($id, $name, $path, $size) + public function create($foreign_key_id, $name, $path, $size) { $values = array( - $this->getForeignKey() => $id, + $this->getForeignKey() => $foreign_key_id, 'name' => substr($name, 0, 255), 'path' => $path, 'is_image' => $this->isImage($name) ? 1 : 0, @@ -152,8 +151,7 @@ abstract class FileModel extends Base if ($result) { $file_id = (int) $this->db->getLastId(); - $event = new FileEvent($values + array('file_id' => $file_id)); - $this->dispatcher->dispatch($this->getEventName(), $event); + $this->fireCreationEvent($file_id); return $file_id; } diff --git a/app/Model/GroupModel.php b/app/Model/GroupModel.php index 0a975570..b43423b3 100644 --- a/app/Model/GroupModel.php +++ b/app/Model/GroupModel.php @@ -116,4 +116,23 @@ class GroupModel extends Base { return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); } + + /** + * Get groupId from externalGroupId and create the group if not found + * + * @access public + * @param string $name + * @param string $external_id + * @return bool|integer + */ + public function getOrCreateExternalGroupId($name, $external_id) + { + $group_id = $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOneColumn('id'); + + if (empty($group_id)) { + $group_id = $this->create($name, $external_id); + } + + return $group_id; + } } diff --git a/app/Model/NotificationModel.php b/app/Model/NotificationModel.php index 4d697b5e..925d646e 100644 --- a/app/Model/NotificationModel.php +++ b/app/Model/NotificationModel.php @@ -70,10 +70,14 @@ class NotificationModel extends Base return e('%s updated a subtask for the task #%d', $event_author, $event_data['task']['id']); case SubtaskModel::EVENT_CREATE: return e('%s created a subtask for the task #%d', $event_author, $event_data['task']['id']); + case SubtaskModel::EVENT_DELETE: + return e('%s removed a subtask for the task #%d', $event_author, $event_data['task']['id']); case CommentModel::EVENT_UPDATE: return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']); case CommentModel::EVENT_CREATE: return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); + case CommentModel::EVENT_DELETE: + return e('%s removed a comment on the task #%d', $event_author, $event_data['task']['id']); case TaskFileModel::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); case TaskModel::EVENT_USER_MENTION: @@ -102,10 +106,14 @@ class NotificationModel extends Base return e('New comment on task #%d', $event_data['comment']['task_id']); case CommentModel::EVENT_UPDATE: return e('Comment updated on task #%d', $event_data['comment']['task_id']); + case CommentModel::EVENT_DELETE: + return e('Comment removed on task #%d', $event_data['comment']['task_id']); case SubtaskModel::EVENT_CREATE: return e('New subtask on task #%d', $event_data['subtask']['task_id']); case SubtaskModel::EVENT_UPDATE: return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); + case SubtaskModel::EVENT_DELETE: + return e('Subtask removed on task #%d', $event_data['subtask']['task_id']); case TaskModel::EVENT_CREATE: return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); case TaskModel::EVENT_UPDATE: @@ -149,9 +157,11 @@ class NotificationModel extends Base return $event_data['file']['task_id']; case CommentModel::EVENT_CREATE: case CommentModel::EVENT_UPDATE: + case CommentModel::EVENT_DELETE: return $event_data['comment']['task_id']; case SubtaskModel::EVENT_CREATE: case SubtaskModel::EVENT_UPDATE: + case SubtaskModel::EVENT_DELETE: return $event_data['subtask']['task_id']; case TaskModel::EVENT_CREATE: case TaskModel::EVENT_UPDATE: diff --git a/app/Model/ProjectFileModel.php b/app/Model/ProjectFileModel.php index b464bb2a..4de4d66d 100644 --- a/app/Model/ProjectFileModel.php +++ b/app/Model/ProjectFileModel.php @@ -61,14 +61,13 @@ class ProjectFileModel extends FileModel } /** - * Get event name + * Fire file creation event * - * @abstract * @access protected - * @return string + * @param integer $file_id */ - protected function getEventName() + protected function fireCreationEvent($file_id) { - return self::EVENT_CREATE; + $this->queueManager->push($this->projectFileEventJob->withParams($file_id, self::EVENT_CREATE)); } } diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index a97bddbf..f3fc72ba 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -4,7 +4,6 @@ namespace Kanboard\Model; use PicoDb\Database; use Kanboard\Core\Base; -use Kanboard\Event\SubtaskEvent; /** * Subtask Model @@ -66,7 +65,7 @@ class SubtaskModel extends Base ->join(TaskModel::TABLE, 'id', 'task_id') ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; } - + /** * Get available status * @@ -235,10 +234,7 @@ class SubtaskModel extends Base $subtask_id = $this->db->table(self::TABLE)->persist($values); if ($subtask_id !== false) { - $this->container['dispatcher']->dispatch( - self::EVENT_CREATE, - new SubtaskEvent(array('id' => $subtask_id) + $values) - ); + $this->queueManager->push($this->subtaskEventJob->withParams($subtask_id, self::EVENT_CREATE)); } return $subtask_id; @@ -255,13 +251,10 @@ class SubtaskModel extends Base public function update(array $values, $fire_events = true) { $this->prepare($values); - $subtask = $this->getById($values['id']); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); if ($result && $fire_events) { - $event = $subtask; - $event['changes'] = array_diff_assoc($values, $subtask); - $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new SubtaskEvent($event)); + $this->queueManager->push($this->subtaskEventJob->withParams($values['id'], self::EVENT_UPDATE, $values)); } return $result; @@ -377,14 +370,8 @@ class SubtaskModel extends Base */ public function remove($subtask_id) { - $subtask = $this->getById($subtask_id); - $result = $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); - - if ($result) { - $this->container['dispatcher']->dispatch(self::EVENT_DELETE, new SubtaskEvent($subtask)); - } - - return $result; + $this->subtaskEventJob->execute($subtask_id, self::EVENT_DELETE); + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); } /** diff --git a/app/Model/TaskCreationModel.php b/app/Model/TaskCreationModel.php index cd70a028..1c0fd7d9 100644 --- a/app/Model/TaskCreationModel.php +++ b/app/Model/TaskCreationModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Creation @@ -42,7 +41,10 @@ class TaskCreationModel extends Base $this->taskTagModel->save($values['project_id'], $task_id, $tags); } - $this->fireEvents($task_id, $values); + $this->queueManager->push($this->taskEventJob->withParams( + $task_id, + array(TaskModel::EVENT_CREATE_UPDATE, TaskModel::EVENT_CREATE) + )); } return (int) $task_id; @@ -51,10 +53,10 @@ class TaskCreationModel extends Base /** * Prepare data * - * @access public + * @access protected * @param array $values Form values */ - public function prepare(array &$values) + protected function prepare(array &$values) { $values = $this->dateParser->convert($values, array('date_due')); $values = $this->dateParser->convert($values, array('date_started'), true); @@ -84,26 +86,4 @@ class TaskCreationModel extends Base $values['date_moved'] = $values['date_creation']; $values['position'] = $this->taskFinderModel->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; } - - /** - * Fire events - * - * @access private - * @param integer $task_id Task id - * @param array $values Form values - */ - private function fireEvents($task_id, array $values) - { - $event = new TaskEvent(array('task_id' => $task_id) + $values); - - $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE_UPDATE); - $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE); - - $this->dispatcher->dispatch(TaskModel::EVENT_CREATE_UPDATE, $event); - $this->dispatcher->dispatch(TaskModel::EVENT_CREATE, $event); - - if (! empty($values['description'])) { - $this->userMentionModel->fireEvents($values['description'], TaskModel::EVENT_USER_MENTION, $event); - } - } } diff --git a/app/Model/TaskFileModel.php b/app/Model/TaskFileModel.php index 7603019a..0163da28 100644 --- a/app/Model/TaskFileModel.php +++ b/app/Model/TaskFileModel.php @@ -61,18 +61,6 @@ class TaskFileModel extends FileModel } /** - * Get event name - * - * @abstract - * @access protected - * @return string - */ - protected function getEventName() - { - return self::EVENT_CREATE; - } - - /** * Get projectId from fileId * * @access public @@ -101,4 +89,15 @@ class TaskFileModel extends FileModel $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png'; return $this->uploadContent($task_id, $original_filename, $blob); } + + /** + * Fire file creation event + * + * @access protected + * @param integer $file_id + */ + protected function fireCreationEvent($file_id) + { + $this->queueManager->push($this->taskFileEventJob->withParams($file_id, self::EVENT_CREATE)); + } } diff --git a/app/Model/TaskModificationModel.php b/app/Model/TaskModificationModel.php index be5f53c8..16b48f3d 100644 --- a/app/Model/TaskModificationModel.php +++ b/app/Model/TaskModificationModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Modification @@ -23,14 +22,14 @@ class TaskModificationModel extends Base */ public function update(array $values, $fire_events = true) { - $original_task = $this->taskFinderModel->getById($values['id']); + $task = $this->taskFinderModel->getById($values['id']); - $this->updateTags($values, $original_task); + $this->updateTags($values, $task); $this->prepare($values); - $result = $this->db->table(TaskModel::TABLE)->eq('id', $original_task['id'])->update($values); + $result = $this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values); if ($fire_events && $result) { - $this->fireEvents($original_task, $values); + $this->fireEvents($task, $values); } return $result; @@ -39,43 +38,56 @@ class TaskModificationModel extends Base /** * Fire events * - * @access public - * @param array $task - * @param array $new_values + * @access protected + * @param array $task + * @param array $changes */ - public function fireEvents(array $task, array $new_values) + protected function fireEvents(array $task, array $changes) { $events = array(); - $event_data = array_merge($task, $new_values, array('task_id' => $task['id'])); - // Values changed - $event_data['changes'] = array_diff_assoc($new_values, $task); - unset($event_data['changes']['date_modification']); - - if ($this->isFieldModified('owner_id', $event_data['changes'])) { + if ($this->isAssigneeChanged($task, $changes)) { $events[] = TaskModel::EVENT_ASSIGNEE_CHANGE; - } elseif (! empty($event_data['changes'])) { + } elseif ($this->isModified($task, $changes)) { $events[] = TaskModel::EVENT_CREATE_UPDATE; $events[] = TaskModel::EVENT_UPDATE; } - foreach ($events as $event) { - $this->logger->debug('Event fired: '.$event); - $this->dispatcher->dispatch($event, new TaskEvent($event_data)); + if (! empty($events)) { + $this->queueManager->push($this->taskEventJob + ->withParams($task['id'], $events, $changes, array(), $task) + ); } } /** + * Return true if the task have been modified + * + * @access protected + * @param array $task + * @param array $changes + * @return bool + */ + protected function isModified(array $task, array $changes) + { + $diff = array_diff_assoc($changes, $task); + unset($diff['date_modification']); + return count($diff) > 0; + } + + /** * Return true if the field is the only modified value * - * @access public - * @param string $field - * @param array $changes - * @return boolean + * @access protected + * @param array $task + * @param array $changes + * @return bool */ - public function isFieldModified($field, array $changes) + protected function isAssigneeChanged(array $task, array $changes) { - return isset($changes[$field]) && count($changes) === 1; + $diff = array_diff_assoc($changes, $task); + unset($diff['date_modification']); + return isset($changes['owner_id']) && $task['owner_id'] != $changes['owner_id'] && count($diff) === 1; } /** diff --git a/app/Model/TaskPositionModel.php b/app/Model/TaskPositionModel.php index 9fdb8f7d..d6d2a0af 100644 --- a/app/Model/TaskPositionModel.php +++ b/app/Model/TaskPositionModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Position @@ -212,8 +211,7 @@ class TaskPositionModel extends Base */ private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id) { - $event_data = array( - 'task_id' => $task['id'], + $changes = array( 'project_id' => $task['project_id'], 'position' => $new_position, 'column_id' => $new_column_id, @@ -226,14 +224,26 @@ class TaskPositionModel extends Base ); if ($task['swimlane_id'] != $new_swimlane_id) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_SWIMLANE); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_SWIMLANE), + $changes, + $changes + )); } elseif ($task['column_id'] != $new_column_id) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_COLUMN); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_COLUMN), + $changes, + $changes + )); } elseif ($task['position'] != $new_position) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_POSITION); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_POSITION, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_POSITION), + $changes, + $changes + )); } } } diff --git a/app/Model/TaskProjectMoveModel.php b/app/Model/TaskProjectMoveModel.php index eda23c0b..ae3ae084 100644 --- a/app/Model/TaskProjectMoveModel.php +++ b/app/Model/TaskProjectMoveModel.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use Kanboard\Event\TaskEvent; - /** * Task Project Move * @@ -32,9 +30,8 @@ class TaskProjectMoveModel extends TaskDuplicationModel $this->checkDestinationProjectValues($values); $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id); - if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) { - $event = new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_PROJECT, $event); + if ($this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update($values)) { + $this->queueManager->push($this->taskEventJob->withParams($task_id, array(TaskModel::EVENT_MOVE_PROJECT), $values)); } return true; diff --git a/app/Model/TaskStatusModel.php b/app/Model/TaskStatusModel.php index 4d573f0e..ea304beb 100644 --- a/app/Model/TaskStatusModel.php +++ b/app/Model/TaskStatusModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Status @@ -101,10 +100,10 @@ class TaskStatusModel extends Base * @param integer $task_id Task id * @param integer $status Task status * @param integer $date_completed Timestamp - * @param string $event Event name + * @param string $event_name Event name * @return boolean */ - private function changeStatus($task_id, $status, $date_completed, $event) + private function changeStatus($task_id, $status, $date_completed, $event_name) { if (! $this->taskFinderModel->exists($task_id)) { return false; @@ -120,8 +119,7 @@ class TaskStatusModel extends Base )); if ($result) { - $this->logger->debug('Event fired: '.$event); - $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinderModel->getById($task_id))); + $this->queueManager->push($this->taskEventJob->withParams($task_id, array($event_name))); } return $result; diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql index 8d2f2dc0..8d494dcf 100644 --- a/app/Schema/Sql/mysql.sql +++ b/app/Schema/Sql/mysql.sql @@ -44,8 +44,8 @@ CREATE TABLE `columns` ( `position` int(11) NOT NULL, `project_id` int(11) NOT NULL, `task_limit` int(11) DEFAULT '0', - `hide_in_dashboard` int(11) DEFAULT '0', `description` text, + `hide_in_dashboard` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_title_project` (`title`,`project_id`), KEY `columns_project_idx` (`project_id`), @@ -671,7 +671,7 @@ CREATE TABLE `users` ( LOCK TABLES `settings` WRITE; /*!40000 ALTER TABLE `settings` DISABLE KEYS */; -INSERT INTO `settings` VALUES ('api_token','19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','1d62395a742260738a406789366a84138ced50a1be62e8862c5cf8d0a561',0,0),('webhook_url','',0,0); +INSERT INTO `settings` VALUES ('api_token','4064ef3d26efa9a0ff78fa7067d8bb9d99323455128edd89e9dc7c53ed76',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','c8f53c0bcd8aead902ad04f180ffafd7889b9c0062c2d510e2297ef543b8',0,0),('webhook_url','',0,0); /*!40000 ALTER TABLE `settings` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -700,4 +700,4 @@ UNLOCK TABLES; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$Kv6fus67I/ZG/3LYJ7bRLeis8bk8455Lwtu12ElgnGm3lhRs/z7Ni', 'app-admin');INSERT INTO schema_version VALUES ('111'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$yUJ9QnhG.f47yO.YvWKo3eMAHULukpluDNTOF9.Z7QQg0vOfFRB6u', 'app-admin');INSERT INTO schema_version VALUES ('112'); diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql index ae8b4fd5..0add9c91 100644 --- a/app/Schema/Sql/postgres.sql +++ b/app/Schema/Sql/postgres.sql @@ -98,8 +98,8 @@ CREATE TABLE "columns" ( "position" integer, "project_id" integer NOT NULL, "task_limit" integer DEFAULT 0, - "hide_in_dashboard" integer DEFAULT 0, - "description" "text" + "description" "text", + "hide_in_dashboard" boolean DEFAULT false ); @@ -2243,7 +2243,8 @@ INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_high INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_public_refresh_interval', '60', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_private_refresh_interval', '10', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_columns', '', 0, 0); -INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', '1aff324d30632aaed0d4f4dc1281be0d5bbc7b4fcddccc4badcd6c8f3d43', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', 'c9a7c2a4523f1724b2ca047c5685f8e2b26bba47eb69baf4f22d5d50d837', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', 'c57a6cb1789269547b616454e4e2f06d3de0514f83baf8fa5b5a8af44a08', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_language', 'en_US', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_timezone', 'UTC', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_url', '', 0, 0); @@ -2261,7 +2262,6 @@ INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('default_co INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('subtask_time_tracking', '1', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('cfd_include_closed_tasks', '1', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('password_reset', '1', 0, 0); -INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', '19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929', 0, 0); -- @@ -2313,4 +2313,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true); -- PostgreSQL database dump complete -- -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$Kv6fus67I/ZG/3LYJ7bRLeis8bk8455Lwtu12ElgnGm3lhRs/z7Ni', 'app-admin');INSERT INTO schema_version VALUES ('90'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$yUJ9QnhG.f47yO.YvWKo3eMAHULukpluDNTOF9.Z7QQg0vOfFRB6u', 'app-admin');INSERT INTO schema_version VALUES ('91'); diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php index 34202052..9383be12 100644 --- a/app/ServiceProvider/ActionProvider.php +++ b/app/ServiceProvider/ActionProvider.php @@ -32,6 +32,7 @@ use Kanboard\Action\TaskMoveColumnUnAssigned; use Kanboard\Action\TaskOpen; use Kanboard\Action\TaskUpdateStartDate; use Kanboard\Action\TaskCloseNoActivity; +use Kanboard\Action\TaskCloseNoActivityColumn; /** * Action Provider @@ -68,6 +69,7 @@ class ActionProvider implements ServiceProviderInterface $container['actionManager']->register(new TaskClose($container)); $container['actionManager']->register(new TaskCloseColumn($container)); $container['actionManager']->register(new TaskCloseNoActivity($container)); + $container['actionManager']->register(new TaskCloseNoActivityColumn($container)); $container['actionManager']->register(new TaskCreation($container)); $container['actionManager']->register(new TaskDuplicateAnotherProject($container)); $container['actionManager']->register(new TaskEmail($container)); diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php index 20281a09..436288dc 100644 --- a/app/ServiceProvider/FilterProvider.php +++ b/app/ServiceProvider/FilterProvider.php @@ -21,6 +21,7 @@ use Kanboard\Filter\TaskDueDateFilter; use Kanboard\Filter\TaskIdFilter; use Kanboard\Filter\TaskLinkFilter; use Kanboard\Filter\TaskModificationDateFilter; +use Kanboard\Filter\TaskPriorityFilter; use Kanboard\Filter\TaskProjectFilter; use Kanboard\Filter\TaskReferenceFilter; use Kanboard\Filter\TaskStatusFilter; @@ -137,6 +138,7 @@ class FilterProvider implements ServiceProviderInterface ->withFilter(TaskColorFilter::getInstance() ->setColorModel($c['colorModel']) ) + ->withFilter(new TaskPriorityFilter()) ->withFilter(new TaskColumnFilter()) ->withFilter(new TaskCommentFilter()) ->withFilter(TaskCreationDateFilter::getInstance() diff --git a/app/ServiceProvider/JobProvider.php b/app/ServiceProvider/JobProvider.php new file mode 100644 index 00000000..c7f323f1 --- /dev/null +++ b/app/ServiceProvider/JobProvider.php @@ -0,0 +1,57 @@ +<?php + +namespace Kanboard\ServiceProvider; + +use Kanboard\Job\CommentEventJob; +use Kanboard\Job\NotificationJob; +use Kanboard\Job\ProjectFileEventJob; +use Kanboard\Job\SubtaskEventJob; +use Kanboard\Job\TaskEventJob; +use Kanboard\Job\TaskFileEventJob; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class JobProvider + * + * @package Kanboard\ServiceProvider + * @author Frederic Guillot + */ +class JobProvider implements ServiceProviderInterface +{ + /** + * Register providers + * + * @access public + * @param \Pimple\Container $container + * @return \Pimple\Container + */ + public function register(Container $container) + { + $container['commentEventJob'] = $container->factory(function ($c) { + return new CommentEventJob($c); + }); + + $container['subtaskEventJob'] = $container->factory(function ($c) { + return new SubtaskEventJob($c); + }); + + $container['taskEventJob'] = $container->factory(function ($c) { + return new TaskEventJob($c); + }); + + $container['taskFileEventJob'] = $container->factory(function ($c) { + return new TaskFileEventJob($c); + }); + + $container['projectFileEventJob'] = $container->factory(function ($c) { + return new ProjectFileEventJob($c); + }); + + $container['notificationJob'] = $container->factory(function ($c) { + return new NotificationJob($c); + }); + + return $container; + } +} diff --git a/app/ServiceProvider/QueueProvider.php b/app/ServiceProvider/QueueProvider.php index 946b436a..570f2e77 100644 --- a/app/ServiceProvider/QueueProvider.php +++ b/app/ServiceProvider/QueueProvider.php @@ -15,9 +15,11 @@ use Pimple\ServiceProviderInterface; class QueueProvider implements ServiceProviderInterface { /** - * Registers services on the given container. + * Register providers * - * @param Container $container + * @access public + * @param \Pimple\Container $container + * @return \Pimple\Container */ public function register(Container $container) { diff --git a/app/Subscriber/BaseSubscriber.php b/app/Subscriber/BaseSubscriber.php index fdea29f6..92441962 100644 --- a/app/Subscriber/BaseSubscriber.php +++ b/app/Subscriber/BaseSubscriber.php @@ -12,28 +12,4 @@ use Kanboard\Core\Base; */ class BaseSubscriber extends Base { - /** - * Method called - * - * @access private - * @var array - */ - private $called = array(); - - /** - * Check if a listener has been executed - * - * @access public - * @param string $key - * @return boolean - */ - public function isExecuted($key = '') - { - if (isset($this->called[$key])) { - return true; - } - - $this->called[$key] = true; - return false; - } } diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php index db11e585..7de24e49 100644 --- a/app/Subscriber/NotificationSubscriber.php +++ b/app/Subscriber/NotificationSubscriber.php @@ -3,7 +3,6 @@ namespace Kanboard\Subscriber; use Kanboard\Event\GenericEvent; -use Kanboard\Job\NotificationJob; use Kanboard\Model\TaskModel; use Kanboard\Model\CommentModel; use Kanboard\Model\SubtaskModel; @@ -26,8 +25,10 @@ class NotificationSubscriber extends BaseSubscriber implements EventSubscriberIn TaskModel::EVENT_ASSIGNEE_CHANGE => 'handleEvent', SubtaskModel::EVENT_CREATE => 'handleEvent', SubtaskModel::EVENT_UPDATE => 'handleEvent', + SubtaskModel::EVENT_DELETE => 'handleEvent', CommentModel::EVENT_CREATE => 'handleEvent', CommentModel::EVENT_UPDATE => 'handleEvent', + CommentModel::EVENT_DELETE => 'handleEvent', CommentModel::EVENT_USER_MENTION => 'handleEvent', TaskFileModel::EVENT_CREATE => 'handleEvent', ); @@ -35,12 +36,7 @@ class NotificationSubscriber extends BaseSubscriber implements EventSubscriberIn public function handleEvent(GenericEvent $event, $eventName) { - if (!$this->isExecuted($eventName)) { - $this->logger->debug('Subscriber executed: ' . __METHOD__); - - $this->queueManager->push(NotificationJob::getInstance($this->container) - ->withParams($event, $eventName, get_class($event)) - ); - } + $this->logger->debug('Subscriber executed: ' . __METHOD__); + $this->queueManager->push($this->notificationJob->withParams($event, $eventName)); } } diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php index 6971a121..7e3c11c3 100644 --- a/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/app/Subscriber/ProjectDailySummarySubscriber.php @@ -22,7 +22,7 @@ class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubsc public function execute(TaskEvent $event) { - if (isset($event['project_id']) && !$this->isExecuted()) { + if (isset($event['project_id'])) { $this->logger->debug('Subscriber executed: '.__METHOD__); $this->queueManager->push(ProjectMetricJob::getInstance($this->container)->withParams($event['project_id'])); } diff --git a/app/Subscriber/ProjectModificationDateSubscriber.php b/app/Subscriber/ProjectModificationDateSubscriber.php index fee04eaa..1ffe0248 100644 --- a/app/Subscriber/ProjectModificationDateSubscriber.php +++ b/app/Subscriber/ProjectModificationDateSubscriber.php @@ -24,9 +24,7 @@ class ProjectModificationDateSubscriber extends BaseSubscriber implements EventS public function execute(GenericEvent $event) { - if (isset($event['project_id']) && !$this->isExecuted()) { - $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->projectModel->updateModificationDate($event['project_id']); - } + $this->logger->debug('Subscriber executed: '.__METHOD__); + $this->projectModel->updateModificationDate($event['task']['project_id']); } } diff --git a/app/Template/column/create.php b/app/Template/column/create.php index 387b6a47..812e9139 100644 --- a/app/Template/column/create.php +++ b/app/Template/column/create.php @@ -13,7 +13,7 @@ <?= $this->form->label(t('Task limit'), 'task_limit') ?> <?= $this->form->number('task_limit', $values, $errors) ?> - <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the Dashboard'), 1) ?> + <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1) ?> <?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php index abd70119..89487298 100644 --- a/app/Template/column/edit.php +++ b/app/Template/column/edit.php @@ -15,7 +15,7 @@ <?= $this->form->label(t('Task limit'), 'task_limit') ?> <?= $this->form->number('task_limit', $values, $errors) ?> - <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the Dashboard'), 1, $values['hide_in_dashboard'] == 1) ?> + <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1, $values['hide_in_dashboard'] == 1) ?> <?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> diff --git a/app/Template/config/about.php b/app/Template/config/about.php index 8e2d1325..8d5a575d 100644 --- a/app/Template/config/about.php +++ b/app/Template/config/about.php @@ -9,7 +9,7 @@ </li> <li> <?= t('Author:') ?> - <strong>Frédéric Guillot</strong> (<a href="https://github.com/fguillot/kanboard/blob/master/CONTRIBUTORS.md" target="_blank"><?= t('contributors') ?></a>) + <strong>Frédéric Guillot</strong> (<a href="https://github.com/kanboard/kanboard/blob/master/CONTRIBUTORS.md" target="_blank"><?= t('contributors') ?></a>) </li> <li> <?= t('License:') ?> diff --git a/app/Template/dashboard/tasks.php b/app/Template/dashboard/tasks.php index 4b83a96a..b3257c33 100644 --- a/app/Template/dashboard/tasks.php +++ b/app/Template/dashboard/tasks.php @@ -9,7 +9,7 @@ <th class="column-5"><?= $paginator->order('Id', 'tasks.id') ?></th> <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> <th><?= $paginator->order(t('Task'), 'title') ?></th> - <th class="column-5"><?= $paginator->order('Priority', 'tasks.priority') ?></th> + <th class="column-5"><?= $paginator->order(t('Priority'), 'tasks.priority') ?></th> <th class="column-20"><?= t('Time tracking') ?></th> <th class="column-10"><?= $paginator->order(t('Due date'), 'date_due') ?></th> <th class="column-10"><?= $paginator->order(t('Column'), 'column_title') ?></th> diff --git a/app/Template/event/comment_delete.php b/app/Template/event/comment_delete.php new file mode 100644 index 00000000..ead7d56a --- /dev/null +++ b/app/Template/event/comment_delete.php @@ -0,0 +1,11 @@ +<p class="activity-title"> + <?= e('%s removed a comment on the task %s', + $this->text->e($author), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> + <span class="activity-date"><?= $this->dt->datetime($date_creation) ?></span> +</p> +<div class="activity-description"> + <p class="activity-task-title"><?= $this->text->e($task['title']) ?></p> + <div class="markdown"><?= $this->text->markdown($comment['comment']) ?></div> +</div> diff --git a/app/Template/event/comment_update.php b/app/Template/event/comment_update.php index 5a0821bd..5be598ac 100644 --- a/app/Template/event/comment_update.php +++ b/app/Template/event/comment_update.php @@ -7,4 +7,7 @@ </p> <div class="activity-description"> <p class="activity-task-title"><?= $this->text->e($task['title']) ?></p> + <?php if (! empty($comment['comment'])): ?> + <div class="markdown"><?= $this->text->markdown($comment['comment']) ?></div> + <?php endif ?> </div> diff --git a/app/Template/event/subtask_delete.php b/app/Template/event/subtask_delete.php new file mode 100644 index 00000000..8ac11853 --- /dev/null +++ b/app/Template/event/subtask_delete.php @@ -0,0 +1,15 @@ +<p class="activity-title"> + <?= e('%s removed a subtask for the task %s', + $this->text->e($author), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> + <span class="activity-date"><?= $this->dt->datetime($date_creation) ?></span> +</p> +<div class="activity-description"> + <p class="activity-task-title"><?= $this->text->e($task['title']) ?></p> + <ul> + <li> + <?= $this->text->e($subtask['title']) ?> (<strong><?= $this->text->e($subtask['status_name']) ?></strong>) + </li> + </ul> +</div> diff --git a/app/Template/notification/comment_delete.php b/app/Template/notification/comment_delete.php new file mode 100644 index 00000000..928623ec --- /dev/null +++ b/app/Template/notification/comment_delete.php @@ -0,0 +1,7 @@ +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Comment removed') ?></h3> + +<?= $this->text->markdown($comment['comment']) ?> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> diff --git a/app/Template/notification/subtask_delete.php b/app/Template/notification/subtask_delete.php new file mode 100644 index 00000000..8c5f262c --- /dev/null +++ b/app/Template/notification/subtask_delete.php @@ -0,0 +1,11 @@ +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Subtask removed') ?></h3> + +<ul> + <li><?= t('Title:') ?> <?= $this->text->e($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= $this->text->e($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= $this->text->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> +</ul> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> diff --git a/app/Template/project_view/show.php b/app/Template/project_view/show.php index 0ad8852b..667a576c 100644 --- a/app/Template/project_view/show.php +++ b/app/Template/project_view/show.php @@ -57,7 +57,7 @@ <th class="column-40"><?= t('Column') ?></th> <th class="column-20"><?= t('Task limit') ?></th> <th class="column-20"><?= t('Active tasks') ?></th> - <th class="column-20"><?= t('Hide tasks in this column in the Dashboard') ?></th> + <th class="column-20"><?= t('Hide tasks in this column in the dashboard') ?></th> </tr> <?php foreach ($stats['columns'] as $column): ?> <tr> diff --git a/app/Template/user_modification/show.php b/app/Template/user_modification/show.php index 396d550d..506c9161 100644 --- a/app/Template/user_modification/show.php +++ b/app/Template/user_modification/show.php @@ -11,16 +11,16 @@ <?= $this->form->text('username', $values, $errors, array('required', isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?> <?= $this->form->label(t('Name'), 'name') ?> - <?= $this->form->text('name', $values, $errors) ?> + <?= $this->form->text('name', $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_name') ? '' : 'readonly')) ?> <?= $this->form->label(t('Email'), 'email') ?> - <?= $this->form->email('email', $values, $errors) ?> + <?= $this->form->email('email', $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_email') ? '' : 'readonly')) ?> <?= $this->form->label(t('Timezone'), 'timezone') ?> - <?= $this->form->select('timezone', $timezones, $values, $errors) ?> + <?= $this->form->select('timezone', $timezones, $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_timezone') ? '' : 'disabled')) ?> <?= $this->form->label(t('Language'), 'language') ?> - <?= $this->form->select('language', $languages, $values, $errors) ?> + <?= $this->form->select('language', $languages, $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_language') ? '' : 'disabled')) ?> <?php if ($this->user->isAdmin()): ?> <?= $this->form->label(t('Role'), 'role') ?> diff --git a/app/Template/user_view/sidebar.php b/app/Template/user_view/sidebar.php index d200a7f5..3dc6b7bc 100644 --- a/app/Template/user_view/sidebar.php +++ b/app/Template/user_view/sidebar.php @@ -12,18 +12,26 @@ </li> <?php endif ?> <?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?> - <li <?= $this->app->checkMenuSelection('UserViewController', 'timesheet') ?>> - <?= $this->url->link(t('Time tracking'), 'UserViewController', 'timesheet', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'lastLogin') ?>> - <?= $this->url->link(t('Last logins'), 'UserViewController', 'lastLogin', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'sessions') ?>> - <?= $this->url->link(t('Persistent connections'), 'UserViewController', 'sessions', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'passwordReset') ?>> - <?= $this->url->link(t('Password reset history'), 'UserViewController', 'passwordReset', array('user_id' => $user['id'])) ?> - </li> + <?php if ($this->user->hasAccess('UserViewController', 'timesheet')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'timesheet') ?>> + <?= $this->url->link(t('Time tracking'), 'UserViewController', 'timesheet', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'lastLogin')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'lastLogin') ?>> + <?= $this->url->link(t('Last logins'), 'UserViewController', 'lastLogin', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'sessions')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'sessions') ?>> + <?= $this->url->link(t('Persistent connections'), 'UserViewController', 'sessions', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'passwordReset')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'passwordReset') ?>> + <?= $this->url->link(t('Password reset history'), 'UserViewController', 'passwordReset', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> <?php endif ?> <?= $this->hook->render('template:user:sidebar:information', array('user' => $user)) ?> @@ -42,13 +50,13 @@ </li> <?php endif ?> - <?php if ($user['is_ldap_user'] == 0): ?> + <?php if ($user['is_ldap_user'] == 0 && $this->user->hasAccess('UserCredentialController', 'changePassword')): ?> <li <?= $this->app->checkMenuSelection('UserCredentialController', 'changePassword') ?>> <?= $this->url->link(t('Change password'), 'UserCredentialController', 'changePassword', array('user_id' => $user['id'])) ?> </li> <?php endif ?> - <?php if ($this->user->isCurrentUser($user['id'])): ?> + <?php if ($this->user->isCurrentUser($user['id']) && $this->user->hasAccess('TwoFactorController', 'index')): ?> <li <?= $this->app->checkMenuSelection('TwoFactorController', 'index') ?>> <?= $this->url->link(t('Two factor authentication'), 'TwoFactorController', 'index', array('user_id' => $user['id'])) ?> </li> @@ -58,18 +66,26 @@ </li> <?php endif ?> - <li <?= $this->app->checkMenuSelection('UserViewController', 'share') ?>> - <?= $this->url->link(t('Public access'), 'UserViewController', 'share', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'notifications') ?>> - <?= $this->url->link(t('Notifications'), 'UserViewController', 'notifications', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'external') ?>> - <?= $this->url->link(t('External accounts'), 'UserViewController', 'external', array('user_id' => $user['id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('UserViewController', 'integrations') ?>> - <?= $this->url->link(t('Integrations'), 'UserViewController', 'integrations', array('user_id' => $user['id'])) ?> - </li> + <?php if ($this->user->hasAccess('UserViewController', 'share')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'share') ?>> + <?= $this->url->link(t('Public access'), 'UserViewController', 'share', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'notifications')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'notifications') ?>> + <?= $this->url->link(t('Notifications'), 'UserViewController', 'notifications', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'external')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'external') ?>> + <?= $this->url->link(t('External accounts'), 'UserViewController', 'external', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserViewController', 'integrations')): ?> + <li <?= $this->app->checkMenuSelection('UserViewController', 'integrations') ?>> + <?= $this->url->link(t('Integrations'), 'UserViewController', 'integrations', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> <?php endif ?> <?php if ($this->user->hasAccess('UserCredentialController', 'changeAuthentication')): ?> diff --git a/app/User/OAuthUserProvider.php b/app/User/OAuthUserProvider.php index dec26250..e5fedcca 100644 --- a/app/User/OAuthUserProvider.php +++ b/app/User/OAuthUserProvider.php @@ -13,14 +13,6 @@ use Kanboard\Core\User\UserProviderInterface; abstract class OAuthUserProvider implements UserProviderInterface { /** - * Get external id column name - * - * @access public - * @return string - */ - abstract public function getExternalIdColumn(); - - /** * User properties * * @access protected @@ -69,7 +61,7 @@ abstract class OAuthUserProvider implements UserProviderInterface */ public function getExternalId() { - return $this->user['id']; + return isset($this->user['id']) ? $this->user['id'] : ''; } /** @@ -102,7 +94,7 @@ abstract class OAuthUserProvider implements UserProviderInterface */ public function getName() { - return $this->user['name']; + return isset($this->user['name']) ? $this->user['name'] : ''; } /** @@ -113,7 +105,7 @@ abstract class OAuthUserProvider implements UserProviderInterface */ public function getEmail() { - return $this->user['email']; + return isset($this->user['email']) ? $this->user['email'] : ''; } /** diff --git a/app/common.php b/app/common.php index 72be3603..15fd7a75 100644 --- a/app/common.php +++ b/app/common.php @@ -46,6 +46,7 @@ $container->register(new Kanboard\ServiceProvider\ActionProvider()); $container->register(new Kanboard\ServiceProvider\ExternalLinkProvider()); $container->register(new Kanboard\ServiceProvider\AvatarProvider()); $container->register(new Kanboard\ServiceProvider\FilterProvider()); +$container->register(new Kanboard\ServiceProvider\JobProvider()); $container->register(new Kanboard\ServiceProvider\QueueProvider()); $container->register(new Kanboard\ServiceProvider\ApiProvider()); $container->register(new Kanboard\ServiceProvider\CommandProvider()); diff --git a/app/constants.php b/app/constants.php index fc120692..40b88fe9 100644 --- a/app/constants.php +++ b/app/constants.php @@ -134,3 +134,5 @@ defined('HTTP_PROXY_PORT') or define('HTTP_PROXY_PORT', '3128'); defined('HTTP_PROXY_USERNAME') or define('HTTP_PROXY_USERNAME', ''); defined('HTTP_PROXY_PASSWORD') or define('HTTP_PROXY_PASSWORD', ''); defined('HTTP_VERIFY_SSL_CERTIFICATE') or define('HTTP_VERIFY_SSL_CERTIFICATE', true); + +defined('TOTP_ISSUER') or define('TOTP_ISSUER', 'Kanboard'); diff --git a/assets/css/app.min.css b/assets/css/app.min.css index 88acd0a0..2ddd4a8b 100644 --- a/assets/css/app.min.css +++ b/assets/css/app.min.css @@ -1 +1 @@ -a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.btn,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}.tag-autocomplete,textarea{width:400px}textarea{border:1px solid #ccc;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}span.select2-container{margin-top:2px}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;padding-top:2px}.form-actions{padding-top:20px;clear:both}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-required{color:red;padding-left:5px;font-weight:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-date,input.form-datetime{width:150px}input.form-input-large{width:400px}input.form-input-small{width:150px}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row}.form-column{margin-right:25px}.form-login{width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:700;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36C;font-size:150%;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}.project-overview-columns,.task-summary-columns{display:-webkit-flex;-webkit-flex-direction:row}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:flex;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;left:15%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:90%}#main .confirm{max-width:700px;font-size:1.1em}.sidebar-container{margin-top:10px;height:100%;width:100%;display:-ms-flexbox;display:-webkit-box;display:-moz-box;display:-ms-box;display:box;-ms-flex-direction:row;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;-ms-box-orient:horizontal;box-orient:horizontal}.sidebar-content{padding-left:10px;-ms-flex:1;-webkit-box-flex:1;-moz-box-flex:1;-ms-box-flex:1;box-flex:1}.sidebar{padding-right:10px;border-right:1px dotted #eee;font-size:.95em;max-width:240px;min-width:190px;width:18%;-ms-flex:0 100px;-webkit-box-flex:0;-moz-box-flex:0;-ms-box-flex:0;box-flex:0}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#666}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid silver;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:flex;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}.project-overview-column{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff}
\ No newline at end of file +a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.btn,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}.tag-autocomplete,textarea{width:400px}textarea{border:1px solid #ccc;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}span.select2-container{margin-top:2px}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;padding-top:2px}.form-actions{padding-top:20px;clear:both}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-required{color:red;padding-left:5px;font-weight:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-date,input.form-datetime{width:150px}input.form-input-large{width:400px}input.form-input-small{width:150px}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row}.form-column{margin-right:25px}.form-login{width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:700;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36C;font-size:150%;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}.project-overview-columns,.task-summary-columns{display:-webkit-flex;-webkit-flex-direction:row}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:flex;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}.markdown-editor-container{max-width:400px}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;left:15%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:90%}#main .confirm{max-width:700px;font-size:1.1em}.sidebar-container{margin-top:10px;height:100%;width:100%;display:-ms-flexbox;display:-webkit-box;display:-moz-box;display:-ms-box;display:box;-ms-flex-direction:row;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;-ms-box-orient:horizontal;box-orient:horizontal}.sidebar-content{padding-left:10px;-ms-flex:1;-webkit-box-flex:1;-moz-box-flex:1;-ms-box-flex:1;box-flex:1}.sidebar{padding-right:10px;border-right:1px dotted #eee;font-size:.95em;max-width:240px;min-width:190px;width:18%;-ms-flex:0 100px;-webkit-box-flex:0;-moz-box-flex:0;-ms-box-flex:0;box-flex:0}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#666}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid silver;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:flex;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}.project-overview-column{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff}
\ No newline at end of file diff --git a/assets/css/print.min.css b/assets/css/print.min.css index 9bc2d615..080626b3 100644 --- a/assets/css/print.min.css +++ b/assets/css/print.min.css @@ -1 +1 @@ -a:hover,th a{text-decoration:none;color:#333}.table-fixed td,.table-fixed th{overflow:hidden}#board td,td{vertical-align:top}#comments form,.board-column-collapsed,.page-header,.sidebar,header{display:none}.table-fixed td,.task-board-collapsed{white-space:nowrap;text-overflow:ellipsis}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none;border:1px dotted #aaa}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}td.board-column-task-collapsed{font-weight:700;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36C;font-size:150%;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:.5}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}
\ No newline at end of file +a:hover,th a{text-decoration:none;color:#333}.table-fixed td,.table-fixed th{overflow:hidden}#board td,td{vertical-align:top}#comments form,.board-column-collapsed,.page-header,.sidebar,header{display:none}.table-fixed td,.task-board-collapsed{white-space:nowrap;text-overflow:ellipsis}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none;border:1px dotted #aaa}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}td.board-column-task-collapsed{font-weight:700;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36C;font-size:150%;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:.5}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}.markdown-editor-container{max-width:400px}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}
\ No newline at end of file diff --git a/assets/css/src/markdown.css b/assets/css/src/markdown.css index 90fd48ea..520961a1 100644 --- a/assets/css/src/markdown.css +++ b/assets/css/src/markdown.css @@ -1,4 +1,8 @@ /* markdown editor */ +.markdown-editor-container { + max-width: 400px; +} + div.CodeMirror, div.CodeMirror-scroll { max-height: 250px; diff --git a/assets/css/vendor.min.css b/assets/css/vendor.min.css index 9d81051d..edd97352 100644 --- a/assets/css/vendor.min.css +++ b/assets/css/vendor.min.css @@ -459,7 +459,7 @@ This file is generated by `grunt build`, do not edit it by hand. } /* @end */ -.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} /*! * FullCalendar v2.7.1 Stylesheet diff --git a/assets/js/vendor.min.js b/assets/js/vendor.min.js index 4b53fb27..08f82895 100644 --- a/assets/js/vendor.min.js +++ b/assets/js/vendor.min.js @@ -1638,8 +1638,9 @@ if(c&&c._defaults.timeOnly&&b.input.val()!==b.lastVal)try{$.datepicker._updateDa }).call(this); -/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.slice(0,n.length-1).concat(a),k=0;k<a.length;k+=1)if(m=a[k],"."===m)a.splice(k,1),k-=1;else if(".."===m){if(1===k&&(".."===a[2]||".."===a[0]))break;k>0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){var d=v.call(arguments,0);return"string"!=typeof d[0]&&1===d.length&&d.push(null),n.apply(b,d.concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n<c.length;n+=1)if(m=o(c[n],f),k=m.f,"require"===k)u[n]=p.require(a);else if("exports"===k)u[n]=p.exports(a),s=!0;else if("module"===k)h=u[n]=p.module(a);else if(e(q,k)||e(r,k)||e(t,k))u[n]=j(k);else{if(!m.p)throw new Error(a+" missing "+k);m.p.load(m.n,g(f,!0),i(k),{}),u[n]=q[k]}l=d?d.apply(q[a],u):void 0,a&&(h&&h.exports!==b&&h.exports!==q[a]?q[a]=h.exports:l===b&&s||(q[a]=l))}else a&&(q[a]=d)},a=c=n=function(a,c,d,e,f){if("string"==typeof a)return p[a]?p[a](c):j(o(a,c).f);if(!a.splice){if(s=a,s.deps&&n(s.deps,s.callback),!c)return;c.splice?(a=c,c=d,d=null):a=b}return c=c||function(){},"function"==typeof d&&(d=e,e=f),e?m(b,a,c,d):setTimeout(function(){m(b,a,c,d)},4),n},n.config=function(a){return n(a)},a._defined=q,d=function(a,b,c){if("string"!=typeof a)throw new Error("See almond README: incorrect module build, no module name");b.splice||(c=b,b=[]),e(q,a)||e(r,a)||(r[a]=[a,b,c])},d.amd={jQuery:!0}}(),b.requirejs=a,b.require=c,b.define=d}}(),b.define("almond",function(){}),b.define("jquery",[],function(){var b=a||$;return null==b&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),b}),b.define("select2/utils",["jquery"],function(a){function b(a){var b=a.prototype,c=[];for(var d in b){var e=b[d];"function"==typeof e&&"constructor"!==d&&c.push(d)}return c}var c={};c.Extend=function(a,b){function c(){this.constructor=a}var d={}.hasOwnProperty;for(var e in b)d.call(b,e)&&(a[e]=b[e]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},c.Decorate=function(a,c){function d(){var b=Array.prototype.unshift,d=c.prototype.constructor.length,e=a.prototype.constructor;d>0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;h<g.length;h++){var i=g[h];d.prototype[i]=a.prototype[i]}for(var j=(function(a){var b=function(){};a in d.prototype&&(b=d.prototype[a]);var e=c.prototype[a];return function(){var a=Array.prototype.unshift;return a.call(arguments,b),e.apply(this,arguments)}}),k=0;k<f.length;k++){var l=f[k];d.prototype[l]=j(l)}return d};var d=function(){this.listeners={}};return d.prototype.on=function(a,b){this.listeners=this.listeners||{},a in this.listeners?this.listeners[a].push(b):this.listeners[a]=[b]},d.prototype.trigger=function(a){var b=Array.prototype.slice;this.listeners=this.listeners||{},a in this.listeners&&this.invoke(this.listeners[a],b.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},d.prototype.invoke=function(a,b){for(var c=0,d=a.length;d>c;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e<c.length;e++){var f=c[e];f=f.substring(0,1).toLowerCase()+f.substring(1),f in d||(d[f]={}),e==c.length-1&&(d[f]=a[b]),d=d[f]}delete a[b]}}return a},c.hasScroll=function(b,c){var d=a(c),e=c.style.overflowX,f=c.style.overflowY;return e!==f||"hidden"!==f&&"visible"!==f?"scroll"===e||"scroll"===f?!0:d.innerHeight()<c.scrollHeight||d.innerWidth()<c.scrollWidth:!1},c.escapeMarkup=function(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<ul class="select2-results__options" role="tree"></ul>');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('<li role="treeitem" aria-live="assertive" class="select2-results__option"></li>'),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c<a.results.length;c++){var d=a.results[c],e=this.option(d);b.push(e)}this.$results.append(b)},c.prototype.position=function(a,b){var c=b.find(".select2-results");c.append(a)},c.prototype.sort=function(a){var b=this.options.get("sorter");return b(a)},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()}),e=b.$results.find(".select2-results__option[aria-selected]");e.each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")});var f=e.filter("[aria-selected=true]");f.length>0?f.first().trigger("mouseenter"):e.first().trigger("mouseenter")})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";a(h);this.template(b,h);for(var i=[],j=0;j<b.children.length;j++){var k=b.children[j],l=this.option(k);i.push(l)}var m=a("<ul></ul>",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b,c){var d=this,e=b.id+"-results";this.$results.attr("id",e),b.on("results:all",function(a){d.clear(),d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("results:append",function(a){d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("query",function(a){d.hideMessages(),d.showLoading(a)}),b.on("select",function(){b.isOpen()&&d.setClasses()}),b.on("unselect",function(){b.isOpen()&&d.setClasses()}),b.on("open",function(){d.$results.attr("aria-expanded","true"),d.$results.attr("aria-hidden","false"),d.setClasses(),d.ensureHighlightVisible()}),b.on("close",function(){d.$results.attr("aria-expanded","false"),d.$results.attr("aria-hidden","true"),d.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=d.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=d.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?d.trigger("close",{}):d.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var e=c-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top,h=f.offset().top,i=d.$results.scrollTop()+(h-g);0===e?d.$results.scrollTop(0):0>h-g&&d.$results.scrollTop(i)}}),b.on("results:next",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a),e=c+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top+d.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=d.$results.scrollTop()+h-g;0===e?d.$results.scrollTop(0):h>g&&d.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){d.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=d.$results.scrollTop(),c=d.$results.get(0).scrollHeight-b+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=d.$results.height();e?(d.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(d.$results.scrollTop(d.$results.get(0).scrollHeight-d.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var c=a(this),e=c.data("data");return"true"===c.attr("aria-selected")?void(d.options.get("multiple")?d.trigger("unselect",{originalEvent:b,data:e}):d.trigger("close",{})):void d.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(b){var c=a(this).data("data");d.getHighlightedResults().removeClass("select2-results__option--highlighted"),d.trigger("results:focus",{data:c,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a,b){var d=this,e=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("selection:update",function(a){c.update(a.data)})},e.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},e.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},e.prototype.selectionContainer=function(){return a("<span></span>")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.prop("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('<ul class="select2-selection__rendered"></ul>'),a},d.prototype.bind=function(b,c){var e=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){e.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!e.options.get("disabled")){var c=a(this),d=c.parent(),f=d.data("data");e.trigger("unselect",{originalEvent:b,data:f})}})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},d.prototype.selectionContainer=function(){var b=a('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">×</span></li>');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d<a.length;d++){var e=a[d],f=this.selectionContainer(),g=this.display(e,f);f.append(g),f.prop("title",e.title||e.text),f.data("data",e),b.push(f)}var h=this.$selection.find(".select2-selection__rendered");c.appendMany(h,b)}},d}),b.define("select2/selection/placeholder",["../utils"],function(a){function b(a,b,c){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c)}return b.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},b.prototype.createPlaceholder=function(a,b){var c=this.selectionContainer();return c.html(this.display(b)),c.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),c},b.prototype.update=function(a,b){var c=1==b.length&&b[0].id!=this.placeholder.id,d=b.length>1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},b}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e<d.length;e++){var f={data:d[e]};if(this.trigger("unselect",f),f.prevented)return}this.$element.val(this.placeholder.id).trigger("change"),this.trigger("toggle",{})}}},c.prototype._handleKeyboardClear=function(a,c,d){d.isOpen()||(c.which==b.DELETE||c.which==b.BACKSPACE)&&this._handleClear(c)},c.prototype.update=function(b,c){if(b.call(this,c),!(this.$selection.find(".select2-selection__placeholder").length>0||0===c.length)){var d=a('<span class="select2-selection__clear">×</span>');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" aria-autocomplete="list" /></li>');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f<a.length;f++){var g=a[f].id;-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")});else{var d=a.id;this.$element.val(d),this.$element.trigger("change")}},d.prototype.unselect=function(a){var b=this;if(this.$element.prop("multiple"))return a.selected=!1, -c(a.element).is("option")?(a.element.selected=!1,void this.$element.trigger("change")):void this.current(function(d){for(var e=[],f=0;f<d.length;f++){var g=d[f].id;g!==a.id&&-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")})},d.prototype.bind=function(a,b){var c=this;this.container=a,a.on("select",function(a){c.select(a.data)}),a.on("unselect",function(a){c.unselect(a.data)})},d.prototype.destroy=function(){this.$element.find("*").each(function(){c.removeData(this,"data")})},d.prototype.query=function(a,b){var d=[],e=this,f=this.$element.children();f.each(function(){var b=c(this);if(b.is("option")||b.is("optgroup")){var f=e.item(b),g=e.matches(a,f);null!==g&&d.push(g)}}),b({results:d})},d.prototype.addOptions=function(a){b.appendMany(this.$element,a)},d.prototype.option=function(a){var b;a.children?(b=document.createElement("optgroup"),b.label=a.text):(b=document.createElement("option"),void 0!==b.textContent?b.textContent=a.text:b.innerText=a.text),a.id&&(b.value=a.id),a.disabled&&(b.disabled=!0),a.selected&&(b.selected=!0),a.title&&(b.title=a.title);var d=c(b),e=this._normalizeItem(a);return e.element=b,c.data(b,"data",e),d},d.prototype.item=function(a){var b={};if(b=c.data(a[0],"data"),null!=b)return b;if(a.is("option"))b={id:a.val(),text:a.text(),disabled:a.prop("disabled"),selected:a.prop("selected"),title:a.prop("title")};else if(a.is("optgroup")){b={text:a.prop("label"),children:[],title:a.prop("title")};for(var d=a.children("option"),e=[],f=0;f<d.length;f++){var g=c(d[f]),h=this.item(g);e.push(h)}b.children=e}return b=this._normalizeItem(b),b.element=a[0],c.data(a[0],"data",b),b},d.prototype._normalizeItem=function(a){c.isPlainObject(a)||(a={id:a,text:a}),a=c.extend({},{text:""},a);var b={selected:!1,disabled:!1};return null!=a.id&&(a.id=a.id.toString()),null!=a.text&&(a.text=a.text.toString()),null==a._resultId&&a.id&&null!=this.container&&(a._resultId=this.generateResultId(this.container,a)),c.extend({},b,a)},d.prototype.matches=function(a,b){var c=this.options.get("matcher");return c(a,b)},d}),b.define("select2/data/array",["./select","../utils","jquery"],function(a,b,c){function d(a,b){var c=b.get("data")||[];d.__super__.constructor.call(this,a,b),this.addOptions(this.convertToOptions(c))}return b.Extend(d,a),d.prototype.select=function(a){var b=this.$element.find("option").filter(function(b,c){return c.value==a.id.toString()});0===b.length&&(b=this.option(a),this.addOptions(b)),d.__super__.select.call(this,a)},d.prototype.convertToOptions=function(a){function d(a){return function(){return c(this).val()==a.id}}for(var e=this,f=this.$element.find("option"),g=f.map(function(){return e.item(c(this)).id}).get(),h=[],i=0;i<a.length;i++){var j=this._normalizeItem(a[i]);if(c.inArray(j.id,g)>=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&""!==a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h<e.length;h++){var i=e[h],j=this._normalizeItem(i),k=this.option(j);this.$element.append(k)}}return b.prototype.query=function(a,b,c){function d(a,f){for(var g=a.results,h=0;h<g.length;h++){var i=g[h],j=null!=i.children&&!d({results:i.children},!0),k=i.text===b.term;if(k||j)return f?!1:(a.data=g,void c(a))}if(f)return!0;var l=e.createTag(b);if(null!=l){var m=e.option(l);m.attr("data-select2-tag",!0),e.addOptions([m]),e.insertTag(g,l)}a.results=g,c(a)}var e=this;return this._removeOldTags(),null==b.term||null!=b.page?void a.call(this,b,c):void a.call(this,b,d)},b.prototype.createTag=function(b,c){var d=a.trim(c.term);return""===d?null:{id:d,text:d}},b.prototype.insertTag=function(a,b,c){b.unshift(c)},b.prototype._removeOldTags=function(b){var c=(this._lastTag,this.$element.find("option[data-select2-tag]"));c.each(function(){this.selected||a(this).remove()})},b}),b.define("select2/data/tokenizer",["jquery"],function(a){function b(a,b,c){var d=c.get("tokenizer");void 0!==d&&(this.tokenizer=d),a.call(this,b,c)}return b.prototype.bind=function(a,b,c){a.call(this,b,c),this.$search=b.dropdown.$search||b.selection.$search||c.find(".select2-search__field")},b.prototype.query=function(a,b,c){function d(a){e.trigger("select",{data:a})}var e=this;b.term=b.term||"";var f=this.tokenizer(b,this.options,d);f.term!==b.term&&(this.$search.length&&(this.$search.val(f.term),this.$search.focus()),b.term=f.term),a.call(this,b,c)},b.prototype.tokenizer=function(b,c,d,e){for(var f=d.get("tokenSeparators")||[],g=c.term,h=0,i=this.createTag||function(a){return{id:a.term,text:a.term}};h<g.length;){var j=g[h];if(-1!==a.inArray(j,f)){var k=g.substr(0,h),l=a.extend({},c,{term:k}),m=i(l);null!=m?(e(m),g=g.substr(h+1)||"",h=0):h++}else h++}return{term:g}},b}),b.define("select2/data/minimumInputLength",[],function(){function a(a,b,c){this.minimumInputLength=c.get("minimumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",b.term.length<this.minimumInputLength?void this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumInputLength",[],function(){function a(a,b,c){this.maximumInputLength=c.get("maximumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",this.maximumInputLength>0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<span class="select2-dropdown"><span class="select2-results"></span></span>');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" /></span>');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('<li class="select2-results__option select2-results__option--load-more"role="treeitem" aria-disabled="true"></li>'),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a("<span></span>"),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.top<f.top-h.height,k=i.bottom>f.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d<b.length;d++){var e=b[d];e.children?c+=a(e.children):c++}return c}function b(a,b,c,d){this.minimumResultsForSearch=c.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),a.call(this,b,c,d)}return b.prototype.showSearch=function(b,c){return a(c.data.results)<this.minimumResultsForSearch?!1:b.call(this,c)},b}),b.define("select2/dropdown/selectOnClose",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("close",function(){d._handleSelectOnClose()})},a.prototype._handleSelectOnClose=function(){var a=this.getHighlightedResults();if(!(a.length<1)){var b=a.data("data");null!=b.element&&b.element.selected||null==b.element&&b.selected||this.trigger("select",{data:b})}},a}),b.define("select2/dropdown/closeOnSelect",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("select",function(a){d._selectTriggered(a)}),b.on("unselect",function(a){d._selectTriggered(a)})},a.prototype._selectTriggered=function(a,b){var c=b.originalEvent;c&&c.ctrlKey||this.trigger("close",{})},a}),b.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(a){var b=a.input.length-a.maximum,c="Please delete "+b+" character";return 1!=b&&(c+="s"),c},inputTooShort:function(a){var b=a.minimum-a.input.length,c="Please enter "+b+" or more characters";return c},loadingMore:function(){return"Loading more results…"},maximumSelected:function(a){var b="You can only select "+a.maximum+" item";return 1!=a.maximum&&(b+="s"),b},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),b.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C){function D(){this.reset()}D.prototype.apply=function(l){if(l=a.extend(!0,{},this.defaults,l),null==l.dataAdapter){if(null!=l.ajax?l.dataAdapter=o:null!=l.data?l.dataAdapter=n:l.dataAdapter=m,l.minimumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L<K.length;L++){var M=K[L],N={};try{N=k.loadPath(M)}catch(O){try{M=this.defaults.amdLanguageBase+M,N=k.loadPath(M)}catch(P){l.debug&&window.console&&console.warn&&console.warn('Select2: The language file for "'+M+'" could not be automatically loaded. A fallback will be used instead.');continue}}J.extend(N)}l.translations=J}else{var Q=k.loadPath(this.defaults.amdLanguageBase+"en"),R=new k(l.language);R.extend(Q),l.translations=R}return l},D.prototype.reset=function(){function b(a){function b(a){return l[a]||a}return a.replace(/[^\u0000-\u007E]/g,b)}function c(d,e){if(""===a.trim(d.term))return e;if(e.children&&e.children.length>0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this._sync=c.bind(this._syncAttributes,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._sync);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._sync)}),this._observer.observe(this.$element[0],{attributes:!0,subtree:!1})):this.$element[0].addEventListener&&this.$element[0].addEventListener("DOMAttrModified",b._sync,!1)},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._sync),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&this.$element[0].removeEventListener("DOMAttrModified",this._sync,!1),this._sync=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},e.prototype.render=function(){var b=a('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults"],function(a,b,c,d){if(null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d;return this.each(function(){var c=a(this).data("select2");null==c&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2.");var e=Array.prototype.slice.call(arguments,1);d=c[b].apply(c,e)}),a.inArray(b,e)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c}); +/*! Select2 4.0.3 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.slice(0,n.length-1).concat(a),k=0;k<a.length;k+=1)if(m=a[k],"."===m)a.splice(k,1),k-=1;else if(".."===m){if(1===k&&(".."===a[2]||".."===a[0]))break;k>0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){var d=v.call(arguments,0);return"string"!=typeof d[0]&&1===d.length&&d.push(null),n.apply(b,d.concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n<c.length;n+=1)if(m=o(c[n],f),k=m.f,"require"===k)u[n]=p.require(a);else if("exports"===k)u[n]=p.exports(a),s=!0;else if("module"===k)h=u[n]=p.module(a);else if(e(q,k)||e(r,k)||e(t,k))u[n]=j(k);else{if(!m.p)throw new Error(a+" missing "+k);m.p.load(m.n,g(f,!0),i(k),{}),u[n]=q[k]}l=d?d.apply(q[a],u):void 0,a&&(h&&h.exports!==b&&h.exports!==q[a]?q[a]=h.exports:l===b&&s||(q[a]=l))}else a&&(q[a]=d)},a=c=n=function(a,c,d,e,f){if("string"==typeof a)return p[a]?p[a](c):j(o(a,c).f);if(!a.splice){if(s=a,s.deps&&n(s.deps,s.callback),!c)return;c.splice?(a=c,c=d,d=null):a=b}return c=c||function(){},"function"==typeof d&&(d=e,e=f),e?m(b,a,c,d):setTimeout(function(){m(b,a,c,d)},4),n},n.config=function(a){return n(a)},a._defined=q,d=function(a,b,c){if("string"!=typeof a)throw new Error("See almond README: incorrect module build, no module name");b.splice||(c=b,b=[]),e(q,a)||e(r,a)||(r[a]=[a,b,c])},d.amd={jQuery:!0}}(),b.requirejs=a,b.require=c,b.define=d}}(),b.define("almond",function(){}),b.define("jquery",[],function(){var b=a||$;return null==b&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),b}),b.define("select2/utils",["jquery"],function(a){function b(a){var b=a.prototype,c=[];for(var d in b){var e=b[d];"function"==typeof e&&"constructor"!==d&&c.push(d)}return c}var c={};c.Extend=function(a,b){function c(){this.constructor=a}var d={}.hasOwnProperty;for(var e in b)d.call(b,e)&&(a[e]=b[e]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},c.Decorate=function(a,c){function d(){var b=Array.prototype.unshift,d=c.prototype.constructor.length,e=a.prototype.constructor;d>0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;h<g.length;h++){var i=g[h];d.prototype[i]=a.prototype[i]}for(var j=(function(a){var b=function(){};a in d.prototype&&(b=d.prototype[a]);var e=c.prototype[a];return function(){var a=Array.prototype.unshift;return a.call(arguments,b),e.apply(this,arguments)}}),k=0;k<f.length;k++){var l=f[k];d.prototype[l]=j(l)}return d};var d=function(){this.listeners={}};return d.prototype.on=function(a,b){this.listeners=this.listeners||{},a in this.listeners?this.listeners[a].push(b):this.listeners[a]=[b]},d.prototype.trigger=function(a){var b=Array.prototype.slice,c=b.call(arguments,1);this.listeners=this.listeners||{},null==c&&(c=[]),0===c.length&&c.push({}),c[0]._type=a,a in this.listeners&&this.invoke(this.listeners[a],b.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},d.prototype.invoke=function(a,b){for(var c=0,d=a.length;d>c;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e<c.length;e++){var f=c[e];f=f.substring(0,1).toLowerCase()+f.substring(1),f in d||(d[f]={}),e==c.length-1&&(d[f]=a[b]),d=d[f]}delete a[b]}}return a},c.hasScroll=function(b,c){var d=a(c),e=c.style.overflowX,f=c.style.overflowY;return e!==f||"hidden"!==f&&"visible"!==f?"scroll"===e||"scroll"===f?!0:d.innerHeight()<c.scrollHeight||d.innerWidth()<c.scrollWidth:!1},c.escapeMarkup=function(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<ul class="select2-results__options" role="tree"></ul>');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('<li role="treeitem" aria-live="assertive" class="select2-results__option"></li>'),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c<a.results.length;c++){var d=a.results[c],e=this.option(d);b.push(e)}this.$results.append(b)},c.prototype.position=function(a,b){var c=b.find(".select2-results");c.append(a)},c.prototype.sort=function(a){var b=this.options.get("sorter");return b(a)},c.prototype.highlightFirstItem=function(){var a=this.$results.find(".select2-results__option[aria-selected]"),b=a.filter("[aria-selected=true]");b.length>0?b.first().trigger("mouseenter"):a.first().trigger("mouseenter"),this.ensureHighlightVisible()},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()}),e=b.$results.find(".select2-results__option[aria-selected]");e.each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")})})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";a(h);this.template(b,h);for(var i=[],j=0;j<b.children.length;j++){var k=b.children[j],l=this.option(k);i.push(l)}var m=a("<ul></ul>",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b,c){var d=this,e=b.id+"-results";this.$results.attr("id",e),b.on("results:all",function(a){d.clear(),d.append(a.data),b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("results:append",function(a){d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("query",function(a){d.hideMessages(),d.showLoading(a)}),b.on("select",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("unselect",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("open",function(){d.$results.attr("aria-expanded","true"),d.$results.attr("aria-hidden","false"),d.setClasses(),d.ensureHighlightVisible()}),b.on("close",function(){d.$results.attr("aria-expanded","false"),d.$results.attr("aria-hidden","true"),d.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=d.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=d.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?d.trigger("close",{}):d.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var e=c-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top,h=f.offset().top,i=d.$results.scrollTop()+(h-g);0===e?d.$results.scrollTop(0):0>h-g&&d.$results.scrollTop(i)}}),b.on("results:next",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a),e=c+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top+d.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=d.$results.scrollTop()+h-g;0===e?d.$results.scrollTop(0):h>g&&d.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){d.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=d.$results.scrollTop(),c=d.$results.get(0).scrollHeight-b+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=d.$results.height();e?(d.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(d.$results.scrollTop(d.$results.get(0).scrollHeight-d.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var c=a(this),e=c.data("data");return"true"===c.attr("aria-selected")?void(d.options.get("multiple")?d.trigger("unselect",{originalEvent:b,data:e}):d.trigger("close",{})):void d.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(b){var c=a(this).data("data");d.getHighlightedResults().removeClass("select2-results__option--highlighted"),d.trigger("results:focus",{data:c,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a,b){var d=this,e=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("focus",function(b){a.isOpen()||c.$selection.focus()}),a.on("selection:update",function(a){c.update(a.data)})},e.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},e.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},e.prototype.selectionContainer=function(){return a("<span></span>")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.prop("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('<ul class="select2-selection__rendered"></ul>'),a},d.prototype.bind=function(b,c){var e=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){e.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!e.options.get("disabled")){var c=a(this),d=c.parent(),f=d.data("data");e.trigger("unselect",{originalEvent:b,data:f})}})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},d.prototype.selectionContainer=function(){var b=a('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">×</span></li>');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d<a.length;d++){var e=a[d],f=this.selectionContainer(),g=this.display(e,f);f.append(g),f.prop("title",e.title||e.text),f.data("data",e),b.push(f)}var h=this.$selection.find(".select2-selection__rendered");c.appendMany(h,b)}},d}),b.define("select2/selection/placeholder",["../utils"],function(a){function b(a,b,c){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c)}return b.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},b.prototype.createPlaceholder=function(a,b){var c=this.selectionContainer();return c.html(this.display(b)),c.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),c},b.prototype.update=function(a,b){var c=1==b.length&&b[0].id!=this.placeholder.id,d=b.length>1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},b}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e<d.length;e++){var f={data:d[e]};if(this.trigger("unselect",f),f.prevented)return}this.$element.val(this.placeholder.id).trigger("change"),this.trigger("toggle",{})}}},c.prototype._handleKeyboardClear=function(a,c,d){d.isOpen()||(c.which==b.DELETE||c.which==b.BACKSPACE)&&this._handleClear(c)},c.prototype.update=function(b,c){if(b.call(this,c),!(this.$selection.find(".select2-selection__placeholder").length>0||0===c.length)){var d=a('<span class="select2-selection__clear">×</span>');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" aria-autocomplete="list" /></li>');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change"); +if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f<a.length;f++){var g=a[f].id;-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")});else{var d=a.id;this.$element.val(d),this.$element.trigger("change")}},d.prototype.unselect=function(a){var b=this;if(this.$element.prop("multiple"))return a.selected=!1,c(a.element).is("option")?(a.element.selected=!1,void this.$element.trigger("change")):void this.current(function(d){for(var e=[],f=0;f<d.length;f++){var g=d[f].id;g!==a.id&&-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")})},d.prototype.bind=function(a,b){var c=this;this.container=a,a.on("select",function(a){c.select(a.data)}),a.on("unselect",function(a){c.unselect(a.data)})},d.prototype.destroy=function(){this.$element.find("*").each(function(){c.removeData(this,"data")})},d.prototype.query=function(a,b){var d=[],e=this,f=this.$element.children();f.each(function(){var b=c(this);if(b.is("option")||b.is("optgroup")){var f=e.item(b),g=e.matches(a,f);null!==g&&d.push(g)}}),b({results:d})},d.prototype.addOptions=function(a){b.appendMany(this.$element,a)},d.prototype.option=function(a){var b;a.children?(b=document.createElement("optgroup"),b.label=a.text):(b=document.createElement("option"),void 0!==b.textContent?b.textContent=a.text:b.innerText=a.text),a.id&&(b.value=a.id),a.disabled&&(b.disabled=!0),a.selected&&(b.selected=!0),a.title&&(b.title=a.title);var d=c(b),e=this._normalizeItem(a);return e.element=b,c.data(b,"data",e),d},d.prototype.item=function(a){var b={};if(b=c.data(a[0],"data"),null!=b)return b;if(a.is("option"))b={id:a.val(),text:a.text(),disabled:a.prop("disabled"),selected:a.prop("selected"),title:a.prop("title")};else if(a.is("optgroup")){b={text:a.prop("label"),children:[],title:a.prop("title")};for(var d=a.children("option"),e=[],f=0;f<d.length;f++){var g=c(d[f]),h=this.item(g);e.push(h)}b.children=e}return b=this._normalizeItem(b),b.element=a[0],c.data(a[0],"data",b),b},d.prototype._normalizeItem=function(a){c.isPlainObject(a)||(a={id:a,text:a}),a=c.extend({},{text:""},a);var b={selected:!1,disabled:!1};return null!=a.id&&(a.id=a.id.toString()),null!=a.text&&(a.text=a.text.toString()),null==a._resultId&&a.id&&null!=this.container&&(a._resultId=this.generateResultId(this.container,a)),c.extend({},b,a)},d.prototype.matches=function(a,b){var c=this.options.get("matcher");return c(a,b)},d}),b.define("select2/data/array",["./select","../utils","jquery"],function(a,b,c){function d(a,b){var c=b.get("data")||[];d.__super__.constructor.call(this,a,b),this.addOptions(this.convertToOptions(c))}return b.Extend(d,a),d.prototype.select=function(a){var b=this.$element.find("option").filter(function(b,c){return c.value==a.id.toString()});0===b.length&&(b=this.option(a),this.addOptions(b)),d.__super__.select.call(this,a)},d.prototype.convertToOptions=function(a){function d(a){return function(){return c(this).val()==a.id}}for(var e=this,f=this.$element.find("option"),g=f.map(function(){return e.item(c(this)).id}).get(),h=[],i=0;i<a.length;i++){var j=this._normalizeItem(a[i]);if(c.inArray(j.id,g)>=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h<e.length;h++){var i=e[h],j=this._normalizeItem(i),k=this.option(j);this.$element.append(k)}}return b.prototype.query=function(a,b,c){function d(a,f){for(var g=a.results,h=0;h<g.length;h++){var i=g[h],j=null!=i.children&&!d({results:i.children},!0),k=i.text===b.term;if(k||j)return f?!1:(a.data=g,void c(a))}if(f)return!0;var l=e.createTag(b);if(null!=l){var m=e.option(l);m.attr("data-select2-tag",!0),e.addOptions([m]),e.insertTag(g,l)}a.results=g,c(a)}var e=this;return this._removeOldTags(),null==b.term||null!=b.page?void a.call(this,b,c):void a.call(this,b,d)},b.prototype.createTag=function(b,c){var d=a.trim(c.term);return""===d?null:{id:d,text:d}},b.prototype.insertTag=function(a,b,c){b.unshift(c)},b.prototype._removeOldTags=function(b){var c=(this._lastTag,this.$element.find("option[data-select2-tag]"));c.each(function(){this.selected||a(this).remove()})},b}),b.define("select2/data/tokenizer",["jquery"],function(a){function b(a,b,c){var d=c.get("tokenizer");void 0!==d&&(this.tokenizer=d),a.call(this,b,c)}return b.prototype.bind=function(a,b,c){a.call(this,b,c),this.$search=b.dropdown.$search||b.selection.$search||c.find(".select2-search__field")},b.prototype.query=function(b,c,d){function e(b){var c=g._normalizeItem(b),d=g.$element.find("option").filter(function(){return a(this).val()===c.id});if(!d.length){var e=g.option(c);e.attr("data-select2-tag",!0),g._removeOldTags(),g.addOptions([e])}f(c)}function f(a){g.trigger("select",{data:a})}var g=this;c.term=c.term||"";var h=this.tokenizer(c,this.options,e);h.term!==c.term&&(this.$search.length&&(this.$search.val(h.term),this.$search.focus()),c.term=h.term),b.call(this,c,d)},b.prototype.tokenizer=function(b,c,d,e){for(var f=d.get("tokenSeparators")||[],g=c.term,h=0,i=this.createTag||function(a){return{id:a.term,text:a.term}};h<g.length;){var j=g[h];if(-1!==a.inArray(j,f)){var k=g.substr(0,h),l=a.extend({},c,{term:k}),m=i(l);null!=m?(e(m),g=g.substr(h+1)||"",h=0):h++}else h++}return{term:g}},b}),b.define("select2/data/minimumInputLength",[],function(){function a(a,b,c){this.minimumInputLength=c.get("minimumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",b.term.length<this.minimumInputLength?void this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumInputLength",[],function(){function a(a,b,c){this.maximumInputLength=c.get("maximumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",this.maximumInputLength>0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<span class="select2-dropdown"><span class="select2-results"></span></span>');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" /></span>');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()&&e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('<li class="select2-results__option select2-results__option--load-more"role="treeitem" aria-disabled="true"></li>'),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a("<span></span>"),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.top<f.top-h.height,k=i.bottom>f.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d<b.length;d++){var e=b[d];e.children?c+=a(e.children):c++}return c}function b(a,b,c,d){this.minimumResultsForSearch=c.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),a.call(this,b,c,d)}return b.prototype.showSearch=function(b,c){return a(c.data.results)<this.minimumResultsForSearch?!1:b.call(this,c)},b}),b.define("select2/dropdown/selectOnClose",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("close",function(a){d._handleSelectOnClose(a)})},a.prototype._handleSelectOnClose=function(a,b){if(b&&null!=b.originalSelect2Event){var c=b.originalSelect2Event;if("select"===c._type||"unselect"===c._type)return}var d=this.getHighlightedResults();if(!(d.length<1)){var e=d.data("data");null!=e.element&&e.element.selected||null==e.element&&e.selected||this.trigger("select",{data:e})}},a}),b.define("select2/dropdown/closeOnSelect",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("select",function(a){d._selectTriggered(a)}),b.on("unselect",function(a){d._selectTriggered(a)})},a.prototype._selectTriggered=function(a,b){var c=b.originalEvent;c&&c.ctrlKey||this.trigger("close",{originalEvent:c,originalSelect2Event:b})},a}),b.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(a){var b=a.input.length-a.maximum,c="Please delete "+b+" character";return 1!=b&&(c+="s"),c},inputTooShort:function(a){var b=a.minimum-a.input.length,c="Please enter "+b+" or more characters";return c},loadingMore:function(){return"Loading more results…"},maximumSelected:function(a){var b="You can only select "+a.maximum+" item";return 1!=a.maximum&&(b+="s"),b},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),b.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C){function D(){this.reset()}D.prototype.apply=function(l){if(l=a.extend(!0,{},this.defaults,l),null==l.dataAdapter){if(null!=l.ajax?l.dataAdapter=o:null!=l.data?l.dataAdapter=n:l.dataAdapter=m,l.minimumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L<K.length;L++){var M=K[L],N={};try{N=k.loadPath(M)}catch(O){try{M=this.defaults.amdLanguageBase+M,N=k.loadPath(M)}catch(P){l.debug&&window.console&&console.warn&&console.warn('Select2: The language file for "'+M+'" could not be automatically loaded. A fallback will be used instead.');continue}}J.extend(N)}l.translations=J}else{var Q=k.loadPath(this.defaults.amdLanguageBase+"en"),R=new k(l.language);R.extend(Q),l.translations=R}return l},D.prototype.reset=function(){function b(a){function b(a){return l[a]||a}return a.replace(/[^\u0000-\u007E]/g,b)}function c(d,e){if(""===a.trim(d.term))return e;if(e.children&&e.children.length>0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e<b.addedNodes.length;e++){var f=b.addedNodes[e];f.selected&&(c=!0)}else b.removedNodes&&b.removedNodes.length>0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null; +},e.prototype.render=function(){var b=a('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults"],function(a,b,c,d){if(null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d,f=Array.prototype.slice.call(arguments,1);return this.each(function(){var c=a(this).data("select2");null==c&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2."),d=c[b].apply(c,f)}),a.inArray(b,e)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c}); !function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return ce.apply(null,arguments)}function b(a){ce=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function e(a,b){var c,d=[];for(c=0;c<a.length;++c)d.push(b(a[c],c));return d}function f(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function g(a,b){for(var c in b)f(b,c)&&(a[c]=b[c]);return f(b,"toString")&&(a.toString=b.toString),f(b,"valueOf")&&(a.valueOf=b.valueOf),a}function h(a,b,c,d){return Ja(a,b,c,d,!0).utc()}function i(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null}}function j(a){return null==a._pf&&(a._pf=i()),a._pf}function k(a){if(null==a._isValid){var b=j(a),c=de.call(b.parsedDateParts,function(a){return null!=a});a._isValid=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.invalidWeekday&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated&&(!b.meridiem||b.meridiem&&c),a._strict&&(a._isValid=a._isValid&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour)}return a._isValid}function l(a){var b=h(NaN);return null!=a?g(j(b),a):j(b).userInvalidated=!0,b}function m(a){return void 0===a}function n(a,b){var c,d,e;if(m(b._isAMomentObject)||(a._isAMomentObject=b._isAMomentObject),m(b._i)||(a._i=b._i),m(b._f)||(a._f=b._f),m(b._l)||(a._l=b._l),m(b._strict)||(a._strict=b._strict),m(b._tzm)||(a._tzm=b._tzm),m(b._isUTC)||(a._isUTC=b._isUTC),m(b._offset)||(a._offset=b._offset),m(b._pf)||(a._pf=j(b)),m(b._locale)||(a._locale=b._locale),ee.length>0)for(c in ee)d=ee[c],e=b[d],m(e)||(a[d]=e);return a}function o(b){n(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),fe===!1&&(fe=!0,a.updateOffset(this),fe=!1)}function p(a){return a instanceof o||null!=a&&null!=a._isAMomentObject}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=q(b)),c}function s(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&r(a[d])!==r(b[d]))&&g++;return g+f}function t(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function u(b,c){var d=!0;return g(function(){return null!=a.deprecationHandler&&a.deprecationHandler(null,b),d&&(t(b+"\nArguments: "+Array.prototype.slice.call(arguments).join(", ")+"\n"+(new Error).stack),d=!1),c.apply(this,arguments)},c)}function v(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),ge[b]||(t(c),ge[b]=!0)}function w(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function x(a){return"[object Object]"===Object.prototype.toString.call(a)}function y(a){var b,c;for(c in a)b=a[c],w(b)?this[c]=b:this["_"+c]=b;this._config=a,this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function z(a,b){var c,d=g({},a);for(c in b)f(b,c)&&(x(a[c])&&x(b[c])?(d[c]={},g(d[c],a[c]),g(d[c],b[c])):null!=b[c]?d[c]=b[c]:delete d[c]);return d}function A(a){null!=a&&this.set(a)}function B(a){return a?a.toLowerCase().replace("_","-"):a}function C(a){for(var b,c,d,e,f=0;f<a.length;){for(e=B(a[f]).split("-"),b=e.length,c=B(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=D(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&s(e,c,!0)>=b-1)break;b--}f++}return null}function D(a){var b=null;if(!ke[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=ie._abbr,require("./locale/"+a),E(b)}catch(c){}return ke[a]}function E(a,b){var c;return a&&(c=m(b)?H(a):F(a,b),c&&(ie=c)),ie._abbr}function F(a,b){return null!==b?(b.abbr=a,null!=ke[a]?(v("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale"),b=z(ke[a]._config,b)):null!=b.parentLocale&&(null!=ke[b.parentLocale]?b=z(ke[b.parentLocale]._config,b):v("parentLocaleUndefined","specified parentLocale is not defined yet")),ke[a]=new A(b),E(a),ke[a]):(delete ke[a],null)}function G(a,b){if(null!=b){var c;null!=ke[a]&&(b=z(ke[a]._config,b)),c=new A(b),c.parentLocale=ke[a],ke[a]=c,E(a)}else null!=ke[a]&&(null!=ke[a].parentLocale?ke[a]=ke[a].parentLocale:null!=ke[a]&&delete ke[a]);return ke[a]}function H(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return ie;if(!c(a)){if(b=D(a))return b;a=[a]}return C(a)}function I(){return he(ke)}function J(a,b){var c=a.toLowerCase();le[c]=le[c+"s"]=le[b]=a}function K(a){return"string"==typeof a?le[a]||le[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)f(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(b,c){return function(d){return null!=d?(O(this,b,d),a.updateOffset(this,c),this):N(this,b)}}function N(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function O(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}function P(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=K(a),w(this[a]))return this[a](b);return this}function Q(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function R(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(pe[a]=e),b&&(pe[b[0]]=function(){return Q(e.apply(this,arguments),b[1],b[2])}),c&&(pe[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function S(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function T(a){var b,c,d=a.match(me);for(b=0,c=d.length;c>b;b++)pe[d[b]]?d[b]=pe[d[b]]:d[b]=S(d[b]);return function(b){var e,f="";for(e=0;c>e;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}function U(a,b){return a.isValid()?(b=V(b,a.localeData()),oe[b]=oe[b]||T(b),oe[b](a)):a.localeData().invalidDate()}function V(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(ne.lastIndex=0;d>=0&&ne.test(a);)a=a.replace(ne,c),ne.lastIndex=0,d-=1;return a}function W(a,b,c){He[a]=w(b)?b:function(a,d){return a&&c?c:b}}function X(a,b){return f(He,a)?He[a](b._strict,b._locale):new RegExp(Y(a))}function Y(a){return Z(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function Z(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function $(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=r(a)}),c=0;c<a.length;c++)Ie[a[c]]=d}function _(a,b){$(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function aa(a,b,c){null!=b&&f(Ie,a)&&Ie[a](b,c._a,c,a)}function ba(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function ca(a,b){return c(this._months)?this._months[a.month()]:this._months[Se.test(b)?"format":"standalone"][a.month()]}function da(a,b){return c(this._monthsShort)?this._monthsShort[a.month()]:this._monthsShort[Se.test(b)?"format":"standalone"][a.month()]}function ea(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],d=0;12>d;++d)f=h([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=je.call(this._shortMonthsParse,g),-1!==e?e:null):(e=je.call(this._longMonthsParse,g),-1!==e?e:null):"MMM"===b?(e=je.call(this._shortMonthsParse,g),-1!==e?e:(e=je.call(this._longMonthsParse,g),-1!==e?e:null)):(e=je.call(this._longMonthsParse,g),-1!==e?e:(e=je.call(this._shortMonthsParse,g),-1!==e?e:null))}function fa(a,b,c){var d,e,f;if(this._monthsParseExact)return ea.call(this,a,b,c);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=h([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function ga(a,b){var c;if(!a.isValid())return a;if("string"==typeof b)if(/^\d+$/.test(b))b=r(b);else if(b=a.localeData().monthsParse(b),"number"!=typeof b)return a;return c=Math.min(a.date(),ba(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ha(b){return null!=b?(ga(this,b),a.updateOffset(this,!0),this):N(this,"Month")}function ia(){return ba(this.year(),this.month())}function ja(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex}function ka(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsStrictRegex:this._monthsRegex):this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex}function la(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;12>b;b++)c=h([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(d.sort(a),e.sort(a),f.sort(a),b=0;12>b;b++)d[b]=Z(d[b]),e[b]=Z(e[b]),f[b]=Z(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}function ma(a){var b,c=a._a;return c&&-2===j(a).overflow&&(b=c[Ke]<0||c[Ke]>11?Ke:c[Le]<1||c[Le]>ba(c[Je],c[Ke])?Le:c[Me]<0||c[Me]>24||24===c[Me]&&(0!==c[Ne]||0!==c[Oe]||0!==c[Pe])?Me:c[Ne]<0||c[Ne]>59?Ne:c[Oe]<0||c[Oe]>59?Oe:c[Pe]<0||c[Pe]>999?Pe:-1,j(a)._overflowDayOfYear&&(Je>b||b>Le)&&(b=Le),j(a)._overflowWeeks&&-1===b&&(b=Qe),j(a)._overflowWeekday&&-1===b&&(b=Re),j(a).overflow=b),a}function na(a){var b,c,d,e,f,g,h=a._i,i=Xe.exec(h)||Ye.exec(h);if(i){for(j(a).iso=!0,b=0,c=$e.length;c>b;b++)if($e[b][1].exec(i[1])){e=$e[b][0],d=$e[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=_e.length;c>b;b++)if(_e[b][1].exec(i[3])){f=(i[2]||" ")+_e[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!Ze.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),Ca(a)}else a._isValid=!1}function oa(b){var c=af.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(na(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function pa(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 100>a&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function qa(a){var b=new Date(Date.UTC.apply(null,arguments));return 100>a&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}function ra(a){return sa(a)?366:365}function sa(a){return a%4===0&&a%100!==0||a%400===0}function ta(){return sa(this.year())}function ua(a,b,c){var d=7+b-c,e=(7+qa(a,0,d).getUTCDay()-b)%7;return-e+d-1}function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return 0>=j?(f=a-1,g=ra(f)+j):j>ra(a)?(f=a+1,g=j-ra(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return 1>g?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(ra(a)-d+e)/7}function ya(a,b,c){return null!=a?a:null!=b?b:c}function za(b){var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}function Aa(a){var b,c,d,e,f=[];if(!a._d){for(d=za(a),a._w&&null==a._a[Le]&&null==a._a[Ke]&&Ba(a),a._dayOfYear&&(e=ya(a._a[Je],d[Je]),a._dayOfYear>ra(e)&&(j(a)._overflowDayOfYear=!0),c=qa(e,0,a._dayOfYear),a._a[Ke]=c.getUTCMonth(),a._a[Le]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[Me]&&0===a._a[Ne]&&0===a._a[Oe]&&0===a._a[Pe]&&(a._nextDay=!0,a._a[Me]=0),a._d=(a._useUTC?qa:pa).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Me]=24)}}function Ba(a){var b,c,d,e,f,g,h,i;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=ya(b.GG,a._a[Je],wa(Ka(),1,4).year),d=ya(b.W,1),e=ya(b.E,1),(1>e||e>7)&&(i=!0)):(f=a._locale._week.dow,g=a._locale._week.doy,c=ya(b.gg,a._a[Je],wa(Ka(),f,g).year),d=ya(b.w,1),null!=b.d?(e=b.d,(0>e||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f),1>d||d>xa(c,f,g)?j(a)._overflowWeeks=!0:null!=i?j(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[Je]=h.year,a._dayOfYear=h.dayOfYear)}function Ca(b){if(b._f===a.ISO_8601)return void na(b);b._a=[],j(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,k=0;for(e=V(b._f,b._locale).match(me)||[],c=0;c<e.length;c++)f=e[c],d=(h.match(X(f,b))||[])[0],d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&j(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),k+=d.length),pe[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f),aa(f,d,b)):b._strict&&!d&&j(b).unusedTokens.push(f);j(b).charsLeftOver=i-k,h.length>0&&j(b).unusedInput.push(h),j(b).bigHour===!0&&b._a[Me]<=12&&b._a[Me]>0&&(j(b).bigHour=void 0),j(b).parsedDateParts=b._a.slice(0),j(b).meridiem=b._meridiem,b._a[Me]=Da(b._locale,b._a[Me],b._meridiem),Aa(b),ma(b)}function Da(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function Ea(a){var b,c,d,e,f;if(0===a._f.length)return j(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e<a._f.length;e++)f=0,b=n({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],Ca(b),k(b)&&(f+=j(b).charsLeftOver,f+=10*j(b).unusedTokens.length,j(b).score=f,(null==d||d>f)&&(d=f,c=b));g(a,c||b)}function Fa(a){if(!a._d){var b=L(a._i);a._a=e([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),Aa(a)}}function Ga(a){var b=new o(ma(Ha(a)));return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function Ha(a){var b=a._i,e=a._f;return a._locale=a._locale||H(a._l),null===b||void 0===e&&""===b?l({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),p(b)?new o(ma(b)):(c(e)?Ea(a):e?Ca(a):d(b)?a._d=b:Ia(a),k(a)||(a._d=null),a))}function Ia(b){var f=b._i;void 0===f?b._d=new Date(a.now()):d(f)?b._d=new Date(f.valueOf()):"string"==typeof f?oa(b):c(f)?(b._a=e(f.slice(0),function(a){return parseInt(a,10)}),Aa(b)):"object"==typeof f?Fa(b):"number"==typeof f?b._d=new Date(f):a.createFromInputFallback(b)}function Ja(a,b,c,d,e){var f={};return"boolean"==typeof c&&(d=c,c=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=e,f._l=c,f._i=a,f._f=b,f._strict=d,Ga(f)}function Ka(a,b,c,d){return Ja(a,b,c,d,!1)}function La(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return Ka();for(d=b[0],e=1;e<b.length;++e)(!b[e].isValid()||b[e][a](d))&&(d=b[e]);return d}function Ma(){var a=[].slice.call(arguments,0);return La("isBefore",a)}function Na(){var a=[].slice.call(arguments,0);return La("isAfter",a)}function Oa(a){var b=L(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+1e3*h*60*60,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=H(),this._bubble()}function Pa(a){return a instanceof Oa}function Qa(a,b){R(a,0,0,function(){var a=this.utcOffset(),c="+";return 0>a&&(a=-a,c="-"),c+Q(~~(a/60),2)+b+Q(~~a%60,2)})}function Ra(a,b){var c=(b||"").match(a)||[],d=c[c.length-1]||[],e=(d+"").match(ff)||["-",0,0],f=+(60*e[1])+r(e[2]);return"+"===e[0]?f:-f}function Sa(b,c){var e,f;return c._isUTC?(e=c.clone(),f=(p(b)||d(b)?b.valueOf():Ka(b).valueOf())-e.valueOf(),e._d.setTime(e._d.valueOf()+f),a.updateOffset(e,!1),e):Ka(b).local()}function Ta(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Ua(b,c){var d,e=this._offset||0;return this.isValid()?null!=b?("string"==typeof b?b=Ra(Ee,b):Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Ta(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?jb(this,db(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Ta(this):null!=b?this:NaN}function Va(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Wa(a){return this.utcOffset(0,a)}function Xa(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Ta(this),"m")),this}function Ya(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Ra(De,this._i)),this}function Za(a){return this.isValid()?(a=a?Ka(a).utcOffset():0,(this.utcOffset()-a)%60===0):!1}function $a(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function _a(){if(!m(this._isDSTShifted))return this._isDSTShifted;var a={};if(n(a,this),a=Ha(a),a._a){var b=a._isUTC?h(a._a):Ka(a._a);this._isDSTShifted=this.isValid()&&s(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function ab(){return this.isValid()?!this._isUTC:!1}function bb(){return this.isValid()?this._isUTC:!1}function cb(){return this.isValid()?this._isUTC&&0===this._offset:!1}function db(a,b){var c,d,e,g=a,h=null;return Pa(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=gf.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:r(h[Le])*c,h:r(h[Me])*c,m:r(h[Ne])*c,s:r(h[Oe])*c,ms:r(h[Pe])*c}):(h=hf.exec(a))?(c="-"===h[1]?-1:1,g={y:eb(h[2],c),M:eb(h[3],c),w:eb(h[4],c),d:eb(h[5],c),h:eb(h[6],c),m:eb(h[7],c),s:eb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=gb(Ka(g.from),Ka(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new Oa(g),Pa(a)&&f(a,"_locale")&&(d._locale=a._locale),d}function eb(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function fb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function gb(a,b){var c;return a.isValid()&&b.isValid()?(b=Sa(b,a),a.isBefore(b)?c=fb(a,b):(c=fb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}function hb(a){return 0>a?-1*Math.round(-1*a):Math.round(a)}function ib(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(v(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=db(c,d),jb(this,e,a),this}}function jb(b,c,d,e){var f=c._milliseconds,g=hb(c._days),h=hb(c._months);b.isValid()&&(e=null==e?!0:e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&O(b,"Date",N(b,"Date")+g*d),h&&ga(b,N(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function kb(a,b){var c=a||Ka(),d=Sa(c,this).startOf("day"),e=this.diff(d,"days",!0),f=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse",g=b&&(w(b[f])?b[f]():b[f]);return this.format(g||this.localeData().calendar(f,this,Ka(c)))}function lb(){return new o(this)}function mb(a,b){var c=p(a)?a:Ka(a);return this.isValid()&&c.isValid()?(b=K(m(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()<this.clone().startOf(b).valueOf()):!1}function nb(a,b){var c=p(a)?a:Ka(a);return this.isValid()&&c.isValid()?(b=K(m(b)?"millisecond":b),"millisecond"===b?this.valueOf()<c.valueOf():this.clone().endOf(b).valueOf()<c.valueOf()):!1}function ob(a,b,c,d){return d=d||"()",("("===d[0]?this.isAfter(a,c):!this.isBefore(a,c))&&(")"===d[1]?this.isBefore(b,c):!this.isAfter(b,c))}function pb(a,b){var c,d=p(a)?a:Ka(a);return this.isValid()&&d.isValid()?(b=K(b||"millisecond"),"millisecond"===b?this.valueOf()===d.valueOf():(c=d.valueOf(),this.clone().startOf(b).valueOf()<=c&&c<=this.clone().endOf(b).valueOf())):!1}function qb(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function rb(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function sb(a,b,c){var d,e,f,g;return this.isValid()?(d=Sa(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=K(b),"year"===b||"month"===b||"quarter"===b?(g=tb(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:q(g)):NaN):NaN}function tb(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function ub(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function vb(){var a=this.clone().utc();return 0<a.year()&&a.year()<=9999?w(Date.prototype.toISOString)?this.toDate().toISOString():U(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):U(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}function wb(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=U(this,b);return this.localeData().postformat(c)}function xb(a,b){return this.isValid()&&(p(a)&&a.isValid()||Ka(a).isValid())?db({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function yb(a){return this.from(Ka(),a)}function zb(a,b){return this.isValid()&&(p(a)&&a.isValid()||Ka(a).isValid())?db({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function Ab(a){return this.to(Ka(),a)}function Bb(a){var b;return void 0===a?this._locale._abbr:(b=H(a),null!=b&&(this._locale=b),this)}function Cb(){return this._locale}function Db(a){switch(a=K(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":case"date":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function Eb(a){return a=K(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function Fb(){return this._d.valueOf()-6e4*(this._offset||0)}function Gb(){return Math.floor(this.valueOf()/1e3)}function Hb(){return this._offset?new Date(this.valueOf()):this._d}function Ib(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function Jb(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function Kb(){return this.isValid()?this.toISOString():null}function Lb(){return k(this)}function Mb(){return g({},j(this))}function Nb(){return j(this).overflow}function Ob(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function Pb(a,b){R(0,[a,a.length],0,b)}function Qb(a){return Ub.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Rb(a){return Ub.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Sb(){return xa(this.year(),1,4)}function Tb(){var a=this.localeData()._week;return xa(this.year(),a.dow,a.doy)}function Ub(a,b,c,d,e){var f;return null==a?wa(this,d,e).year:(f=xa(a,d,e),b>f&&(b=f),Vb.call(this,a,b,c,d,e))}function Vb(a,b,c,d,e){var f=va(a,b,c,d,e),g=qa(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}function Wb(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Xb(a){return wa(a,this._week.dow,this._week.doy).week}function Yb(){return this._week.dow}function Zb(){return this._week.doy}function $b(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function _b(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function ac(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function bc(a,b){return c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]}function cc(a){return this._weekdaysShort[a.day()]}function dc(a){return this._weekdaysMin[a.day()]}function ec(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;7>d;++d)f=h([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=je.call(this._weekdaysParse,g),-1!==e?e:null):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:null):(e=je.call(this._minWeekdaysParse,g),-1!==e?e:null):"dddd"===b?(e=je.call(this._weekdaysParse,g),-1!==e?e:(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:(e=je.call(this._minWeekdaysParse,g),-1!==e?e:null))):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:(e=je.call(this._weekdaysParse,g),-1!==e?e:(e=je.call(this._minWeekdaysParse,g),-1!==e?e:null))):(e=je.call(this._minWeekdaysParse,g),-1!==e?e:(e=je.call(this._weekdaysParse,g),-1!==e?e:(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:null)))}function fc(a,b,c){var d,e,f;if(this._weekdaysParseExact)return ec.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;7>d;d++){if(e=h([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}function gc(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=ac(a,this.localeData()),this.add(a-b,"d")):b}function hc(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function ic(a){return this.isValid()?null==a?this.day()||7:this.day(this.day()%7?a:a-7):null!=a?this:NaN}function jc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex}function kc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex}function lc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex}function mc(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],i=[],j=[],k=[];for(b=0;7>b;b++)c=h([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),i.push(e),j.push(f),k.push(d),k.push(e),k.push(f);for(g.sort(a),i.sort(a),j.sort(a),k.sort(a),b=0;7>b;b++)i[b]=Z(i[b]),j[b]=Z(j[b]),k[b]=Z(k[b]);this._weekdaysRegex=new RegExp("^("+k.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}function nc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function oc(){return this.hours()%12||12}function pc(){return this.hours()||24}function qc(a,b){R(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function rc(a,b){return b._meridiemParse}function sc(a){return"p"===(a+"").toLowerCase().charAt(0)}function tc(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function uc(a,b){b[Pe]=r(1e3*("0."+a))}function vc(){return this._isUTC?"UTC":""}function wc(){return this._isUTC?"Coordinated Universal Time":""}function xc(a){return Ka(1e3*a)}function yc(){return Ka.apply(null,arguments).parseZone()}function zc(a,b,c){var d=this._calendar[a];return w(d)?d.call(b,c):d}function Ac(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function Bc(){return this._invalidDate}function Cc(a){return this._ordinal.replace("%d",a)}function Dc(a){return a}function Ec(a,b,c,d){var e=this._relativeTime[c];return w(e)?e(a,b,c,d):e.replace(/%d/i,a)}function Fc(a,b){var c=this._relativeTime[a>0?"future":"past"];return w(c)?c(b):c.replace(/%s/i,b)}function Gc(a,b,c,d){var e=H(),f=h().set(d,b);return e[c](f,a)}function Hc(a,b,c){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return Gc(a,b,c,"month");var d,e=[];for(d=0;12>d;d++)e[d]=Gc(a,d,c,"month");return e}function Ic(a,b,c,d){"boolean"==typeof a?("number"==typeof b&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,"number"==typeof b&&(c=b,b=void 0),b=b||"");var e=H(),f=a?e._week.dow:0;if(null!=c)return Gc(b,(c+f)%7,d,"day");var g,h=[];for(g=0;7>g;g++)h[g]=Gc(b,(g+f)%7,d,"day");return h}function Jc(a,b){return Hc(a,b,"months")}function Kc(a,b){return Hc(a,b,"monthsShort")}function Lc(a,b,c){return Ic(a,b,c,"weekdays")}function Mc(a,b,c){return Ic(a,b,c,"weekdaysShort")}function Nc(a,b,c){return Ic(a,b,c,"weekdaysMin")}function Oc(){var a=this._data;return this._milliseconds=Jf(this._milliseconds),this._days=Jf(this._days),this._months=Jf(this._months),a.milliseconds=Jf(a.milliseconds),a.seconds=Jf(a.seconds),a.minutes=Jf(a.minutes),a.hours=Jf(a.hours),a.months=Jf(a.months),a.years=Jf(a.years),this}function Pc(a,b,c,d){var e=db(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function Qc(a,b){return Pc(this,a,b,1)}function Rc(a,b){return Pc(this,a,b,-1)}function Sc(a){return 0>a?Math.floor(a):Math.ceil(a)}function Tc(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*Sc(Vc(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=q(f/1e3),i.seconds=a%60,b=q(a/60),i.minutes=b%60,c=q(b/60),i.hours=c%24,g+=q(c/24),e=q(Uc(g)),h+=e,g-=Sc(Vc(e)),d=q(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function Uc(a){return 4800*a/146097}function Vc(a){return 146097*a/4800}function Wc(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+Uc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(Vc(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function Xc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*r(this._months/12)}function Yc(a){return function(){return this.as(a)}}function Zc(a){ return a=K(a),this[a+"s"]()}function $c(a){return function(){return this._data[a]}}function _c(){return q(this.days()/7)}function ad(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function bd(a,b,c){var d=db(a).abs(),e=Zf(d.as("s")),f=Zf(d.as("m")),g=Zf(d.as("h")),h=Zf(d.as("d")),i=Zf(d.as("M")),j=Zf(d.as("y")),k=e<$f.s&&["s",e]||1>=f&&["m"]||f<$f.m&&["mm",f]||1>=g&&["h"]||g<$f.h&&["hh",g]||1>=h&&["d"]||h<$f.d&&["dd",h]||1>=i&&["M"]||i<$f.M&&["MM",i]||1>=j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,ad.apply(null,k)}function cd(a,b){return void 0===$f[a]?!1:void 0===b?$f[a]:($f[a]=b,!0)}function dd(a){var b=this.localeData(),c=bd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function ed(){var a,b,c,d=_f(this._milliseconds)/1e3,e=_f(this._days),f=_f(this._months);a=q(d/60),b=q(a/60),d%=60,a%=60,c=q(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(0>m?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"} //! moment.js locale configuration diff --git a/composer.json b/composer.json index d82f3f0c..443fb826 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "eluceo/ical": "0.10.1", "erusev/parsedown" : "1.6.0", "fguillot/json-rpc" : "1.2.1", - "fguillot/picodb" : "1.0.12", + "fguillot/picodb" : "1.0.14", "fguillot/simpleLogger" : "1.0.1", "fguillot/simple-validator" : "1.0.1", "fguillot/simple-queue" : "1.0.1", diff --git a/composer.lock b/composer.lock index 03c5e523..33c2ca71 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "ab5b2c960b3a6d9f93883606269085e0", - "content-hash": "bd5f17c3382d7f85e33a68023927704c", + "hash": "daa76b43d528f87e3bed91133fdb9259", + "content-hash": "dde1b92fc6f9ca106cf927f4cd141a21", "packages": [ { "name": "christian-riesen/base32", @@ -245,16 +245,16 @@ }, { "name": "fguillot/picodb", - "version": "v1.0.12", + "version": "v1.0.14", "source": { "type": "git", "url": "https://github.com/fguillot/picoDb.git", - "reference": "dd088cb75e9035d083f511cdc77b268bc8e110b6" + "reference": "86a831302ab10af800c83dbe4b3b01c88d5433f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/picoDb/zipball/dd088cb75e9035d083f511cdc77b268bc8e110b6", - "reference": "dd088cb75e9035d083f511cdc77b268bc8e110b6", + "url": "https://api.github.com/repos/fguillot/picoDb/zipball/86a831302ab10af800c83dbe4b3b01c88d5433f1", + "reference": "86a831302ab10af800c83dbe4b3b01c88d5433f1", "shasum": "" }, "require": { @@ -281,7 +281,7 @@ ], "description": "Minimalist database query builder", "homepage": "https://github.com/fguillot/picoDb", - "time": "2016-05-28 22:00:54" + "time": "2016-07-16 22:59:59" }, { "name": "fguillot/simple-queue", diff --git a/doc/docker.markdown b/doc/docker.markdown index e55130e5..5b77da76 100644 --- a/doc/docker.markdown +++ b/doc/docker.markdown @@ -93,4 +93,4 @@ References - [Official Kanboard images](https://registry.hub.docker.com/u/kanboard/kanboard/) - [Docker documentation](https://docs.docker.com/) - [Dockerfile stable version](https://github.com/kanboard/docker) -- [Dockerfile dev version](https://github.com/fguillot/kanboard/blob/master/Dockerfile) +- [Dockerfile dev version](https://github.com/kanboard/kanboard/blob/master/Dockerfile) diff --git a/doc/heroku.markdown b/doc/heroku.markdown index 43b15c72..1891efb0 100644 --- a/doc/heroku.markdown +++ b/doc/heroku.markdown @@ -4,7 +4,7 @@ Deploy Kanboard on Heroku You can try Kanboard for free on [Heroku](https://www.heroku.com/). You can use this one click install button or follow the manual instructions below: -[](https://heroku.com/deploy?template=https://github.com/fguillot/kanboard) +[](https://heroku.com/deploy?template=https://github.com/kanboard/kanboard) Requirements ------------ @@ -17,7 +17,7 @@ Manual instructions ```bash # Get the last development version -git clone https://github.com/fguillot/kanboard.git +git clone https://github.com/kanboard/kanboard.git cd kanboard # Push the code to Heroku (You can also use SSH if git over HTTP doesn't work) diff --git a/doc/installation.markdown b/doc/installation.markdown index 2ebe4d14..4955612f 100644 --- a/doc/installation.markdown +++ b/doc/installation.markdown @@ -28,7 +28,7 @@ From the repository (development version) You must install [composer](https://getcomposer.org/) to use this method. -1. `git clone https://github.com/fguillot/kanboard.git` +1. `git clone https://github.com/kanboard/kanboard.git` 2. `composer install --no-dev` 3. Go to the third step just above diff --git a/doc/plugin-authentication.markdown b/doc/plugin-authentication.markdown index 06fdfd8d..e1ca6f01 100644 --- a/doc/plugin-authentication.markdown +++ b/doc/plugin-authentication.markdown @@ -35,6 +35,6 @@ This object must implement the interface `Kanboard\Core\User\UserProviderInterfa Example of authentication plugins --------------------------------- -- [Authentication providers included in Kanboard](https://github.com/fguillot/kanboard/tree/master/app/Auth) +- [Authentication providers included in Kanboard](https://github.com/kanboard/kanboard/tree/master/app/Auth) - [Reverse-Proxy Authentication with LDAP support](https://github.com/kanboard/plugin-reverse-proxy-ldap) - [SMS Two-Factor Authentication](https://github.com/kanboard/plugin-sms-2fa) diff --git a/doc/plugin-group-provider.markdown b/doc/plugin-group-provider.markdown index 4d73b740..31c61aaf 100644 --- a/doc/plugin-group-provider.markdown +++ b/doc/plugin-group-provider.markdown @@ -52,4 +52,4 @@ $groupManager->register(new MyCustomLdapBackendGroupProvider($this->container)); Examples -------- -- [Group providers included in Kanboard (LDAP and Database)](https://github.com/fguillot/kanboard/tree/master/app/Group) +- [Group providers included in Kanboard (LDAP and Database)](https://github.com/kanboard/kanboard/tree/master/app/Group) diff --git a/doc/plugins.markdown b/doc/plugins.markdown index 475bc249..cff3eb6c 100644 --- a/doc/plugins.markdown +++ b/doc/plugins.markdown @@ -5,7 +5,7 @@ Note: The plugin API is **considered alpha** at the moment. Plugins are useful to extend the core functionalities of Kanboard, adding features, creating themes or changing the default behavior. -Plugin creators should specify explicitly the compatible versions of Kanboard. Internal code of Kanboard may change over time and your plugin must be tested with new versions. Always check the [ChangeLog](https://github.com/fguillot/kanboard/blob/master/ChangeLog) for breaking changes. +Plugin creators should specify explicitly the compatible versions of Kanboard. Internal code of Kanboard may change over time and your plugin must be tested with new versions. Always check the [ChangeLog](https://github.com/kanboard/kanboard/blob/master/ChangeLog) for breaking changes. - [Creating your plugin](plugin-registration.markdown) - [Using plugin hooks](plugin-hooks.markdown) diff --git a/doc/ru_RU/2fa.markdown b/doc/ru_RU/2fa.markdown new file mode 100644 index 00000000..0787c720 --- /dev/null +++ b/doc/ru_RU/2fa.markdown @@ -0,0 +1,37 @@ +Двух-уровневая аутентификация +============================= + +Любой пользователь может включить [двух-уровневую аутентификацию](http://en.wikipedia.org/wiki/Two_factor_authentication). После успешного входа, разовый код (6 знаков) запрашивается у пользователя для получения доступа в Канборд. + +Этот код присылается в программу на вашем смартфоне. + +Канборд использует [Time-based One-time Password Algorithm](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) основанный на [RFC 6238](http://tools.ietf.org/html/rfc6238). + +Имеется много программ совместимых со стандартной системой TOTP. Например, вы можете использовать эти приложения, бесплатные и с открытым исходным кодом: + +- [Google Authenticator](https://github.com/google/google-authenticator/) (Android, iOS, Blackberry) +- [FreeOTP](https://fedorahosted.org/freeotp/) (Android, iOS) +- [OATH Toolkit](http://www.nongnu.org/oath-toolkit/) (Command line utility on Unix/Linux) + +Эти системы могут работать офлайн и вам не нужно иметь мобильную связь. + +Настройка +--------- + +1. Перейдите в пользовательский профиль +2. Слева нажмите **Двухфакторная авторизация** и поставте галочку в чекбоке +3. Секретный ключ сгенерируется для вас + + + +Рисунок. Двухуровневая аутентификация. + + +- Вы должны сохранить секретный ключ в вашей TOTP программе. Если вы используете сматрфон, то просто сосканируйте QR код с помощью FreeOTP или Google Authenticator. +- Каждый раз, когда вы будете входить в Канборд, будет запрашиваться новый код +- Не забудьте протестировать ваше устройство, перед тем как закрыть вашу сессию + +Новый секретный ключ генерируется каждый раз при включении/выключении этой возможности. + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/analytics-tasks.markdown b/doc/ru_RU/analytics-tasks.markdown new file mode 100644 index 00000000..176a4616 --- /dev/null +++ b/doc/ru_RU/analytics-tasks.markdown @@ -0,0 +1,37 @@ +Аналитика для задач +=================== + +На странице детального просмотра задачи, в левом боковом меню, для каждой задачи имеется раздел аналитики. + +Затраченное время и время цикла +------------------------------- + + + +Рисунок. Затраченное время и время цикла + + +- Затраченное время - время между созданием задачи и датой завершения (закрытие задачи). +- Время цикла - время между началом испольнения задачи и датой завершения. +- Если задача не закрыта, то для расчета используется текущее время вместо даты завершения. +- Если дата начала выполнения задачи не указана, то время цикла не может быть расчитано. + + +**Заметка**: Вы можете настроить автоматическое создание даты начала выполения задачи, когда вы перемещаете задачу в определенную колонку. + + +Время затраченное в каждой колонке +---------------------------------- + + + +Рисунок. Время затраченное в каждой колонке + + + +- Этот график показывает сколько времени задача находилась в каждой колонке. +- Затраченное время расчитывается до закрытия задачи. + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/analytics.markdown b/doc/ru_RU/analytics.markdown new file mode 100644 index 00000000..2af6de34 --- /dev/null +++ b/doc/ru_RU/analytics.markdown @@ -0,0 +1,95 @@ +Аналитика +========= + +Каждый проект имеет анлитический раздел. В зависимости от того как вы используете Канборд, вы можете видеть подобные отчеты: + +Перераспределение(загрузка) пользователей +----------------------------------------- + + + +Перераспределение(загрузка) пользователей + + +Круговая диаграмма, представленная выше, показыает количество открытых задач назначенных определенным пользователям. + + +Распределение задач +------------------- + + + +Рисунок. Распределение задач + + + +На рисунке выше, представлена круговая диаграмма, которая показывает количество открытых задач в определенных колонках. + + + +Накопительная диаграмма +----------------------- + + + +Рисунок. Накопительная диаграмма + + +- Эта диаграмма отображает количество задач выполненных в каждой колонке в определенный промежуток времени. +- Счетчик задач записывается для каждой колонки каждый день. +- Если вы хотите исключить закрытые задачи, измените [глобальные настройки проекта](project-configuration.markdown). + + +Заметка: Для того чтобы увидеть этот график, вам нужно иметь, как минимум, данные за два дня. + + +Диаграмма сгорания +------------------ + + + +Рисунок. Диаграмма сгорания + + + +[Диаграмма сгорания](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%B0%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B0_%D1%81%D0%B3%D0%BE%D1%80%D0%B0%D0%BD%D0%B8%D1%8F_%D0%B7%D0%B0%D0%B4%D0%B0%D1%87) доступна для каждого проекта. + + +- Эта диаграмма отображает время затраченное на выполнение работы. +- Канборд использует историю задач для генерации этой диаграммы. +- Сумма историй задач для каждой колонки пересчитывается каждый день. + +Среднее время затраченное в каждой колонке +------------------------------------------ + + + +Рисунок. Среднее время затраченное в каждой колонке + + +Этот график показывает среднее время затраченное в каждой колонке для последних 1000 задач. + +- Канборд использует для подсчета данных переходы задач между колонками. +- Затраченное время подсчитывается до закрытия задачи. + +Среднее время выполнения и время цикла +-------------------------------------- + + + +Рисунок. Среднее время затраченное в каждой колонке + +Эта диаграмма показывает Среднее время выполнения и цикла для последних 1000 задач. +- Время выполнения - время между созданием задачи и датой завершения. +- Время цикла - время между указанной датой начала выполнения задачи и датой завершения. +- Если задача не закрыта, текущая дата будет использована вместо даты завершения. + +Эти данные подсчитываются и записываются каждый день на протяжении жизни проекта. + +Заметка: Не забудьте выполнить [ежедневные cronjob](cronjob.markdown) для того чтобы иметь точную статистику. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/api-json-rpc.markdown b/doc/ru_RU/api-json-rpc.markdown new file mode 100644 index 00000000..257f83ec --- /dev/null +++ b/doc/ru_RU/api-json-rpc.markdown @@ -0,0 +1,78 @@ +Json-RPC API +============ + + +API пользователя и приложения +----------------------------- + + +Имеется два типа доступа к API: + +### API приложения[¶](#application-api "Ссылка на этот заголовок") + +- Доступ к API осуществляется с использованием пользователя “jsonrpc” и ключа, доступного в настройках +- Доступ ко всем процедурам +- Не проверяются права доступа +- Нет пользовательской сессии на сервере +- Этот доступ можно использовать для: утилит миграции/импорта данных, создания задач из других систем и т.д. + +### API пользователя[¶](#user-api "Ссылка на этот заголовок") + +- Доступ к API под пользовательскими учетными данными (имя пользователя и пароль) +- Доступ к ограниченному набору процедур +- Проверка прав доступа к проекту +- На сервере создается пользовательская сессия +- Этот доступ можно использовать для клиентов: мобильных/десктопных приложений, утилит коммандной строки и т.д. + +Безопасность +------------ + +- Всегда используйте протокол HTTPS с действительным сертификатом +- Если вы делаете мобильное приложение, позаботьтесь о безопасном хранении учетных данных пользователя на мобильном устройстве +- После 3 неправильных подключений к пользовательскому api, пользователь может разблокировать свою учетную запись только с использованием формы входа +- Двухуровневая аутентификация пока не доступна через API + + + +Протокол +-------- + + +Канборд использует протокол Json-RPC для взаимодействия с внешними программами. + +JSON-RPC - протокол удаленного вызова процедур в формате JSON. По сути своей, тот же XML-RPC, но использующий формат JSON. + +Мы используем [протокол версии 2](http://www.jsonrpc.org/specification). Вы можете вызывать API используя `POST`{.docutils .literal} HTTP запрос. + +Канборд поддерживает пакетные запросы, поэтому вы можете делать многократные API вызовы в одном HTTP запросе. Это, в частности, удобно для мобильных клиентов с высокой сетевой задержкой. + + +Использование +------------- + +- [Аутентификация](api-authentication.markdown) +- [Примеры](api-examples.markdown) +- [Приложение](api-application-procedures.markdown) +- [Проекты](api-project-procedures.markdown) +- [Права доступа к проекту](api-project-permission-procedures.markdown) +- [Доски](api-board-procedures.markdown) +- [Колонки](api-column-procedures.markdown) +- [Дорожки](api-swimlane-procedures.markdown) +- [Категории](api-category-procedures.markdown) +- [Автоматические дейсвия](api-action-procedures.markdown) +- [Задачи](api-task-procedures.markdown) +- [Подзадачи](api-subtask-procedures.markdown) +- [Файлы](api-file-procedures.markdown) +- [Ссылки](api-link-procedures.markdown) +- [Комментарии](api-comment-procedures.markdown) +- [Пользователи](api-user-procedures.markdown) +- [Группы](api-group-procedures.markdown) +- [Члены группы](api-group-member-procedures.markdown) +- [Специфичные запросы пользователя](api-me-procedures.markdown) + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/application-configuration.markdown b/doc/ru_RU/application-configuration.markdown new file mode 100644 index 00000000..d8b2661e --- /dev/null +++ b/doc/ru_RU/application-configuration.markdown @@ -0,0 +1,54 @@ +Настройки приложения +==================== + +Некоторые параметры для приложения могут быть изменены на странице настроек. Только администратор может сделать эти настройки. +Выберите в правом выпадающем меню **Настройки**, затем в слева выберите **Настройки приложения**. + + + +Рисунок. Настройки приложения + + +URL приложения[¶](#application-url "Ссылка на этот заголовок") +-------------------------------------------------------------- + +Этот параметр используется для email уведомлений. В тексте сообщения будет содержаться ссылка на задачу в Канборде. + + +Язык[¶](#language "Ссылка на этот заголовок") +--------------------------------------------- + +Язык приложения может быть изменен в любое время. Язык устанавливается для всех пользователей Канборд. + + +Часовой пояс[¶](#time-zone "Ссылка на этот заголовок") +------------------------------------------------------ + +По умолчанию, Канборд использует часовой пояс UTC, но вы можете указать любой часовой пояс. Список содержит все поддерживаемые часовые пояса для вашего веб сервера. + + +Формат даты[¶](#date-format "Ссылка на этот заголовок") +------------------------------------------------------- + +Формать даты, который используется для полей дата. Например, дата завершения задачи. + +Канборд поддерживает 4 разных формата: + +- ДД/ММ/ГГГГ +- ММ/ДД/ГГГГ (по умолчанию) +- ГГГГ/ММ/ДД +- ММ.ДД.ГГГГ + +Формат [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) всегда принимается (YYYY-MM-DD or YYYY\_MM\_DD). + + +Пользовательский стиль CSS[¶](#custom-stylesheet "Ссылка на этот заголовок") +---------------------------------------------------------------------------- + +Вы можете сделать свой стиль CSS для Канборд или улучшить имеющийся стиль. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/assets.markdown b/doc/ru_RU/assets.markdown new file mode 100644 index 00000000..9a0124c5 --- /dev/null +++ b/doc/ru_RU/assets.markdown @@ -0,0 +1,53 @@ +Как создать asset (Javascript и CSS файлы) +========================================== + + +Файлы CSS стилей и Javascript объединены вместе и минимизированы. + +- Оригинальные файлы CSS хранятся в каталоге `assets/css/src/*.css`{.docutils .literal} +- Оригинальные файлы Javascript хранятся в каталоге `assets/js/src/*.js`{.docutils .literal} +- `assets/*/vendor.min.*`{.docutils .literal} - внешние зависимости объединены и минимизированы +- `assets/*/app.min.*`{.docutils .literal} - исходный код приложения объединены и минимизированы + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + +- [NodeJS](https://nodejs.org/) с `npm`{.docutils .literal} + + +Сборка файлов Javascript и CSS[¶](#building-javascript-and-css-files "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------ + + +Канборд использует [Gulp](http://gulpjs.com/) для сборки asset и [Bower](http://bower.io/) для управления зависимостями. Эти утилиты устанавлены в проекте как зависимости NodeJS. + + +### Запустить все[¶](#run-everything "Ссылка на этот заголовок") + + make static + +### Собрать `vendor.min.js`{.docutils .literal} и `vendor.min.css`{.docutils .literal}[¶](#build-vendor-min-js-and-vendor-min-css "Ссылка на этот заголовок") + + gulp vendor + +### Собрать `app.min.js`{.docutils .literal}[¶](#build-app-min-js "Ссылка на этот заголовок") + + gulp js + + +### Собрать `app.min.css`{.docutils .literal}[¶](#build-app-min-css "Ссылка на этот заголовок") + + gulp css + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + +Сборка asset невозможна из архива Kanboard, вы должны клонировать репозиторий. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/automatic-actions.markdown b/doc/ru_RU/automatic-actions.markdown new file mode 100644 index 00000000..1e0631c3 --- /dev/null +++ b/doc/ru_RU/automatic-actions.markdown @@ -0,0 +1,128 @@ +Автоматизация процессов +======================= + + +Для минимизации пользовательских действий, Kanboard поддерживает автоматизацию процессов. + +Каждый автоматизированный процесс представляет следующее: + +- Ожидание наступления события +- Выполняется действие при наступлении этого события +- В результате устанавливается определенный параметр + +Каждый проект может иметь свой набор автоматических процессов. Автоматические процессы доступны в панеле настроек (**Меню** -\> **Настройки**) **Автоматические действия**. + + +Добавление нового действия[¶](#add-a-new-action "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + +Нажмите на ссылку **Добавить новое действие**. + + + +Рисунок. Автоматическое действие. + + +- Выберете действие +- Затем, выберете событие +- И в завершении, задайте параметр + + +Список доступных действий[¶](#list-of-available-actions "Ссылка на этот заголовок") +----------------------------------------------------------------------------------- + + +- Создать комментарий из внешнего источника +- Добавлять запись при перемещении задачи между колонками +- Автоматически назначать категорию по цвету +- Изменить категорию основываясь на внешнем ярлыке +- Автоматически назначать категории на основе ссылки +- Автоматически назначать цвет по категории +- Назначить цвет, когда задача перемещается в определенную колонку +- Изменение цвета задач при использовании ссылки на определенные задачи +- Назначить определенный цвет пользователю +- Назначить задачу тому кто выполнит действие +- Назначить задачу пользователю, который произвел изменение в колонке +- Назначить задачу определенному пользователю +- Изменить назначенного основываясь на внешнем имени пользователя +- Закрыть задачу +- Закрыть задачу в выбранной колонке +- Создать задачу из внешнего источника +- Создать дубликат задачи в другом проекте +- Отправить задачу по email +- Переместить задачу в другой проект +- Переместить задачу в другую колонку, когда она назначена пользователю +- Переносить задачи в другую колонку при изменении категории +- Переместить задачу в другую колонку, когда назначение снято +- Открыть задачу +- Автоматическое обновление даты начала + + +Примеры[¶](#examples "Ссылка на этот заголовок") +------------------------------------------------ + + +Здесь предствалены примеры использованные в реальной жизни: + +### Когда я перемещаю задачу в колонку “Выполнено”, автоматически закрывать эту задачу[¶](#when-i-move-a-task-to-the-column-done-automatically-close-this-task "Ссылка на этот заголовок") + +- Выберите действия: **Закрыть задачу в выбранной колонке** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = Выполнено** (это колонка в которую будет перемещена задача) + +### Когда я перемещаю задачу в колонку “На утверждение”, назначить эту задачу определенному пользователю.[¶](#when-i-move-a-task-to-the-column-to-be-validated-assign-this-task-to-a-specific-user "Ссылка на этот заголовок") + +- Выберите действие: **Назначить задачу определенному пользователю** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = На утверждение** и **Пользователь = Петр** (Петр - наш тестировщик) + +### Когда я перемещаю задачу в колонку “В работе”, назначить эту задачу определенному пользователю[¶](#when-i-move-a-task-to-the-column-work-in-progress-assign-this-task-to-the-current-user "Ссылка на этот заголовок") + +- Выберите действие: **Назначить задачу пользователю, который произвел изменение в колонке** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = В работе** + + +### Когда задача выполнена, скопировать эту задачу в другой проект[¶](#when-a-task-is-completed-duplicate-this-task-to-another-project "Ссылка на этот заголовок") + +Предположим, мы имеем два проекта “Заказы покупателей” и “Производство”. Когда заказ в проекте “Заказы покупателей” утвержден, копируем этот заказ в проект “Производство”. + +- Выбираем действие: **Создать дубликат задачи в другом проекте** +- Выбираем событие: **Завершение задачи** +- Установите параметр действия: **Колонка = Утвержден** и **Проект = Производство** + + +### Когда задача перемещена в последнюю колонку, переместить эту задачу в другой проект[¶](#when-a-task-is-moved-to-the-last-column-move-the-exact-same-task-to-another-project "Ссылка на этот заголовок") + + +Предположим, мы имеем два проекта “Идеи” и “Разработка”, когда идея утверждена, перемещаем эту задачу в проект “Разработка”. + +- Выберите действие: **Переместить задачу в другой проект** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = Утверждена** и **Проект = Разработка** + +### Я хочу назначать автоматически цвет для пользователя Петр[¶](#i-want-to-assign-automatically-a-color-to-the-user-bob "Ссылка на этот заголовок") + +- Выберите действие: **Назначить определенный цвет пользователю** +- Выберите событие: **Изменен назначенный** +- Установите параметр действия: **Цвет = Зеленый** и **Назначена = Петр** + + +### Я хочу назначить цвет автоматически для определенной категории “Важные запросы”[¶](#i-want-to-assign-a-color-automatically-to-the-defined-category-feature-request "Ссылка на этот заголовок") + +- Выберите действие: **Автоматически назначать цвет по категории** +- Выберите событие: **Создание или изменение задачи** +- Установите параметр действия: **Цвет = Голубой** и **Категория = Важные запросы** + + +### Я хочу устанавливать дату начала автоматически когда задача перемещена в колонку “В работе”[¶](#i-want-to-set-the-start-date-automatically-when-the-task-is-moved-to-the-column-work-in-progress "Ссылка на этот заголовок") + +- Выберите действие: **Автоматическое обновление даты начала** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = В работе** + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-collapsed-expanded.markdown b/doc/ru_RU/board-collapsed-expanded.markdown new file mode 100644 index 00000000..a19981a5 --- /dev/null +++ b/doc/ru_RU/board-collapsed-expanded.markdown @@ -0,0 +1,31 @@ +Компактное и развернутое отображение задач +========================================== + +Задачи на Доске могут быть отображены в компактном или развернутом виде. Переключение между компактным и развернутым видом может быть выполнено с помощью горячей клавиши **“s”** или в раскрывающемся Меню (слева вверху) -\> Развернуть задачи или Свернуть задачи. + + +Компактный вид[¶](#collapsed-mode "Ссылка на этот заголовок") +------------------------------------------------------------- + + + + +Рисунок. Задачи представлены в компактном виде + +- Если для задачи назначен исполнитель, то инициалы исполнителя показываются рядом с номером задачи; +- Если заголовок задачи слишком длинный, вы можете подвести курсор мышки над задачей и полный заголовок задачи отобразится во всплывающем окне. + + + +Развернутый вид[¶](#expanded-mode "Ссылка на этот заголовок") +------------------------------------------------------------- + + + +Рисунок. Развернутый вид + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-configuration.markdown b/doc/ru_RU/board-configuration.markdown new file mode 100644 index 00000000..fb4fb58d --- /dev/null +++ b/doc/ru_RU/board-configuration.markdown @@ -0,0 +1,39 @@ +Настройка Доски +=============== + + +В правом верхнем выпадающем меню выберите **Настройки**, затем, слева, выберите **Настройки Доски**. + + + +Рисунок. Настройка Доски + + +Подстветка задач[¶](#task-highlighting "Ссылка на этот заголовок") +------------------------------------------------------------------ + +Эта опция позволяет подсвечивать задачу, которая была перенесена недавно. + +Установите значение 0 для выключения подсветки. По умолчанию установлено значение 172800 секунд (2 дня) + +Перемещенные задачи будут подсвечиваться в течении двух дней. + + +Период обновления для публичных досок[¶](#refresh-interval-for-public-board "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------- + +Если вы создаете публичную доску, то страница, по умолчанию, будет обновляться каждые 60 секунд. + + +Период обновления для частных досок[¶](#refresh-interval-for-private-board "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------ + +Когда в вашем браузере открыта Доска, Канборд проверяет обновления изменение каждые 10 секунд. + +Процесс обновления реализован по технологии Ajax. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown b/doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown new file mode 100644 index 00000000..9eaa5c9e --- /dev/null +++ b/doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown @@ -0,0 +1,19 @@ +Горизонтальная прокрутка и компактный вид +========================================= + +Когда ширины экрана не хватает для отображения всех колонок, то внизу появляется горизонтальная прокрутка. + +Однако, можно переключится на компактный вид доски для отображения всех колонок на вашем экране. + + + + +Рисунок. Переключение на компактное представление. + +Переключится между горизонтальной прокруткой и компактным видом можно с помощью горячей клавиши **“c”** или в левом верхнем раскрывающемся “Меню” -\> “Компактный вид” или “Широкий вид”. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-show-hide-columns.markdown b/doc/ru_RU/board-show-hide-columns.markdown new file mode 100644 index 00000000..5c333b5c --- /dev/null +++ b/doc/ru_RU/board-show-hide-columns.markdown @@ -0,0 +1,25 @@ +Показать и скрыть колонки на Доске +================================== + +Вы можете показать и скрыть колонки на Доске очень просто: + + + +Рисунок. Спрятать колонку. + + +Чтобы скрыть (спрятать) колонку , откройте выпадающее меню колонки. + + + +Рисунок.Показать колонку. + + +Для отображения скрытой колонки нажмите “иконку плюс” + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/bruteforce-protection.markdown b/doc/ru_RU/bruteforce-protection.markdown new file mode 100644 index 00000000..25e50880 --- /dev/null +++ b/doc/ru_RU/bruteforce-protection.markdown @@ -0,0 +1,37 @@ +Защита от Brute Force +===================== + +Защита от Brute Force (подбор пароля методом перебора) в Канборде работает на уровне учетной записи пользователя: + +- После 3 неправильных вводов пароля для одного и того же пользователя, на форме входа появляется капча для предотвращения дальнейшего подбора программой-роботом. +- После 6 неудачных вводов пароля, учетная запись пользователя блокируется на 15 минут. + +Эта возможность работает только для метода аутентификации с использованием формы входа на веб странице. + +Однако, **после трех ошибочных аутентификаций через пользовательский API, учетная запись может быть разблокирована с использованием формы входа на веб странице** + +В Канборде нет блокировок по IP адресу, потому что программы-роботы используют множество анонимных прокси. Однако, вы можете использовать внешнюю утилиту, например [fail2ban](http://www.fail2ban.org) , чтобы избежать массового сканирования. + +Настройки защиты от Brute Force могут быть изменены в следующих переменных: + + // Enable captcha after 3 authentication failure + + define('BRUTEFORCE_CAPTCHA', 3); + + + + // Lock the account after 6 authentication failure + + define('BRUTEFORCE_LOCKDOWN', 6); + + + + // Lock account duration in minutes + + define('BRUTEFORCE_LOCKDOWN_DURATION', 15); + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/calendar-configuration.markdown b/doc/ru_RU/calendar-configuration.markdown new file mode 100644 index 00000000..bd6d604e --- /dev/null +++ b/doc/ru_RU/calendar-configuration.markdown @@ -0,0 +1,59 @@ +Настройки календаря +=================== + +В правом верхнем выпадающем меню выберите **Настройки**, затем, слева, выберите **Настройки календаря**. + + + + +Рисунок. Настройки календаря + + +В Канборде имеется два вида Календаря: + +- Календарь проекта +- Пользовательский календарь (доступен в левом меню Инфопанели) + + +Календарь проекта[¶](#project-calendar "Ссылка на этот заголовок") +------------------------------------------------------------------ + +Эти календари показывают задачи с указанной датой создания или датой начала и датой завершения. + +### Показать задачи в зависимости от даты создания[¶](#show-tasks-based-on-the-creation-date "Ссылка на этот заголовок") + +- Дата начала в календаре показывает дату создания задачи. +- Конечная дата показывает дату завершения. + + +### Показать задачи в зависимости от даты начала[¶](#show-tasks-based-on-the-start-date "Ссылка на этот заголовок") + +- Дата начала в календаре показывает дату начала задачи. +- Эта дата должна быть установлена вручную. +- Конечная дата показывает дату завершения. +- Если не указать дату начала, то задача не будет отображена в календаре. + + + +Пользовательский календарь[¶](#user-calendar "Ссылка на этот заголовок") +------------------------------------------------------------------------ + +Пользовательский календарь показывает только задачи назначенные пользователю и, опционально, информацию о подзадачах. + + +### Показать подзадачи, основанные на отслеживании времени[¶](#show-sub-tasks-based-on-the-time-tracking "Ссылка на этот заголовок") + +- Показывает подзадачи в календаре из записей таблицы отслеживания времени. +- Пересечения в пользовательской таблице времени также подсчитываются. + + +### Показывать оценку подзадач (прогнозирование будущих работ)[¶](#show-sub-task-estimates-forecast-of-future-work "Ссылка на этот заголовок") + +- Показывает оценку будущих работ для подзадач в статусе “для исполнения” и с указанным значением “оценка”. + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/calendar.markdown b/doc/ru_RU/calendar.markdown new file mode 100644 index 00000000..f0658c89 --- /dev/null +++ b/doc/ru_RU/calendar.markdown @@ -0,0 +1,31 @@ +Календарь +========= + + +Календарь может быть представлен в двух видах: + +- Представление в проекте с использование фильтров (доступно на Доске) +- Пользовательское представление (доступно в рабочей панели и в пользовательском разделе) + +В Календаре можно увидеть следующую информацию: + +- Задачи с “датой испольнения”, отображаются наверху. **Дата испольнения может быть изменена перемещением задачи на другой день**. +- Задачи с датой создания или датой начала. **Эти события не могут быть изменены в календаре**. +- Отслеживание времени подзадачи. Все записанные временные диапазоны будут отображены в Календаре. +- Подсчеты, прогнозы затрачиваемого время на подзадачу. + + + +Рисунок. Календарь + + +Настроки Календаря могут быть изменены на странице **Настройки** + +Заметка: Дата исполения не содержит информацию о времени. + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/centos-installation.markdown b/doc/ru_RU/centos-installation.markdown new file mode 100644 index 00000000..95808586 --- /dev/null +++ b/doc/ru_RU/centos-installation.markdown @@ -0,0 +1,127 @@ +Инсталяция Канборд на Centos +============================ + + +**Внимание**: Некоторые возможности Канборда требуют [запуск ежедневных фоновых задач](cronjob.markdown). + + +Centos 7[¶](#centos-7 "Ссылка на этот заголовок") +------------------------------------------------- + +Установите PHP и Apache: + + + yum install -y php php-mbstring php-pdo php-gd unzip wget + + +По умолчанию, Centos 7 использует PHP 5.4.16 и Apache 2.4.6. + + + +Перезапустите Apache: + + + + systemctl restart httpd.service + + + +Установите Канборд: + + + + cd /var/www/html + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R apache:apache kanboard/data + + rm kanboard-latest.zip + + + +Если включен SELinux, убедитесь что пользователь веб сервера Apache имеет права на запись в директорию data: + + + + chcon -R -t httpd_sys_content_rw_t /var/www/html/kanboard/data + + + +Убедитесь, что Канборд может посылать email сообщения и делать внешние сетевые запросы, например с SELinux: + + + + setsebool -P httpd_can_network_connect=1 + + + +Позволяет делать внешние подключения если используется LDAP, SMTP, Web hooks или другая интеграция. + + + +Centos 6.x[¶](#centos-6-x "Ссылка на этот заголовок") +----------------------------------------------------- + + + +Установите PHP и Apache: + + + + yum install -y php php-mbstring php-pdo php-gd unzip wget + + + +По умолчанию, Centos 6.5 использует PHP 5.3.3 и Apache 2.2.15. + + + +Включите короткие теги: + + + +- Отредактируйте файл `/etc/php.ini`{.docutils .literal} + + + +- Измените строку `short_open_tag = On`{.docutils .literal} (вместо `short_open_tag = Off`{.docutils .literal}) + + + +Перезапустите Apache: + + + + service httpd restart + + + +Установите Канборд: + + + + cd /var/www/html + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R apache:apache kanboard/data + + rm kanboard-latest.zip + + + +Готово. Можете работать с Канборд. Откройте в браузере `http://ваш_сервер/kanboard/`{.docutils .literal}. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/cli.markdown b/doc/ru_RU/cli.markdown new file mode 100644 index 00000000..9c7b56a7 --- /dev/null +++ b/doc/ru_RU/cli.markdown @@ -0,0 +1,331 @@ +Интерфейс командной строки +========================== + + + +Канборд обеспечивает простой интерфейс командной строки, которым можно воспользоваться только из Unix терминала. Эта возможность доступна только с локальной машины. + + + +Интерфейс командной строки полезен для выполнения команд вне процессов веб сервера. + + + +Использование[¶](#usage "Ссылка на этот заголовок") +--------------------------------------------------- + + + +- Откройте терминал и перейдите в директорию Канборд (например: `cd /var/www/kanboard`) + + + +- Выполните команду `./kanboard` + + + +<!-- --> + + + + Kanboard version master + + + + Usage: + + command [options] [arguments] + + + + Options: + + -h, --help Display this help message + + -q, --quiet Do not output any message + + -V, --version Display this application version + + --ansi Force ANSI output + + --no-ansi Disable ANSI output + + -n, --no-interaction Do not ask any interactive question + + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + + + + Available commands: + + cronjob Execute daily cronjob + + help Displays help for a command + + list Lists commands + + export + + export:daily-project-column-stats Daily project column stats CSV export (number of tasks per column and per day) + + export:subtasks Subtasks CSV export + + export:tasks Tasks CSV export + + export:transitions Task transitions CSV export + + locale + + locale:compare Compare application translations with the fr_FR locale + + locale:sync Synchronize all translations based on the fr_FR locale + + notification + + notification:overdue-tasks Send notifications for overdue tasks + + plugin + + plugin:install Install a plugin from a remote Zip archive + + plugin:uninstall Remove a plugin + + plugin:upgrade Update all installed plugins + + projects + + projects:daily-stats Calculate daily statistics for all projects + + trigger + + trigger:tasks Trigger scheduler event for all tasks + + user + + user:reset-2fa Remove two-factor authentication for a user + + user:reset-password Change user password + + + +Доступные команды[¶](#available-commands "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +### Экспорт задач в формате CSV[¶](#tasks-csv-export "Ссылка на этот заголовок") + + + +Применение: + + + + ./kanboard export:tasks <project_id> <start_date> <end_date> + + + +Пример: + + + + ./kanboard export:tasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +Данные CSV передаются в `stdout`. + + + +### Экспорт подзадач в формате CSV[¶](#subtasks-csv-export "Ссылка на этот заголовок") + + + +Применение: + + + + ./kanboard export:subtasks <project_id> <start_date> <end_date> + + + +Пример: + + + + ./kanboard export:subtasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +### Экспорт перемещения задач в формате CSV[¶](#task-transitions-csv-export "Ссылка на этот заголовок") + + + +Применение: + + + + ./kanboard export:transitions <project_id> <start_date> <end_date> + + + +Пример: + + + + ./kanboard export:transitions 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +### Экспорт ежедневных сведений в формате CSV[¶](#export-daily-summaries-data-in-csv "Ссылка на этот заголовок") + + + +Экспортированные данные будут выведены в стандартный вывод: + + + + ./kanboard export:daily-project-column-stats <project_id> <start_date> <end_date> + + + +Пример: + + + + ./kanboard export:daily-project-column-stats 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +### Отправка уведомлений для просроченных задач[¶](#send-notifications-for-overdue-tasks "Ссылка на этот заголовок") + + + +Email сообщения будут отправлены всем пользователям, у которых включено оповещение. + + + + ./kanboard notification:overdue-tasks + + + +Необязательные параметры: + + + +- `--show`: Показывать отправку уведомлений + + + +- `--group`: Группировать все просроченные задачи для одного пользователя (со всех проектов) на один email + + + +- `--manager`: Посылать все просроченные задачи менеджеру (менеджерам) проекта в одном email сообщении + + + +Вы можете просмотреть просроченные задачи с помощью параметра `--show`: + + + +```bash +./kanboard notification:overdue-tasks --show ++-----+---------+------------+------------+--------------+----------+ +| Id | Title | Due date | Project Id | Project name | Assignee | ++-----+---------+------------+------------+--------------+----------+ +| 201 | Test | 2014-10-26 | 1 | Project #0 | admin | +| 202 | My task | 2014-10-28 | 1 | Project #0 | | ++-----+---------+------------+------------+--------------+----------+ +``` + + +### Запуск ежедневной калькуляции статистики[¶](#run-daily-project-stats-calculation "Ссылка на этот заголовок") + + + +Эта команда считает статистику для каждого проекта: + + + + ./kanboard projects:daily-stats + + Run calculation for Project #0 + + Run calculation for Project #1 + + Run calculation for Project #10 + + + +### Триггеры для задач[¶](#trigger-for-tasks) + + + +Эта команда посылает “событие для ежедневных фоновых заданий” для всех открытых задач в каждом проекте. + + + + ./kanboard trigger:tasks + + Trigger task event: project_id=2, nb_tasks=1 + + + +### Сброс пароля пользователя[¶](#reset-user-password "Ссылка на этот заголовок") + + + + ./kanboard user:reset-password my_user + + + +Будет запрошен пароль и подтверждение. Символы не отображаются на экране. + + + +### Удаление двухуровневой аутентификации для пользователя[¶](#remove-two-factor-authentication-for-a-user "Ссылка на этот заголовок") + + + + ./kanboard user:reset-2fa my_user + + + +### Установка плагина[¶](#install-a-plugin "Ссылка на этот заголовок") + + + + ./kanboard plugin:install https://github.com/kanboard/plugin-github-auth/releases/download/v1.0.1/GithubAuth-1.0.1.zip + + + +Заметка: Установленные файлы будут иметь теже права, что и у текущего пользователя + + + +### Удаление плагина[¶](#remove-a-plugin "Ссылка на этот заголовок") + + + + ./kanboard plugin:uninstall Budget + + + +### Обновление всех плагинов[¶](#upgrade-all-plugins "Ссылка на этот заголовок") + + + + ./kanboard plugin:upgrade + + * Updating plugin: Budget Planning + + * Plugin up to date: Github Authentication + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/closing-tasks.markdown b/doc/ru_RU/closing-tasks.markdown new file mode 100644 index 00000000..ae91757d --- /dev/null +++ b/doc/ru_RU/closing-tasks.markdown @@ -0,0 +1,30 @@ +Закрытие задач +============== + +Когда задача закрыта, то она скрывается на Доске. + +Не смотря на это, вы можете в любой момент зайти в список закрытых задач используя запрос **status:closed** в любой форме поиска или просто выбрать фильтр “Закрытые задачи” в выпадающем меню. + +Имеется два пути для закрытия задачи: - На Доске выбрать задачу и выпадающем меню выбрать **Закрыть задачу** + + + +Рисунок. Закрытие задачи, используя выпадающее меню. + + +или - Используя детальное представление задачи, выбрать **Закрыть задачу** в меню боковой панели (слева) + + + + +Рисунок. Закрытие задачи. + + + +**Заметка**: Когда вы закрываете задачу, у всех не выполненных подзадач будет изменен статус на “Выполнено” + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/cloudron.markdown b/doc/ru_RU/cloudron.markdown new file mode 100644 index 00000000..2e41d0d0 --- /dev/null +++ b/doc/ru_RU/cloudron.markdown @@ -0,0 +1,45 @@ +Как запустить Канборд на Cloudron +================================= + + +[Cloudron](https://cloudron.io) приватный смартсервер, на котором вы можете установить веб приложения, такие как Канборд. Вы можете установить Канборд в определенном домене, при этом каждой инсталяции создавается резервная копия и поддерживается новая версия Канборда автоматически. + + + +[](https://cloudron.io/button.html?app=net.kanboard.cloudronapp) + + + +Учетные записи[¶](#accounts "Ссылка на этот заголовок") +------------------------------------------------------- + + +Приложение плотно интегрируется с системой Управления пользователями Cloudron (через LDAP). Только пользователи Cloudron могут войти в Канборд. Плюс, любой администратор Cloudron становится администратором Канборда автоматически. + + +Установка плагинов[¶](#installing-plugins "Ссылка на этот заголовок") +--------------------------------------------------------------------- + + + +Плагины могут быть установлены и настроены с помощью утилиты [Cloudron CLI](https://cloudron.io/references/cli.html). Для подробной информации смотрите [описание приложения](https://cloudron.io/appstore.html?app=net.kanboard.cloudronapp). + + + +Исходный код приложения[¶](#application-source-code "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Исходный код приложения Cloudron находится [здесь](https://github.com/cloudron-io/kanboard-app). + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/coding-standards.markdown b/doc/ru_RU/coding-standards.markdown new file mode 100644 index 00000000..b6100375 --- /dev/null +++ b/doc/ru_RU/coding-standards.markdown @@ -0,0 +1,64 @@ +Стандарты используемые при написании кода +========================================= + + + +Код PHP[¶](#php-code "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Отступ: 4 пробела + + + +- Перевод строки: Unix =\> `\n`{.docutils .literal} + + + +- Кодировка: UTF-8 + + + +- Используйте только открытые теги `<?php`{.docutils .literal} or `<?=`{.docutils .literal} для templates, но **никогда** не используйте `<?`{.docutils .literal} + + + +- Всегда пишите коментарии PHPdoc для свойств методов и классов + + + +- Стиль кодирования: [PSR-1](http://www.php-fig.org/psr/psr-1/) и [PSR-2](http://www.php-fig.org/psr/psr-2/) + + + +Код JavaScript[¶](#javascript-code "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +- Отступ: 4 пробела + + + +- Перевод строки: Unix =\> `\n`{.docutils .literal} + + + +Код CSS[¶](#css-code "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Отступ: 4 пробела + + + +- Перевод строки: Unix =\> `\n`{.docutils .literal} + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/config.markdown b/doc/ru_RU/config.markdown new file mode 100644 index 00000000..b0419966 --- /dev/null +++ b/doc/ru_RU/config.markdown @@ -0,0 +1,523 @@ +Конфигурационный файл +===================== + + + +Вы можете изменить базовые настройки Канборда добавив файл `config.php` в корень проекта или в каталог `data`. Вы, также, можете переименовать файл `config.default.php` в `config.php` и установить желаемые значения. + + +Включение/выключение режима отладки[¶](#enable-disable-debug-mode "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------- + + + + define('DEBUG', true); + + define('LOG_DRIVER', 'file'); // Other drivers are: syslog, stdout, stderr or file + + + +Обработчик логов может быть определен если вы включите режим отладки. Режим отладки фиксирует все SQL запросы и время затрачиваемое на генерацию страниц. + + + +Плагины[¶](#plugins "Ссылка на этот заголовок") +----------------------------------------------- + + + +Каталог плагинов: + + + + define('PLUGINS_DIR', 'data/plugins'); + + + +Включение/выключение установки плагинов через интерфейс пользователя: + + + + define('PLUGIN_INSTALLER', true); // Default is true + + + +Каталог для загружаемых файлов[¶](#folder-for-uploaded-files "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------- + + + + define('FILES_DIR', 'data/files'); + + + +Включение/выключение переопределения url адресов[¶](#enable-disable-url-rewrite "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------- + + + + define('ENABLE_URL_REWRITE', false); + + + +Настройка email[¶](#email-configuration "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + + // E-mail address for the "From" header (notifications) + + define('MAIL_FROM', 'notifications@kanboard.local'); + + + + // Mail transport to use: "smtp", "sendmail" or "mail" (PHP mail function) + + define('MAIL_TRANSPORT', 'mail'); + + + + // SMTP configuration to use when the "smtp" transport is chosen + + define('MAIL_SMTP_HOSTNAME', ''); + + define('MAIL_SMTP_PORT', 25); + + define('MAIL_SMTP_USERNAME', ''); + + define('MAIL_SMTP_PASSWORD', ''); + + define('MAIL_SMTP_ENCRYPTION', null); // Valid values are "null", "ssl" or "tls" + + + + // Sendmail command to use when the transport is "sendmail" + + define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); + + + +Настройки базы данных[¶](#database-settings "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + + // Database driver: sqlite, mysql or postgres (sqlite by default) + + define('DB_DRIVER', 'sqlite'); + + + + // Mysql/Postgres username + + define('DB_USERNAME', 'root'); + + + + // Mysql/Postgres password + + define('DB_PASSWORD', ''); + + + + // Mysql/Postgres hostname + + define('DB_HOSTNAME', 'localhost'); + + + + // Mysql/Postgres database name + + define('DB_NAME', 'kanboard'); + + + + // Mysql/Postgres custom port (null = default port) + + define('DB_PORT', null); + + + + // Mysql SSL key + + define('DB_SSL_KEY', null); + + + + // Mysql SSL certificate + + define('DB_SSL_CERT', null); + + + + // Mysql SSL CA + + define('DB_SSL_CA', null); + + + +Настройки LDAP[¶](#ldap-settings "Ссылка на этот заголовок") +------------------------------------------------------------ + + + + // Enable LDAP authentication (false by default) + + define('LDAP_AUTH', false); + + + + // LDAP server hostname + + define('LDAP_SERVER', ''); + + + + // LDAP server port (389 by default) + + define('LDAP_PORT', 389); + + + + // By default, require certificate to be verified for ldaps:// style URL. Set to false to skip the verification + + define('LDAP_SSL_VERIFY', true); + + + + // Enable LDAP START_TLS + + define('LDAP_START_TLS', false); + + + + // By default Kanboard lowercase the ldap username to avoid duplicate users (the database is case sensitive) + + // Set to true if you want to preserve the case + + define('LDAP_USERNAME_CASE_SENSITIVE', false); + + + + // LDAP bind type: "anonymous", "user" or "proxy" + + define('LDAP_BIND_TYPE', 'anonymous'); + + + + // LDAP username to use with proxy mode + + // LDAP username pattern to use with user mode + + define('LDAP_USERNAME', null); + + + + // LDAP password to use for proxy mode + + define('LDAP_PASSWORD', null); + + + + // LDAP DN for users + + // Example for ActiveDirectory: CN=Users,DC=kanboard,DC=local + + // Example for OpenLDAP: ou=People,dc=example,dc=com + + define('LDAP_USER_BASE_DN', ''); + + + + // LDAP pattern to use when searching for a user account + + // Example for ActiveDirectory: '(&(objectClass=user)(sAMAccountName=%s))' + + // Example for OpenLDAP: 'uid=%s' + + define('LDAP_USER_FILTER', ''); + + + + // LDAP attribute for username + + // Example for ActiveDirectory: 'samaccountname' + + // Example for OpenLDAP: 'uid' + + define('LDAP_USER_ATTRIBUTE_USERNAME', 'uid'); + + + + // LDAP attribute for user full name + + // Example for ActiveDirectory: 'displayname' + + // Example for OpenLDAP: 'cn' + + define('LDAP_USER_ATTRIBUTE_FULLNAME', 'cn'); + + + + // LDAP attribute for user email + + define('LDAP_USER_ATTRIBUTE_EMAIL', 'mail'); + + + + // LDAP attribute to find groups in user profile + + define('LDAP_USER_ATTRIBUTE_GROUPS', 'memberof'); + + + + // LDAP attribute for user avatar image: thumbnailPhoto or jpegPhoto + + define('LDAP_USER_ATTRIBUTE_PHOTO', ''); + + + + // LDAP attribute for user language, example: 'preferredlanguage' + + // Put an empty string to disable language sync + + define('LDAP_USER_ATTRIBUTE_LANGUAGE', ''); + + + + // Allow automatic LDAP user creation + + define('LDAP_USER_CREATION', true); + + + + // LDAP DN for administrators + + // Example: CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local + + define('LDAP_GROUP_ADMIN_DN', ''); + + + + // LDAP DN for managers + + // Example: CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local + + define('LDAP_GROUP_MANAGER_DN', ''); + + + + // Enable LDAP group provider for project permissions + + // The end-user will be able to browse LDAP groups from the user interface and allow access to specified projects + + define('LDAP_GROUP_PROVIDER', false); + + + + // LDAP Base DN for groups + + define('LDAP_GROUP_BASE_DN', ''); + + + + // LDAP group filter + + // Example for ActiveDirectory: (&(objectClass=group)(sAMAccountName=%s*)) + + define('LDAP_GROUP_FILTER', ''); + + + + // LDAP user group filter + + // If this filter is configured, Kanboard will search user groups in LDAP_GROUP_BASE_DN + + // Example for OpenLDAP: (&(objectClass=posixGroup)(memberUid=%s)) + + define('LDAP_GROUP_USER_FILTER', ''); + + + + // LDAP attribute for the group name + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +Настройки аутентификации Reverse-Proxy[¶](#reverse-proxy-authentication-settings "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------ + + + + // Enable/disable the reverse proxy authentication + + define('REVERSE_PROXY_AUTH', false); + + + + // Header name to use for the username + + define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); + + + + // Username of the admin, by default blank + + define('REVERSE_PROXY_DEFAULT_ADMIN', ''); + + + + // Default domain to use for setting the email address + + define('REVERSE_PROXY_DEFAULT_DOMAIN', ''); + + + +Настройки аутентификации RememberMe[¶](#rememberme-authentication-settings "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------ + + + + // Enable/disable remember me authentication + + define('REMEMBER_ME_AUTH', true); + + + +Настройки Secure HTTP headers[¶](#secure-http-headers-settings "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------ + + + + // Enable or disable "Strict-Transport-Security" HTTP header + + define('ENABLE_HSTS', true); + + + + // Enable or disable "X-Frame-Options: DENY" HTTP header + + define('ENABLE_XFRAME', true); + + + +Запись событий[¶](#logging "Ссылка на этот заголовок") +------------------------------------------------------ + + + +По умолчанию, Канборд записывает не все события. Если вы хотите включить запись событий, вы должны установить обработчик логов. + + + + // Available log drivers: syslog, stderr, stdout or file + + define('LOG_DRIVER', ''); + + + + // Log filename if the log driver is "file" + + define('LOG_FILE', __DIR__.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR.'debug.log'); + + + +Защита от Brute-force[¶](#brute-force-protection "Ссылка на этот заголовок") +---------------------------------------------------------------------------- + + + + // Enable captcha after 3 authentication failure + + define('BRUTEFORCE_CAPTCHA', 3); + + + + // Lock the account after 6 authentication failure + + define('BRUTEFORCE_LOCKDOWN', 6); + + + + // Lock account duration in minute + + define('BRUTEFORCE_LOCKDOWN_DURATION', 15); + + + +Сессии[¶](#session "Ссылка на этот заголовок") +---------------------------------------------- + + + + // Session duration in second (0 = until the browser is closed) + + // See http://php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime + + define('SESSION_DURATION', 0); + + + +Проксирование клиентских HTTP[¶](#http-client-proxy "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Если внешние запросы HTTP необходимо пробрасывать через прокси: + + + + define('HTTP_PROXY_HOSTNAME', ''); + + define('HTTP_PROXY_PORT', '3128'); + + define('HTTP_PROXY_USERNAME', ''); + + define('HTTP_PROXY_PASSWORD', ''); + + + +Другие настройки[¶](#various-settings "Ссылка на этот заголовок") +----------------------------------------------------------------- + + + + // Escape html inside markdown text + + define('MARKDOWN_ESCAPE_HTML', true); + + + + // API alternative authentication header, the default is HTTP Basic Authentication defined in RFC2617 + + define('API_AUTHENTICATION_HEADER', ''); + + + + // Hide login form, useful if all your users use Google/Github/ReverseProxy authentication + + define('HIDE_LOGIN_FORM', false); + + + + // Disabling logout (for external SSO authentication) + + define('DISABLE_LOGOUT', false); + + + + // Override API token stored in the database, useful for automated tests + + define('API_AUTHENTICATION_TOKEN', 'My unique API Token'); + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/contributing.markdown b/doc/ru_RU/contributing.markdown new file mode 100644 index 00000000..54917067 --- /dev/null +++ b/doc/ru_RU/contributing.markdown @@ -0,0 +1,96 @@ +Руководство для участников проекта +================================== + + + +Как я могу помочь проекту?[¶](#how-can-i-help "Ссылка на этот заголовок") +------------------------------------------------------------------------- + + + +Канборд пока не идеален, поэтому есть несколько вариантов помочь проекту: + + + +- Присылать отзывы +- Сообщать об ошибках +- Добавлять или обновлять переводы +- Улучшать документацию +- Писать код +- Рассказать друзьям, что Канборд отличная программа :) + + + +Перед тем как начать большое дело, создайте новое “обсуждение вопроса” (issue) на [https://github.com/fguillot/kanboard/issues](https://github.com/fguillot/kanboard/issues) и объясните ваше предложение. + + + +Я хочу внести предложения по проекту[¶](#i-want-to-give-feedback "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------- + + + +- У вас есть идея по улучшению (пользовательский интерфейс или другие возможности) +- Посмотрите в обсуждениях (issue), может ваша идея уже предложена кем-то +- Откройте новое обсуждение (issue) +- Опишите вашу идею +- Вы можете проголосовать +1 за имеющиеся предложения + + +Я хочу сообщить об ошибке[¶](#i-want-to-report-a-bug "Ссылка на этот заголовок") +-------------------------------------------------------------------------------- + +- Убедитесь, что обсуждение вопроса (issue) ранее не публиковалось +- Откройте новую заявку (ticket) +- Опишите, что именно не работает +- Опишите, как воспроизвести ошибку (последовательность, как вы вышли на данную ошибку) +- Опишите ваше окружение (версию Канборда, какая ОС, веб сервер, версию PHP, база данных и версия, хостинг провайдер) + + +Я хочу перевести Канборд на другой язык[¶](#i-want-to-translate-kanboard "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------- + +Канборд уже переведен на несколько языков. Вы можете улучшить эти переводы. Некоторые переводы еще не завершены. Для того, чтобы сделать перевод, ознакомтесь с [руководством по переводу на другой язык](translations.markdown). + + +Я хочу улучшить документацию[¶](#i-want-to-improve-the-documentation "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------ + +- Вы считаете, что что-то недостаточно хорошо описано, имеются грамматические или орфографические ошибки, что-то еще. +- Документация написана в формате Markdown и хранится в каталоге `docs`{.docutils .literal}. +- Редактируйте файлы и присылайте pull-request. +- Документация на официальном вебсайте синхронизируется с репозиторием. + + +Я хочу внести свой вклад в код[¶](#i-want-to-contribute-to-the-code "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------- + +Pull-requests всегда приветствуются, однако, чтобы они были приняты, вы должны следовать следующим указаниям: + +- **Перед тем как внести большое изменение или переделать дизайн, откройте новую заявку (ticket) для обсуждения.** +- Если вы хотите добавить новую возможность, уважайте филосовию Канборда: **Мы фокусируемся на простоте**, мы не хотим иметь раздутую программу. +- Это же относится и к пользовательскому интерфейсу: **простота и производительность** +- Присылайте только по одному pull-request для новой возможности или исправления ошибки. +- Небольшие pull-request легче просмотреть и быстрее влить в проект. +- Убедитесь, что [модульные тесты выполняются успешно](tests.markdown). +- Уважайте [стандарты кодирования](coding-standards.markdown). +- Пишите код, который могут поддерживать другие, избегайте дублирования, используйте лучше практики PHP. + +В любом случае, если вы не уверены в чем-то - открывайте новую заявку (ticket) + + +Рассказать друзьям, что Канборд отличная программа :)[¶](#tell-your-friends-that-kanboard-is-awesome "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------------------- + +Если вы используете Канборд, покажите его и окружающим. Расскажите всем о прелестях бесплатного и опенсурсного программного обеспечения. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/create-tasks-by-email.markdown b/doc/ru_RU/create-tasks-by-email.markdown new file mode 100644 index 00000000..baddc682 --- /dev/null +++ b/doc/ru_RU/create-tasks-by-email.markdown @@ -0,0 +1,61 @@ +Создание задач через email +========================== + + +Вы можете создавать задачи отправляя email (сообщения через электронную почту). Эта возможность доступна при использовании плагинов. + +В настоящий момент, Канборд поддерживает три внешних плагина: + + +- [Mailgun](https://github.com/kanboard/plugin-mailgun) +- [Sendgrid](https://github.com/kanboard/plugin-sendgrid) +- [Postmark](https://github.com/kanboard/plugin-postmark) + +Эти плагины позволяют обрабатывать входящие электронные сообщения (email) без дополнительной настройки SMTP сервера. + +При получении плагином email сообщения, плагин передает это сообщение в веб транслятор Канборда. + + +Обработка входящих email сообщений[¶](#incoming-emails-workflow "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------- + + +1. Вы отправляете email сообщение на определенный адрес, например **something+myproject@inbound.mydomain.tld** +2. Email сообщение перенаправляется на SMTP сервер +3. SMTP провайдер передает в веб сервис Канборда email сообщение в JSON формате или в формате multipart/form-data +4. Канборд обрабатывает полученное email сообщение и создает задачу в указанном проекте + +**Заметка**: Новые задачи автоматически создаются в первой колонке. + + +Формат email сообщения[¶](#email-format "Ссылка на этот заголовок") +------------------------------------------------------------------- + +- Email адрес до знака **@** должен содержать разделитель **плюс**, например **kanboard+project123** +- Строка следующая после знака плюс означает **Идентификатор проекта**, например, проект **Проект 123** может иметь идентификатор проекта **project123**. Идентификатор проекта можно задать в свойствах проекта **Меню** -\> **Настройки** -\> **Изменить проект** -\> **Идентификатор**. **Идентификатор** должен быть из цифр и латинских букв. +- Тема из email сообщения становится названием задачи +- Текст email сообщения становится описанием задачи (в формате Markdown) + +Email сообщения могут быть написаны в текстовом или HTML формате. **Канборд сам переконвертирует формат сообщения в Markdown** + + +Безопастность и требования[¶](#security-and-requirements "Ссылка на этот заголовок") +------------------------------------------------------------------------------------ + +- Веб транслятор Канборд защищен случайным ключом +- Email адрес отправителя должен быть такой же как и у пользователя Канборд +- Проект в Канборде должен иметь уникальный идентификатор +- Отправитель email сообщения должен быть участником проекта + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/creating-projects.markdown b/doc/ru_RU/creating-projects.markdown new file mode 100644 index 00000000..b878a538 --- /dev/null +++ b/doc/ru_RU/creating-projects.markdown @@ -0,0 +1,62 @@ +Создание проектов +================= + + +Kanboard может содержать одновременно несколько проектов. Проекты могут быть следующих типов: + +- Командный проект +- Приватный проект для одного пользователя + +Создание проекта для нескольких пользователей[¶](#creating-projects-for-multiple-users "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------ + +- Только пользователи с ролью администратор и менеджер могут создавать такие проекты +- Можно добавлять к проекту пользователей и группы + +На рабочей панели нажмите ссылку **Новый проект**: + + + +Рисунок. Форма создания проекта. + + +Теперь надо только добавить название для проекта! Легко, не правда ли? + + +Создание приватного проекта[¶](#creating-a-private-project "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + +- Любой пользователь Kanboard может создать приватный проект +- **Нет** возможности добавлять участников к приватному проекту +- Только владелец приватного проекта и администратор могут получить доступ к проекту + + +На рабочей панели нажмите **Новый проект с ограниченным доступом**. + + + +Создание проекта из другого проекта[¶](#creating-projects-from-another-project "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------- + +При создании нового проекта у вас есть возможность использовать данные другого (ранее созданного) проекта: + +- Разрешения +- Действия +- Дорожки +- Категории +- Задачи + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/creating-tasks.markdown b/doc/ru_RU/creating-tasks.markdown new file mode 100644 index 00000000..ec2922a8 --- /dev/null +++ b/doc/ru_RU/creating-tasks.markdown @@ -0,0 +1,42 @@ +Создание задач +============== + + +На Доске нажмите значок плюс рядом с названием колонки: + + + + +Рисунок. Создание задачи на Доске + + +Далее появится форма создания задачи: + + + +Рисунок. Форма создания задачи. + + +Только поле **Название** является обязательным полем для заполнения. + + +Описание полей: + +- **Название**: Название вашей задачи, которое будет отображаться на доске. +- **Описание**: Позволяет вам добавить больше информации о задаче, содержимое может содержать синтаксис [Markdown](syntax-guide.markdown). +- **Создать другую задачу**: Отметьте этот чекбокс если вы хотите создать похожую задачу (некоторые поля будут заполнены). +- **Назначена**: Пользователь, которому будет назначена для выполнения эта задача. +- **Категория**: Только одна категория может быть назначена задаче. +- **Колонка**: Колонка в которой задача будет создана, ваша задача будет помещена вниз. +- **Цвет**: Выберите цвет для карточки. +- **Сложность**: используется в быстрых управлениях проектами (Scrum); сложность - это число, которое говорит команде проекта насколько тяжело выполнить задачу. Обычно пользователи используют шкалу Фибоначи. +- **Запланировано часов**: Планирование времени, которое будет затрачено на выполнение задачи. Измеряется в часах. +- **Сделать до**: Просроченные задачи будут иметь дату завершения красного цвета, а предстоящие задачи будут иметь дату завершения черного цвета. + +**-** + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/cronjob.markdown b/doc/ru_RU/cronjob.markdown new file mode 100644 index 00000000..c3bb5f6d --- /dev/null +++ b/doc/ru_RU/cronjob.markdown @@ -0,0 +1,41 @@ +Ежедневные фоновые задачи +========================= + + +Для корректной работы, Канборд должен запускать ежедневные фоновые задачи. На Unix платформах этот процесс выполнятся в `cron`. + +Фоновые задачи необходимы для следующих возможностей: + +- Отчеты и аналитика (подсчет ежедневной статистики для каждого проекта) +- Рассылка оповещений для просроченных задач +- Выполнение автоматических действий подключенных к событиям “Ежедневные фоновые процессы для задач” + + +Настройка на Unix и Linux платформах[¶](#configuration-on-unix-and-linux-platforms "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------- + +Для создания фоновых задач под операционной системой Unix/Linux используются разные решения. Здесь приведен пример для Ubuntu 14.04. Для других систем процедура похожа. + + +Отредактируйте crontab под пользователем вашего веб сервера: + + + sudo crontab -u www-data -e + + +Пример запуска ежедневной фоновой задачи в 8 утра: + + + 0 8 * * * cd /path/to/kanboard && ./kanboard cronjob >/dev/null 2>&1 + + +Примечание: процес выполнения фоновых задач должен иметь права доступа к вашей базе данных в случае если вы используете Sqlite. Обычно, достаточно запускать фоновую задачу под пользователем веб сервера. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/currency-rate.markdown b/doc/ru_RU/currency-rate.markdown new file mode 100644 index 00000000..6d7dbc3e --- /dev/null +++ b/doc/ru_RU/currency-rate.markdown @@ -0,0 +1,43 @@ +Курсы валют +=========== + + +Каждый пользователь может иметь предопределенный ежечасный курс для разных валют. Если вы хотите вручную занести курсы валют, то вы можете указать ставку в соответсвии с курсом. + +Эта опция используются для расчета бюджета проекта. + + + +Рисунок. Курсы валют + + +Для настроек курса валют выберите, справа вверху в выпадающем меню, **Настройки** -\> затем, слева, **Курсы валют**. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/custom-filters.markdown b/doc/ru_RU/custom-filters.markdown new file mode 100644 index 00000000..60630860 --- /dev/null +++ b/doc/ru_RU/custom-filters.markdown @@ -0,0 +1,36 @@ +Пользовательские фильтры +======================== + +Пользовательские фильтры позволяют вам сохранять любые поисковые запросы. Таким образом, вы можете легко расширить стандартные фильтры и сохранить часто используемые поисковые запросы. + +- Пользовательские фильтры сохраняются в проекте и имеют привязку к создателю. +- Если создатель фильтра является менеджером проекта, то он может предоставить этот фильтр всем участникам проекта. + + +Создание фильтра[¶](#filter-creation "Ссылка на этот заголовок") +---------------------------------------------------------------- + + +Перейдите в **Меню** -\> **Пользовательские фильтры** или **Меню** -\> **Настройки** -\> **Пользовательские фильтры** + + + +Рисунок. Создание пользовательского фильтра. + + + +Созданый фильтр появится на Доске рядом со стандартными фильтрами + + + +Рисунок. Выпадающий список - Пользовательский фильтр. + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/debian-installation.markdown b/doc/ru_RU/debian-installation.markdown new file mode 100644 index 00000000..2c33465e --- /dev/null +++ b/doc/ru_RU/debian-installation.markdown @@ -0,0 +1,104 @@ +Как установить Канборд на Debian? +================================= + +Некоторые возможности Канборда требуют [запуск ежедневных фоновых задач](cronjob.markdown). + + +Debian 8 (Jessie)[¶](#debian-8-jessie "Ссылка на этот заголовок") +----------------------------------------------------------------- + + +Установите Apache и PHP: + + + apt-get update + + apt-get install -y php5 php5-sqlite php5-gd unzip + + service apache2 restart + + + +Установите Канборд: + + + cd /var/www/html + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R www-data:www-data kanboard/data + + rm kanboard-latest.zip + + + +Debian 7 (Wheezy)[¶](#debian-7-wheezy "Ссылка на этот заголовок") +----------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + apt-get update + + apt-get install -y php5 php5-sqlite php5-gd unzip + + + +Установите Канборд: + + + + cd /var/www + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R www-data:www-data kanboard/data + + rm kanboard-latest.zip + + + +Debian 6 (Squeeze)[¶](#debian-6-squeeze "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + apt-get update + + apt-get install -y libapache2-mod-php5 php5-sqlite php5-gd unzip + + + +Установите Канборд: + + + + cd /var/www + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R www-data:www-data kanboard/data + + rm kanboard-latest.zip + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/docker.markdown b/doc/ru_RU/docker.markdown new file mode 100644 index 00000000..358ade73 --- /dev/null +++ b/doc/ru_RU/docker.markdown @@ -0,0 +1,134 @@ +Как запустить Канборд с Docker? +=============================== + + +Канборд можно легко запустить с [Docker](https://www.docker.com). + + +Размер образа, приблизительно, **50MB** содержит: + +- [Alpine Linux](http://alpinelinux.org/) +- The [process manager S6](http://skarnet.org/software/s6/) +- Nginx +- PHP-FPM + + +Канборд запускает фоновые задачи каждый день в полночь. Переписывание URL (URL rewriting) включено в базовой конфигурации. + +Когда контейнер запущен, использование памяти около **20MB**. + + +Использование стабильной версии[¶](#use-the-stable-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + + +Для получения последней стабильной версии Канборда используйте тег **stable**: + + + + docker pull kanboard/kanboard + + docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:stable + + + +Использование разрабатываемой версии (автоматической сборки)[¶](#use-the-development-version-automated-build "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------------------------------------- + + + +Каждый новый коммит в репозитории вызывает новую сборку в [Docker Hub](https://registry.hub.docker.com/u/kanboard/kanboard/). + + + + docker pull kanboard/kanboard + + docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:latest + + + +Используя **разрабатываемую версию** Канборда с тегом **latest**, вы принимаете на себя все риски нестабильной версии. + + + +Создание своего образа Docker[¶](#build-your-own-docker-image "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------- + +Для сборки своего образа, в репозитории Канборда имеется `Dockerfile`{.docutils .literal}. Склонируйте репозиторий Канборда и выполните следующую команду: + + + + docker build -t youruser/kanboard:master . + + + +или + + + + make docker-image + + + +Для запуска вашего контейнера в фоновом режиме на порту 80: + + + + docker run -d --name kanboard -p 80:80 -t youruser/kanboard:master + + + +Тома[¶](#volumes "Ссылка на этот заголовок") +-------------------------------------------- + + +Вы можете прикрепить 2 тома к вашему контейнеру: + +- Каталог с данными: `/var/www/kanboard/data` +- Каталог с плагинами: `/var/www/kanboard/plugins` + + + +Используйте опцию `-v` для монтирования тома на удаленной машине как описано в [официальной документации Docker](https://docs.docker.com/engine/userguide/containers/dockervolumes/). + + + +Обновление вашего контейнера[¶](#upgrade-your-container "Ссылка на этот заголовок") +----------------------------------------------------------------------------------- + +- Загрузите новый образ +- Удалите старый контейнер +- Перезапустите новый контейнер с теми же томами + + +Переменные окружения[¶](#environment-variables "Ссылка на этот заголовок") +-------------------------------------------------------------------------- + + +Список переменных окружения доступен на [этой странице](env.markdown). + + + +Файлы конфигурации[¶](#config-files "Ссылка на этот заголовок") +--------------------------------------------------------------- + +- Контейнер уже содержит конфигурационный файл расположенный в `/var/www/kanboard/config.php`. +- Вы можете сохранить свой конфиг файл в томе с данными: `/var/www/kanboard/data/config.php`. + + + +Ссылки[¶](#references "Ссылка на этот заголовок") +------------------------------------------------- + +- [Официальные образы Канборд](https://registry.hub.docker.com/u/kanboard/kanboard/) +- [Документация Docker](https://docs.docker.com/) +- [Стабильная версия Dockerfile](https://github.com/kanboard/docker) +- [Разрабатываемая версия Dockerfile](https://github.com/fguillot/kanboard/blob/master/Dockerfile) + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/duplicate-move-tasks.markdown b/doc/ru_RU/duplicate-move-tasks.markdown new file mode 100644 index 00000000..48cec06c --- /dev/null +++ b/doc/ru_RU/duplicate-move-tasks.markdown @@ -0,0 +1,79 @@ +Дублирование и перенос задач +============================ + + +Создание копии задачи в том же проекте[¶](#duplicate-a-task-into-the-same-project "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------- + + +Перейдите в детальное представление задачи и выберите в боковой панели (слева) **Клонировать**. + + + +Рисунок. Создание копии задачи. + + +Новая задача будет создана с теми же свойствами как и у оригинальной задачи. + + +Создание копии задачи в другой проект[¶](#duplicate-a-task-to-another-project "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------- + + +Перейдите в детальное представление задачи и выберите в боковом меню (слева) **Клонировать в другой проект**. + + + +Рисунок. Создание копии задачи в другой проект. + + +При выборе проекта в выпадающем списке, показываются только те проекты в которых вы являетесь участниками. + +Перед тем как скопировать задачу, Канборд просит вас указать свойства проекта (куда будет копироваться), потому что проекты могуг иметь разные столбцы, дорожки и т.д. + +Вам нужно указать: + +- Дорожку, в которую скопируется задача +- Колонку +- Категорию +- Испольнителя + +Перемещение задачи в другой проект[¶](#move-a-task-to-another-project "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------- + +Перейдите в детальное представление задачи и выберите в боковом меню **Переместить в другой проект** + +Процедура перемещения задачи в другой проект такая же как и при копировании, вы должны указать новые свойства для задачи. + + +Список копируемых полей[¶](#list-of-fields-duplicated "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + +Ниже приведен список полей (свойств), которые будут скопированы: + +- заголовок +- описание +- дата\_исполнение +- цвет\_id +- проект\_id +- колонка\_id +- владелец\_id +- оценка +- категория\_id +- время\_запланировано +- дорожка\_id +- повторение\_статус +- повторение\_триггер +- повторение\_фактор +- повторение\_timeframe +- повторение\_basedate + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/editing-projects.markdown b/doc/ru_RU/editing-projects.markdown new file mode 100644 index 00000000..5ff81f90 --- /dev/null +++ b/doc/ru_RU/editing-projects.markdown @@ -0,0 +1,25 @@ +Редактирование проектов +======================= + + +Проект может быть переименован и выключен в любое время + +Для переименования проекта нажмите на ссылку **“Изменить проект”** (для перехода выберите **Меню** -\> **Настройки**) + + + + +Рисунок. Изменение проекта. + +- Дата начала и дата завершения используются при генерации диаграммы Ганта +- Описание отображается как подсказка на Доске и на странице со списком проектов +- Администраторы и менеджеры проекта могут сделать приватный проект доступным для других пользователей установив галочку в чекбоксе **“Приватный проект”** +- Вы можете сделать публичный проект приватным. + +Внимание: Когда вы делаете приватный проект из публичного, все пользователи ранее присоединенные к проекту будут иметь доступ. Ограничьте список пользователей для вашего приватного проекта. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/email-configuration.markdown b/doc/ru_RU/email-configuration.markdown new file mode 100644 index 00000000..e04aca7b --- /dev/null +++ b/doc/ru_RU/email-configuration.markdown @@ -0,0 +1,156 @@ +Настройка email +=============== + + +Настройки пользователя[¶](#user-settings "Ссылка на этот заголовок") +-------------------------------------------------------------------- + +Для получение уведомлений на email, пользователи Канборда должны иметь: + +- Включенные уведомления, должны быть включены в профиле пользователя +- Должен быть прописан правильный email адрес в профиле пользователя +- Быть участником проекта, который отсылает уведомления + + +Примечание: Пользователь, выполнивший вход в Канборд и выполняющий действие, не будет получать уведомления. Уведомления будут получать только другие участники проекта. + + + +Email шлюзы[¶](#email-transports "Ссылка на этот заголовок") +------------------------------------------------------------ + +В Канборд доступны несколько шлюзов для email: + +- SMTP +- Sendmail +- Встроенная mail функция PHP +- Другие методы могут предоставить внешние плагины: Postmark, Sendgrid and Mailgun + + +Настройки сервера[¶](#server-settings "Ссылка на этот заголовок") +----------------------------------------------------------------- + +По умолчанию, Канборд использует встроенную в PHP функцию для передачи email сообщений. Обычно не требуется дополнительных настроек, если ваш сервер уже может отправлять email сообщения. + +Если вы захотите использовать другие методы: SMTP протокол и Sendmail, то ниже приведены инструкции по настройке. + +### Настройка SMTP[¶](#smtp-configuration "Ссылка на этот заголовок") + +Переименуйте файл `config.default.php`{.docutils .literal} в `config.php`{.docutils .literal} и измените следующие значения: + + + // We choose "smtp" as mail transport + + define('MAIL_TRANSPORT', 'smtp'); + + + + // We define our server settings + + define('MAIL_SMTP_HOSTNAME', 'mail.example.com'); + + define('MAIL_SMTP_PORT', 25); + + + + // Credentials for authentication on the SMTP server (not mandatory) + + define('MAIL_SMTP_USERNAME', 'username'); + + define('MAIL_SMTP_PASSWORD', 'super password'); + + + +Возможно понадобится использовать шифрованное подключение TLS или SSL: + + + define('MAIL_SMTP_ENCRYPTION', 'ssl'); // Valid values are "null", "ssl" or "tls" + + +### Настройка Sendmail[¶](#sendmail-configuration "Ссылка на этот заголовок") + +По умолчанию команда отправки сообщений выглядит так `/usr/sbin/sendmail -bs`{.docutils .literal}, но вы можете изменить ее в файле конфигурации. + +Например: + + + + // We choose "sendmail" as mail transport + + define('MAIL_TRANSPORT', 'sendmail'); + + + + // If you need to change the sendmail command, replace the value + + define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); + + + +### Встроенная mail функция PHP[¶](#php-native-mail-function "Ссылка на этот заголовок") + +Это конфигурация по умолчанию: + + + + define('MAIL_TRANSPORT', 'mail'); + + + +### Email адрес отправителя[¶](#the-sender-email-address "Ссылка на этот заголовок") + +По умолчанию, сообщения отправляются с адресом отправителя `notifications@kanboard.local`{.docutils .literal}. На этот адрес нельзя ответить. + +Вы можете настроить этот адрес изменив значение константы `MAIL_FROM`{.docutils .literal} в вашем конфигурационном файле. + + + define('MAIL_FROM', 'kanboard@mydomain.tld'); + + +Это может быть полезным, если ваш SMTP сервер не принимает неправильные адреса. + + +### Как отобразить ссылку на задачу в уведомлении?[¶](#how-to-display-a-link-to-the-task-in-notifications "Ссылка на этот заголовок") + +Чтобы сделать это, вы должны указать URL вашего установленного Канборда в [Настройках приложения](application-configuration.markdown). + +Например: + + + +- [http://demo.kanboard.ru/](http://demo.kanboard.ru/) + + + +- <http:/>/имясервера/kanboard/ + + + +- [http://kanboard.mydomain.com/](http://kanboard.mydomain.com/) + + + +Не забудьте добавить в конце слеш `/`{.docutils .literal}. + + + +Вы должны сделать это вручную, потому что Канборд не может угадать URL из скрипта командной строки и некоторые конфигурации веб серверов очень специфичны. + + +Решение проблем[¶](#troubleshooting "Ссылка на этот заголовок") +--------------------------------------------------------------- + +Если email сообщения не отправляются и вы уверены, что все настроили правильно: + +- Проверьте папку Спам +- Включите режим отладки и посмотрите отладочный файл `data/debug.log`{.docutils .literal}, вы можете увидеть конкретную ошибку +- Убедитесь, что ваш сервер или ваш хостинг провайдер позволяет вам отсылать email сообщения +- Если вы используете SeLinux, разрешите PHP отсылать email сообщения. + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/env.markdown b/doc/ru_RU/env.markdown new file mode 100644 index 00000000..3764e98e --- /dev/null +++ b/doc/ru_RU/env.markdown @@ -0,0 +1,21 @@ +Переменные окружения +==================== + +Переменные окружения могут пригодится когда Канборд развертывается как контейнер (Docker). + + +| Переменная | Описание | +|---------|------------------------------------------------------------------| +| DATABASE\_URL | `[database type]://[username]:[password]@[host]:[port]/[database name]`, например: `postgres://foo:foo@myserver:5432/kanboard` | +| DEBUG | Включение/выключение режима отладки: “true” или “false” | +| LOG\_DRIVER | Logging driver: stdout, stderr, file or syslog | + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ext-search.markdown b/doc/ru_RU/ext-search.markdown new file mode 100644 index 00000000..1d6e7fe1 --- /dev/null +++ b/doc/ru_RU/ext-search.markdown @@ -0,0 +1,235 @@ +Синтаксис расширенного поиска +============================= + + +В Канборде используется простой язык запросов для расширенного поиска. Вы можете искать задачи, комментарии, подзадачи, ссылки, но только активные. + + +Пример запроса[¶](#example-of-query "Ссылка на этот заголовок") +--------------------------------------------------------------- + + + +В этом примере показываются как отобразить задачи назначенные мне с датой окончания завтра и название содержит “my title”: + + + + assigne:me due:tomorrow my title + + + +Глобальный поиск[¶](#global-search "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +### Поиск по id задачи или названию задачи[¶](#search-by-task-id-or-title "Ссылка на этот заголовок") + +- Поиск задачи по id: `#123` +- Поиск по id задачи и названию задачи: `123` +- Поиск по названию задачи: `любые слова и цифры`, но не должны содержать атрибуты поиска + + +### Поиск по статусу[¶](#search-by-status "Ссылка на этот заголовок") + +Атрибут: **status** + +- Запрос на поиск открытых задач: `status:open` +- Запрос на поиск закрытых задач: `status:closed` + + + +### Поиск по испольнителю[¶](#search-by-assignee "Ссылка на этот заголовок") + + +Атрибут: **assignee** + +- Поиск по полному имени испольнителя: `assignee:"Петр Иванов"` +- Поиск исполнителя по имени пользователя: `assignee:pivanov` +- Отбор нескольких испольнителей: `assignee:tsemenov assignee:"Петр Иванов"` +- Поиск задач без исполнителя: `assignee:nobody` +- Поиск задач назначенных мне: `assignee:me` + + +### Поиск по создателю задач[¶](#search-by-task-creator "Ссылка на этот заголовок") + + +Атрибут: **creator** + +- Отбор задач созданных мной: `creator:me` +- Отбор задач которые создал Петр Иванов: `creator:"Петр Иванов"` +- Отбор задач созданных пользователем с id \#1: `creator:1` + + +### Поиск по исполнителю подзадач[¶](#search-by-subtask-assignee "Ссылка на этот заголовок") + +Атрибут: **subtask:assignee** + +- Например: `subtask:assignee:"Петр Иванов"` + + +### Поиск по цвету[¶](#search-by-color "Ссылка на этот заголовок") + +Атрибут: **color** + +- Отбор по цвету с id blue: `color:blue` +- Отбор по названию цвета: `color:"Deep Orange"` + + +### Отбор по “Сделать до”[¶](#search-by-the-due-date "Ссылка на этот заголовок") + + +Атрибут: **due** + +- Поиск задач со сроком испольнения до сегодня: `due:today` +- Поиск задач со сроком исполнения завтра: `due:tomorrow` +- Поиск задач со сроком исполнения вчера: `due:yesterday` +- Поиск задач с конкретной датой исполнения: `due:2016-06-29` + +Дата должна быть в формате ISO 8601: **YYYY-MM-DD**. + +Все строковые форматы поддерживаемые функцией `strtotime()` допустимы. Например, `next Thursday`, `-2 days`{.docutils .literal}, `+2 months`, `tomorrow` и т.д. + + +Операторы сравнения с датой: + +- Старше чем: **due:\>2015-06-29** +- Моложе чем: **due:\<2015-06-29** +- Старше чем или равно: **due:\>=2015-06-29** +- Моложе чем или равно: **due:\<=2015-06-29** + + +### Поиск по дате изменения[¶](#search-by-modification-date "Ссылка на этот заголовок") + +Атрибут: **modified** или **updated** + +Формат даты такой же как и у “Сделать до” + +Отфильтровать недавно измененные задачи: `modified:recently`. + +Этот запрос использует тоже значение что и в настройках Доски - “Время подсвечивания задачи”. + + +### Поиск по дате создания[¶](#search-by-creation-date "Ссылка на этот заголовок") + +Атрибут: **created** + +Работает также как и поиск по дате изменения. + + +### Поиск по описанию[¶](#search-by-description "Ссылка на этот заголовок") + +Атрибут: **description** or **desc** + +Например: `description:"здесь пишем тескт для поиска"` + + +### Поиск по внешним ссылкам[¶](#search-by-external-reference "Ссылка на этот заголовок") + +Например: нужно найти задачу, которая содержит ссылку на id или название другой задачи. + +- `ref:1234` или `reference:TICKET-1234` + + +### Поиск по категории[¶](#search-by-category "Ссылка на этот заголовок") + +Атрибут: **category** + +- Найти задачи с указанной категорией: `category:"Важные запросы"` +- Найти задачи, которые содержать указанные категории: `category:"Ошибки" category:"Изменения"` +- Найти задачи без категорий: `category:none` + + +### Поиск проектов[¶](#search-by-project "Ссылка на этот заголовок") + +Атрибут: **project** + +- Поиск задач по имени проекта: `project:"Какой-то проект"` +- Поиск задач по id проекта: `project:23` +- Поиск задач в нескольких проектах: `project:"Проект A" project:"Проект B"` + + +### Поиск в колонках[¶](#search-by-columns "Ссылка на этот заголовок") + +Атрибут: **column** + +- Поиск задач в указанной колонке: `column:"В работе"` +- Поиск задач в нескольких колонках: `column:"Невыполненные заказы" column:ready` + + +### Поиск в Дорожках[¶](#search-by-swim-lane "Ссылка на этот заголовок") + +Атрибут: **swimlane** + +- Поиск задач в указанной Дорожке: `swimlane:"Версия 42"` +- Поиск задач в базовой Дорожке: `swimlane:default` +- Поиск задач в нескольких Дорожках: `swimlane:"Версия 1.2" swimlane:"Версия 1.3"` + + +### Поиск ссылки на задачу[¶](#search-by-task-link "Ссылка на этот заголовок") + +Атрибут: **link** + +- Поиск задач содержащих ссылку: `link:"это веха задачи "` +- Поиск задач по нескольким ссылкам: `link:"веха задачи " link:"относится к"` + + +### Поиск по комментарию[¶](#search-by-comment "Ссылка на этот заголовок") + +Атрибут: **comment** + +- Найти комментарии, которые содержат указанное название: `comment:"Какое-то название"` + + +Поиск активности задач[¶](#activity-stream-search "Ссылка на этот заголовок") +----------------------------------------------------------------------------- + + + +### Поиск событий по названию задачи[¶](#search-events-by-task-title "Ссылка на этот заголовок") + + + +Атрибут: **title** или без ничего (по умолчанию) + +- Например: `title:"My task"` +- Поиск задачи по id: `#123` + + +### Поиск событий по статусу задачи[¶](#search-events-by-task-status "Ссылка на этот заголовок") + +Атрибут: **status** + + + +### Поиск событий по создателю[¶](#search-by-event-creator "Ссылка на этот заголовок") + +Атрибут: **creator** + + + +### Поиск событий по дате создания[¶](#search-by-event-creation-date "Ссылка на этот заголовок") + +Атрибут: **created** + + + +### Поиск событий по проекту[¶](#search-events-by-project "Ссылка на этот заголовок") + +Атрибут: **project** + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/faq.markdown b/doc/ru_RU/faq.markdown new file mode 100644 index 00000000..0730f2c8 --- /dev/null +++ b/doc/ru_RU/faq.markdown @@ -0,0 +1,162 @@ +Часто задаваемые вопросы +======================== + + +Вы можете порекомендовать веб хостинг провайдера для Канборд?[¶](#can-you-recommend-a-web-hosting-provider-for-kanboard "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------------- + +Работу Канборд поддерживают несколько крупных провайдеров VPS, такие как [Digital Ocean](https://www.digitalocean.com/?refcode=4b541f47aae4), [Linode](https://www.linode.com/?r=4e381ac8a61116f40c60dc7438acc719610d8b11) или [Gandi](https://www.gandi.net/). + +Для получения большей производительности, выбирайте провайдера с быстрыми дисками чтения/записи, потому что Канборд использует по умолчанию Sqlite. Избегайте провайдеров которые используют подключения NFS. + + +У меня выводится пустая страница после установки или обновления Канборд[¶](#i-get-a-blank-page-after-installing-or-upgrading-kanboard "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +- Проверьте, установили ли вы всё на сервер, что было указано в требованиях +- Посмотрите ошибки в PHP и Apache логах +- Проверьте права доступа к файлам +- Если вы используете кеширование OPcode, перезапустите ваш веб сервер или php-fpm + + +У меня выводится ошибка “There is no suitable CSPRNG installed on your system”[¶](#i-have-the-error-there-is-no-suitable-csprng-installed-on-your-system "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +Если вы используете PHP \< 7.0, то вам нужно включить расширение openssl или доступ из приложения к `/dev/urandom`, если имеются ограничения от `open_basedir`. + + +Страница не найдена и URL выглядит криво (&)[¶](#page-not-found-and-the-url-seems-wrong-amp "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------- + +- UTL выглядит как `/?controller=auth&action=login&redirect_query=` вместо `?controller=auth&action=login&redirect_query=` +- Канборд выдает ошибку “Страница не найдена” + + +Эта ошибка исходит из настроек конфигурации вашего PHP, значение `arg_separator.output` отсутствует в базовой настройке. Есть разные пути решения этой проблемы: + +Измените значение прямо в вашем `php.ini`: + + + arg_separator.output = "&" + + +Переделайте значение с помощью `.htaccess`: + + + php_value arg_separator.output "&" + + +Иначе Канборд будет брать значение напрямую из PHP. + + + +Ошибка аутентификации в API и Apache + PHP-FPM[¶](#authentication-failure-with-the-api-and-apache-php-fpm "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------------- + +По умолчанию, php-cgi под Apache не передает HTTP Basic user/pass в PHP. Чтобы это окружение заработало, добавьте эти строки в ваш файл `.htaccess`: + + + + RewriteCond %{HTTP:Authorization} ^(.+)$ + + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + + +Проблемы с eAccelerator[¶](#known-issues-with-eaccelerator "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + +Канборд не очень хорошо работает с [eAccelerator](http://eaccelerator.net). Проблема в том, что выдается чистая страница или падает Apache: + + + [Wed Mar 05 21:36:56 2014] [notice] child pid 22630 exit signal Segmentation fault (11) + + +Лучшее решение, чтобы избежать этой проблемы, выключить eAccelerator или прописать в конфиге какие файлы вы хотите кешировать (параметр `eaccelerator.filter`). + + + +Проект [eAccelerator выглядит мертвым и не обновляется с 2012](https://github.com/eaccelerator/eaccelerator/commits/master). Мы рекомендуем перейти на последнюю версию PHP, потому что в него включен [OPcache](http://php.net/manual/en/intro.opcache.php). + + +Почему минимальная рекомендуемая версия PHP 5.3.3?[¶](#why-the-minimum-requirement-is-php-5-3-3 "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------- + +Канборд использует функцию `password_hash()` для шифрования пароля, а эта функция доступна только для PHP \>= 5.5. + +Однако, имеется back-port для [более ранних версий PHP](https://github.com/ircmaxell/password_compat#requirements). Эта библиотека требует минимум PHP 5.3.7 для корректной работы. + +По всей видимости, патчи безопасности back-port имеются в Centos и Debian, поэтому PHP 5.3.3 подходит для работы Канборд. + +Канборд v1.0.10 и v1.0.11 требует минимум PHP 5.3.7, но эти изменения возвращены на PHP 5.3.3 в Канборде \>= v1.0.12 + + + +Как проверить работу Канборда со встроенным веб-сервером PHP?[¶](#how-to-test-kanboard-with-the-php-built-in-web-server "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------------- + +Если вы не хотите устанавливать веб сервер типа Apache, то вы можете протестировать работу Канборда на [встроенном в PHP веб сервере](http://www.php.net/manual/en/features.commandline.webserver.php): + + + unzip kanboard-VERSION.zip + + cd kanboard + + php -S localhost:8000 + + open http://localhost:8000/ + + + +Как установить Канборд на Yunohost?[¶](#how-to-install-kanboard-on-yunohost "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------- + +[YunoHost](https://yunohost.org/) это серверная операционная система, цель которой предоставить хостинг для всех. + +Отсюда можно [загрузить инсталяционный пакет Kanboard для Yunohost](https://github.com/mbugeia/kanboard_ynh). + + +Где я могу найти список связанных с Канборд проектов?[¶](#where-can-i-find-a-list-of-related-projects "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------- + +- [Kanboard API python client by @freekoder]([https://github.com/freekoder/kanboard-py](https://github.com/freekoder/kanboard-py)) + +- [Kanboard Presenter by David Eberlein](https://github.com/davideberlein/kanboard-presenter) + +- [CSV2Kanboard by @ashbike]([https://github.com/ashbike/csv2kanboard](https://github.com/ashbike/csv2kanboard)) + +- [Kanboard for Yunohost by @mbugeia]([https://github.com/mbugeia/kanboard\_ynh](https://github.com/mbugeia/kanboard_ynh)) + +- [Trello import script by @matueranet]([https://github.com/matueranet/kanboard-import-trello](https://github.com/matueranet/kanboard-import-trello)) + +- [Chrome extension by Timo](https://chrome.google.com/webstore/detail/kanboard-quickmenu/akjbeplnnihghabpgcfmfhfmifjljneh?utm_source=chrome-ntp-icon), [Source code](https://github.com/BlueTeck/kanboard_chrome_extension) + +- [Python client script by @dzudek]([https://gist.github.com/fguillot/84c70d4928eb1e0cb374](https://gist.github.com/fguillot/84c70d4928eb1e0cb374)) + +- [Shell script for SQLite to MySQL/MariaDB migration by @oliviermaridat]([https://github.com/oliviermaridat/kanboard-sqlite2mysql](https://github.com/oliviermaridat/kanboard-sqlite2mysql)) + +- [Git hooks for integration with Kanboard by Gene Pavlovsky](https://github.com/gene-pavlovsky/kanboard-git-hooks) + + + +Имеются ли руководства по Канборду на других языках?[¶](#are-there-some-tutorials-about-kanboard-in-other-languages "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------------------- + +- [Серия статей про Kanboard на немецком языке](http://demaya.de/wp/2014/07/kanboard-eine-jira-alternative-im-detail-installation/) . +- [Русская документация по Канборд](http://kanboard.ru/doc/). + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/freebsd-installation.markdown b/doc/ru_RU/freebsd-installation.markdown new file mode 100644 index 00000000..b014e354 --- /dev/null +++ b/doc/ru_RU/freebsd-installation.markdown @@ -0,0 +1,187 @@ +Инсталяция на FreeBSD 10 +======================== + + +Инсталяция из пакетов[¶](#install-from-packages "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + $ pkg update + + $ pkg upgrade + + $ pkg install apache24 mod_php56 kanboard + + + +Включите Apache в `/etc/rc.conf`{.docutils .literal}: + + + + $ echo apache24_enable="YES" >> /etc/rc.conf + + + +Установите PHP для Apache: + + + + $ echo "AddType application/x-httpd-php .php" >> /usr/local/etc/apache24/Includes/php.conf + + $ echo "DirectoryIndex index.php index.html" >> /usr/local/etc/apache24/Includes/php.conf + + + +Затем, запустите Apache: + + + + $ service apache24 start + + + +Создайте символическую ссылку на каталог Kanboard в корне Apache: + + + + cd /usr/local/www/apache24/data + + ln -s /usr/local/www/kanboard + + + +Готово. Можете перейти в <http:/>/вашвебсервер/kanboard и начинать работать! + + + +*Примечание*: Если вы хотите добавить дополнительные возможности, типа интеграции LDAP, то нужно установить соответствующий PHP модуль. Также, вам необходимо настроить соответсвующие права на каталог data. + + + +Установка из портов[¶](#installing-from-ports "Ссылка на этот заголовок") +------------------------------------------------------------------------- + + +Нужно установить 3 основных элемента: + + + +- Apache + +- mod\_php for Apache + +- Kanboard + + + +Загрузите и распакуйте порты: + + + + $ portsnap fetch + + $ portsnap extract + + + +или обновите имеющиеся: + + + + $ portsnap fetch + + $ portsnap update + + + +Дополнительную информацию о дереве портов вы можете посмотреть на [FreeBSD Handbook](https://www.freebsd.org/doc/handbook/ports-using.html). + + + +Установка Apache: + + + + $ cd /usr/ports/www/apache24 + + $ make install clean + + + +Включите Apache в `/etc/rc.conf`{.docutils .literal}: + + + + $ echo apache24_enable="YES" >> /etc/rc.conf + + + +Установите mod\_php для Apache: + + + + $ cd /usr/ports/www/mod_php5 + + $ make install clean + + + +Установите Kanboard из портов: + + + + $ cd /usr/ports/www/kanboard + + $ make install clean + + + +Установите PHP для Apache: + + + + $ echo "AddType application/x-httpd-php .php" >> /usr/local/etc/apache24/Includes/php.conf + + $ echo "DirectoryIndex index.php index.html" >> /usr/local/etc/apache24/Includes/php.conf + + + +Затем, запустите Apache: + + + + $ service apache24 start + + + +Готово. Можете перейти в <http:/>/вашвебсервер/kanboard и начинать работать! + + + +*Примечание*: Если вы хотите использовать дополнительные возможности, типа интеграции LDAP, то нужно установить PHP модуль из `lang/php5-extensions`{.docutils .literal}. + + + +Установка из архива[¶](#manual-installation "Ссылка на этот заголовок") +----------------------------------------------------------------------- + +Начина с версии 1.0.16 Kanboard имеется в портах FreeBSD, поэтому нет необходимости устанавливать вручную. + + + +Обратите внимание[¶](#please-note "Ссылка на этот заголовок") +------------------------------------------------------------- + +- Порт расположен на хостинге [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Делайте комментарии, ответвления и предлагайте обновления! +- Некоторые возможности Канборд требуют [запуск ежедневных фоновых задач](cronjob.markdown). + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/gantt-chart-projects.markdown b/doc/ru_RU/gantt-chart-projects.markdown new file mode 100644 index 00000000..d440a85d --- /dev/null +++ b/doc/ru_RU/gantt-chart-projects.markdown @@ -0,0 +1,60 @@ +Диаграмма Ганта для всех проектов +================================= + + + +Цель диаграммы Ганта для проектов - показать прогресс проектов основанный на дате начала и дате завершения. + + + +- Диаграмма Ганта для проектов доступна из раздела **Управление проектами** + + + +- Только менеджеры проекта и администраторы имеют доступ в этот раздел + + + +- Менеджеры проекта могут видеть только те проекты, в которых они являются участниками + + + +- Приватные проекты не показывают этот график + + + + + +Рисунок. Диаграмма Ганта для всех проектов + + + +- **Дата начала** и **дата завершения** проекта используются для рисования графика + + + +- Горизонтальные полосы (столбики) могут быть расширены (сжаты) и перемещены горизонтально с помощью мыши + + + +- Перемещение по вертикали невозможно + + + +- Полосы (столбики) проекта отображаются черным, когда проект не имеет дату начала и завершения + + + +- Информационная подсказка показывает список менеджеров и участников проекта + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/gantt-chart-tasks.markdown b/doc/ru_RU/gantt-chart-tasks.markdown new file mode 100644 index 00000000..1b8c4a2c --- /dev/null +++ b/doc/ru_RU/gantt-chart-tasks.markdown @@ -0,0 +1,66 @@ +Диаграмма Ганта для задач +========================= + + + +Цель диаграммы Ганта - показать время отведенное на задачу в заданном проекте. + + + +- Диаграмма Ганта доступна в рабочем окружении проекта + + + +- Только менеджеры проектов могут иметь доступ в этот раздел + + + + + +Рисунок. Диаграмма Ганта. + + + +- Дата начала и дата завершения задач используется для рисования диаграммы + + + +- Задача может быть расширена и перемещена горизонтально с помощью мыши + + + +- Перемещение по вертикали невозможно + + + +- Полоса (горизонтальный столбик) на диаграмме имеет такой же цвет как и задача + + + +- Каждая полоса отображает статус прогресса в процентах. Проценты подсчитываются с учетом позиции задачи в колонке на Доске. + + + +- Для соответсвия модели Kanban, задачи могут быть отсортированы в соответствии с позициями на доске или по дате начала + + + +- Новые задачи созданные через диаграмму Ганта будут показаны на Доске в первой колонке на первой позиции + + + +- Задачи отображаются черным цветом, если не указана дата начала или дата исполнения + + + + + +Рисунок. Задача без указанных дат начала или завершения + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/genindex.markdown b/doc/ru_RU/genindex.markdown new file mode 100644 index 00000000..ceb48d17 --- /dev/null +++ b/doc/ru_RU/genindex.markdown @@ -0,0 +1,15 @@ +Алфавитный указатель +==================== + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/groups.markdown b/doc/ru_RU/groups.markdown new file mode 100644 index 00000000..5ab043d4 --- /dev/null +++ b/doc/ru_RU/groups.markdown @@ -0,0 +1,35 @@ +Управление группами +=================== + + + +В Канборде каждый пользователь может быть членом одной или нескольких групп. Группа - это что-то вроде команды или организации. + + + +Только администраторы могут создавать новую группу и добавлять туда пользователей. + + + +Настройка групп доступна через **Управление пользователями** (выпадающее меню справа вверху) -\> **Просмотр всех пользователей**. Здесь вы можете создавать новые группы и добавлять пользователей в группы. + + + + + +Рисунок. Управление группами. + + + +Менеджеры проектов могут предоставлять доступ группам к проектам на [странице Разрешения проекта](project-permissions.markdown). + + + +Внешние id в основном используются для предоставления доступа внешним группам. Канборд поддерживает группы из LDAP посредством [автоматической синхронизации групп из LDAP сервера](ldap-group-sync.markdown). + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/heroku.markdown b/doc/ru_RU/heroku.markdown new file mode 100644 index 00000000..6e2bd945 --- /dev/null +++ b/doc/ru_RU/heroku.markdown @@ -0,0 +1,72 @@ +Развертывание Канборд на Heroku +=============================== + +Вы можете бесплатно испытать работу Kanboard на [Heroku](https://www.heroku.com/). Вам нужно нажать кнопку **Deploy to Heroku** и следовать руководству приведенному ниже: + +[](https://heroku.com/deploy?template=https://github.com/fguillot/kanboard) + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Учетная запись на Heroku. Вы можете зарегистрироваться бесплатно. +- Установленная утилита командной строки Heroku + + + +Руководство по установке[¶](#manual-instructions "Ссылка на этот заголовок") +---------------------------------------------------------------------------- + + + # Get the last development version + + git clone https://github.com/fguillot/kanboard.git + + cd kanboard + + + + # Push the code to Heroku (You can also use SSH if git over HTTP doesn't work) + + heroku create + + git push heroku master + + + + # Start a new dyno with a Postgresql database + + heroku ps:scale web=1 + + heroku addons:add heroku-postgresql:hobby-dev + + + + # Open your browser + + heroku open + + + +Ограничения[¶](#limitations "Ссылка на этот заголовок") +------------------------------------------------------- + +- Хранилище на Heroku эфимерное. Это означает, что файлы, загружаемые через Канборд, будут отсутствовать в системе после перезагрузки. Вы можете установить плагин для хранения файлов в облаке, например [Amazon S3](https://github.com/kanboard/plugin-s3). +- Некоторые возможности Канборда требуют, чтобы вы выполняли [запуск ежедневных фоновых задач](cronjob.markdown). + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ical.markdown b/doc/ru_RU/ical.markdown new file mode 100644 index 00000000..77b6340e --- /dev/null +++ b/doc/ru_RU/ical.markdown @@ -0,0 +1,111 @@ +Синхронизация вашего календаря +============================== + + +Канборд поддерживает iCal транслятор для проектов и пользователей. Эта возможность позволяет вам импортировать задачи из Канборд в любую программу календарь (например, Microsoft Outlook, Apple Calendar, Mozilla Thunderbird и Google Calendar). + +Подписки на календарь возможны только на **чтение**, т.е. вы не можете создавать задачи во внешнем календаре. Данные из Календаря экспортируются в стандарте iCal. + +Заметка: Только задачи в промежутке от -2 месяцев до +6 месяцев (прошедшие два месяца и предстоящие 6 месяцев) экспортируются в iCalendar транслятор. + + +Календарь проекта[¶](#project-calendars "Ссылка на этот заголовок") +------------------------------------------------------------------- + +- Каждый проект имеет свой календарь. +- Ссылка на подписку уникальна для каждого проекта. Ссылка становится активной, когда вы включаете общий доступ к вашему проекту: **Меню** -\> **Настройки** -\> **Общий доступ** +- Этот календарь показывает только задачи для выбранного проекта. + + +Календарь пользователя[¶](#user-calendars "Ссылка на этот заголовок") +--------------------------------------------------------------------- + +- Каждый пользователь имеет свой собственный календарь. +- Ссылка на подписку уникальная для каждого пользователя. Ссылка становится активной, когда вы включите общий доступ для пользователя: в правом верхнем выпадающем меню - **Мой профиль** -\> в левом меню - **Общий доступ**. +- Этот календарь показывает задачи назначенные пользователю во всех проектах. + + +Добавление Канборд календаря в календарь Apple[¶](#adding-your-kanboard-calendar-to-apple-calendar "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------ + +- Откройте календарь +- Выберите **Файл** -\> **Новая подписка на календарь** +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + + + +Рисунок. Добавление подписки на календарь. + + +- Вы можете выбрать синхронизацию календаря с iCloud, чтобы иметь доступ к календарю с любых ваших устройств +- Не забудьте указать частоту синхронизации + + + + +Рисунок. Редактирование подписки на календарь. + + +Добавление вашего календаря из Канборд в Microsoft Outlook[¶](#adding-your-kanboard-calendar-to-microsoft-outlook "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------- + + + +Рисунок. Добавление в Outlook календаря из интернет + +- Откройте Outlook +- Выберите **Открыть календарь** -\> **Из интернета** +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + + + + +Рисунок. Настройка интернет календаря в Outlook. + + +Добавление вашего календаря из Канборд в Mozilla Thunderbird[¶](#adding-your-kanboard-calendar-to-mozilla-thunderbird "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------------------------- + + +- Установите в Thunderbird Дополнение **Lightning** +- Выберите **Файл** -\> **Новый календарь** +- В диалоговом окне, выберите **Из сети** + + + +Рисунок. Создание календаря в Thunderbird, шаг 1. + + + +- Выберите формат iCalendar +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + + + +Рисунок. Создание календаря в Thunderbird, шаг 2. + +- Выберите цвета и другие настройки и в завершении нажмите **Сохранить**. + + +Добавление вашего календаря Канборд в календарь Google[¶](#adding-your-kanboard-calendar-to-google-calendar "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------- + +- Нажмите иконку “треугольник” рядом с **Другие календари** (слева). +- Вставьте ссылку на календарь из Канборд в поле “Добавить календарь друга” +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + + + + +Рисунок. Календарь Google. + +Ваш календарь из Канборд будет доступен на планшетах и смартфонах, нужно только сделать синхронизацию. + + +[Справка по настройке календаря Google](https://support.google.com/calendar/?hl=ru#topic=3417969). + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/index.markdown b/doc/ru_RU/index.markdown new file mode 100644 index 00000000..c4a12d52 --- /dev/null +++ b/doc/ru_RU/index.markdown @@ -0,0 +1,248 @@ +Документация +============ + + +Как работать в Kanboard[¶](#using-kanboard "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + +### Введение[¶](#introduction "Ссылка на этот заголовок") + + +- [Что такое Kanban?](what-is-kanban.markdown) + +- [Kanban против Todo списков и Scrum](kanban-vs-todo-and-scrum.markdown) + +- [Где можно использовать Kanboard](usage-examples.markdown) + + +### Использование доски[¶](#using-the-board "Ссылка на этот заголовок") + + +- [Доска, Календарь, Список и Гант представления](project-views.markdown) + +- [Компактное или развернутое отображение задач](board-collapsed-expanded.markdown) + +- [Горизонтальная прокрутка и компактный вид](board-horizontal-scrolling-and-compact-view.markdown) + +- [Отображение и скрытие колонок](board-show-hide-columns.markdown) + + +### Работа с проектами[¶](#working-with-projects "Ссылка на этот заголовок") + +- [Типы проектов](project-types.markdown) + +- [Создание проектов](creating-projects.markdown) + +- [Редактирование проектов](editing-projects.markdown) + +- [Публичные доски и задачи](sharing-projects.markdown) + +- [Автоматизация процессов](automatic-actions.markdown) + +- [Права доступа к проекту](project-permissions.markdown) + +- [Дорожки](swimlanes.markdown) + +- [Календарь](calendar.markdown) + +- [Аналитика](analytics.markdown) + +- [Диаграмма Ганта для задач](gantt-chart-tasks.markdown) + +- [Диаграмма Ганта для проектов](gantt-chart-projects.markdown) + +- [Пользовательские фильтры](custom-filters.markdown) + + + +### Работа с задачами[¶](#working-with-tasks "Ссылка на этот заголовок") + +- [Создание задач](creating-tasks.markdown) + +- [Закрытие задач](closing-tasks.markdown) + +- [Дублирование и перенос задач](duplicate-move-tasks.markdown) + +- [Добавление снимка экрана](screenshots.markdown) + +- [Ссылки на задачу](task-links.markdown) + +- [Перемещения](transitions.markdown) + +- [Отслеживание времени](time-tracking.markdown) + +- [Повторяющиеся задачи](recurring-tasks.markdown) + +- [Создание задач через email](create-tasks-by-email.markdown) + +- [Подзадачи](subtasks.markdown) + +- [Аналитика для задач](analytics-tasks.markdown) + +- [Ссылка на пользователя](user-mentions.markdown) + + + +### Работа с пользователями и группами[¶](#working-with-users-and-groups "Ссылка на этот заголовок") + +- [Роли](roles.markdown) + +- [Типы пользователей](user-types.markdown) + +- [Управление группами](groups.markdown) + +- [Управление пользователями](user-management.markdown) + +- [Уведомления](notifications.markdown) + +- [Двухуровневая аутентификация](2fa.markdown) + + + +### Настройки[¶](#settings "Ссылка на этот заголовок") + +- [Горячие клавиши](keyboard-shortcuts.markdown) + +- [Настройки приложения](application-configuration.markdown) + +- [Настройки проекта](project-configuration.markdown) + +- [Настройка Доски](board-configuration.markdown) + +- [Настройки календаря](calendar-configuration.markdown) + +- [Настройка ссылок](link-labels.markdown) + +- [Курсы валют](currency-rate.markdown) + + +### Встроенные возможности[¶](#integrations "Ссылка на этот заголовок") + +- [iCalendar подписки](ical.markdown) + +- [RSS/Atom подписки](rss.markdown) + +- [Json-RPC API](api-json-rpc.markdown) + +- [Webhooks](webhooks.markdown) + +- [Плагины](plugins.markdown) + + +### Дополнительно[¶](#more "Ссылка на этот заголовок") + +- [Синтаксис расширенного поиска](ext-search.markdown) + +- [Интерфейс командной строки](cli.markdown) + +- [Руководство по синтаксису](syntax-guide.markdown) + +- [Защита от Brute force](bruteforce-protection.markdown) + +- [Часто задаваемые вопросы](faq.markdown) + + + +Технические детали[¶](#technical-details "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + +### Инсталяция[¶](#installation "Ссылка на этот заголовок") + +- [Требования](requirements.markdown) + +- [Инструкция по инсталяции](installation.markdown) + +- [Обновление Kanboard до новой версии](update.markdown) + +- [Инсталяция на Ubuntu](ubuntu-installation.markdown) + +- [Инсталяция на Debian](debian-installation.markdown) + +- [Инсталяция на Centos](centos-installation.markdown) + +- [Инсталяция на OpenSuse](suse-installation.markdown) + +- [Инсталяция на FreeBSD](freebsd-installation.markdown) + +- [Инсталяция на Windows Server и IIS](windows-iis-installation.markdown) + +- [Инсталяция на Windows Server и Apache](windows-apache-installation.markdown) + +- [Инсталяция на Heroku](heroku.markdown) + +- [Запуск Kanboard под Docker](docker.markdown) + +- [Запуск Kanboard под Vagrant](vagrant.markdown) + +- [Запуск Kanboard на Cloudron](cloudron.markdown) + +- [Запуск Kanboard на Nitrous](nitrous.markdown) + + +### Настройка[¶](#configuration "Ссылка на этот заголовок") + +- [Ежедневные фоновые задачи](cronjob.markdown) + +- [Конфигурационный файл](config.markdown) + +- [Переменные окружения](env.markdown) + +- [Настройка email](email-configuration.markdown) + +- [Переопределение URL](nice-urls.markdown) + +- [Директория плагинов](plugin-directory.markdown) + + + +### База данных[¶](#database "Ссылка на этот заголовок") + +- [База данных Sqlite](sqlite-database.markdown) + +- [Как использовать Mysql](mysql-configuration.markdown) + +- [Как использовать Postgresql](postgresql-configuration.markdown) + + +### Аутентификация[¶](#authentication "Ссылка на этот заголовок") + +- [LDAP аутентификация](ldap-authentication.markdown) + +- [Синхронизация групп LDAP](ldap-group-sync.markdown) + +- [Изображения из профиля LDAP](ldap-profile-picture.markdown) + +- [Параметры LDAP](ldap-parameters.markdown) + +- [Пример конфигурации LDAP](ldap-configuration-examples.markdown) + +- [Аутентификация Reverse proxy](reverse-proxy-authentication.markdown) + + +### Участие в проекте[¶](#contributors "Ссылка на этот заголовок") + +- [Руководство для участников проекта](contributing.markdown) + +- [Переводы на другие языки](translations.markdown) + +- [Стандарты при написании кода](coding-standards.markdown) + +- [Выполнение тестов](tests.markdown) + +- [Создание assets](assets.markdown) + + +[Документация](https://github.com/fguillot/kanboard/tree/master/doc) написана в формате [Markdown](https://ru.wikipedia.org/wiki/Markdown). Если вы желаете улучшить документацию - пошлите pull-request. + + + +* [Проект перевода документации Канборд на русский язык](https://github.com/hairetdin/kanboard-doc-ru). [Русская документация Канборд в формате html](http://kanboard.ru/doc/). + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/installation.markdown b/doc/ru_RU/installation.markdown new file mode 100644 index 00000000..e59e43d2 --- /dev/null +++ b/doc/ru_RU/installation.markdown @@ -0,0 +1,117 @@ +Инсталяция +========== + + + +В первую очередь, ознакомтесь с [требованиями](requirements.markdown). + + + +Инсталяция из архива (стабильная версия)[¶](#from-the-archive-stable-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------- + + + +1. У вас должен быть установлен веб сервер с PHP + +2. Скачайте исходный код и скопируйте директорию `kanboard` в каталог веб сервера + +3. Проверьте, чтобы директория `data` была доступна на запись + +4. В вашем браузере перейдите по ссылке <http:/>/вашвебсервер/kanboard + +5. Логин и пароль по умолчанию - **admin/admin** + +6. Все, теперь вы можете работать в Канборд + +7. Не забудьте сменить пароль! + + + +Место хранения данных: + + +- База данных Sqlite: `db.sqlite` + +- Файл отладки: `debug.log` (если включена отладка) + +- Загруженные файлы: `files/*` + +- Изображения: `files/thumbnails/*` + + + +Те, кто использует удаленную базу данных (Mysql/Postgresql) и удаленное файловое хранилище (Aws S3 или подобное), могут не назначать права доступа к локальным данным. + + +Инсталяция из репозитория (разрабатываемая версия)[¶](#from-the-repository-development-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------------- + + + +Вы можете установить [composer](https://getcomposer.org/) для этого метода инсталяции. + + +1. `git clone https://github.com/fguillot/kanboard.git` + +2. `composer install --no-dev` + +3. Далее, перейдите к третьему шагу [Инсталяция из архива](installation.html#from-the-archive-stable-version) + + + +**Внимание**: Инсталируя **текущую разрабатываемую версию**, вы должны понимать, что это нестабильная версия и берете все риски по работе Канборд на себя. + + + +Инсталяция в другой каталог[¶](#installation-outside-of-the-document-root "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------- + + + +Если вы хотите инсталировать Канборд в другую директорию, вне корневого каталога веб сервера, вам нужно создать, как минимум, следующие символьные ссылки: + + . + + ├── assets -> ../kanboard/assets + ├── doc -> ../kanboard/doc + ├── favicon.ico -> ../kanboard/favicon.ico + ├── index.php -> ../kanboard/index.php + ├── jsonrpc.php -> ../kanboard/jsonrpc.php + └── robots.txt -> ../kanboard/robots.txt + + + +`.htaccess` необязательно, потому что его содержимое может быть включена прямо в конфигурацию Apache. + + +Вы можете указать текущее месторасположение директорий плагинов и файлов изменив [конфигурационный файл](config.markdown). + + + +Безопасность[¶](#security "Ссылка на этот заголовок") +----------------------------------------------------- + +- Не забудьте изменить логин и пароль пользователя admin, назначенный по умолчанию + +- Не предоставляйте всем права на директорию `data` через URL. + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + + +- Некоторые возможности Канборда требуют, чтобы [ежедневно выполнялись фоновые задачи](cronjob.markdown). + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/kanban-vs-todo-and-scrum.markdown b/doc/ru_RU/kanban-vs-todo-and-scrum.markdown new file mode 100644 index 00000000..7c1b205b --- /dev/null +++ b/doc/ru_RU/kanban-vs-todo-and-scrum.markdown @@ -0,0 +1,75 @@ +Сравнение Kanban, Todo lists и Scrum +==================================== + + +Сравнение Kanban и Todo lists[¶](#kanban-vs-todo-lists "Ссылка на этот заголовок") +---------------------------------------------------------------------------------- + + +### Todo lists (списки для исполнения):[¶](#todo-lists "Ссылка на этот заголовок") + +- Имеют одну фазу (только список пунктов) + +- Возможна многозадачность (не эффективна) + + + +### Kanban:[¶](#kanban "Ссылка на этот заголовок") + + + +- Имеет много фаз, каждая колонка представлена как шаг процесса + +- Концентрация внимания и исключение многозадачности, потому что вы можете установить этап процесса заданной колонкой + + + +Сравнение Kanban и Scrum[¶](#kanban-vs-scrum "Ссылка на этот заголовок") +------------------------------------------------------------------------ + + +### [Scrum:](https://ru.wikipedia.org/wiki/Scrum)[¶](#scrum "Ссылка на этот заголовок") + + +- Спринты жестко фиксированные временем, обычно 2 или 4 недели + +- Не позволяет вносить изменения в течении итерации + +- Обязательна предварительная оценка + +- Используется скорость как единица измерения по умолчанию + +- Доска Scrum очищается между спринтами + +- Scrum имеет преопределенные роли, такие как, мастер, владелец продукта и команда + +- Множество встреч: планирование, беклог груминг (причесывание), ежедневные совещания, ретроспектива + + + +### Kanban:[¶](#id1 "Ссылка на этот заголовок") + +- Непрерывный поток + +- Гибкость - изменения могут быть сделаны в любое время + +- Предварительная оценка опциональна + +- Используется время выполнения (lead time) и время цикла (cycle time) для измерения производительности + +- Доска Kanboar постоянна + +- Kanban не навязывает строгих ограничений или встреч, процессы более гибкие + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/keyboard-shortcuts.markdown b/doc/ru_RU/keyboard-shortcuts.markdown new file mode 100644 index 00000000..a09c92bc --- /dev/null +++ b/doc/ru_RU/keyboard-shortcuts.markdown @@ -0,0 +1,99 @@ +Горячие клавиши +=============== + + +Горячие клавиши доступны в зависимости от страницы на которой вы находитесь. + + + +В проекте (Доска, Календарь, Список, Гант)[¶](#project-views-board-calendar-list-gantt "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------ + +- Переключиться на Обзор проектов = **v o** (переключите клавиатуру в английскую раскладку и нажмите клавиши **v** и **o** ) + + + +- Переключиться на Доску = **v b** + + + +- Переключиться на Календарь = **v c** + + + +- Переключиться на список = **v l** + + + +- Переключиться на диаграмму Ганта = **v g** + + + +На Доске[¶](#board-view "Ссылка на этот заголовок") +--------------------------------------------------- + +- Новая задача = **n** + + + +- Свернуть/развернуть задачи = **s** + + + +- Компактный/широкий вид = **c** + + + +В Задаче[¶](#task-view "Ссылка на этот заголовок") +-------------------------------------------------- + +- Редактировать задачу = **e** + + + +- Новая подзадача = **s** + + + +- Новый комментарий = **c** + + + +- Новая внутренняя ссылка = **l** + + + +В приложении (главное окно Канборд)[¶](#application "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + +- Показать список горячих клавиш = **?** + + + +- Открыть переключатель проектов = **b** + + + +- Переход в окно поиска = **f** + + + +- Очистить окно поиска = **r** + + + +- Закрыть окно диалога = **ESC** + + + +- Расширенный поиск = **CTRL+ENTER** or **⌘+ENTER** + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-authentication.markdown b/doc/ru_RU/ldap-authentication.markdown new file mode 100644 index 00000000..a94d8f89 --- /dev/null +++ b/doc/ru_RU/ldap-authentication.markdown @@ -0,0 +1,327 @@ +Аутентификация LDAP +=================== + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Включенное в PHP раширение LDAP + + + +- Сервер LDAP: + + + + - OpenLDAP + + - Microsoft Active Directory + + - Novell eDirectory + + + +Рабочий процесс[¶](#workflow "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +Когда активирована аутентификация LDAP, процесс входа выглядит следующим образом: + + + +1. Выполняется попытка аутентификации пользователя в базе данных Канборда + +2. Если пользователь не найден в базе Канборда, выполняется аутентификация LDAP + +3. Если аутентификация LDAP выполнена успешно, по умолчанию, локальный пользователь (в Канборде) создается автоматически без пароля и помечается как пользователь LDAP. + + + +Полное имя и email адрес автоматически подгружаются из сервера LDAP. + + + +Типы аутентификации[¶](#authentication-types "Ссылка на этот заголовок") +------------------------------------------------------------------------ + + +| Тип | Описание | +|--------------|-------------------------------------------------------------| +| Proxy User | Использовать специального пользователя для просмотра директории LDAP | +| User | Использовать учетные данные конечного пользователя для просмотра директории LDAP | +| Anonymous | Не надо выполнять аутентификацию для доступа к каталогу LDAP | + + +**Рекомендуемый метод аутентификации - “Proxy”**. + + + +### Анонимный (Anonymous) метод[¶](#anonymous-mode "Ссылка на этот заголовок") + + + + define('LDAP_BIND_TYPE', 'anonymous'); + + define('LDAP_USERNAME', null); + + define('LDAP_PASSWORD', null); + + + +Этот метод используется по умолчанию, но некоторые сервера LDAP не поддерживают доступ анонимам, из соображений безопасности. + + + +### Proxy метод[¶](#proxy-mode "Ссылка на этот заголовок") + + + +Специальный пользователь используется для доступа к директории LDAP: + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'my proxy user'); + + define('LDAP_PASSWORD', 'my proxy password'); + + + +### Пользовательский метод (user)[¶](#user-mode "Ссылка на этот заголовок") + + + +Этот метод используется для доступа под учетной записью конечного пользователя. + + + +Например, Microsoft Active Directory не разрешает подключение под анонимным пользователем и если вы не хотите использовать пользователя proxy, то используйте этот метод. + + + + define('LDAP_BIND_TYPE', 'user'); + + define('LDAP_USERNAME', '%s@kanboard.local'); + + define('LDAP_PASSWORD', null); + + + +В этом методе, константа `LDAP_USERNAME` использутся как шаблон для пользователя ldap, например: + + + +- `%s@kanboard.local` будет заменен `my_user@kanboard.local` + + + +- `KANBOARD\\%s` будет заменен на `KANBOARD\my_user` + + + +Фильтр пользователей LDAP[¶](#user-ldap-filter "Ссылка на этот заголовок") +-------------------------------------------------------------------------- + + +Параметр конфигурации `LDAP_USER_FILTER` используется для поиска пользователей по директории LDAP. + + + +Например: + + + +- `(&(objectClass=user)(sAMAccountName=%s))` будет заменено на `(&(objectClass=user)(sAMAccountName=указанный_пользователь))` + + +- `uid=%s` is replaced by `uid=указанный_пользователь` + + + +Другие примеры [фильтров для Active Directory](http://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx) + + + +Пример фильра доступа в Канборд: + + + +`(&(objectClass=user)(sAMAccountName=%s)(memberOf=CN=Kanboard Users,CN=Users,DC=kanboard,DC=local))` + + + +Этот пример разрешает подключатся к Канборду только пользователям участникам группы “Kanboard Users” + + + +Пример для Microsoft Active Directory[¶](#example-for-microsoft-active-directory "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------ + + + +Предположим, что мы имеем домен `KANBOARD` (kanboard.local) и контролер домена `myserver.kanboard.local`. + + + +Первый пример для метода прокси (proxy): + + + + <?php + + + + // Enable LDAP authentication (false by default) + + define('LDAP_AUTH', true); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'administrator@kanboard.local'); + + define('LDAP_PASSWORD', 'my super secret password'); + + + + // LDAP server hostname + + define('LDAP_SERVER', 'myserver.kanboard.local'); + + + + // LDAP properties + + define('LDAP_USER_BASE_DN', 'CN=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', '(&(objectClass=user)(sAMAccountName=%s))'); + + + +Второй пример с пользовательским методом (user): + + + + <?php + + + + // Enable LDAP authentication (false by default) + + define('LDAP_AUTH', true); + + + + define('LDAP_BIND_TYPE', 'user'); + + define('LDAP_USERNAME', '%s@kanboard.local'); + + define('LDAP_PASSWORD', null); + + + + // LDAP server hostname + + define('LDAP_SERVER', 'myserver.kanboard.local'); + + + + // LDAP properties + + define('LDAP_USER_BASE_DN', 'CN=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', '(&(objectClass=user)(sAMAccountName=%s))'); + + + +Пример для OpenLDAP[¶](#example-for-openldap "Ссылка на этот заголовок") +------------------------------------------------------------------------ + + + +Наш сервер LDAP - `myserver.example.com` и все пользователи хранятся в `ou=People,dc=example,dc=com`. + + + +Для этого примера мы использовали анонимное подключение. + + + + <?php + + + + // Enable LDAP authentication (false by default) + + define('LDAP_AUTH', true); + + + + // LDAP server hostname + + define('LDAP_SERVER', 'myserver.example.com'); + + + + // LDAP properties + + define('LDAP_USER_BASE_DN', 'ou=People,dc=example,dc=com'); + + define('LDAP_USER_FILTER', 'uid=%s'); + + + +Выключение автоматического создания учетных записей[¶](#disable-automatic-account-creation "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------------------- + + + +По умолчанию, Канборд автоматически создает учетную запись пользователя, если такой пользователь не найден. + + + +Вы можете выключить это поведение, если вы предпочитаете создавать учетные записи вручную. + + + +Для этого установите значение `LDAP_ACCOUNT_CREATION` в `false`: + + + + // Automatically create user account + + define('LDAP_ACCOUNT_CREATION', false); + + + +Устранение неисправностей[¶](#troubleshooting "Ссылка на этот заголовок") +------------------------------------------------------------------------- + +Если включен SELinux, вы должны разрешить Apache доступ к вашему серверу LDAP. + + + +- Вы должны переключить SELinux в разрешающий режим (permissive mode) или совсем выключить (не рекомендуется) + +- Вы должны разрешить все сетевые подключения, например `setsebool -P httpd_can_network_connect=1` или назначить более ограничивающие правила + + + +В любом случае, ознакомтесь с официальной документацией Redhat/Centos. + + + +Если вам не удается настроить аутентификацию LDAP, то вы можете [включить режим отладки](config.markdown) и посмотреть файлы событий. + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-configuration-examples.markdown b/doc/ru_RU/ldap-configuration-examples.markdown new file mode 100644 index 00000000..32b8a29d --- /dev/null +++ b/doc/ru_RU/ldap-configuration-examples.markdown @@ -0,0 +1,438 @@ +Пример конфигурации LDAP +======================== + + +Microsoft Active Directory[¶](#microsoft-active-directory "Ссылка на этот заголовок") +------------------------------------------------------------------------------------- + +- Аутентификация пользователя + + + +- Загрузка пользовательского профиля из Active Directory + + + +- Установка языка пользователя из атрибутов LDAP + + + +- Роли в Канборд сопоставляются с группами в Active Directory + + + +- Поставщики групп LDAP включены + + + +<!-- --> + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'administrator@kanboard.local'); + + define('LDAP_PASSWORD', 'secret'); + + + + define('LDAP_USER_BASE_DN', 'CN=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', '(&(objectClass=user)(sAMAccountName=%s))'); + + + + define('LDAP_USER_ATTRIBUTE_USERNAME', 'samaccountname'); + + define('LDAP_USER_ATTRIBUTE_FULLNAME', 'displayname'); + + define('LDAP_USER_ATTRIBUTE_PHOTO', 'jpegPhoto'); + + define('LDAP_USER_ATTRIBUTE_LANGUAGE', 'preferredLanguage'); + + + + define('LDAP_GROUP_ADMIN_DN', 'CN=Kanboard Admins,CN=Users,DC=kanboard,DC=local'); + + define('LDAP_GROUP_MANAGER_DN', 'CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'CN=Users,DC=kanboard,DC=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=group)(sAMAccountName=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +OpenLDAP с memberOf overlay[¶](#openldap-with-memberof-overlay "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------ + +Пример LDIF пользователя: + + + dn: uid=manager,ou=Users,dc=kanboard,dc=local + + objectClass: top + + objectClass: person + + objectClass: organizationalPerson + + objectClass: inetOrgPerson + + uid: manager + + sn: Lastname + + givenName: Firstname + + cn: Kanboard Manager + + displayName: Kanboard Manager + + mail: manager@kanboard.local + + userPassword: password + + memberOf: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + + +Пример LDIF группы: + + + + dn: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + objectClass: top + + objectClass: groupOfNames + + cn: Kanboard Managers + + member: uid=manager,ou=Users,dc=kanboard,dc=local + + + +Конфигурация Канборд: + + +- Аутентификация пользователя + + + +- Роли в Канборд сопоставляются с группами LDAP + + + +- Поставщики групп LDAP включены + + + +<!-- --> + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'cn=admin,DC=kanboard,DC=local'); + + define('LDAP_PASSWORD', 'password'); + + + + define('LDAP_USER_BASE_DN', 'OU=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', 'uid=%s'); + + + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=groupOfNames)(cn=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +OpenLDAP с Posix groups (memberUid)[¶](#openldap-with-posix-groups-memberuid "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------- + + +Пример LDIF пользователя: + + + + dn: uid=manager,ou=Users,dc=kanboard,dc=local + + objectClass: inetOrgPerson + + objectClass: posixAccount + + objectClass: shadowAccount + + uid: manager + + sn: Lastname + + givenName: Firstname + + cn: Kanboard Manager + + displayName: Kanboard Manager + + uidNumber: 10001 + + gidNumber: 8000 + + userPassword: password + + homeDirectory: /home/manager + + mail: manager@kanboard.local + + + +Пример LDIF группы: + + + + dn: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + objectClass: posixGroup + + cn: Kanboard Managers + + gidNumber: 5001 + + memberUid: manager + + + +Конфигурация Канборд: + + + +- Аутентификация пользователя + + + +- Роли в Канборд сопоставляются с группами LDAP + + + +- Поставщики групп LDAP включены + + + +<!-- --> + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'cn=admin,DC=kanboard,DC=local'); + + define('LDAP_PASSWORD', 'password'); + + + + define('LDAP_USER_BASE_DN', 'OU=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', 'uid=%s'); + + + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + + // This filter is used to find the groups of our user + + define('LDAP_GROUP_USER_FILTER', '(&(objectClass=posixGroup)(memberUid=%s))'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=posixGroup)(cn=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +OpenLDAP с groupOfNames[¶](#openldap-with-groupofnames "Ссылка на этот заголовок") +---------------------------------------------------------------------------------- + + +Пример LDIF пользователя: + + + + dn: uid=manager,ou=Users,dc=kanboard,dc=local + + objectClass: top + + objectClass: person + + objectClass: organizationalPerson + + objectClass: inetOrgPerson + + uid: manager + + sn: Lastname + + givenName: Firstname + + cn: Kanboard Manager + + displayName: Kanboard Manager + + mail: manager@kanboard.local + + userPassword: password + + + +Пример LDIF группы: + + + + dn: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + objectClass: top + + objectClass: groupOfNames + + cn: Kanboard Managers + + member: uid=manager,ou=Users,dc=kanboard,dc=local + + + +Конфигурация Канборд: + + + +- Аутентификация пользователя + + + +- Роли в Канборд сопоставляются с группами LDAP + + + +- Поставщики групп LDAP включены + + + +<!-- --> + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'cn=admin,DC=kanboard,DC=local'); + + define('LDAP_PASSWORD', 'password'); + + + + define('LDAP_USER_BASE_DN', 'OU=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', 'uid=%s'); + + + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + + // This filter is used to find the groups of our user + + define('LDAP_GROUP_USER_FILTER', '(&(objectClass=groupOfNames)(member=uid=%s,ou=Users,dc=kanboard,dc=local))'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=groupOfNames)(cn=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-group-sync.markdown b/doc/ru_RU/ldap-group-sync.markdown new file mode 100644 index 00000000..87d9d1cc --- /dev/null +++ b/doc/ru_RU/ldap-group-sync.markdown @@ -0,0 +1,153 @@ +Синхронизация групп LDAP +======================== + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Правильно настроенную аутентификацию LDAP + + + +- Используется сервер LDAP, который поддерживает `memberOf` или `memberUid` (PosixGroups) + + + +Автоматическое определение ролей пользователей на основании LDAP групп[¶](#define-automatically-user-roles-based-on-ldap-groups "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +Используйте следующие константы в вашем конфигурационном файле: + + + +- `LDAP_GROUP_ADMIN_DN`: Уникальные имена (Distinguished Names) для администраторов приложения + + + +- `LDAP_GROUP_MANAGER_DN`: Уникальные имена (Distinguished Names) для менеджеров приложения + + + +### Пример для Active Directory:[¶](#example-for-active-directory "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_ADMIN_DN', 'CN=Kanboard Admins,CN=Users,DC=kanboard,DC=local'); + + define('LDAP_GROUP_MANAGER_DN', 'CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local'); + + + +- Участники группы “Kanboard Admins” будут иметь роль “Администратор” + + + +- Участники группы “Kanboard Managers” будут иметь роль “Менеджер” + + + +- Все, кто не попадает под предыдущие определения, будут иметь роль “Пользователь” + + + +### Пример OpenLDAP с Posix Groups:[¶](#example-for-openldap-with-posix-groups "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_USER_FILTER', '(&(objectClass=posixGroup)(memberUid=%s))'); + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + +Вы **должны определить параметр** `LDAP_GROUP_USER_FILTER`, если ваше сервер LDAP использует `memberUid` вместо `memberOf`. Все параметры в этом примере обязательные. + + + +Автоматическая загрузка групп LDAP для Канборд проекта[¶](#automatically-load-ldap-groups-for-project-permissions "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------- + + + +Эта возможность позволяет вам синхронизировать автоматически группы LDAP с группами Канборд. Каждая группа может иметь разные роли в проектах. + + + +В проекте на странице *Разрешения*, можно ввести имя группы (имеется автодополнение) и Канборд будет искать группу во всех подключенных поставщиках. + + + +Если группа не найдена в локальной базе данных, то она будет автоматически синхронизированна. + + + +- `LDAP_GROUP_PROVIDER`: Включение поставщика группы LDAP + + + +- `LDAP_GROUP_BASE_DN`: Уникальное имя (Distinguished Names) для поиска группы в LDAP директории + + + +- `LDAP_GROUP_FILTER`: фильтр LDAP используемый для выполнения запроса + + + +- `LDAP_GROUP_ATTRIBUTE_NAME`: атрибут LDAP используемый для получения имени группы + + + +### Пример для Active Directory:[¶](#id1 "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'CN=Groups,DC=kanboard,DC=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=group)(sAMAccountName=%s*))'); + + + +С помощью фильтра, в примере выше, Канборд будет искать группы соответсвующие запросу. Если пользователь введет текст “Мои группы” в автозаполняемое поле, Канборд вернет все группы которые соответсвуют шаблону: `(&(objectClass=group)(sAMAccountName=Мои группы*))`. + + + +- Примечание 1: Спец символ `*` очень важен, в противном случает **будет выбрано только точное совпадение** + + + +- Примечание 2: Эта функция возможна только с аутентификацией LDAP настроенной на метод “proxy” или “anonymous” + + + +[Больше примеров фильтров LDAP для Active Directory](http://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx) + + + +### Пример OpenLDAP с Posix Groups:[¶](#id2 "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=posixGroup)(cn=%s*))'); + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-parameters.markdown b/doc/ru_RU/ldap-parameters.markdown new file mode 100644 index 00000000..5d00913d --- /dev/null +++ b/doc/ru_RU/ldap-parameters.markdown @@ -0,0 +1,49 @@ +Параметры LDAP для конфигурации +=============================== + + + +Список доступных параметров LDAP: + + +| Параметр | Значение по умолчанию |Описание | +|---------------------------|------------------------------|-----------------------------| +| `LDAP_AUTH` | false | Включить аутентификацию LDAP | +| `LDAP_SERVER` | Нет значения | Имя сервера LDAP | +| `LDAP_PORT` | 389 | Порт сервера LDAP | +| `LDAP_SSL_VERIFY` | true | Проверка сертификата для URL `ldaps://` | +| `LDAP_START_TLS` | false | Включение LDAP start TLS | +| `LDAP_USERNAME_CASE_SENSITIVE` | false | Включение/выключение нижнего и верхнего регистра букв в Канборд для пользователей ldap для исключения дублирования пользователей (база данных чувствительна к регистру) | +| `LDAP_BIND_TYPE` | anonymous | Тип подключения: “anonymous”, “user” or “proxy” | +| `LDAP_USERNAME` | null | Имя пользователя LDAP для использования в методе proxy или шаблон имени пользователя для использования в методе user | +| `LDAP_PASSWORD` | null | Пароль LDAP при использовании метода proxy | +| `LDAP_USER_BASE_DN`| Нет значения | Уникальное имя (DN) LDAP для пользователей (Пример: “CN=Users,DC=kanboard,DC=local”) | +| `LDAP_USER_FILTER` | Нет значения | Шаблон LDAP, который используется для поиска пользователей (Пример: “(&(objectClass=user)(sAMAccountName=%s))”) | +| `LDAP_USER_ATTRIBUTE_USERNAME` | uid | Атрибут LDAP для имени пользователя (Например: “samaccountname”) | +| `LDAP_USER_ATTRIBUTE_FULLNAME` | cn | Атрибут LDAP полного имени пользователя (Например: “displayname”) | +| `LDAP_USER_ATTRIBUTE_EMAIL` | mail | Атрибут LDAP для email пользователя | +| `LDAP_USER_ATTRIBUTE_GROUPS` | memberof | Атрибут LDAP для поиска групп в профиле пользователя | +| `LDAP_USER_ATTRIBUTE_PHOTO` | Нет значения | Атрибут LDAP для поиска фотографии пользователя (jpegPhoto или thumbnailPhoto) | +| `LDAP_USER_ATTRIBUTE_LANGUAGE` | Нет значения | Атрибут LDAP для языка пользователя (preferredlanguage), применимый формат языка - “ru-RU” | +| `LDAP_USER_CREATION` | true | Включение автоматического создания пользователя из LDAP | +| `LDAP_GROUP_ADMIN_DN` | Нет значения | Уникальное имя (DN) LDAP для администраторов (Например: “CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local”) | +| `LDAP_GROUP_MANAGER_DN` | Нет значения | Уникальное имя (DN) LDAP для менеджеров (Например: “CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local”) | +| `LDAP_GROUP_PROVIDER` | false | Включение поставщика групп LDAP для “Разрешения” в проектах | +| `LDAP_GROUP_BASE_DN` | Нет значения | Уникальное имя (Base DN) LDAP для групп | +| `LDAP_GROUP_FILTER` | Нет значения | Фильтр групп LDAP (Например: “(&(objectClass=group)(sAMAccountName=%s\*))”) | +| `LDAP_GROUP_USER_FILTER` | Empty | Если определено, то Канборд будет искать группы пользователей в LDAP\_GROUP\_BASE\_DN с помощью этого фильтра, это удобно только для posixGroups (Например: `(&(objectClass=posixGroup)(memberUid=%s))`| +| `LDAP_GROUP_ATTRIBUTE_NAME` | cn | атрибут LDAP для имени группы | + + +Примечание + + + +- Атрибуты LDAP должны быть в нижнем регистре + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-profile-picture.markdown b/doc/ru_RU/ldap-profile-picture.markdown new file mode 100644 index 00000000..9d6bb543 --- /dev/null +++ b/doc/ru_RU/ldap-profile-picture.markdown @@ -0,0 +1,46 @@ +Фотография пользователя из профиля LDAP +======================================= + + + +Канборд может автоматически загружать фотографию пользователя из сервера LDAP. + + + +Эта функция возможна только если активирована аутентификация LDAP и указан параметр `LDAP_USER_ATTRIBUTE_PHOTO`. + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +В вашем `config.php`, вы должны установить атрибут LDAP, используемый для хранения изображения. + + + + define('LDAP_USER_ATTRIBUTE_PHOTO', 'jpegPhoto'); + + + +Обычно используются атрибуты `jpegPhoto` или `thumbnailPhoto`. Изображения могут хранится в формате JPEG или PNG. + + + +Для загрузки изображения в пользовательски профиль, администраторы Active Directory могут использовать программу [AD Photo Edit](http://www.cjwdev.co.uk/Software/ADPhotoEdit/Info.html). + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + +Изображение из профиля **загружается при входе, только если изображение не было загружено ранее**. + +Для смены изображения, нужно вручную удалить ранее загруженное изображение из профиля пользователя. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/link-labels.markdown b/doc/ru_RU/link-labels.markdown new file mode 100644 index 00000000..d091a33c --- /dev/null +++ b/doc/ru_RU/link-labels.markdown @@ -0,0 +1,23 @@ +Настройки ссылки +================ + + +Связи в задачах могут быть изменены в настройках приложения (**Настройки** -\> **Настройки ссылки**) + + + +Рисунок. Метки для ссылок. + + +Каждая метка может иметь противоположное опеределение. Если нет противоположного значения, метка считается двунаправленная. + + + +Рисунок. Создание ссылки. + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/mysql-configuration.markdown b/doc/ru_RU/mysql-configuration.markdown new file mode 100644 index 00000000..82c02b37 --- /dev/null +++ b/doc/ru_RU/mysql-configuration.markdown @@ -0,0 +1,128 @@ +Настройка Mysql/MariaDB +======================= + + + +По умолчанию Канборд использует для хранения данных Sqlite. Вместо Sqlite возможно использовать Mysql или MariaDB. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Сервер Mysql + + + +- Установленное расширение PHP - `pdo_mysql` + + + +Примечание: работа Канборда протестирована с **Mysql \>= 5.5 и MariaDB \>= 10.0** + + + +Настройка Mysql[¶](#mysql-configuration "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + +### Создание базы данных[¶](#create-a-database "Ссылка на этот заголовок") + + + +Первым шагом надо создать базу данных на вашем сервере Mysql. Например, вы можете создать базу в командной строке клиента mysql: + + + + CREATE DATABASE kanboard; + + + +### Создание файла конфигурации[¶](#create-a-config-file "Ссылка на этот заголовок") + + + +Файл `config.php` должен содержать следующие значения: + + + + <?php + + + + // We choose to use Mysql instead of Sqlite + + define('DB_DRIVER', 'mysql'); + + + + // Mysql parameters + + define('DB_USERNAME', 'REPLACE_ME'); + + define('DB_PASSWORD', 'REPLACE_ME'); + + define('DB_HOSTNAME', 'REPLACE_ME'); + + define('DB_NAME', 'kanboard'); + + + +Примечание: Вы можете переименовать демонстрационный файл `config.default.php` в `config.php`. + + + +### Импорт SQL дампа (альтернативный метод)[¶](#importing-sql-dump-alternative-method "Ссылка на этот заголовок") + + + +В первый раз, Канборд запускает по очереди каждую миграцию базы данных и этот процес может занять некоторое время, в зависимости от вашей конфигурации. + + + +Чтобы избежать задержек, вы можете инициализировать базу данных напрямую, имопртируя SQL схему: + + + + mysql -u root -p my_database < app/Schema/Sql/mysql.sql + + + +Файл [\`\`](#id1)app/Schema/Sql/mysql.sql\`\`это SQL дамп, который представляет последнюю версию базы данных. + + + +Конфигурация SSL[¶](#ssl-configuration "Ссылка на этот заголовок") +------------------------------------------------------------------ + + + +Эти параметры должны быть указаны для включения соединения Mysql SSL: + + + + // Mysql SSL key + + define('DB_SSL_KEY', '/path/to/client-key.pem'); + + + + // Mysql SSL certificate + + define('DB_SSL_CERT', '/path/to/client-cert.pem'); + + + + // Mysql SSL CA + + define('DB_SSL_CA', '/path/to/ca-cert.pem'); + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/nice-urls.markdown b/doc/ru_RU/nice-urls.markdown new file mode 100644 index 00000000..ca516f78 --- /dev/null +++ b/doc/ru_RU/nice-urls.markdown @@ -0,0 +1,233 @@ +Переопределение URL +=================== + + + +Канборд может работать и с переопределенными URL и с простыми. + + + +- Пример переопределенного URL: `/board/123` + + + +- По другому: `?controller=board&action=show&project_id=123` + + + +Если вы используете Канборд с Apache и включенным mode rewrite, красивые URL будут использоваться автоматически. В случае, если вы получаете ошибку “404 Not Found”, то возможно надо внести изменения в DocumentRoot: + + + + <Directory /var/www/kanboard/> + + AllowOverride FileInfo Options=All,MultiViews AuthConfig + + </Directory> + + + +URL ярлыки[¶](#url-shortcuts "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +- Перейти к задаче \#123: **/t/123** + + + +- Перейти на доску в проект \#2: **/b/2** + + + +- Перейти в календарь проекта \#5: **/c/5** + + + +- Перейти к просмотру списком проекта \#8: **/l/8** + + + +- Перейти к настройкам проекта для проекта id \#42: **/p/42** + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +По умолчанию, Канборд проверяет включен ли в Apache mode rewrite. + + + +Для исключения автоматической проверки переопределения URL на веб сервере, вы должны включить эту опцию в вашем конфигурационном фале: + + + + define('ENABLE_URL_REWRITE', true); + + + +Когда константа имеет значение `true`: + + + +- Сгенерированные из утилиты командной строки URL будут также преобразованы + + + +- Если вы используете другой веб сервер вместо Apache, например Nginx или Microsoft IIS, вы можете сами настроить переопределение URL + + + +Примечание: Канборд всегда использует URL по “старинке”, если данная константа не настроена. Эта настройка опциональна. + + + +Пример настройки Nginx[¶](#nginx-configuration-example "Ссылка на этот заголовок") +---------------------------------------------------------------------------------- + + + +В разделе `server`, вашего конфигурационного файла Nginx, вы можете использовать этот пример: + + + + index index.php; + + + + location / { + + try_files $uri $uri/ /index.php$is_args$args; + + + + # If Kanboard is under a subfolder + + # try_files $uri $uri/ /kanboard/index.php; + + } + + + + location ~ \.php$ { + + try_files $uri =404; + + fastcgi_split_path_info ^(.+\.php)(/.+)$; + + fastcgi_pass unix:/var/run/php5-fpm.sock; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + + fastcgi_index index.php; + + include fastcgi_params; + + } + + + + # Deny access to the directory data + + location ~* /data { + + deny all; + + return 404; + + } + + + + # Deny access to .htaccess + + location ~ /\.ht { + + deny all; + + return 404; + + } + + + +В конфигурационном файле Канборда `config.php`: + + + + define('ENABLE_URL_REWRITE', true); + + + +Адаптируйте пример приведенный выше к вашей конфигурации. + + + +Пример настройки IIS[¶](#iis-configuration-example "Ссылка на этот заголовок") +------------------------------------------------------------------------------ + + + +Создайте web.config в каталоге где установлен Канборд: + + + + <?xml version="1.0" encoding="UTF-8"?> + + <configuration> + + <system.webServer> + + <rewrite> + + <rules> + + <rule name="Imported Rule 1" stopProcessing="true"> + + <match url="^" ignoreCase="false" /> + + <conditions logicalGrouping="MatchAll"> + + <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" /> + + </conditions> + + <action type="Rewrite" url="index.php" appendQueryString="true" /> + + </rule> + + </rules> + + </rewrite> + + </system.webServer> + + </configuration> + + + +В конфигурационном файле Канборда `config.php`: + + + + define('ENABLE_URL_REWRITE', true); + + + +Адаптируйте пример приведенный выше к вашей конфигурации. + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/nitrous.markdown b/doc/ru_RU/nitrous.markdown new file mode 100644 index 00000000..8b975b0d --- /dev/null +++ b/doc/ru_RU/nitrous.markdown @@ -0,0 +1,16 @@ +Nitrous быстрый старт +===================== + + +Создайте свободное окружение разработки для проекта Kanboard в облаке на [Nitrous.io](https://www.nitrous.io). + +Зайдите на ваш сайт через ссылку в IDE `Preview > 3000`{.docutils .literal}. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/notifications.markdown b/doc/ru_RU/notifications.markdown new file mode 100644 index 00000000..8fc37876 --- /dev/null +++ b/doc/ru_RU/notifications.markdown @@ -0,0 +1,111 @@ +Уведомления +=========== + + + +Канборд имеет возможность отправлять сообщения по нескольким каналам: + + + +- Email + +- Веб (уведомления в Канборд) + + + +Внешние плагины позволяют вам посылать уведомления в Slack, Hipchat, Jabber или другие чат системы. + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +Любой пользователь может включить уведоления в своем профиле: в правом верхнем углу выберите во всплывающем меню **Мой профиль** -\> **Уведомления**. Уведомления по умолчанию выключены. + + + +Для получения уведомлений по email вам надо иметь электронную почту (email), которая должна быть указана в вашем профиле, и Канборд должен быть настроен на отправку электронной почты. + + + + + +Рисунок. Уведомления + + + +Вы можете выбрать, удобный для вас, способ получения уведомлений: + + + +- Email + + + +- Веб (смотрите ниже) + + + +Для каждого проекта в котором вы являетесь участником, вы можете выбрать получение уведомления для: + + + +- Всех задач + + + +- Только для задач назначеных вам + + + +- Только для задач, которые создали вы + + + +- Только для задач, созданных вами и назначенных вам + + + +Также, вы можете выбрать проекты из которых хотите получать уведомления. По умолчанию - все проекты, в которых вы являетесь участником. + + + +Веб уведомления[¶](#web-notifications "Ссылка на этот заголовок") +----------------------------------------------------------------- + + + +Веб уведомления доступны на рабочей панели **Мои уведомления** или вверху в виде иконки: + + + + + +Рисунок. Иконка веб уведомления. + + + +Уведомления отображаются списком. Вы можете выбрать действие **Пометить как прочитанное** для каждого сообщения или отметить сразу все. + + + + + +Рисунок. Веб уведомления. + + + +Таким образом, вы можете получать веб уведомления без использования электронной почты. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/plugin-directory.markdown b/doc/ru_RU/plugin-directory.markdown new file mode 100644 index 00000000..1920c91d --- /dev/null +++ b/doc/ru_RU/plugin-directory.markdown @@ -0,0 +1,38 @@ +Настройка директории плагинов +============================= + + + +Для установки, обновления и удаления плагинов в интерфейсе пользователя, вам необходимо выполнить следующие пункты: + + + +- Директория плагинов должна быть доступна на запись от пользователя веб сервера + + + +- Расширение zip должно быть доступно на вашем сервере + + + +- Параметр в конфигурации `PLUGIN_INSTALLER` должен быть установлен в `true` + + + +Для отключения этой возможности, измените значение в конфигурационном файле `PLUGIN_INSTALLER` на `false`. Также, вы должны изменить права доступа на директорию плагинов. + + + +Только администраторы могут устанавливать плагины через пользовательский интерфейс. + + + +По умолчанию, доступны только плагины из списка на веб сайте Канборда. + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/plugins.markdown b/doc/ru_RU/plugins.markdown new file mode 100644 index 00000000..e5ec2719 --- /dev/null +++ b/doc/ru_RU/plugins.markdown @@ -0,0 +1,167 @@ +Разработка плагина +================== + + + +**Внимание: API плагинов на данный момент в состоянии альфа.** + + + +Плагины удобны для расширения базового функционала Канборда: добавление возможностей, создание тем или изменения базового поведения. + + + +Создатели плагина должны указать точную версию Канборда, под которую написан плагин. Внутренний код Канборда может изменяться и ваш плагин должен тестироваться на совместимость с новой версией. Всегда следите за [ChangeLog](https://github.com/fguillot/kanboard/blob/master/ChangeLog) для внесения изменений. + + + +- [Создание вашего плагина](plugin-registration.markdown) + + + +- [Использование plugin hooks](plugin-hooks.markdown) + + + +- [События](plugin-events.markdown) + + + +- [Изменение базового поведения приложений](plugin-overrides.markdown) + + + +- [Добавление миграции схемы для плагинов](plugin-schema-migrations.markdown) + + + +- [Пользовательские маршруты](plugin-routes.markdown) + + + +- [Добавление обработчиков](plugin-helpers.markdown) + + + +- [Добавление почтовых трансляторов](plugin-mail-transports.markdown) + + + +- [Добавление типов оповещений](plugin-notifications.markdown) + + + +- [Добавление автоматических действий](plugin-automatic-actions.markdown) + + + +- [Расширение данных пользователей, задач и проектов](plugin-metadata.markdown) + + + +- [Архитектура аутентификации](plugin-authentication-architecture.markdown) + + + +- [Регистрация плагина аутентификации](plugin-authentication.markdown) + + + +- [Архитектура авторизации](plugin-authorization-architecture.markdown) + + + +- [Провайдер пользовательской группы](plugin-group-provider.markdown) + + + +- [Провайдер внешней ссылки](plugin-external-link.markdown) + + + +- [Добавление провайдера аватара](plugin-avatar-provider.markdown) + + + +- [Клиент LDAP](plugin-ldap-client.markdown) + + + +Примеры плагинов[¶](#examples-of-plugins "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +- [Двухуровневая аутентификация SMS](https://github.com/kanboard/plugin-sms-2fa) + + + +- [Аутентификация Reverse-Proxy с поддержкой LDAP](https://github.com/kanboard/plugin-reverse-proxy-ldap) + + + +- [Slack](https://github.com/kanboard/plugin-slack) + + + +- [Hipchat](https://github.com/kanboard/plugin-hipchat) + + + +- [Jabber](https://github.com/kanboard/plugin-jabber) + + + +- [Sendgrid](https://github.com/kanboard/plugin-sendgrid) + + + +- [Mailgun](https://github.com/kanboard/plugin-mailgun) + + + +- [Postmark](https://github.com/kanboard/plugin-postmark) + + + +- [Amazon S3](https://github.com/kanboard/plugin-s3) + + + +- [Планирование бюджета](https://github.com/kanboard/plugin-budget) + + + +- [Расписание пользователя](https://github.com/kanboard/plugin-timetable) + + + +- [Прогнозирование подзадач](https://github.com/kanboard/plugin-subtask-forecast) + + + +- [Пример автоматических действий](https://github.com/kanboard/plugin-example-automatic-action) + + + +- [Пример плагина темы](https://github.com/kanboard/plugin-example-theme) + + + +- [Пример плагина CSS](https://github.com/kanboard/plugin-example-css) + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/postgresql-configuration.markdown b/doc/ru_RU/postgresql-configuration.markdown new file mode 100644 index 00000000..9407ce59 --- /dev/null +++ b/doc/ru_RU/postgresql-configuration.markdown @@ -0,0 +1,92 @@ +Настройка Postgresql +==================== + + + +По умолчанию, Канборд использует для хранения данных Sqlite, но возможно использовать и Postgresql. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Установленный и настроенный сервер Postgresql + + + +- Установленное PHP расширение - `pdo_pgsql` (Debian/Ubuntu: `apt-get install php5-pgsql`) + + + +Примечание: работа Канборда протестирована с **Postgresql 9.3 и 9.4** + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +### Создайте пустую базу данных выполнив команду `pgsql`:[¶](#create-an-empty-database-with-the-command-pgsql "Ссылка на этот заголовок") + + + + CREATE DATABASE kanboard; + + + +### Создание конфигурационного файла[¶](#create-a-config-file "Ссылка на этот заголовок") + + + +Файл `config.php` должен содержать следующие значения: + +```php +<?php + +// We choose to use Postgresql instead of Sqlite +define('DB_DRIVER', 'postgres'); + +// Mysql parameters +define('DB_USERNAME', 'REPLACE_ME'); +define('DB_PASSWORD', 'REPLACE_ME'); +define('DB_HOSTNAME', 'REPLACE_ME'); +define('DB_NAME', 'kanboard'); +``` + + + +Примечание: Вы можете переименовать демонстрационный файл `config.default.php` в `config.php`. + + + +### Импортирование дампа SQL (альтернативный метод)[¶](#importing-sql-dump-alternative-method "Ссылка на этот заголовок") + + + +В первый раз, Канборд запускает по очереди каждую миграцию базы данных и этот процес может занять некоторое время, в зависимости от вашей конфигурации. + + + +Для избежания проблем или задержек вы можете инициализировать базу данных напрямую посредством импорта схемы SQL: + +```bash +psql -U postgres my_database < app/Schema/Sql/postgres.sql +``` + +Файл `app/Schema/Sql/postgres.sql` - это sql дамп, который представляет последнюю версию базы данных. + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/project-configuration.markdown b/doc/ru_RU/project-configuration.markdown new file mode 100644 index 00000000..af4863d7 --- /dev/null +++ b/doc/ru_RU/project-configuration.markdown @@ -0,0 +1,105 @@ +Настройки проекта +================= + + + +В правом верхнем выпадающем меню выберите **Настройки**, затем выберите **Настройки проекта** слева. + + + + + +Рисунок. Настройки проекта. + + + +Колонки по умолчанию для новых проектов[¶](#default-columns-for-new-projects "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------- + + + +Вы можете изменить колонки по умолчанию. Это удобно, когда вы создаете однотипные проекты с одними и теми же колонками. + + + +Название колонок должны быть разделены запятой. + + + +По умолчанию, в Канборде используются следующие колонки: Ожидающие, Готовые, В процессе, Выполнено + + + +Стандартные категории для новых проектов[¶](#default-categories-for-new-projects "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------ + + + +Категории не являются общими для приложения, но могут использоваться в проектах. Каждый проект может иметь разные категории. + + + +Однако, если вы постоянно создаете одни и теже категории для разных проектов, то вы можете задать список категорий, которые будут автоматически создаваться при создании проекта. + + + +Разрешена только одна подзадача в разработке одновременно для одного пользователя[¶](#allow-only-one-subtask-in-progress-at-the-same-time-for-a-user "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +Когда эта опция включена, пользователь может работать только с одной подзадачей одновременно. + + + +Если другая подзадача перейдет статус “В процессе”, то пользователь увидит следующее диалоговое окно: + + + + + +Рисунок. Ограничение пользовательских подзадач. + + + +Триггер автоматического отслеживания времени подзадач[¶](#trigger-automatically-subtask-time-tracking "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------- + + + +- Если этот триггер включен когда статус подзадачи меняется на “В процессе”, то таймер автоматически запускается. + + + +- Выключите эту опцию, если вы не хотите отслеживать время. + + + +Включить в диаграмму закрытые задачи[¶](#include-closed-tasks-in-the-cumulative-flow-diagram "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------ + + + +- Если эта опция включена, закрытые задачи будут добавлены в накопительную диаграмму. + + + +- Если выключена, то только открытые задачи будут подсчитаны. + + + +- Эта опция влияет на колонку “total” (всего) в таблице “project\_daily\_column\_stats” (проект\_ежедневно\_колонка\_статистика) + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/project-permissions.markdown b/doc/ru_RU/project-permissions.markdown new file mode 100644 index 00000000..580162fb --- /dev/null +++ b/doc/ru_RU/project-permissions.markdown @@ -0,0 +1,55 @@ +Права доступа к проекту +======================= + + + +Все проекты изолированы и отделены друг от друга. Доступ к проекту может назначать владелец проекта. + + + +Каждый пользователь и каждая группа могут иметь разные назначенные роли. Имеются [три типа ролей для проектов](roles.markdown): + + + +- Менеджер проекта + + + +- Участник проекта + + + +- Наблюдатель проекта + + + +Только администратор может иметь полный доступ ко всему. + + + +Назначение ролей доступно через **Меню -\> Настройки -\> Разрешения** + + + + + +Рисунок. Права доступа к проекту + + + +Если вы выберите **Разрешить любому**, то все пользователи Канборд будут считаться участниками Проекта. В таком случае, нет необходимости назначать роли. Потому что, разрешения, назначенные пользователям и группам, на доступ к Проекту не будут работать. + + + +Приватный проект не позволяет устанавливать разрешения. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/project-types.markdown b/doc/ru_RU/project-types.markdown new file mode 100644 index 00000000..d1169241 --- /dev/null +++ b/doc/ru_RU/project-types.markdown @@ -0,0 +1,27 @@ +Типы проектов +============= + + + +Проекты могут быть двух типов: + + + +| Тип | Описание | +|-----------------|----------------------------------------------------------| +| Командный проект| В проекте могут принимать участие пользователи и группы | +| Приватный проект| Проект принадлежит только одному пользователю, к проекту нельзя присоединить участников| + + + +- Командный проект могут создавать только пользователи с ролью Администратор и Менеджер. +- Приватный проект могут создавать все пользователи. + + +[Читать документацию про роли в Kanboard](roles.markdown) + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/project-views.markdown b/doc/ru_RU/project-views.markdown new file mode 100644 index 00000000..1d1f1117 --- /dev/null +++ b/doc/ru_RU/project-views.markdown @@ -0,0 +1,154 @@ +Представления Доска, Календарь, Список и Гант +============================================= + + + +В каждом проекте задачи могут быть отображены в разных представлениях: **Доска, Календарь, Список и Гант**. Для отображения представлений используется фильтр в верхней части рабочей панели. Для поиска используется [расширенный синтаксис](ext-search.markdown). + + + +Представление - Доска[¶](#board-view "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + + + +Рисунок. Представление зачад в виде доски + + + +- В этом представлении вы можете мышкой перемещать задачи между колонками. + + + +- Также, для перемещения задач на доске, можно использовать горячие клавиши **“v b”**. + + + +- Затемнения вокруг задачи показывает активную или измененную задачу. + + + + + +Рисунок. Лимит задач на Доске + + + +Когда лимит задач для колонки достигнут, тогда фон колонки становится красный. Это означает, что слишком много задач выполняются одновременно. + + + +[Ознакомится с настройками Доски](board-configuration.markdown) + + + +Представление - Календарь[¶](#calendar-view "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + + + +Рисунок. Представление в виде календаря + + + +- В этом представлении вы можете видеть задачи на конкретные даты. + + + +- Вы можете сделать настройки, которые позволят вам видеть задачи в работе. + + + +- Вы можете использовать горячие клавиши **“v c”** для перехода на представление Календарь. + + + +- [Ознакомится с настройками Календаря](calendar-configuration.markdown) + + + +Представление - Список[¶](#list-view "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + + + +Рисунок. Представление списком. + + + +- С помощью этого представления все результаты отображаются в виде таблицы. + + + +- Для быстрого перехода на представление Список вы можете использовать горячие клавиши **“v l”**. + + + +Представление - Гант.[¶](#gantt-view "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + + + +Рисунок. Представление диаграммой Ганта. + + + +- Представление Гант отображает задачи горизонтальными графиками + + + +- Для построения графика используется дата начала и срок выполнения + + + +- Для быстрого перехода к представлению Гант используйте горячие клавиши : **“v g”** + + + +Обзор Проекта[¶](#project-overview "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + + + +Рисунок. Представления проекта + + + +- Отображает описание проекта + + + +- Показывает прикрепленные и загруженные документы проекта + + + +- Показывает список участников проекта + + + +- Показывает последнюю активность в проекте + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/recurring-tasks.markdown b/doc/ru_RU/recurring-tasks.markdown new file mode 100644 index 00000000..a6572f2c --- /dev/null +++ b/doc/ru_RU/recurring-tasks.markdown @@ -0,0 +1,67 @@ +Повторяющиеся задачи +==================== + + + +Для соответсвия методологии Канбан, повторяющиеся задачи не имеют в качестве основы дату, а запускаются при наступлении событий на Доске. + + + +- Повторяющиеся задачи копируются (появляются вновь) в первой колонке Доски когда наступает определенное событие + + + +- Дата завершения (срок выполнения задачи) пересчитывается автоматически + + + +- Each task records the task id of the parent task that created it and the child task created + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +Перейдите на страницу детального представления задачи или используйте выпадающее меню на доске, выберите **Редактировать повторы**. + + + + + +Рисунок. Редактировать повторы. + + + +В редактировании повторов имеется выбор 3 триггеров для генерации периодической задачи: + + + +- Когда задача перемещается из первой колонки + + + +- Когда задача перемещается в последнюю колонку + + + +- Когда задача закрывается + + + +Дата завершения, если установлена для текущей задачи, может быть пересчитана с учетом **Коэффициента для расчета новой даты** и **Период для рассчета новой даты завершения** (например, 7 дней, 6 месяцев, 1 год). Базовой датой вычисления новой даты завершения может быть и имеющаяся дата завершения, или дата действия. + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/requirements.markdown b/doc/ru_RU/requirements.markdown new file mode 100644 index 00000000..aa6933b9 --- /dev/null +++ b/doc/ru_RU/requirements.markdown @@ -0,0 +1,137 @@ +Системные требования +==================== + + + +На сервере[¶](#server-side "Ссылка на этот заголовок") +------------------------------------------------------ + + + +### Поддерживаемые операционные системы[¶](#compatible-operating-systems "Ссылка на этот заголовок") + +|Операционная система| +|-----------------------------------| +|Linux Ubuntu Xenial Xerus 16.04 LTS| +| Linux Ubuntu Trusty 14.04 LTS| +| Linux Centos 6.x| +| Linux Centos 7.x| +| Linux Redhat 6.x| +|Linux Redhat 7.x| +| Linux Debian 8| +| FreeBSD 10.x| +| Microsoft Windows 2012 R2| +| Microsoft Windows 2008| + + + +### Поддерживаемые базы данных[¶](#compatible-databases "Ссылка на этот заголовок") + + +|База данных | +|----------------------| +|Sqlite 3.x | +|Mysql \>= 5.5 | +|MariaDB \>= 10 | +| Postgresql \>= 9.3 | + + + +Какую базу данных выбрать? + + +| Тип | Когда использовать | +|--------------------|--------------------------------------------------------| +| Sqlite | Один пользователь или небольшая команда | +| Mysql/Postgres | Большая команда, конфигурация высокой доступности | + + + + +Не используйте Sqlite на смонтированном NFS. Используйте Sqlite только на дисках с высокой скоростью чтение/запись. + + + +### Совместимые веб сервера[¶](#compatible-web-servers "Ссылка на этот заголовок") + +Apache HTTP Server, Nginx , Microsoft IIS + +Канборд изначально сконфигурирован для работы с Apache (URL rewriting). + + + +### Версии PHP[¶](#php-versions "Ссылка на этот заголовок") + + +PHP \>= 5.3.3, PHP 5.4, PHP 5.5, PHP 5.6, PHP 7.x + + + +### Требуемые расширения для PHP[¶](#php-extensions-required "Ссылка на этот заголовок") + + +| Требуемые расширения для PHP | Примечание | +|----------------------------------|-----------------------------------------| +| pdo\_sqlite | Только при использовании Sqlite | +| pdo\_mysql | Только при использоании Mysql/MariaDB | +| pdo\_pgsql | Только при использовании Postgres | +| gd | | +| mbstring | | +| openssl | | +| json | | +| hash | | +| ctype | | +| session | | +| ldap | Только для аутентификации LDAP | +| Zend OPcache | Рекомендуется | + + +### Рекомендуется[¶](#recommendations "Ссылка на этот заголовок") + + + +- Современная Linux или Unix операционная система. + + + +- Высокая производительность достигается с последней версией PHP со включенным кешированием OPcode. + + + +На клиенте[¶](#client-side "Ссылка на этот заголовок") +------------------------------------------------------ + + + +### Браузеры[¶](#browsers "Ссылка на этот заголовок") + + + +Используйте современные браузеры, обновленные до последней версии: + +|Браузер | +|-----------------| +| Safari | +| Google Chrome | +| Mozilla Firefox | +| Microsoft Internet Explorer \>= 11| +| Microsoft Edge | + + + +### Устройства[¶](#devices "Ссылка на этот заголовок") + + +| Устройство | Разрешение экрана | +|--------------------------------------|--------------------------------------| +| Персональный компьютер или ноутбук | \>= 1366 x 768 | +| Планшет | \>= 1024 x 768 | + + +Канборд, пока, не оптимизирован для работы на смартфонах. Конечно, он работает, но пользовательский интерфейс не совсем удобный для использования. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/reverse-proxy-authentication.markdown b/doc/ru_RU/reverse-proxy-authentication.markdown new file mode 100644 index 00000000..2d97a6e4 --- /dev/null +++ b/doc/ru_RU/reverse-proxy-authentication.markdown @@ -0,0 +1,138 @@ +Аутентификация Reverse Proxy +============================ + + + +Этот метод аутентификации часто используется для [SSO](https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%85%D0%BD%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%8F_%D0%B5%D0%B4%D0%B8%D0%BD%D0%BE%D0%B3%D0%BE_%D0%B2%D1%85%D0%BE%D0%B4%D0%B0) (Технология единого входа), особенно удобно в больших организациях. + + + +Аутентификация выполняется с помощью другой системы, поэтому Канборд не знает вашего пароля и допускает вас к приложению, так как вы уже прошли аутентификацию. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Правильно сконфигурированный reverse proxy + + + +или + + + +- Apache Auth на том же сервере + + + +Как это работает?[¶](#how-does-this-work "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +1. Ваш reverse proxy аутентифицирует пользователя и посылает имя пользователя через заголовок HTTP. + + + +2. Канборд извлекает имя пользователя из запроса + + + + - Пользователь создается в Канборд автоматически (опция настраивается) + + + + - Открывается новая сессия Канборд (дополнительная аутентификация в Канборд не нужна) + + + +Инструкция по установке[¶](#installation-instructions "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + + +### Настройка вашего reverse proxy[¶](#setting-up-your-reverse-proxy "Ссылка на этот заголовок") + + + +В рамках данной документации не рассматривается установка и настройка reverse proxy. Вы должны убедится, что логин пользователя отправляется с reverse proxy в заголовке HTTP. + + + +### Настройки Канборда[¶](#setting-up-kanboard "Ссылка на этот заголовок") + + + +Создайте свой файл конфигурации `config.php` или скопируйте конфигурацию из файла `config.default.php`: + + + + <?php + + + + // Enable/disable reverse proxy authentication + + define('REVERSE_PROXY_AUTH', true); // Set this value to true + + + + // The HTTP header to retrieve. If not specified, REMOTE_USER is the default + + define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); + + + + // The default Kanboard admin for your organization. + + // Since everything should be filtered by the reverse proxy, + + // you should want to have a bootstrap admin user. + + define('REVERSE_PROXY_DEFAULT_ADMIN', 'myadmin'); + + + + // The default domain to assume for the email address. + + // In case the username is not an email address, it + + // will be updated automatically as USER@mydomain.com + + define('REVERSE_PROXY_DEFAULT_DOMAIN', 'mydomain.com'); + + + +Примечание: + + + +- Если proxy находится на том же сервере, что и Канборд, то в соответствии с протоколом \<[http://www.ietf.org/rfc/rfc3875](http://www.ietf.org/rfc/rfc3875)\>\`\_\_ имя заголовка будет `REMOTE_USER`. Например, Apache добавляет `REMOTE_USER` по умолчанию, если установлено `Require valid-user`. + + + +- Если Apache служит reverse proxy для другого Apache выполняющего Канборд, то заголовок `REMOTE_USER` не установлен (это же относится к IIS и Nginx). + + + +- Если у вас имеется действующий reverse proxy, то [проект HTTP ICAP](http://tools.ietf.org/html/draft-stecher-icap-subid-00#section-3.4) предполагает, что заголовок должен быть `X-Authenticated-User`. Этот стандарт де-факто был принят разными инструментами. + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/roles.markdown b/doc/ru_RU/roles.markdown new file mode 100644 index 00000000..5af8a937 --- /dev/null +++ b/doc/ru_RU/roles.markdown @@ -0,0 +1,44 @@ +Пользовательские роли +===================== + + + +Роли в приложениях[¶](#application-roles "Ссылка на этот заголовок") + +-------------------------------------------------------------------- + + + +Каждый пользователь системы Канборд имеет одну из этих ролей + + + +| Роль | Описание | +|----------------|-----------------------------------------------------------| +| Администратор | Имеет доступ ко всему | +| Менеджер | Может создавать командные проекты, но не может изменять настройки приложения | +| Пользователь | Может создавать только приватные проекты | + + + + +Роли в проектах[¶](#project-roles "Ссылка на этот заголовок") + +------------------------------------------------------------- + + + +В каждом командном проекте могут быть назначены разные роли для пользователей и групп: + + +| Роль | Описание | +|-----------------|----------------------------------------------------------| +| Менеджер проекта| Может изменять настройки проекта, имеет доступ к диаграмме Ганта и отчетам | +| Участник проекта| Может создавать задачи и пользоваться доской | +| Наблюдатель проекта | Имеет доступ к доске и задачам только на просмотр (чтение) | + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/rss.markdown b/doc/ru_RU/rss.markdown new file mode 100644 index 00000000..c4718880 --- /dev/null +++ b/doc/ru_RU/rss.markdown @@ -0,0 +1,58 @@ +RSS/Atom подписки +================= + + + +Канборд поддерживает RSS ленты для проектов и пользователей. + + + +- RSS/Atom лента для проекта - содержит только активность в проекте + + + +- RSS/Atom лента пользователя - содержит поток активности пользователя во всех проектах, в которых пользователь является участником + + + +Эти подписки доступны только при включенном общем доступе в пользовательском профиле или в настройках проекта. + + + +Включение/выключение RSS ленты проекта[¶](#enable-disable-project-rss-feeds "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------- + + + +Перейдите в **Настройки проекта** -\> **Общий доступ** + + + + + +Рисунок. Выключение общего доступа. + + + +Включение/выключение RSS ленты пользователя[¶](#enable-disable-user-rss-feeds "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------- + + + +Перейдите в **Мой профиль** -\> **Общий доступ** + + + +Ссылка на RSS ленту защищена случайным ключом, только пользователи, которые знают URL ссылку, могут иметь доступ к ленте. + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/screenshots.markdown b/doc/ru_RU/screenshots.markdown new file mode 100644 index 00000000..2260f258 --- /dev/null +++ b/doc/ru_RU/screenshots.markdown @@ -0,0 +1,74 @@ +Добавление снимков экрана (скриншота) +===================================== + + + +Для экономии времени вы можете копировать и вставлять изображения прямо в Канборде. Загруженные изображения прикрепляются к задаче. + + + +Например, очень удобно для решения проблемы прикрепить снимок экрана. + + + +Вы можете добавить снимок экрана прямо из Доски нажав на выпадающее меню задачи и выбрав **Прикрепить картинку** или на странице детального просмотра задачи. + + + + + + + +Рисунок. Выпадающее меню задачи - **Прикрепить картинку**. + + + +Для добавления нового снимка экрана (скриншота), сделайте снимок экрана (нажмите клавиши Ctrl+PrtScn) и вставьте его используя сочетания клавиш CTRL+V или Command+V + + + + + +Рисунок. Прикрепить картинку. + + + +В Mac OS X вы можете использовать следующие горячие клавиши для создания снимка экрана: + + + +- Command-Control-Shift-3: Делает снимок экрана и сохраняет его в буфер обмена + + + +- Command-Control-Shift-4 и выделите необходимую область на экране: Делает снимок экрана для области экрана и сохраняет ее в буфер обмена + + + +- Command-Control-Shift-4, затем пробел, затем нажать на окно: Делает снимок окна и сохраняет его в буфер обмена + + + +Имеется много разных других программ для создания снимков с экрана с примечаниями и разными формами. + + + +**Заметка**: Эта возможность работает не во всех браузерах. Например, не работает в Safari из-за этой ошибки: [https://bugs.webkit.org/show\_bug.cgi?id=49141](https://bugs.webkit.org/show_bug.cgi?id=49141) + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/search.markdown b/doc/ru_RU/search.markdown new file mode 100644 index 00000000..14c3f5b1 --- /dev/null +++ b/doc/ru_RU/search.markdown @@ -0,0 +1,24 @@ +Поиск + +===== + + + +Для работы поиска включите JavaScript в браузере. + + + +Здесь можно делать поиск по всем разделам этой документации. Введите ключевые слова в текстовое поле и нажмите кнопку «искать». Внимание: будут найдены только те страницы, в которых есть все указанные слова. Страницы, где есть только часть этих слов, отобраны не будут. + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/sharing-projects.markdown b/doc/ru_RU/sharing-projects.markdown new file mode 100644 index 00000000..e8448189 --- /dev/null +++ b/doc/ru_RU/sharing-projects.markdown @@ -0,0 +1,82 @@ +Публичные доски и задачи +======================== + + + +По умолчанию, Доска имеет приватный доступ, но имеется возможность сделать Доску публичной. + + + +Публичная доска **не может быть изменена (имеется только доступ на чтение)**. Доступ к доске защищен случайно сгенерированным ключом, только пользователи знающие правильный URL могут увидеть публичную Доску. + + + +Публичная Доска автоматически обновляется каждые 60 секунд. Детали задач, также, доступны только для чтения. + + + +Пример использования: + + + +- Публикация вашей Доски для кого-либо снаружи (работник из другой организации) + + + +- Отображение Доски на большом экране в вашем офисе + + + +Включение общего доступа[¶](#enable-public-access "Ссылка на этот заголовок") +----------------------------------------------------------------------------- + + + +Выберете ваш проект, затем нажмите на ссылку **“Общий доступ”** и в завершении нажмите на кнопку **“Включить общий доступ”** + + + + + +Рисунок. Включение общего доступа + + + +Когда общий доступ к проекту включен, сгенерируется несколько ссылок: + + + +- Ссылка для просмотра + + + +- RSS лента + + + +- iCalendar данные + + + + + +Рисунок. Отключить общий доступ. + + + +Вы можете выключить общий доступ к проекту в любой момент. + + + +Каждый раз, когда вы включаете или выключаете общий доступ, генерируется новый ключ. **Доступ по предыдущей ссылке будет невозможен**. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/sqlite-database.markdown b/doc/ru_RU/sqlite-database.markdown new file mode 100644 index 00000000..202452cb --- /dev/null +++ b/doc/ru_RU/sqlite-database.markdown @@ -0,0 +1,96 @@ +Настройка базы данных Sqlite +============================ + + + +Канборд использует для хранения данных Sqlite по умолчанию. Все задачи, проекты и учетные записи пользователей храняться в этой базе данных. + + + +База данных Sqlite хранит данные в файле `db.sqlite` в директории `data`. + + + +Экспорт/Резервное копирование[¶](#export-backup "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + + +### Командная строка[¶](#command-line "Ссылка на этот заголовок") + + + +Создание резервных копий выполняется просто, надо скопировать файл `data/db.sqlite` туда, где у вас будут хранится резервные копии. + + + +### Пользовательский интерфейс[¶](#user-interface "Ссылка на этот заголовок") + + + +Также, в любое время, вы можете скачать базу данных прямо через меню **Настройки**. + + + +Выгружаемая база данных упакована с помощью Gzip и имя базы выглядитит как `db.sqlite.gz`. + + + +Импорт/Восстановление[¶](#import-restoration "Ссылка на этот заголовок") +------------------------------------------------------------------------ + + + +Загрузить базу данных через пользовательский интерфейс невозможно. Восстановление должно быть выполнено вручную, когда никто не работает с программой. + + + +- Для восстановления резервной копии, достаточно заменить рабочий файл `data/db.sqlite`. + + + +- Для разархивирования базы данных упакованной с помощью gzip, выполните следующую команду в терминале: `gunzip db.sqlite.gz`. + + + +Оптимизация[¶](#optimization "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +Время от времени, рекомендуется оптимизировать базу данных выполнив команду `VACUUM`. Эта команда пересоздает всю базу данных и используется в следующих случаях: + + + +- Для уменьшения размера файла базы данных. В процессе работы пользователей, после удаления записей, в базе данных остается пустое пространство и, соответственно, размер файла базы данных остается прежним. + + + +- Дефрагментация, база данных фрагментирована выполнением частыми вставками или обновлениями. + + + +### Выполнение оптимизации в командной строке[¶](#from-the-command-line "Ссылка на этот заголовок") + + + + sqlite3 data/db.sqlite 'VACUUM' + + + +### Выполнение оптимизации через пользовательский интерфейс[¶](#from-the-user-interface "Ссылка на этот заголовок") + + + +Перейдите в правое выпадающее меню **Настройки** и нажмите на ссылку **Оптимизировать базу данных** + + + +Для дополнительной информации, изучите [документацию Sqlite](https://sqlite.org/lang_vacuum.html). + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/subtasks.markdown b/doc/ru_RU/subtasks.markdown new file mode 100644 index 00000000..c78aee73 --- /dev/null +++ b/doc/ru_RU/subtasks.markdown @@ -0,0 +1,111 @@ +Подзадачи +========= + + +Подзадачи - это прекрасная возможность разделить основную задачу на части. + + + +Каждая подзадача: + + + +- Может быть назначена участнику проекта + + + +- Имеет 3 разных статуса: **Для исполнения**, **В работе**, **Выполнено** + + + +- Имеет информацию по отслеживанию времени: **затраченное время** и **запланированное время** + + + +- Может быть перемещена в списке, для изменения порядка выполнения + + + +Создание подзадачи[¶](#creating-subtasks "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +В детальном представлении задачи, в левой боковой панели нажмите **Добавить подзадачу**: + + + + + +Рисунок. Добавление подзадачи. + + + +Вы, также, можете быстро добавить подзадачу нажав на заголовок: + + + + + +Рисунок. Добавление подзадачи на странице детального просмотра задачи. + + + +Изменение статуса подзадачи[¶](#change-subtask-status "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + + +Когда вы нажимаете на заголовок подзадачи стату меняется: + + + + + +Рисунок. Выполнение подзадачи. + + + +Иконка перед названием подзадачи обновляется в соответсвии со статусом. + + + + + +Рисунок. Подзадача выполнена. + + + +**Заметка**: Когда задача закрыта, то все подзадачи меняют статус на **Выполнена**. + + + +Таймер подзадачи[¶](#subtask-timer "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +- Когда подзадача выполняется, таймер должен быт запущен. Таймер можно запустить и остановить в любое время. + + + +- Время таймера записывается автоматически в затраченное время. Так же, вы можете изменить вручную значение **затраченного времени** при редактировании подзадачи. + + + +- Подсчитываемое время округляется до 15 минут. Эта информация записывается в отдельную таблицу. + + + +- Время, затраченное на выполнение задачи, и запланированнное время обновляется автоматически, в соответсвии с суммой всех подзадач. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/suse-installation.markdown b/doc/ru_RU/suse-installation.markdown new file mode 100644 index 00000000..6d508708 --- /dev/null +++ b/doc/ru_RU/suse-installation.markdown @@ -0,0 +1,36 @@ +Инсталяция на OpenSuse +====================== + + + +OpenSuse Leap 42.1[¶](#opensuse-leap-42-1 "Ссылка на этот заголовок") +--------------------------------------------------------------------- + + + + sudo zypper install php5 php5-sqlite php5-gd php5-json php5-mcrypt php5-mbstring php5-openssl + + cd /srv/www/htdocs + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chmod -R 777 kanboard + + sudo rm kanboard-latest.zip + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/swimlanes.markdown b/doc/ru_RU/swimlanes.markdown new file mode 100644 index 00000000..d6e36fdd --- /dev/null +++ b/doc/ru_RU/swimlanes.markdown @@ -0,0 +1,81 @@ +Дорожки +======= + + + +Дорожки - это горизонтальное разделение вашей Доски. Например, очень удобно разделять релизы программ, разделить ваши задачи для разных продуктов, команд или чего-то еще. + + + +Доска с дорожками[¶](#board-with-swimlanes "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + + + +Рисунок. Дорожки + + + +- Вы можете свернуть дорожку нажав на иконку слева + + + +- “Стандатная дорожка” всегда расположена сверху + + + +Управление дорожками[¶](#managing-swimlanes "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + +- Все проекты имеют дорожку по умолчанию - **Стандартная дорожка** + + + +- Если имеется больше одной дорожки, то на Доске будут показаны все имеющиеся дорожки. + + + +- Вы можете перемещать мышкой задачи между дорожками. + + + +Для настройки дорожек перейдите на страницу **настройки проекта** (Меню -\> Настройки) и нажмите **Дорожки** (слева). + + + + + +Рисунок. Настройка Дорожек. + + + +Теперь вы можете добавить новую дорожку или переименовать стандартную дорожку. Также, вы можете выключить дорожку или изменить расположение любой дорожки. + + + +- Стандартная дорожка всегда расположена сверху, но вы можете ее выключить и она не будет отображаться на Доске. + + + +- Выключенные дорожки не отображаются на Доске. + + + +- **Удаление дорожки не влечет за собой удаление расположенных на этой дорожке задач**, эти задачи будут перемещены в “Стандартную дорожку”. + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/syntax-guide.markdown b/doc/ru_RU/syntax-guide.markdown new file mode 100644 index 00000000..9d7414a8 --- /dev/null +++ b/doc/ru_RU/syntax-guide.markdown @@ -0,0 +1,246 @@ +Руководство по синтаксису +========================= + + + +Канборд использует [Markdown синтаксис](https://ru.wikipedia.org/wiki/Markdown) для комментариев или описания задач. Далее приведены примеры: + + + +Жирный и курсив[¶](#bold-and-italic "Ссылка на этот заголовок") +--------------------------------------------------------------- + +- Жирный текст: Используйте 2 звездочки или 2 подчеркивания вокруг слов(а) + + + +- Курсив: Используйте 1 звездочку или 1 подчеркивание вокруг слов(а) + + + +### Пример написания (источник)[¶](#source "Ссылка на этот заголовок") + + + + This **word** is very __important__. + + + + And here, an *italic* word with one _underscore_. + + + +### Результат[¶](#result "Ссылка на этот заголовок") + + + +This **word** is very **important**. + + + +And here, an *italic* word with one *underscore*. + + + +Неупорядоченные списки[¶](#unordered-lists "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + +Неупорядоченный список использует звездочки, минусы или плюсы вначале абзаца + + + +### Пример написания (источник)[¶](#id1 "Ссылка на этот заголовок") + + + + - Item 1 + + - Item 2 + + - Item 3 + + + + or + + + + * Item 1 + + * Item 2 + + * Item 3 + + + +### Результат[¶](#id2 "Ссылка на этот заголовок") + + + +- Item 1 + +- Item 2 + +- Item 3 + + + +Упорядоченные списки[¶](#ordered-lists "Ссылка на этот заголовок") +------------------------------------------------------------------ + + + +Упорядоченные списки префиксом имеют цифру: + + + +### Пример написания (источник)[¶](#id3 "Ссылка на этот заголовок") + + + + 1. Do that first + + 2. Do this + + 3. And that + + + +### Результат[¶](#id4 "Ссылка на этот заголовок") + + + +1. Do that first + +2. Do this + +3. And that + + + +Ссылки[¶](#links "Ссылка на этот заголовок") +-------------------------------------------- + + + +### Пример написания (источник)[¶](#id5 "Ссылка на этот заголовок") + + + + [My link title](https://kanboard.net/) + + + + <https://kanboard.net> + + + +### Результат[¶](#id6 "Ссылка на этот заголовок") + + + +[My link title](https://kanboard.net/) + + + +[https://kanboard.net](https://kanboard.net) + + + +Исходный код[¶](#source-code "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +### Код встраиваемый в текст[¶](#inline-code "Ссылка на этот заголовок") + + + +Используйте обратные кавычки (переключитесь на анлийскую раскладку и нажмите ё) + + + + Execute this command: `tail -f /var/log/messages`. + + + +### Результат[¶](#id7 "Ссылка на этот заголовок") + + + +Execute this command: `tail -f /var/log/messages`{.docutils .literal}. + + + +### Блоки кода[¶](#code-blocks "Ссылка на этот заголовок") + + + +Используйте 3 обратных кавычки с указанием языка программирования + + + + ```php + + <?php + + + + phpinfo(); + + + + ?> + + ``` + + + +### Результат[¶](#id8 "Ссылка на этот заголовок") + + + + <?php + + + + phpinfo(); + + + + ?> + + + +Заголовки[¶](#titles "Ссылка на этот заголовок") +------------------------------------------------ + + + +### Пример написания (источник)[¶](#id9 "Ссылка на этот заголовок") + + + + # Title level 1 + + + + ## Title level 2 + + + + ### Title level 3 + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/task-links.markdown b/doc/ru_RU/task-links.markdown new file mode 100644 index 00000000..2912f91b --- /dev/null +++ b/doc/ru_RU/task-links.markdown @@ -0,0 +1,93 @@ +Ссылки на задачи +================ + + + +Задачи могут быть созданы вместе с предопределенными связями: + + + + + +Рисунок. Ссылки на задачи + + + +Связи по умолчанию: + + + +- **относится к** + + + +- **блокирована**| блокирует + + + +- **блокирует** | блокирована + + + +- **дублирована** | дублирует + + + +- **дублирует** | дублирована + + + +- **является продолжением** | является началом для + + + +- **является началом для** | является продолжением + + + +- **часть вехи** | является вехой для + + + +- **является вехой для** | часть вехи + + + +- **исправлено** | исправляет + + + +- **исправляет** | исправлено + + + +Эти названия могут быть быть изменены в настройках приложения. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/tests.markdown b/doc/ru_RU/tests.markdown new file mode 100644 index 00000000..2373d030 --- /dev/null +++ b/doc/ru_RU/tests.markdown @@ -0,0 +1,262 @@ +Автоматизированные тесты +======================== + + + +[PHPUnit](https://phpunit.de/) используется для запуска автоматизированных тестов в Канборд. + + + +Вы можете запускать тесты для разных баз данных (Sqlite, Mysql and Postgresql), чтобы убедится, что результаты будут одинаковые. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Компьютер Linux/Unix + + + +- PHP cli + + + +- Установленный PHPUnit + + + +- Mysql и Postgresql (опционально) + + + +Unit тесты[¶](#unit-tests "Ссылка на этот заголовок") +----------------------------------------------------- + + + +### Тестирование с Sqlite[¶](#test-with-sqlite "Ссылка на этот заголовок") + + + +Sqlite тестирование использует базу данных в памяти, без использования записи на файловую систему. + + + +Конфигурационный файл PHPUnit - `tests/units.sqlite.xml`. Из директории Kanboard запустите команду `phpunit -c tests/units.sqlite.xml`. + + + +Пример: + + + + phpunit -c tests/units.sqlite.xml + + + + PHPUnit 5.0.0 by Sebastian Bergmann and contributors. + + + + ............................................................... 63 / 649 ( 9%) + + ............................................................... 126 / 649 ( 19%) + + ............................................................... 189 / 649 ( 29%) + + ............................................................... 252 / 649 ( 38%) + + ............................................................... 315 / 649 ( 48%) + + ............................................................... 378 / 649 ( 58%) + + ............................................................... 441 / 649 ( 67%) + + ............................................................... 504 / 649 ( 77%) + + ............................................................... 567 / 649 ( 87%) + + ............................................................... 630 / 649 ( 97%) + + ................... 649 / 649 (100%) + + + + Time: 1.22 minutes, Memory: 151.25Mb + + + + OK (649 tests, 43595 assertions) + + + +### Тестирование с Mysql[¶](#test-with-mysql "Ссылка на этот заголовок") + + + +У вас должна быть локально установлена база данных Mysql или MariaDb. + + + +По умолчанию, используются следующие учетные данные: + + + +- Hostname: **localhost** + +- Username: **root** + +- Password: none + +- Database: **kanboard\_unit\_test** + + + +При каждом выполнении база данных удаляется и создается снова. + + + +Конфигурационный файл HPUnit - `tests/units.mysql.xml`. Из директории Kanboard запустите команду `phpunit -c tests/units.mysql.xml`. + + + +### Тестирование с Postgresql[¶](#test-with-postgresql "Ссылка на этот заголовок") + + + +У вас должен быть локально установлен Postgresql. + + + +По умолчанию, используются следующие учетные данные: + + + +- Hostname: **localhost** + +- Username: **postgres** + +- Password: none + +- Database: **kanboard\_unit\_test** + + + +Убедитесь, что пользователь `postgres` может создавать и удалять базу данных. База данных пересоздается при каждом выполнении теста. + + + +Конфигурационных файл PHPUnit - `tests/units.postgres.xml`. Из директории Kanboard, запустите команду `phpunit -c tests/units.postgres.xml`. + + + +Тесты интеграции[¶](#integration-tests "Ссылка на этот заголовок") +------------------------------------------------------------------ + + + +Фактически тестируются только вызовы API. + + + +Реальные HTTP calls выполняются с этими тестами. Поэтому, необходим локальный экземпляр Канборда, который слушает на `http://localhost:8000/`. + + + +Все данные будут удалены/изменены при тестировании. Более того скрипт будет сброшен и установлен новый ключ API. + + + +1. Запустите локольный экземпляр Канборда: `php -S 127.0.0.1:8000` + + + +2. Запустите тест в другом терминале + + + +Этот же метод используется для запуска тестов для разных баз данных: + + + +- Sqlite: `phpunit -c tests/integration.sqlite.xml` + +- Mysql: `phpunit -c tests/integration.mysql.xml` + +- Postgresql: `phpunit -c tests/integration.postgres.xml` + + + +Пример: + + + + phpunit -c tests/integration.sqlite.xml + + + + PHPUnit 5.0.0 by Sebastian Bergmann and contributors. + + + + ............................................................... 63 / 135 ( 46%) + + ............................................................... 126 / 135 ( 93%) + + ......... 135 / 135 (100%) + + + + Time: 1.18 minutes, Memory: 14.75Mb + + + + OK (135 tests, 526 assertions) + + + +Непрерывная интеграция с Travis-CI[¶](#continuous-integration-with-travis-ci "Ссылка на этот заголовок") + +-------------------------------------------------------------------------------------------------------- + + + +После каждого commit влитого в мой репозиторий, юнит тесты выполняются для 5 различных версий PHP: + + + +- PHP 7.0 + +- PHP 5.6 + +- PHP 5.5 + +- PHP 5.4 + +- PHP 5.3 + + + +При тестировании каждой версии PHP используются 3 поддерживаемые базы данных: Sqlite, Mysql and Postgresql. + + + +Конфигурационный файл Travis - `.travis.yml` - находится в корневой директории Kanboard. + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/time-tracking.markdown b/doc/ru_RU/time-tracking.markdown new file mode 100644 index 00000000..98364d38 --- /dev/null +++ b/doc/ru_RU/time-tracking.markdown @@ -0,0 +1,112 @@ +Отслеживание времени +==================== + + + +Отслеживание времени (контроль времени) может быть использовано для уровня задач или для уровня подзадач. + + + +Отслеживание времени испольнения задач[¶](#task-time-tracking "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------- + + + + + +Рисунок. Отслеживание времени испольнения задач + + + +Задачи имеют два поля: + + + +- Запланировано времени + + + +- Затрачено времени + + + +Эти значения показывают время работы и могут быть установлены вручную + + + +Отслеживание времени подзадач[¶](#subtask-time-tracking "Ссылка на этот заголовок") +----------------------------------------------------------------------------------- + + + + + +Рисунок. Отслеживание времени подзадач + + + +Подзадачи тоже имеют поля “Запланировано” и “Затрачено” время. + + + +Когда вы меняете значения в этих полях, **отслеживание времени задачи обновляется автоматически и формируется суммарное время всех подзадач** + + + +Канборд записывает время между изменениями статуса каждой подзадачи в отдельную таблицу. + + + +- При изменении статуса подзадачи с **“Для испольнения”** на **“В работе”**, записывается время начала + + + +- При изменении статуса подзадачи с **“В работе”** на **“Выполнено”**, записывается как время окончания и, при этом, обновляется **затраченное время** в подзадаче и в задаче. + + + +Анализ всех записей можно увидеть на странице детального просмотра задачи: + + + + + +Рисунок. Таблица учета времени. + + + +Для каждой подзадачи, таймер может быть остановлен и запущен в любое время: + + + + + +Рисунок. Таймер подзадач. + + + +- Таймер не зависит от статуса подзадачи + + + +- Вы можете запустить таймер для новой записи, созданной в таблице отслеживания задач, в любое время + + + +- Вы можете остановить учет времени даты завершения в таблице отслеживания задач, в любое время + + + +- Подсчет затраченного времени округляется до четверти часа + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/transitions.markdown b/doc/ru_RU/transitions.markdown new file mode 100644 index 00000000..efb95c50 --- /dev/null +++ b/doc/ru_RU/transitions.markdown @@ -0,0 +1,60 @@ +Перемещения задач +================= + + + +Запись о перемещении отражает каждое движение задачи между колонками. + + + + + +Рисунок. Перемещения. + + + +Перемещение доступно в боковом меню в детальном представлении задачи (**Перемещения**). Вы можете увидеть следующую информацию: + + + +- Дата, когда было выполенено перемещение + + + +- Исходная колонка - колонка, из которой было сделано перемещение + + + +- Колонка назначения - колонка, в которую была перемещена задача + + + +- Исполнитель (пользователь, который переместил задачу) + + + +- Время проведенное в колонке (сколько времени было затрачено на выполнение задачи в указанной колонке) + + + +Данные о перемещении задач, также, могут быть экспортированы со страницы настроек проекта (**Меню** -\> **Экспорт**). + + + + + +Рисунок. Экспорт перемещений задач. + + + +Для указанного промежутка времени вы можете сформировать CSV файл, который вы можете импортировать в любое программное обеспечение с электронными таблицами (например, Excell). + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/translations.markdown b/doc/ru_RU/translations.markdown new file mode 100644 index 00000000..f4bcafc0 --- /dev/null +++ b/doc/ru_RU/translations.markdown @@ -0,0 +1,155 @@ +Переводы на другие языки (локализация) +====================================== + + + +Как перевести Канборд на новый язык?[¶](#how-to-translate-kanboard-to-a-new-language "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------------- + + + +- Переводы хранятся в директории `app/Locale` + + + +- В этой директории есть поддиректории для разных языков, например, для русского имеется `ru_RU`, для французского - `fr_FR` и т.д. + + + +- Переводы находятся в PHP файле, который возвращает массив с парой ключ-значение + + + +- Ключ - оригинальный текст на английском и значение - перевод на соответсвующем языке + + + +- **Французские переводы всегда в актуальном состоянии** + + + +- Всегда используйте последнюю версию (branch master) + + + +### Создание нового перевода[¶](#create-a-new-translation "Ссылка на этот заголовок") + + + +1. Создайте новую директорию: `app/Locale/xx_XX`, например `app/Locale/fr_CA` для канадского фрацузского + + + +2. Создайте новый файл для перевода: `app/Locale/xx_XX/translations.php` + + + +3. Используйте как образец содержимое французского перевода (локализации) и замените значения + + + +4. Внесите изменения в файл `app/Model/Language.php` + + + +5. Проверьте добавленный язык на локальной версии Канборда + + + +6. Пошлите [pull-request на Github](https://help.github.com/articles/using-pull-requests/) + + + +Как обновить имеющийся перевод?[¶](#how-to-update-an-existing-translation "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------- + + + +1. Откройте файл перевода `app/Locale/xx_XX/translations.php` + + + +2. Отсутсвующие переводы закоментированы - `//` и значения пустые, нужно заполнить значения и удалить коментарий + + + +3. Проверьте внесенные изменения на локальной версии Канборда и пошлите [pull-request](https://help.github.com/articles/using-pull-requests/) + + + +Как добавить новый текст перевода в приложение?[¶](#how-to-add-new-translated-text-in-the-application "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------- + + + +Переводы отображаются с помощью функций в исходном коде: + + + +- `t()`: показывает текст с HTML escaping + + + +- `e()`: показывает текст без HTML escaping + + + +Всегда используйте английскую версию исходного кода. + + + +Текстовые строки используют функцию `sprintf()` для замены элементов: + + + +- `%s` используется для замены строки + + + +- `%d` используется для замены цифры + + + +Ознакомится с доступными форматами вы можете в [документации PHP](http://php.net/sprintf). + + + +Как найти отсутствующие переводы в приложении?[¶](#how-to-find-missing-translations-in-the-applications "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------- + + + +Из терминала запустите следующую команду: + + + + ./kanboard locale:compare + + + +Все отсутствующие и неиспользуемые переводы будут показаны на экране. Добавьте их во французскую локализацию и синхронизируйте с другими локализациями (смотрите ниже) + + + +Как синхронизировать файлы переводов?[¶](#how-to-synchronize-translation-files "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------- + + + +В оболочке Unix запустите следующую команду: + + + + ./kanboard locale:sync + + + +Французский перевод используется для ссылки на другие локализации. + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ubuntu-installation.markdown b/doc/ru_RU/ubuntu-installation.markdown new file mode 100644 index 00000000..ac3cb565 --- /dev/null +++ b/doc/ru_RU/ubuntu-installation.markdown @@ -0,0 +1,111 @@ +Как инсталировать Канборд на Ubuntu? +==================================== + + + +Ubuntu Xenial 16.04 LTS[¶](#ubuntu-xenial-16-04-lts "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + sudo apt-get update + + sudo apt-get install -y apache2 libapache2-mod-php7.0 php7.0-cli php7.0-mbstring php7.0-sqlite3 \ + + php7.0-opcache php7.0-json php7.0-mysql php7.0-pgsql php7.0-ldap php7.0-gd + + + +Установите Канборд: + + + + cd /var/www/html + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chown -R www-data:www-data kanboard/data + + sudo rm kanboard-latest.zip + + + +Ubuntu Trusty 14.04 LTS[¶](#ubuntu-trusty-14-04-lts "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + sudo apt-get update + + sudo apt-get install -y php5 php5-sqlite php5-mysql php5-pgsql php5-ldap php5-gd php5-json php5-mcrypt unzip + + + +Установите Канборд: + + + + cd /var/www/html + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chown -R www-data:www-data kanboard/data + + sudo rm kanboard-latest.zip + + + +Ubuntu Precise 12.04 LTS[¶](#ubuntu-precise-12-04-lts "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + sudo apt-get update + + sudo apt-get install -y php5 php5-sqlite php5-mysql php5-pgsql php5-ldap php5-gd php5-json php5-mcrypt unzip + + + +Установите Канборд: + + + + cd /var/www + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chown -R www-data:www-data kanboard/data + + sudo rm kanboard-latest.zip + + + +Некоторые возможности Канборда требуют [запуска ежедневных фоновых задач](cronjob.markdown). + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/update.markdown b/doc/ru_RU/update.markdown new file mode 100644 index 00000000..7cfabdb0 --- /dev/null +++ b/doc/ru_RU/update.markdown @@ -0,0 +1,57 @@ +Обновление Канборд до новой версии +================================== + + +Обновление Канборда до новой версии бесшовное. Процесс сводится к тому, что надо просто скопировать каталог с данными из старой версии в новый Канборд. Канборд запустит миграцию баз данных автоматически. + + + +Важные замечания перед обновлением[¶](#important-things-to-do-before-updating "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------- + +- Перед обновлением, обязательно сделайте копию ваших данных со старой версии Канборда + +- Всегда следите за [историей изменений](https://github.com/fguillot/kanboard/blob/master/ChangeLog) для отслеживания критических изменений + +- Всегда закрывайте все пользовательские сессии (очищайте все сессии на сервере) + + +Обновление из архива (стабильная версия)[¶](#from-the-archive-stable-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------- + + + +1. Скачайте и распакуйте архив с новой версией + +2. Скопируйте содержимое каталога с данными старой версии во вновь распакованный каталог + +3. Скопируйте из старой версии Канборда `config.php`, если вы его создавали + +4. Скопируйте плагины, если есть + +5. Убедитесь, что директория `data` имеет права на запись от пользователя веб сервера + +6. Проверьте работу новой версии + +7. Удалите старую версию Канборда + + +Обновление из репозитория (разрабатываемая версия)[¶](#from-the-repository-development-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------------- + + + +1. `git pull` + +2. `composer install --no-dev` + +3. Выполните вход и проверьте, что все работает корректно + + +**Внимание**: Выполняя обновление из разрабатываемой версии, вы должны понимать, что это нестабильная версия и берете все риски по работе Канборд на себя. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/usage-examples.markdown b/doc/ru_RU/usage-examples.markdown new file mode 100644 index 00000000..d0d580e8 --- /dev/null +++ b/doc/ru_RU/usage-examples.markdown @@ -0,0 +1,193 @@ +Примеры использования +===================== + + + +Вы можете настроить вашу доску в соответсвии с вашими бизнес-процессами + + + +Разработка программного обеспечения[¶](#software-development "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------- + + + +- Заказ + + + +- Готов + + + +- В работе + + + +- Требуется утверждение + + + +- Утверждено + + + +- Развернуто в продакшн + + + +Отслеживание ошибок[¶](#bug-tracking "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + +- Сообщение + + + +- Подтверждено + + + +- В работе + + + +- Проверено + + + +- Исправлено + + + +Продажи[¶](#sales "Ссылка на этот заголовок") +--------------------------------------------- + + + +- Клиенты + + + +- Встречи + + + +- Предложения + + + +- Приобретение + + + +Эффективное управление бизнесом[¶](#lean-business-management "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------- + + + +- Идеи + + + +- События + + + +- Мероприятия + + + +- Анализы + + + +- Исполненно + + + +Подбор персонала[¶](#recruiting-process "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + +- Предложения о работе + + + +- Кандидаты + + + +- Телефонный отбор + + + +- Собеседование + + + +- Наем + + + +Онлайн магазин[¶](#online-shops "Ссылка на этот заголовок") +----------------------------------------------------------- + + + +- Заказы + + + +- Упаковка + + + +- Готов к отправке + + + +- Отправлен + + + +Производство[¶](#manufactory "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +- Заказы покупателей + + + +- Сборка + + + +- Проверка + + + +- Упаковка + + + +- Готово к отгрузке + + + +- Отправлен + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/user-management.markdown b/doc/ru_RU/user-management.markdown new file mode 100644 index 00000000..ce74b7f8 --- /dev/null +++ b/doc/ru_RU/user-management.markdown @@ -0,0 +1,89 @@ +Управление пользователями +========================= + + + +Создание нового пользователя[¶](#add-a-new-user "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + + +Только администратор может создавать нового пользователя. + + + +1. В выпадающем меню, в правом верхнем углу, выберите **Управление пользователями** + + + +2. Вверху имеются ссылки - **Новый локальный пользователь** и **Новый удаленный пользователь** + + + +3. При создании пользователя нужно заполнить форму и сохранить + + + + + +Рисунок. Форма создания нового пользователя. + + + +При создании **Локального пользователя** вы должны, как минимум, заполнить следующие поля: + + + +- **Имя пользователя**: это поле является уникальным идентификатором вашего пользователя (логин) + + + +- **Пароль**: Пароль пользователя должен иметь минимум 6 символов + + + +Для **удаленных пользователей** обязательно только **Имя пользователя**. + + + +Редактирование пользователей[¶](#edit-users "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + +После перехода в **Управление пользователями**, вам будет доступен список пользователей. Кликните на пользователя в столбце **Имя пользователя**. Далее, вам будет доступно редактирование настроек и профиля пользователя. + + + +- Если вы имеете права пользователя, то вы сможете только изменить ваш профиль + + + +- Для редактирования любого пользователя вам должны быть назначены права администратора + + + +Удаеление пользователей[¶](#remove-users "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +В списке пользователей выберите в колонке **Действия** в выпадающем меню **Удалить**. Эта ссылка доступна только для администраторов. + + + +Если вы удалите пользователя, то все задачи назначенные пользователю перестанут быть назначенными. + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/user-mentions.markdown b/doc/ru_RU/user-mentions.markdown new file mode 100644 index 00000000..766103e3 --- /dev/null +++ b/doc/ru_RU/user-mentions.markdown @@ -0,0 +1,49 @@ +Ссылка на пользователя +====================== + + + +В Канборде есть возможность посылать уведомления пользователю, если кто-то ссылается на него в тексте. + + + +Если вы хотите заострить внимание о ком-либо в комментарии или в задаче, то вы можете использовать символ @ и следом указать имя пользователя. Канборд автоматически предлагает список пользователей: + + + + + +Рисунок. Ссылка на пользователя. + + + +- В данный момент, добавлять ссылку на пользователя можно только в описании задачи и тексте комментария. + + + +- Ссылка на пользователя работает только в задачах и при создании комментария. + + + +- Для получения уведомления, пользователь, на которого ссылаются, должен быть участником проекта, в котором создается ссылка. + + + +- Если была создана ссылка на пользователя, то этот пользователь получит уведомление. + + + +- @username - выглядит как ссылка на публичный профиль пользователя. + + + +Уведомление посылаются пользователю в соответсвии с пользовательскими настройками: это может быть email, уведомление на веб странице или даже сообщение в Slack/Hipchat/Jabber, если вы установили соответсвующие плагины. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/user-types.markdown b/doc/ru_RU/user-types.markdown new file mode 100644 index 00000000..9afb58b8 --- /dev/null +++ b/doc/ru_RU/user-types.markdown @@ -0,0 +1,26 @@ +Типы пользователей +================== + + + +В Канборде могут быть два типа пользователей: + + + +| Тип | Описание | +|--------------|-------------------------------------------------------------| +| Локальный пользователь | Пароль пользователя хранится в базе данных Канборда| +| Удаленный пользователь | Учетные данные пользователя управляются (контролируются) другой системой (например, LDAP сервер). Другими словами, аутентификация пользователя происходит во внешней системе, не в Канборде.| + + + +Примеры удаленных пользователей: + +- LDAP пользователь + +- Аутентификация пользователя через реверс-прокси + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/vagrant.markdown b/doc/ru_RU/vagrant.markdown new file mode 100644 index 00000000..59e920cc --- /dev/null +++ b/doc/ru_RU/vagrant.markdown @@ -0,0 +1,51 @@ +Запуск Канборда с Vagrant +========================= + + + +Вы можете легко развернуть Канборд с Vagrant: + + + +- Склонируйте проект с репозитория git + + + +- Выполните `vagrant up` + + + +- Для входа в приложение используйте URL `http://localhost:8001/` + + + +Виртуальная машина построена на Ubuntu 14.04 с PHP 5.5. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/webhooks.markdown b/doc/ru_RU/webhooks.markdown new file mode 100644 index 00000000..c598abf9 --- /dev/null +++ b/doc/ru_RU/webhooks.markdown @@ -0,0 +1,536 @@ +Web Hooks +========= + + + +Webhooks служат для взаимодействия с внешними приложениями. Webhook посылает уведомление стороннему приложению о событиях, которые произошли в Канборд. + + + +- Webhooks могут быть использованы для создания задач вызовом простого URL (Вы можете сделать это и при помощи API) + + + +- Обращение к внешнему приложению может происходить автоматически, когда наступает какое-либо событие в Канборд (создана задача, обновлен комментарий и т.д.) + + + +Как написать webhook приемник во внешнем приложении?[¶](#how-to-write-a-web-hook-receiver "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------- + + + +Все внутренние события в Канборде могут быть посланы во внешний URL. + + + +- Webhook URL (url приемник внешнего приложения) может быть задан в **Настройки** -\> **Webhooks** -\> **Webhook URL** + + + +- Когда в Канборде происходит событие, Канборд обращается к указанному URL автоматически + + + +- Данные конвертируются в формат JSON и передаются с помощью POST HTTP запроса + + + +- Webhook ключ передается в составе запроса в виде строкового параметра. Таким образом, вы можете проверить, что запрос на самом деле пришел из Канборда. + + + +- **Созданный вами URL должен среагировать в течении 1 секунды**. Это желательно сделать потому, что запросы являются синхронными (ограничения языка PHP) и возможны тормоза в пользовательском интерфейсе, если скрипт будет слишком медленный! + + + +### Список поддерживаемых событий[¶](#list-of-supported-events "Ссылка на этот заголовок") + + + +- comment.create (комментарий.создать) + + + +- comment.update (комментарий.обновить) + + + +- file.create (файл.создать) + + + +- task.move.project (задача.переместить.проект) + + + +- task.move.column (задача.переместить.колонка) + + + +- task.move.position (задача.переместить.место) + + + +- task.move.swimlane (задача.переместить.дорожка) + + + +- task.update (задача.обновить) + + + +- task.create (задача.создать) + + + +- task.close (задача.закрыть) + + + +- task.open (задача.открыть) + + + +- task.assignee\_change (задача.назначить\_изменить) + + + +- subtask.update (подзадача.обновить) + + + +- subtask.create (подзадача.создать) + + + +### Пример HTTP запроса[¶](#example-of-http-request "Ссылка на этот заголовок") + + + + POST https://your_webhook_url/?token=WEBHOOK_TOKEN_HERE + + User-Agent: Kanboard Webhook + + Content-Type: application/json + + Connection: close + + + + { + + "event_name": "task.move.column", + + "event_data": { + + "task_id": "1", + + "project_id": "1", + + "position": 1, + + "column_id": "1", + + "swimlane_id": "0", + + "src_column_id": "2", + + "dst_column_id": "1", + + "date_moved": "1431991532", + + "recurrence_status": "0", + + "recurrence_trigger": "0" + + } + + } + + + +Функциональная часть всех событий имеет следующий формат: + + + + { + + "event_name": "model.event_name", + + "event_data": { + + "key1": "value1", + + "key2": "value2", + + ... + + } + + } + + + +Значения `event_data`{.docutils .literal} могут быть неупорядочены в событиях. + + + +### Пример функциональной части события[¶](#examples-of-event-payloads "Ссылка на этот заголовок") + + + +Создание задачи: + + + + { + + "event_name": "task.create", + + "event_data": { + + "title": "Demo", + + "description": "", + + "project_id": "1", + + "owner_id": "1", + + "category_id": 0, + + "swimlane_id": 0, + + "column_id": "2", + + "color_id": "yellow", + + "score": 0, + + "time_estimated": 0, + + "date_due": 0, + + "creator_id": 1, + + "date_creation": 1431991532, + + "date_modification": 1431991532, + + "date_moved": 1431991532, + + "position": 1, + + "task_id": 1 + + } + + } + + + +Изменение задачи: + + + + { + + "event_name": "task.update", + + "event_data": { + + "id": "1", + + "title": "Demo", + + "description": "", + + "date_creation": "1431991532", + + "color_id": "yellow", + + "project_id": "1", + + "column_id": "1", + + "owner_id": "1", + + "position": "1", + + "is_active": "1", + + "date_completed": null, + + "score": "0", + + "date_due": "0", + + "category_id": "2", + + "creator_id": "1", + + "date_modification": 1431991603, + + "reference": "", + + "date_started": 1431993600, + + "time_spent": 0, + + "time_estimated": 0, + + "swimlane_id": "0", + + "date_moved": "1431991572", + + "recurrence_status": "0", + + "recurrence_trigger": "0", + + "recurrence_factor": "0", + + "recurrence_timeframe": "0", + + "recurrence_basedate": "0", + + "recurrence_parent": null, + + "recurrence_child": null, + + "task_id": "1", + + "changes": { + + "category_id": "2" + + } + + } + + } + + + +События изменеия задачи имеют поле `changes`{.docutils .literal}, которое содержит обновленные значения. + + + +Перемещение задачи в другую колонку: + + + + { + + "event_name": "task.move.column", + + "event_data": { + + "task_id": "1", + + "project_id": "1", + + "position": 1, + + "column_id": "1", + + "swimlane_id": "0", + + "src_column_id": "2", + + "dst_column_id": "1", + + "date_moved": "1431991532", + + "recurrence_status": "0", + + "recurrence_trigger": "0" + + } + + } + + + +Перемещение задачи в другое место: + + + + { + + "event_name": "task.move.position", + + "event_data": { + + "task_id": "2", + + "project_id": "1", + + "position": 1, + + "column_id": "1", + + "swimlane_id": "0", + + "src_column_id": "1", + + "dst_column_id": "1", + + "date_moved": "1431996905", + + "recurrence_status": "0", + + "recurrence_trigger": "0" + + } + + } + + + +Создание комментария: + + + + { + + "event_name": "comment.create", + + "event_data": { + + "id": 1, + + "task_id": "1", + + "user_id": "1", + + "comment": "test", + + "date_creation": 1431991615 + + } + + } + + + +Изменение комментария: + + + + { + + "event_name": "comment.update", + + "event_data": { + + "id": "1", + + "task_id": "1", + + "user_id": "1", + + "comment": "test edit" + + } + + } + + + +Создание подзадачи: + + + + { + + "event_name": "subtask.create", + + "event_data": { + + "id": 3, + + "task_id": "1", + + "title": "Test", + + "user_id": "1", + + "time_estimated": "2", + + "position": 3 + + } + + } + + + +Изменение подзадачи: + + + + { + + "event_name": "subtask.update", + + "event_data": { + + "id": "1", + + "status": 1, + + "task_id": "1" + + } + + } + + + +Загрузка файла: + + + + { + + "event_name": "file.create", + + "event_data": { + + "task_id": "1", + + "name": "test.png" + + } + + } + + + +Создан снимок экрана: + + + + { + + "event_name": "file.create", + + "event_data": { + + "task_id": "2", + + "name": "Screenshot taken May 19, 2015 at 10:56 AM" + + } + + } + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/what-is-kanban.markdown b/doc/ru_RU/what-is-kanban.markdown new file mode 100644 index 00000000..46196bbb --- /dev/null +++ b/doc/ru_RU/what-is-kanban.markdown @@ -0,0 +1,80 @@ +Что такое Kanban? +================= + + + +Kanban - методология, которая первоначально применила компания Toyota для увеличения производительности. Описание в википедии - [Канбан доска](https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D0%BD%D0%B1%D0%B0%D0%BD-%D0%B4%D0%BE%D1%81%D0%BA%D0%B0) + + + +Смысл Kanban заключается в следующем: + + + +- Визуализация рабочих процессов + + + +- Уменьшение времени для достижения цели + + + +Визуализация рабочих процессов[¶](#visualize-your-workflow "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + + + +- Ваш рабочий процесс отображается на доске и вы ясно видете картину вашего проекта + + + +- Каждая колонка представляет шаг вашего рабочего процесса + + + +Сосредоточте внимание и избегайте многозадачности[¶](#bring-focus-and-avoid-multitasking "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------- + + + +- Каждая фаза может иметь работу ограниченную временем + + + +- Уменьшайте объем для определения узких мест + + + +- Ограничте количество одновременно выполняемых задач + + + +Подсчет производительности и улучшений[¶](#measure-performance-and-improvement "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------- + + + +Kanban использует время выполнения (lead time) и время цикла (cycle time) для подсчета производительности: + + + +- **Время выполнения**: Время между созданием задачи и ее завершением + + + +- **Время цикла**: Время между началом выполнения задачи и ее завершением + + + +Например, вам заложено время выполнения - 100 дней, а затратили на выполнение задачи (время цикла) всего 1 час. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/windows-apache-installation.markdown b/doc/ru_RU/windows-apache-installation.markdown new file mode 100644 index 00000000..7f181e10 --- /dev/null +++ b/doc/ru_RU/windows-apache-installation.markdown @@ -0,0 +1,253 @@ +Установка Канборд на Windows Server и Apache +============================================ + + + +Это руководство поможет вам шаг за шагом установить Канборд на Windows Server с Apache и PHP + + + +**Внимание**: Если у вас 64 разрядная платформа, то вам нужно выбрать “x64”, и выберите “x86” для 32 разрядной операционной системы. + + + +Установка распространяемого пакета Visual C++[¶](#visual-c-redistributable-installation "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------- + + + +PHP и Apache скомпилированы с Visual Studio, поэтому вам нужно установить эту библиотеку, если вы не сделали это ранее. + + + +1. Скачайте библиотеку с [официального вебсайта Microsoft](http://www.microsoft.com/en-us/download/details.aspx?id=30679) + + + +2. Запустите установку `vcredist_x64.exe` или `vcredist_x86.exe`, в соответствии с вашей платформой + + + +Установка Apache[¶](#apache-installation "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +1. Скачайте исходники Apache с [Apache Lounge](http://www.apachelounge.com/download/) + + + +2. Разархивируйте Apache24 в каталог `C:\Apache24` + + + +### Назначение имени сервера[¶](#define-the-server-name "Ссылка на этот заголовок") + + + +Откройте файл `C:\Apache24\conf\httpd.conf` и добавьте директиву: + + + + ServerName localhost + + + +### Установка сервиса Apache[¶](#install-the-apache-service "Ссылка на этот заголовок") + + + +Откройте консоль (`cmd.exe`), перейдите в каталог `C:\Apache24\bin` и установите сервис Apache: + + + + cd C:\Apache24\bin + + + + # Install the windows service + + httpd.exe -k install + + + +### Установка ApacheMonitor[¶](#install-apachemonitor "Ссылка на этот заголовок") + + + +- Выполните `C:\Apache24\bin\ApacheMonitor.exe` и добавьте его в автозагрузку. + + + +- Теперь во всплывающем меню, при нажатии правой кнопки мыши на иконке, нажмите запустить Apache + + + +### Проверка работы Apache[¶](#check-the-apache-installation "Ссылка на этот заголовок") + + + +В браузере откройте <http://localhost/>. Вы должны увидеть пустую страницу и текст “It works!”. + + + +Установка PHP[¶](#php-installation "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +1. Скачайте последнюю стабильную версию PHP с [официального сайта PHP](http://windows.php.net/download/), выберите версию **Thread Safe** и используйте соответствующую разрядность: x86 or x64. + + + +2. Разархивируйте файлы в `C:\php` + + + +3. Перейдите в каталог PHP (`C:\php`) и переименуйе файл `php.ini-production` в `php.ini` + + + +Отредактируйте `php.ini`: + + + +Раскоментируйте директорию расширений: + + + + extension_dir = "C:/php/ext" + + + +Раскоментируйте следующие модули PHP: + + + + extension=php_gd2.dll + + extension=php_ldap.dll + + extension=php_mbstring.dll + + extension=php_openssl.dll + + extension=php_pdo_sqlite.dll + + + +Установите часовой пояс: + + + + date.timezone = America/Montreal + + + +Список всех поддерживаемых часовых поясов можно посмотреть в [документации PHP](http://php.net/manual/en/timezones.america.php). + + + +Загрузка модулей PHP для Apache: + + + +Добавьте следующие строки конфигурации в файл `C:\Apache24\conf\httpd.conf`: + + + + LoadModule php5_module "c:/php/php5apache2_4.dll" + + AddHandler application/x-httpd-php .php + + + + # configure the path to php.ini + + PHPIniDir "C:/php" + + + + # change this directive + + DirectoryIndex index.php index.html + + + +Перезапустите Apache. + + + +Проверка работы PHP: + + + +Создайте файл `phpinfo.php` в каталоге `C:\Apache24\htdocs`: + + + + <?php + + + + phpinfo(); + + + + ?> + + + +Откройте в браузере [http://localhost/phpinfo.php](http://localhost/phpinfo.php) и вы должны увидеть информацию о PHP. + + + +Устновка Канборд[¶](#kanboard-installation "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + +- [Скачайте zip файл](https://kanboard.net/downloads) + + + +- Разархивируйте архив в `C:\Apache24\htdocs\kanboard` + + + +- Откройте в браузере <http://localhost/kanboard/>. Ура. Теперь вы можете работать в Канборд. Все легко и просто. + + + +- Учетная запись и пароль по умолчанию - **admin/admin** + + + +Протестировано на[¶](#tested-configuration "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + +- Windows 2008 R2 / Apache 2.4.12 / PHP 5.6.8 + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Некоторые функции Канборда требуют выполнять [запуск ежедневных фоновых задач](cronjob.markdown). + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/windows-iis-installation.markdown b/doc/ru_RU/windows-iis-installation.markdown new file mode 100644 index 00000000..0aabca6a --- /dev/null +++ b/doc/ru_RU/windows-iis-installation.markdown @@ -0,0 +1,150 @@ +Инсталяция Kanboard на Windows 2008/2012 с IIS +============================================== + + + +Это пошаговое руководство поможет вам установить Канборд на Windows Server с IIS и PHP. + + + +Установка PHP[¶](#php-installation "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +- Установите IIS на ваш Windows сервер (Добавьте новую роль и не забудьте включить CGI/FastCGI) + + + +- При инсталяции PHP можете использовать следующую официальную документацию: + + + + - [Microsoft IIS 5.1 and IIS 6.0](http://php.net/manual/en/install.windows.iis6.php) + + - [Microsoft IIS 7.0 and later](http://php.net/manual/en/install.windows.iis7.php) + + - [PHP for Windows is available here](http://windows.php.net/download/) + + + +Отредактируйте `php.ini`, раскоментируйте эти PHP модули: + + + + extension=php_gd2.dll + + extension=php_ldap.dll + + extension=php_mbstring.dll + + extension=php_openssl.dll + + extension=php_pdo_sqlite.dll + + + +Установите часовой пояс + + + + date.timezone = America/Montreal + + + +Список поддерживаемых часовых поясов можно посмотреть в [документации PHP](http://php.net/manual/en/timezones.america.php). + + + +Проверьте, что PHP работает корректно: + + + +Перейдите в корневой каталог IIS `C:\inetpub\wwwroot` и создайте файл `phpinfo.php`, со следующим содержимым: + + + + <?php + + + + phpinfo(); + + + + ?> + + + +В браузере откройте страницу `http://localhost/phpinfo.php` и вы должны увидеть текущие настройки PHP. Если вы видите ошибку 500, значит что-то сделано неправильно при установке. + + + +Примечание: + + + +- Если вы используете PHP \< 5.4, то необходимо включить короткие теги (short tags) в php.ini + + + +- Не забудьте включить необходимые php расширения, упомянутые выше + + + +- Если вы наблюдаете ошибку “the library MSVCP110.dll is missing”, то возможно вам нужно скачать распространяемый пакет Visual C++ для Visual Studio с сайта Microsoft. + + + +Установка Канборд[¶](#kanboard-installation "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + +- Скачайте zip файл + + + +- Распакуйте архив в `C:\inetpub\wwwroot\kanboard` (например) + + + +- Убедитесь, что у пользователя вебсервера IIS имеется доступ на запись на директорию `data` + + + +- Откройте веб браузер и используйте Kanboard <http://localhost/kanboard/> + + + +- Пользователь и пароль по умолчанию - **admin/admin** + + + +Работа Канборд тестировалось на[¶](#tested-configurations "Ссылка на этот заголовок") +------------------------------------------------------------------------------------- + + + +- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16 + +- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29 + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Некоторые возможности Канборда требуют [запуск выполнения ежедневных фоновых задач](cronjob.markdown). + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/update.markdown b/doc/update.markdown index 44f81ff0..4aa59fff 100644 --- a/doc/update.markdown +++ b/doc/update.markdown @@ -10,7 +10,7 @@ Important things to do before updating - **Always make a backup of your data before upgrading** - Check that your backup is valid -- Always read the [change log](https://github.com/fguillot/kanboard/blob/master/ChangeLog) to check for breaking changes +- Always read the [change log](https://github.com/kanboard/kanboard/blob/master/ChangeLog) to check for breaking changes - Always close all user sessions (flush all sessions on the server) From the archive (stable version) diff --git a/doc/web.config b/doc/web.config new file mode 100644 index 00000000..1461fe2d --- /dev/null +++ b/doc/web.config @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<configuration> + <system.webServer> + <defaultDocument> + <files> + <clear /> + <add value="index.php" /> + </files> + </defaultDocument> + <rewrite> + <rules> + <rule name="Kanboard URL Rewrite" stopProcessing="true"> + <match url="^(.*)$" ignoreCase="false" /> + <conditions logicalGrouping="MatchAll"> + <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" appendQueryString="true" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> diff --git a/tests/units/Action/BaseActionTest.php b/tests/units/Action/BaseActionTest.php index 1d50c70e..feeba3f9 100644 --- a/tests/units/Action/BaseActionTest.php +++ b/tests/units/Action/BaseActionTest.php @@ -23,7 +23,7 @@ class DummyAction extends Kanboard\Action\Base public function getEventRequiredParameters() { - return array('p1', 'p2'); + return array('p1', 'p2', 'p3' => array('p4')); } public function doAction(array $data) @@ -60,7 +60,7 @@ class BaseActionTest extends Base public function testGetEventRequiredParameters() { $dummyAction = new DummyAction($this->container); - $this->assertEquals(array('p1', 'p2'), $dummyAction->getEventRequiredParameters()); + $this->assertEquals(array('p1', 'p2', 'p3' => array('p4')), $dummyAction->getEventRequiredParameters()); } public function testGetCompatibleEvents() @@ -113,7 +113,7 @@ class BaseActionTest extends Base $dummyAction = new DummyAction($this->container); $dummyAction->setProjectId(1234); - $this->assertTrue($dummyAction->hasRequiredParameters(array('p1' => 12, 'p2' => 34))); + $this->assertTrue($dummyAction->hasRequiredParameters(array('p1' => 12, 'p2' => 34, 'p3' => array('p4' => 'foobar')))); $this->assertFalse($dummyAction->hasRequiredParameters(array('p1' => 12))); $this->assertFalse($dummyAction->hasRequiredParameters(array())); } @@ -125,7 +125,7 @@ class BaseActionTest extends Base $dummyAction->addEvent('my.event', 'My Event Overrided'); $events = $dummyAction->getEvents(); - $this->assertcount(2, $events); + $this->assertCount(2, $events); $this->assertEquals(array('my.event', 'foobar'), $events); } @@ -136,7 +136,7 @@ class BaseActionTest extends Base $dummyAction->setParam('p1', 'something'); $dummyAction->addEvent('foobar', 'FooBar'); - $event = new GenericEvent(array('project_id' => 1234, 'p1' => 'something', 'p2' => 'abc')); + $event = new GenericEvent(array('project_id' => 1234, 'p1' => 'something', 'p2' => 'abc', 'p3' => array('p4' => 'a'))); $this->assertTrue($dummyAction->execute($event, 'foobar')); $this->assertFalse($dummyAction->execute($event, 'foobar')); diff --git a/tests/units/Action/CommentCreationMoveTaskColumnTest.php b/tests/units/Action/CommentCreationMoveTaskColumnTest.php index 5eaf515e..b3d21287 100644 --- a/tests/units/Action/CommentCreationMoveTaskColumnTest.php +++ b/tests/units/Action/CommentCreationMoveTaskColumnTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\CommentModel; @@ -22,7 +22,7 @@ class CommentCreationMoveTaskColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array('task' => array('project_id' => 1, 'column_id' => 2), 'task_id' => 1)); $action = new CommentCreationMoveTaskColumn($this->container); $action->setProjectId(1); @@ -45,7 +45,7 @@ class CommentCreationMoveTaskColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array('task' => array('project_id' => 1, 'column_id' => 3), 'task_id' => 1)); $action = new CommentCreationMoveTaskColumn($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskAssignCategoryColorTest.php b/tests/units/Action/TaskAssignCategoryColorTest.php index 09c08264..5a0f7d03 100644 --- a/tests/units/Action/TaskAssignCategoryColorTest.php +++ b/tests/units/Action/TaskAssignCategoryColorTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\CategoryModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; @@ -23,7 +23,13 @@ class TaskAssignCategoryColorTest extends Base $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'color_id' => 'red')); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'color_id' => 'red', + ) + )); $action = new TaskAssignCategoryColor($this->container); $action->setProjectId(1); @@ -47,7 +53,13 @@ class TaskAssignCategoryColorTest extends Base $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'color_id' => 'blue')); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'color_id' => 'blue', + ) + )); $action = new TaskAssignCategoryColor($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskAssignCategoryLinkTest.php b/tests/units/Action/TaskAssignCategoryLinkTest.php index 712c3c02..d7e68f72 100644 --- a/tests/units/Action/TaskAssignCategoryLinkTest.php +++ b/tests/units/Action/TaskAssignCategoryLinkTest.php @@ -14,19 +14,19 @@ class TaskAssignCategoryLinkTest extends Base { public function testAssignCategory() { - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - $c = new CategoryModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); $action = new TaskAssignCategoryLink($this->container); $action->setProjectId(1); $action->setParam('category_id', 1); $action->setParam('link_id', 2); - $this->assertEquals(1, $p->create(array('name' => 'P1'))); - $this->assertEquals(1, $c->create(array('name' => 'C1', 'project_id' => 1))); - $this->assertEquals(1, $tc->create(array('title' => 'T1', 'project_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1))); $event = new TaskLinkEvent(array( 'project_id' => 1, @@ -37,25 +37,24 @@ class TaskAssignCategoryLinkTest extends Base $this->assertTrue($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['category_id']); } public function testWhenLinkDontMatch() { - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - $c = new CategoryModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); $action = new TaskAssignCategoryLink($this->container); $action->setProjectId(1); $action->setParam('category_id', 1); $action->setParam('link_id', 1); - $this->assertEquals(1, $p->create(array('name' => 'P1'))); - $this->assertEquals(1, $c->create(array('name' => 'C1', 'project_id' => 1))); - $this->assertEquals(1, $tc->create(array('title' => 'T1', 'project_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1))); $event = new TaskLinkEvent(array( 'project_id' => 1, @@ -69,19 +68,19 @@ class TaskAssignCategoryLinkTest extends Base public function testThatExistingCategoryWillNotChange() { - $tc = new TaskCreationModel($this->container); - $p = new ProjectModel($this->container); - $c = new CategoryModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); $action = new TaskAssignCategoryLink($this->container); $action->setProjectId(1); $action->setParam('category_id', 2); $action->setParam('link_id', 2); - $this->assertEquals(1, $p->create(array('name' => 'P1'))); - $this->assertEquals(1, $c->create(array('name' => 'C1', 'project_id' => 1))); - $this->assertEquals(2, $c->create(array('name' => 'C2', 'project_id' => 1))); - $this->assertEquals(1, $tc->create(array('title' => 'T1', 'project_id' => 1, 'category_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'C2', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1, 'category_id' => 1))); $event = new TaskLinkEvent(array( 'project_id' => 1, diff --git a/tests/units/Action/TaskAssignColorCategoryTest.php b/tests/units/Action/TaskAssignColorCategoryTest.php index 6502035f..16ad1290 100644 --- a/tests/units/Action/TaskAssignColorCategoryTest.php +++ b/tests/units/Action/TaskAssignColorCategoryTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\CategoryModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; @@ -23,7 +23,13 @@ class TaskAssignColorCategoryTest extends Base $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'category_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'category_id' => 1, + ) + )); $action = new TaskAssignColorCategory($this->container); $action->setProjectId(1); @@ -45,7 +51,13 @@ class TaskAssignColorCategoryTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'category_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'category_id' => 2, + ) + )); $action = new TaskAssignColorCategory($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskAssignColorColumnTest.php b/tests/units/Action/TaskAssignColorColumnTest.php index d4ba8e01..ccfb9e88 100644 --- a/tests/units/Action/TaskAssignColorColumnTest.php +++ b/tests/units/Action/TaskAssignColorColumnTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -20,7 +20,13 @@ class TaskAssignColorColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskAssignColorColumn($this->container); $action->setProjectId(1); @@ -42,7 +48,13 @@ class TaskAssignColorColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskAssignColorColumn($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskAssignColorPriorityTest.php b/tests/units/Action/TaskAssignColorPriorityTest.php index 2fce8e66..0ea874cd 100644 --- a/tests/units/Action/TaskAssignColorPriorityTest.php +++ b/tests/units/Action/TaskAssignColorPriorityTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\CategoryModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; @@ -23,7 +23,13 @@ class TaskAssignColorPriorityTest extends Base $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'priority' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'priority' => 1, + ) + )); $action = new TaskAssignColorPriority($this->container); $action->setProjectId(1); @@ -45,7 +51,13 @@ class TaskAssignColorPriorityTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'priority' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'priority' => 2, + ) + )); $action = new TaskAssignColorPriority($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskAssignColorUserTest.php b/tests/units/Action/TaskAssignColorUserTest.php index 370f9070..45faa3ff 100644 --- a/tests/units/Action/TaskAssignColorUserTest.php +++ b/tests/units/Action/TaskAssignColorUserTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -20,7 +20,13 @@ class TaskAssignColorUserTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'owner_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'owner_id' => 1, + ) + )); $action = new TaskAssignColorUser($this->container); $action->setProjectId(1); @@ -42,7 +48,13 @@ class TaskAssignColorUserTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'owner_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'owner_id' => 2, + ) + )); $action = new TaskAssignColorUser($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskAssignCurrentUserColumnTest.php b/tests/units/Action/TaskAssignCurrentUserColumnTest.php index 6fdbda63..3b64d718 100644 --- a/tests/units/Action/TaskAssignCurrentUserColumnTest.php +++ b/tests/units/Action/TaskAssignCurrentUserColumnTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -22,7 +22,13 @@ class TaskAssignCurrentUserColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskAssignCurrentUserColumn($this->container); $action->setProjectId(1); @@ -45,7 +51,13 @@ class TaskAssignCurrentUserColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskAssignCurrentUserColumn($this->container); $action->setProjectId(1); @@ -62,7 +74,13 @@ class TaskAssignCurrentUserColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskAssignCurrentUserColumn($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskAssignSpecificUserTest.php b/tests/units/Action/TaskAssignSpecificUserTest.php index 78ec314f..0e63fc13 100644 --- a/tests/units/Action/TaskAssignSpecificUserTest.php +++ b/tests/units/Action/TaskAssignSpecificUserTest.php @@ -3,6 +3,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -20,7 +21,13 @@ class TaskAssignSpecificUserTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'owner_id' => 0))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskAssignSpecificUser($this->container); $action->setProjectId(1); @@ -42,7 +49,13 @@ class TaskAssignSpecificUserTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskAssignSpecificUser($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskCloseColumnTest.php b/tests/units/Action/TaskCloseColumnTest.php index f9a938f0..7afb0478 100644 --- a/tests/units/Action/TaskCloseColumnTest.php +++ b/tests/units/Action/TaskCloseColumnTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -20,7 +20,13 @@ class TaskCloseColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskCloseColumn($this->container); $action->setProjectId(1); @@ -41,7 +47,13 @@ class TaskCloseColumnTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskCloseColumn($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskCloseNoActivityColumnTest.php b/tests/units/Action/TaskCloseNoActivityColumnTest.php new file mode 100644 index 00000000..243d3359 --- /dev/null +++ b/tests/units/Action/TaskCloseNoActivityColumnTest.php @@ -0,0 +1,49 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Action\TaskCloseNoActivityColumn; +use Kanboard\Event\TaskListEvent; +use Kanboard\Model\TaskCreationModel; +use Kanboard\Model\TaskFinderModel; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskModel; + +class TaskCloseNoActivityColumnTest extends Base +{ + public function testClose() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2))); + + $this->container['db']->table(TaskModel::TABLE)->in('id', array(1, 3))->update(array('date_modification' => strtotime('-10days'))); + + $tasks = $taskFinderModel->getAll(1); + $event = new TaskListEvent(array('tasks' => $tasks, 'project_id' => 1)); + + $action = new TaskCloseNoActivityColumn($this->container); + $action->setProjectId(1); + $action->setParam('duration', 2); + $action->setParam('column_id', 2); + + $this->assertTrue($action->execute($event, TaskModel::EVENT_DAILY_CRONJOB)); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['is_active']); + + $task = $taskFinderModel->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['is_active']); + + $task = $taskFinderModel->getById(3); + $this->assertNotEmpty($task); + $this->assertEquals(0, $task['is_active']); + } +} diff --git a/tests/units/Action/TaskCloseTest.php b/tests/units/Action/TaskCloseTest.php index 3df10cb8..589ef133 100644 --- a/tests/units/Action/TaskCloseTest.php +++ b/tests/units/Action/TaskCloseTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -19,7 +19,12 @@ class TaskCloseTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + ) + )); $action = new TaskClose($this->container); $action->setProjectId(1); @@ -40,7 +45,11 @@ class TaskCloseTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1)); + $event = new TaskEvent(array( + 'task' => array( + 'project_id' => 1, + ) + )); $action = new TaskClose($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskDuplicateAnotherProjectTest.php b/tests/units/Action/TaskDuplicateAnotherProjectTest.php index 98ff187f..5cd0c977 100644 --- a/tests/units/Action/TaskDuplicateAnotherProjectTest.php +++ b/tests/units/Action/TaskDuplicateAnotherProjectTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\TaskCreationModel; @@ -21,7 +21,13 @@ class TaskDuplicateAnotherProjectTest extends Base $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskDuplicateAnotherProject($this->container); $action->setProjectId(1); @@ -43,7 +49,13 @@ class TaskDuplicateAnotherProjectTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskDuplicateAnotherProject($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskEmailTest.php b/tests/units/Action/TaskEmailTest.php index df71aaf8..421c89ca 100644 --- a/tests/units/Action/TaskEmailTest.php +++ b/tests/units/Action/TaskEmailTest.php @@ -2,7 +2,8 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; +use Kanboard\Model\TaskFinderModel; use Kanboard\Model\TaskModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\ProjectModel; @@ -16,16 +17,20 @@ class TaskEmailTest extends Base $userModel = new UserModel($this->container); $projectModel = new ProjectModel($this->container); $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); $this->assertTrue($userModel->update(array('id' => 1, 'email' => 'admin@localhost'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => $taskFinderModel->getDetails(1) + )); $action = new TaskEmail($this->container); $action->setProjectId(1); - $action->setParam('column_id', 2); + $action->setParam('column_id', 1); $action->setParam('user_id', 1); $action->setParam('subject', 'My email subject'); @@ -47,7 +52,13 @@ class TaskEmailTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskEmail($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskMoveAnotherProjectTest.php b/tests/units/Action/TaskMoveAnotherProjectTest.php index d36df47b..a41fd03f 100644 --- a/tests/units/Action/TaskMoveAnotherProjectTest.php +++ b/tests/units/Action/TaskMoveAnotherProjectTest.php @@ -3,6 +3,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\TaskCreationModel; @@ -21,7 +22,13 @@ class TaskMoveAnotherProjectTest extends Base $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskMoveAnotherProject($this->container); $action->setProjectId(1); @@ -44,7 +51,13 @@ class TaskMoveAnotherProjectTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskMoveAnotherProject($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskMoveColumnAssignedTest.php b/tests/units/Action/TaskMoveColumnAssignedTest.php index f8982969..aa9d3592 100644 --- a/tests/units/Action/TaskMoveColumnAssignedTest.php +++ b/tests/units/Action/TaskMoveColumnAssignedTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\TaskCreationModel; @@ -19,9 +19,12 @@ class TaskMoveColumnAssignedTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'owner_id' => 1))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 1, 'owner_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => $taskFinderModel->getDetails(1), + )); $action = new TaskMoveColumnAssigned($this->container); $action->setProjectId(1); @@ -43,7 +46,14 @@ class TaskMoveColumnAssignedTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3, 'owner_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + 'owner_id' => 1, + ) + )); $action = new TaskMoveColumnAssigned($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskMoveColumnCategoryChangeTest.php b/tests/units/Action/TaskMoveColumnCategoryChangeTest.php index c42383f8..7e0856df 100644 --- a/tests/units/Action/TaskMoveColumnCategoryChangeTest.php +++ b/tests/units/Action/TaskMoveColumnCategoryChangeTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\CategoryModel; use Kanboard\Model\TaskModel; use Kanboard\Model\TaskFinderModel; @@ -24,7 +24,16 @@ class TaskMoveColumnCategoryChangeTest extends Base $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 1, 'category_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 1, + 'category_id' => 1, + 'position' => 1, + 'swimlane_id' => 0, + ) + )); $action = new TaskMoveColumnCategoryChange($this->container); $action->setProjectId(1); @@ -50,7 +59,14 @@ class TaskMoveColumnCategoryChangeTest extends Base $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2, 'category_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + 'category_id' => 1, + ) + )); $action = new TaskMoveColumnCategoryChange($this->container); $action->setProjectId(1); @@ -72,7 +88,14 @@ class TaskMoveColumnCategoryChangeTest extends Base $this->assertEquals(2, $categoryModel->create(array('name' => 'c2', 'project_id' => 1))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 1, 'category_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 1, + 'category_id' => 2, + ) + )); $action = new TaskMoveColumnCategoryChange($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskMoveColumnUnAssignedTest.php b/tests/units/Action/TaskMoveColumnUnAssignedTest.php index befae36b..b45dec08 100644 --- a/tests/units/Action/TaskMoveColumnUnAssignedTest.php +++ b/tests/units/Action/TaskMoveColumnUnAssignedTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\TaskCreationModel; @@ -21,7 +21,16 @@ class TaskMoveColumnUnAssignedTest extends Base $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 1, 'owner_id' => 0)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 1, + 'owner_id' => 0, + 'position' => 1, + 'swimlane_id' => 0, + ) + )); $action = new TaskMoveColumnUnAssigned($this->container); $action->setProjectId(1); @@ -43,7 +52,14 @@ class TaskMoveColumnUnAssignedTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2, 'owner_id' => 0)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + 'owner_id' => 0, + ) + )); $action = new TaskMoveColumnUnAssigned($this->container); $action->setProjectId(1); @@ -60,7 +76,14 @@ class TaskMoveColumnUnAssignedTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 1, 'owner_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 1, + 'owner_id' => 1, + ) + )); $action = new TaskMoveColumnUnAssigned($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskOpenTest.php b/tests/units/Action/TaskOpenTest.php index 1018e2ea..825c6ac9 100644 --- a/tests/units/Action/TaskOpenTest.php +++ b/tests/units/Action/TaskOpenTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -19,7 +19,12 @@ class TaskOpenTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'is_active' => 0))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + ) + )); $action = new TaskOpen($this->container); $action->setProjectId(1); @@ -40,7 +45,11 @@ class TaskOpenTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1)); + $event = new TaskEvent(array( + 'task' => array( + 'project_id' => 1, + ) + )); $action = new TaskOpen($this->container); $action->setProjectId(1); diff --git a/tests/units/Action/TaskUpdateStartDateTest.php b/tests/units/Action/TaskUpdateStartDateTest.php index ddd9eafd..8d609b3e 100644 --- a/tests/units/Action/TaskUpdateStartDateTest.php +++ b/tests/units/Action/TaskUpdateStartDateTest.php @@ -3,6 +3,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Event\GenericEvent; +use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -20,7 +21,13 @@ class TaskUpdateStartDateTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 2)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 2, + ) + )); $action = new TaskUpdateStartDate($this->container); $action->setProjectId(1); @@ -41,7 +48,13 @@ class TaskUpdateStartDateTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'column_id' => 3)); + $event = new TaskEvent(array( + 'task_id' => 1, + 'task' => array( + 'project_id' => 1, + 'column_id' => 3, + ) + )); $action = new TaskUpdateStartDate($this->container); $action->setProjectId(1); diff --git a/tests/units/Auth/TotpAuthTest.php b/tests/units/Auth/TotpAuthTest.php index c8dcfb28..3a82c01c 100644 --- a/tests/units/Auth/TotpAuthTest.php +++ b/tests/units/Auth/TotpAuthTest.php @@ -35,16 +35,17 @@ class TotpAuthTest extends Base public function testGetUrl() { $provider = new TotpAuth($this->container); + $this->assertEmpty($provider->getQrCodeUrl('me')); $this->assertEmpty($provider->getKeyUrl('me')); $provider->setSecret('mySecret'); $this->assertEquals( - 'https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=M|0&chl=otpauth%3A%2F%2Ftotp%2Fme%3Fsecret%3DmySecret', + 'https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=M|0&chl=otpauth%3A%2F%2Ftotp%2Fme%3Fsecret%3DmySecret%26issuer%3DKanboard', $provider->getQrCodeUrl('me') ); - $this->assertEquals('otpauth://totp/me?secret=mySecret', $provider->getKeyUrl('me')); + $this->assertEquals('otpauth://totp/me?secret=mySecret&issuer=Kanboard', $provider->getKeyUrl('me')); } public function testAuthentication() diff --git a/tests/units/Base.php b/tests/units/Base.php index 9dbfb280..c471ee31 100644 --- a/tests/units/Base.php +++ b/tests/units/Base.php @@ -41,6 +41,7 @@ abstract class Base extends PHPUnit_Framework_TestCase $this->container->register(new Kanboard\ServiceProvider\RouteProvider()); $this->container->register(new Kanboard\ServiceProvider\AvatarProvider()); $this->container->register(new Kanboard\ServiceProvider\FilterProvider()); + $this->container->register(new Kanboard\ServiceProvider\JobProvider()); $this->container->register(new Kanboard\ServiceProvider\QueueProvider()); $this->container['dispatcher'] = new TraceableEventDispatcher( diff --git a/tests/units/Core/Filter/LexerBuilderTest.php b/tests/units/Core/Filter/LexerBuilderTest.php index 23726f32..31e237dc 100644 --- a/tests/units/Core/Filter/LexerBuilderTest.php +++ b/tests/units/Core/Filter/LexerBuilderTest.php @@ -8,6 +8,7 @@ use Kanboard\Filter\TaskTitleFilter; use Kanboard\Model\ProjectModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; +use Kanboard\Model\UserModel; class LexerBuilderTest extends Base { @@ -103,4 +104,49 @@ class LexerBuilderTest extends Base $this->assertFalse($builder === $clone); $this->assertFalse($builder->build('test')->getQuery() === $clone->build('test')->getQuery()); } + + public function testBuilderWithMixedCaseSearchAttribute() + { + $project = new ProjectModel($this->container); + $taskCreation = new TaskCreationModel($this->container); + $taskFinder = new TaskFinderModel($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $project->create(array('name' => 'Project'))); + $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'Test'))); + + $builder = new LexerBuilder(); + $builder->withFilter(new TaskAssigneeFilter()); + $builder->withFilter(new TaskTitleFilter(), true); + $builder->withQuery($query); + $tasks = $builder->build('AsSignEe:nobody')->toArray(); + + $this->assertCount(1, $tasks); + $this->assertEquals('Test', $tasks[0]['title']); + } + + public function testWithOrCriteria() + { + $taskFinder = new TaskFinderModel($this->container); + $taskCreation = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $userModel = new UserModel($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test 1', 'project_id' => 1, 'owner_id' => 2))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test 2', 'project_id' => 1, 'owner_id' => 1))); + $this->assertEquals(3, $taskCreation->create(array('title' => 'Test 3', 'project_id' => 1, 'owner_id' => 0))); + + $builder = new LexerBuilder(); + $builder->withFilter(new TaskAssigneeFilter()); + $builder->withFilter(new TaskTitleFilter(), true); + $builder->withQuery($query); + $tasks = $builder->build('assignee:admin assignee:foobar')->toArray(); + + $this->assertCount(2, $tasks); + $this->assertEquals('Test 1', $tasks[0]['title']); + $this->assertEquals('Test 2', $tasks[1]['title']); + } } diff --git a/tests/units/Core/Filter/OrCriteriaTest.php b/tests/units/Core/Filter/OrCriteriaTest.php index a46726c3..cf520f36 100644 --- a/tests/units/Core/Filter/OrCriteriaTest.php +++ b/tests/units/Core/Filter/OrCriteriaTest.php @@ -22,8 +22,9 @@ class OrCriteriaTest extends Base $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar'))); $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); - $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 2))); - $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test 1', 'project_id' => 1, 'owner_id' => 2))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test 2', 'project_id' => 1, 'owner_id' => 1))); + $this->assertEquals(3, $taskCreation->create(array('title' => 'Test 3', 'project_id' => 1, 'owner_id' => 0))); $criteria = new OrCriteria(); $criteria->withQuery($query); diff --git a/tests/units/Core/Http/RequestTest.php b/tests/units/Core/Http/RequestTest.php index 6fa796f7..1db0100c 100644 --- a/tests/units/Core/Http/RequestTest.php +++ b/tests/units/Core/Http/RequestTest.php @@ -169,6 +169,9 @@ class RequestTest extends Base $request = new Request($this->container, array(), array(), array(), array(), array()); $this->assertEquals('Unknown', $request->getIpAddress()); + $request = new Request($this->container, array('HTTP_X_REAL_IP' => '192.168.1.1,127.0.0.1'), array(), array(), array(), array()); + $this->assertEquals('192.168.1.1', $request->getIpAddress()); + $request = new Request($this->container, array('HTTP_X_FORWARDED_FOR' => '192.168.0.1,127.0.0.1'), array(), array(), array(), array()); $this->assertEquals('192.168.0.1', $request->getIpAddress()); diff --git a/tests/units/EventBuilder/CommentEventBuilderTest.php b/tests/units/EventBuilder/CommentEventBuilderTest.php new file mode 100644 index 00000000..a490799e --- /dev/null +++ b/tests/units/EventBuilder/CommentEventBuilderTest.php @@ -0,0 +1,37 @@ +<?php + +use Kanboard\EventBuilder\CommentEventBuilder; +use Kanboard\Model\CommentModel; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; + +require_once __DIR__.'/../Base.php'; + +class CommentEventBuilderTest extends Base +{ + public function testWithMissingComment() + { + $commentEventBuilder = new CommentEventBuilder($this->container); + $commentEventBuilder->withCommentId(42); + $this->assertNull($commentEventBuilder->build()); + } + + public function testBuild() + { + $commentModel = new CommentModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $commentEventBuilder = new CommentEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'bla bla', 'user_id' => 1))); + + $commentEventBuilder->withCommentId(1); + $event = $commentEventBuilder->build(); + + $this->assertInstanceOf('Kanboard\Event\CommentEvent', $event); + $this->assertNotEmpty($event['comment']); + $this->assertNotEmpty($event['task']); + } +} diff --git a/tests/units/EventBuilder/ProjectFileEventBuilderTest.php b/tests/units/EventBuilder/ProjectFileEventBuilderTest.php new file mode 100644 index 00000000..bfe22719 --- /dev/null +++ b/tests/units/EventBuilder/ProjectFileEventBuilderTest.php @@ -0,0 +1,33 @@ +<?php + +use Kanboard\EventBuilder\ProjectFileEventBuilder; +use Kanboard\Model\ProjectFileModel; +use Kanboard\Model\ProjectModel; + +require_once __DIR__.'/../Base.php'; + +class ProjectFileEventBuilderTest extends Base +{ + public function testWithMissingFile() + { + $projectFileEventBuilder = new ProjectFileEventBuilder($this->container); + $projectFileEventBuilder->withFileId(42); + $this->assertNull($projectFileEventBuilder->build()); + } + + public function testBuild() + { + $projectModel = new ProjectModel($this->container); + $projectFileModel = new ProjectFileModel($this->container); + $projectFileEventBuilder = new ProjectFileEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $projectFileModel->create(1, 'Test', '/tmp/test', 123)); + + $event = $projectFileEventBuilder->withFileId(1)->build(); + + $this->assertInstanceOf('Kanboard\Event\ProjectFileEvent', $event); + $this->assertNotEmpty($event['file']); + $this->assertNotEmpty($event['project']); + } +} diff --git a/tests/units/EventBuilder/SubtaskEventBuilderTest.php b/tests/units/EventBuilder/SubtaskEventBuilderTest.php new file mode 100644 index 00000000..062bdfb4 --- /dev/null +++ b/tests/units/EventBuilder/SubtaskEventBuilderTest.php @@ -0,0 +1,62 @@ +<?php + +use Kanboard\EventBuilder\SubtaskEventBuilder; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\SubtaskModel; +use Kanboard\Model\TaskCreationModel; + +require_once __DIR__.'/../Base.php'; + +class SubtaskEventBuilderTest extends Base +{ + public function testWithMissingSubtask() + { + $subtaskEventBuilder = new SubtaskEventBuilder($this->container); + $subtaskEventBuilder->withSubtaskId(42); + $this->assertNull($subtaskEventBuilder->build()); + } + + public function testBuildWithoutChanges() + { + $subtaskModel = new SubtaskModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $subtaskEventBuilder = new SubtaskEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('task_id' => 1, 'title' => 'test'))); + + $event = $subtaskEventBuilder->withSubtaskId(1)->build(); + + $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); + $this->assertNotEmpty($event['subtask']); + $this->assertNotEmpty($event['task']); + $this->assertArrayNotHasKey('changes', $event); + } + + public function testBuildWithChanges() + { + $subtaskModel = new SubtaskModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $subtaskEventBuilder = new SubtaskEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('task_id' => 1, 'title' => 'test'))); + + $event = $subtaskEventBuilder + ->withSubtaskId(1) + ->withValues(array('title' => 'new title', 'user_id' => 1)) + ->build(); + + $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); + $this->assertNotEmpty($event['subtask']); + $this->assertNotEmpty($event['task']); + $this->assertNotEmpty($event['changes']); + $this->assertCount(2, $event['changes']); + $this->assertEquals('new title', $event['changes']['title']); + $this->assertEquals(1, $event['changes']['user_id']); + } +} diff --git a/tests/units/EventBuilder/TaskEventBuilderTest.php b/tests/units/EventBuilder/TaskEventBuilderTest.php new file mode 100644 index 00000000..e6334fe2 --- /dev/null +++ b/tests/units/EventBuilder/TaskEventBuilderTest.php @@ -0,0 +1,100 @@ +<?php + +use Kanboard\EventBuilder\TaskEventBuilder; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; + +require_once __DIR__.'/../Base.php'; + +class TaskEventBuilderTest extends Base +{ + public function testWithMissingTask() + { + $taskEventBuilder = new TaskEventBuilder($this->container); + $taskEventBuilder->withTaskId(42); + $this->assertNull($taskEventBuilder->build()); + } + + public function testBuildWithTask() + { + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskEventBuilder = new TaskEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'before', 'project_id' => 1))); + + $event = $taskEventBuilder + ->withTaskId(1) + ->withTask(array('title' => 'before')) + ->withChanges(array('title' => 'after')) + ->build(); + + $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); + $this->assertNotEmpty($event['task']); + $this->assertEquals(1, $event['task_id']); + $this->assertEquals(array('title' => 'after'), $event['changes']); + } + + public function testBuildWithoutChanges() + { + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskEventBuilder = new TaskEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + + $event = $taskEventBuilder->withTaskId(1)->build(); + + $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); + $this->assertNotEmpty($event['task']); + $this->assertEquals(1, $event['task_id']); + $this->assertArrayNotHasKey('changes', $event); + } + + public function testBuildWithChanges() + { + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskEventBuilder = new TaskEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + + $event = $taskEventBuilder + ->withTaskId(1) + ->withChanges(array('title' => 'new title')) + ->build(); + + $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); + $this->assertNotEmpty($event['task']); + $this->assertNotEmpty($event['changes']); + $this->assertEquals('new title', $event['changes']['title']); + } + + public function testBuildWithChangesAndValues() + { + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskEventBuilder = new TaskEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + + $event = $taskEventBuilder + ->withTaskId(1) + ->withChanges(array('title' => 'new title', 'project_id' => 1)) + ->withValues(array('key' => 'value')) + ->build(); + + $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); + $this->assertNotEmpty($event['task']); + $this->assertNotEmpty($event['changes']); + $this->assertNotEmpty($event['key']); + $this->assertEquals('value', $event['key']); + + $this->assertCount(1, $event['changes']); + $this->assertEquals('new title', $event['changes']['title']); + } +} diff --git a/tests/units/EventBuilder/TaskFileEventBuilderTest.php b/tests/units/EventBuilder/TaskFileEventBuilderTest.php new file mode 100644 index 00000000..c253b913 --- /dev/null +++ b/tests/units/EventBuilder/TaskFileEventBuilderTest.php @@ -0,0 +1,36 @@ +<?php + +use Kanboard\EventBuilder\TaskFileEventBuilder; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; +use Kanboard\Model\TaskFileModel; + +require_once __DIR__.'/../Base.php'; + +class TaskFileEventBuilderTest extends Base +{ + public function testWithMissingFile() + { + $taskFileEventBuilder = new TaskFileEventBuilder($this->container); + $taskFileEventBuilder->withFileId(42); + $this->assertNull($taskFileEventBuilder->build()); + } + + public function testBuild() + { + $taskFileModel = new TaskFileModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskFileEventBuilder = new TaskFileEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $taskFileModel->create(1, 'Test', '/tmp/test', 123)); + + $event = $taskFileEventBuilder->withFileId(1)->build(); + + $this->assertInstanceOf('Kanboard\Event\TaskFileEvent', $event); + $this->assertNotEmpty($event['file']); + $this->assertNotEmpty($event['task']); + } +} diff --git a/tests/units/Filter/TaskPriorityFilterTest.php b/tests/units/Filter/TaskPriorityFilterTest.php new file mode 100644 index 00000000..4c95ddce --- /dev/null +++ b/tests/units/Filter/TaskPriorityFilterTest.php @@ -0,0 +1,47 @@ +<?php + +use Kanboard\Filter\TaskPriorityFilter; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; +use Kanboard\Model\TaskFinderModel; + +require_once __DIR__.'/../Base.php'; + +class TaskPriorityFilterTest extends Base +{ + public function testWithDefinedPriority() + { + $taskFinder = new TaskFinderModel($this->container); + $taskCreation = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'priority' => 2))); + + $filter = new TaskPriorityFilter(); + $filter->withQuery($query); + $filter->withValue(2); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + } + + public function testWithNoPriority() + { + $taskFinder = new TaskFinderModel($this->container); + $taskCreation = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + + $filter = new TaskPriorityFilter(); + $filter->withQuery($query); + $filter->withValue(2); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } +} diff --git a/tests/units/Formatter/TaskAutoCompleteFormatterTest.php b/tests/units/Formatter/TaskAutoCompleteFormatterTest.php new file mode 100644 index 00000000..20baf549 --- /dev/null +++ b/tests/units/Formatter/TaskAutoCompleteFormatterTest.php @@ -0,0 +1,34 @@ +<?php + +use Kanboard\Formatter\TaskAutoCompleteFormatter; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; +use Kanboard\Model\TaskFinderModel; + +require_once __DIR__.'/../Base.php'; + +class TaskAutoCompleteFormatterTest extends Base +{ + public function testFormat() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'My Project'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task 1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task 2', 'project_id' => 1))); + + $tasks = TaskAutoCompleteFormatter::getInstance($this->container) + ->withQuery($taskFinderModel->getExtendedQuery()) + ->format(); + + $this->assertCount(2, $tasks); + $this->assertEquals('My Project > #1 Task 1', $tasks[0]['label']); + $this->assertEquals('Task 1', $tasks[0]['value']); + $this->assertEquals(1, $tasks[0]['id']); + $this->assertEquals('My Project > #2 Task 2', $tasks[1]['label']); + $this->assertEquals('Task 2', $tasks[1]['value']); + $this->assertEquals(2, $tasks[1]['id']); + } +} diff --git a/tests/units/Job/CommentEventJobTest.php b/tests/units/Job/CommentEventJobTest.php new file mode 100644 index 00000000..8571af8e --- /dev/null +++ b/tests/units/Job/CommentEventJobTest.php @@ -0,0 +1,52 @@ +<?php + +use Kanboard\Job\CommentEventJob; +use Kanboard\Model\CommentModel; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; + +require_once __DIR__.'/../Base.php'; + +class CommentEventJobTest extends Base +{ + public function testJobParams() + { + $commentEventJob = new CommentEventJob($this->container); + $commentEventJob->withParams(123, 'foobar'); + + $this->assertSame(array(123, 'foobar'), $commentEventJob->getJobParams()); + } + + public function testWithMissingComment() + { + $this->container['dispatcher']->addListener(CommentModel::EVENT_CREATE, function() {}); + + $commentEventJob = new CommentEventJob($this->container); + $commentEventJob->execute(42, CommentModel::EVENT_CREATE); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertEmpty($called); + } + + public function testTriggerEvents() + { + $this->container['dispatcher']->addListener(CommentModel::EVENT_CREATE, function() {}); + $this->container['dispatcher']->addListener(CommentModel::EVENT_UPDATE, function() {}); + $this->container['dispatcher']->addListener(CommentModel::EVENT_DELETE, function() {}); + + $commentModel = new CommentModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'foobar', 'user_id' => 1))); + $this->assertTrue($commentModel->update(array('id' => 1, 'comment' => 'test'))); + $this->assertTrue($commentModel->remove(1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(CommentModel::EVENT_CREATE.'.closure', $called); + $this->assertArrayHasKey(CommentModel::EVENT_UPDATE.'.closure', $called); + $this->assertArrayHasKey(CommentModel::EVENT_DELETE.'.closure', $called); + } +} diff --git a/tests/units/Job/ProjectFileEventJobTest.php b/tests/units/Job/ProjectFileEventJobTest.php new file mode 100644 index 00000000..f266d293 --- /dev/null +++ b/tests/units/Job/ProjectFileEventJobTest.php @@ -0,0 +1,43 @@ +<?php + +use Kanboard\Job\ProjectFileEventJob; +use Kanboard\Model\ProjectFileModel; +use Kanboard\Model\ProjectModel; + +require_once __DIR__.'/../Base.php'; + +class ProjectFileEventJobTest extends Base +{ + public function testJobParams() + { + $projectFileEventJob = new ProjectFileEventJob($this->container); + $projectFileEventJob->withParams(123, 'foobar'); + + $this->assertSame(array(123, 'foobar'), $projectFileEventJob->getJobParams()); + } + + public function testWithMissingFile() + { + $this->container['dispatcher']->addListener(ProjectFileModel::EVENT_CREATE, function() {}); + + $projectFileEventJob = new ProjectFileEventJob($this->container); + $projectFileEventJob->execute(42, ProjectFileModel::EVENT_CREATE); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertEmpty($called); + } + + public function testTriggerEvents() + { + $this->container['dispatcher']->addListener(ProjectFileModel::EVENT_CREATE, function() {}); + + $projectModel = new ProjectModel($this->container); + $projectFileModel = new ProjectFileModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $projectFileModel->create(1, 'Test', '/tmp/test', 123)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(ProjectFileModel::EVENT_CREATE.'.closure', $called); + } +} diff --git a/tests/units/Job/SubtaskEventJobTest.php b/tests/units/Job/SubtaskEventJobTest.php new file mode 100644 index 00000000..66c3db05 --- /dev/null +++ b/tests/units/Job/SubtaskEventJobTest.php @@ -0,0 +1,52 @@ +<?php + +use Kanboard\Job\SubtaskEventJob; +use Kanboard\Model\SubtaskModel; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; + +require_once __DIR__.'/../Base.php'; + +class SubtaskEventJobTest extends Base +{ + public function testJobParams() + { + $subtaskEventJob = new SubtaskEventJob($this->container); + $subtaskEventJob->withParams(123, 'foobar', array('k' => 'v')); + + $this->assertSame(array(123, 'foobar', array('k' => 'v')), $subtaskEventJob->getJobParams()); + } + + public function testWithMissingSubtask() + { + $this->container['dispatcher']->addListener(SubtaskModel::EVENT_CREATE, function() {}); + + $subtaskEventJob = new SubtaskEventJob($this->container); + $subtaskEventJob->execute(42, SubtaskModel::EVENT_CREATE); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertEmpty($called); + } + + public function testTriggerEvents() + { + $this->container['dispatcher']->addListener(SubtaskModel::EVENT_CREATE, function() {}); + $this->container['dispatcher']->addListener(SubtaskModel::EVENT_UPDATE, function() {}); + $this->container['dispatcher']->addListener(SubtaskModel::EVENT_DELETE, function() {}); + + $subtaskModel = new SubtaskModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('task_id' => 1, 'title' => 'before'))); + $this->assertTrue($subtaskModel->update(array('id' => 1, 'title' => 'after'))); + $this->assertTrue($subtaskModel->remove(1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(SubtaskModel::EVENT_CREATE.'.closure', $called); + $this->assertArrayHasKey(SubtaskModel::EVENT_UPDATE.'.closure', $called); + $this->assertArrayHasKey(SubtaskModel::EVENT_DELETE.'.closure', $called); + } +} diff --git a/tests/units/Job/TaskEventJobTest.php b/tests/units/Job/TaskEventJobTest.php new file mode 100644 index 00000000..c399faad --- /dev/null +++ b/tests/units/Job/TaskEventJobTest.php @@ -0,0 +1,189 @@ +<?php + +use Kanboard\Job\TaskEventJob; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\SwimlaneModel; +use Kanboard\Model\TaskCreationModel; +use Kanboard\Model\TaskModel; +use Kanboard\Model\TaskModificationModel; +use Kanboard\Model\TaskPositionModel; +use Kanboard\Model\TaskProjectMoveModel; +use Kanboard\Model\TaskStatusModel; + +require_once __DIR__.'/../Base.php'; + +class TaskEventJobTest extends Base +{ + public function testJobParams() + { + $taskEventJob = new TaskEventJob($this->container); + $taskEventJob->withParams(123, array('foobar'), array('k' => 'v'), array('k1' => 'v1'), array('k2' => 'v2')); + + $this->assertSame( + array(123, array('foobar'), array('k' => 'v'), array('k1' => 'v1'), array('k2' => 'v2')), + $taskEventJob->getJobParams() + ); + } + + public function testWithMissingTask() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function() {}); + + $taskEventJob = new TaskEventJob($this->container); + $taskEventJob->execute(42, array(TaskModel::EVENT_CREATE)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertEmpty($called); + } + + public function testTriggerCreateEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function() {}); + $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.closure', $called); + $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called); + } + + public function testTriggerUpdateEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_UPDATE, function() {}); + $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $taskModificationModel = new TaskModificationModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertTrue($taskModificationModel->update(array('id' => 1, 'title' => 'new title'))); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_UPDATE.'.closure', $called); + $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called); + } + + public function testTriggerAssigneeChangeEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_ASSIGNEE_CHANGE, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $taskModificationModel = new TaskModificationModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertTrue($taskModificationModel->update(array('id' => 1, 'owner_id' => 1))); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_ASSIGNEE_CHANGE.'.closure', $called); + } + + public function testTriggerCloseEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_CLOSE, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $taskStatusModel = new TaskStatusModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertTrue($taskStatusModel->close(1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_CLOSE.'.closure', $called); + } + + public function testTriggerOpenEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_OPEN, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $taskStatusModel = new TaskStatusModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertTrue($taskStatusModel->close(1)); + $this->assertTrue($taskStatusModel->open(1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_OPEN.'.closure', $called); + } + + public function testTriggerMovePositionEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_POSITION, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'test 2', 'project_id' => 1))); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 1, 2)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_MOVE_POSITION.'.closure', $called); + } + + public function testTriggerMoveColumnEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_COLUMN, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 2, 2)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_MOVE_COLUMN.'.closure', $called); + } + + public function testTriggerMoveSwimlaneEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_SWIMLANE, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $projectModel = new ProjectModel($this->container); + $swimlaneModel = new SwimlaneModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $swimlaneModel->create(array('name' => 'S1', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 1, 1, 1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_MOVE_SWIMLANE.'.closure', $called); + } + + public function testTriggerMoveProjectEvent() + { + $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_PROJECT, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskProjectMoveModel = new TaskProjectMoveModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + $this->assertTrue($taskProjectMoveModel->moveToProject(1, 1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskModel::EVENT_MOVE_PROJECT.'.closure', $called); + } +} diff --git a/tests/units/Job/TaskFileEventJobTest.php b/tests/units/Job/TaskFileEventJobTest.php new file mode 100644 index 00000000..921fe801 --- /dev/null +++ b/tests/units/Job/TaskFileEventJobTest.php @@ -0,0 +1,46 @@ +<?php + +use Kanboard\Job\TaskFileEventJob; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskCreationModel; +use Kanboard\Model\TaskFileModel; + +require_once __DIR__.'/../Base.php'; + +class TaskFileEventJobTest extends Base +{ + public function testJobParams() + { + $taskFileEventJob = new TaskFileEventJob($this->container); + $taskFileEventJob->withParams(123, 'foobar'); + + $this->assertSame(array(123, 'foobar'), $taskFileEventJob->getJobParams()); + } + + public function testWithMissingFile() + { + $this->container['dispatcher']->addListener(TaskFileModel::EVENT_CREATE, function() {}); + + $taskFileEventJob = new TaskFileEventJob($this->container); + $taskFileEventJob->execute(42, TaskFileModel::EVENT_CREATE); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertEmpty($called); + } + + public function testTriggerEvents() + { + $this->container['dispatcher']->addListener(TaskFileModel::EVENT_CREATE, function() {}); + + $taskFileModel = new TaskFileModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $taskFileModel->create(1, 'Test', '/tmp/test', 123)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskFileModel::EVENT_CREATE.'.closure', $called); + } +} diff --git a/tests/units/Model/CommentTest.php b/tests/units/Model/CommentModelTest.php index 574b5a87..4178a839 100644 --- a/tests/units/Model/CommentTest.php +++ b/tests/units/Model/CommentModelTest.php @@ -6,7 +6,7 @@ use Kanboard\Model\TaskCreationModel; use Kanboard\Model\ProjectModel; use Kanboard\Model\CommentModel; -class CommentTest extends Base +class CommentModelTest extends Base { public function testCreate() { @@ -75,7 +75,7 @@ class CommentTest extends Base $this->assertEquals('bla', $comment['comment']); } - public function validateRemove() + public function testRemove() { $commentModel = new CommentModel($this->container); $taskCreationModel = new TaskCreationModel($this->container); diff --git a/tests/units/Model/GroupTest.php b/tests/units/Model/GroupModelTest.php index 85c2c5d9..4ad0a167 100644 --- a/tests/units/Model/GroupTest.php +++ b/tests/units/Model/GroupModelTest.php @@ -4,7 +4,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\GroupModel; -class GroupTest extends Base +class GroupModelTest extends Base { public function testCreation() { @@ -57,4 +57,13 @@ class GroupTest extends Base $this->assertTrue($groupModel->remove(1)); $this->assertEmpty($groupModel->getById(1)); } + + public function testGetOrCreateExternalGroupId() + { + $groupModel = new GroupModel($this->container); + $this->assertEquals(1, $groupModel->create('Group 1', 'ExternalId1')); + $this->assertEquals(1, $groupModel->getOrCreateExternalGroupId('Group 1', 'ExternalId1')); + $this->assertEquals(1, $groupModel->getOrCreateExternalGroupId('Group 2', 'ExternalId1')); + $this->assertEquals(2, $groupModel->getOrCreateExternalGroupId('Group 2', 'ExternalId2')); + } } diff --git a/tests/units/Model/SubtaskModelTest.php b/tests/units/Model/SubtaskModelTest.php index 6451189d..7e438651 100644 --- a/tests/units/Model/SubtaskModelTest.php +++ b/tests/units/Model/SubtaskModelTest.php @@ -9,64 +9,6 @@ use Kanboard\Model\TaskFinderModel; class SubtaskModelTest extends Base { - public function onSubtaskCreated($event) - { - $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); - $data = $event->getAll(); - - $this->assertArrayHasKey('id', $data); - $this->assertArrayHasKey('title', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('time_estimated', $data); - $this->assertArrayHasKey('time_spent', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('task_id', $data); - $this->assertArrayHasKey('user_id', $data); - $this->assertArrayHasKey('position', $data); - $this->assertNotEmpty($data['task_id']); - $this->assertNotEmpty($data['id']); - } - - public function onSubtaskUpdated($event) - { - $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); - $data = $event->getAll(); - - $this->assertArrayHasKey('id', $data); - $this->assertArrayHasKey('title', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('time_estimated', $data); - $this->assertArrayHasKey('time_spent', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('task_id', $data); - $this->assertArrayHasKey('user_id', $data); - $this->assertArrayHasKey('position', $data); - $this->assertArrayHasKey('changes', $data); - $this->assertArrayHasKey('user_id', $data['changes']); - $this->assertArrayHasKey('status', $data['changes']); - - $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $data['changes']['status']); - $this->assertEquals(1, $data['changes']['user_id']); - } - - public function onSubtaskDeleted($event) - { - $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); - $data = $event->getAll(); - - $this->assertArrayHasKey('id', $data); - $this->assertArrayHasKey('title', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('time_estimated', $data); - $this->assertArrayHasKey('time_spent', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('task_id', $data); - $this->assertArrayHasKey('user_id', $data); - $this->assertArrayHasKey('position', $data); - $this->assertNotEmpty($data['task_id']); - $this->assertNotEmpty($data['id']); - } - public function testCreation() { $taskCreationModel = new TaskCreationModel($this->container); @@ -75,9 +17,6 @@ class SubtaskModelTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->container['dispatcher']->addListener(SubtaskModel::EVENT_CREATE, array($this, 'onSubtaskCreated')); - $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); $subtask = $subtaskModel->getById(1); @@ -101,8 +40,6 @@ class SubtaskModelTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); - $this->container['dispatcher']->addListener(SubtaskModel::EVENT_UPDATE, array($this, 'onSubtaskUpdated')); - $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); $this->assertTrue($subtaskModel->update(array('id' => 1, 'user_id' => 1, 'status' => SubtaskModel::STATUS_INPROGRESS))); @@ -128,8 +65,6 @@ class SubtaskModelTest extends Base $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); - $this->container['dispatcher']->addListener(SubtaskModel::EVENT_DELETE, array($this, 'onSubtaskDeleted')); - $subtask = $subtaskModel->getById(1); $this->assertNotEmpty($subtask); @@ -269,7 +204,6 @@ class SubtaskModelTest extends Base $this->assertTrue($subtaskModel->duplicate(1, 2)); $subtasks = $subtaskModel->getAll(2); - $this->assertNotFalse($subtasks); $this->assertNotEmpty($subtasks); $this->assertEquals(2, count($subtasks)); @@ -383,7 +317,7 @@ class SubtaskModelTest extends Base $this->assertEquals(2, $task['time_spent']); $this->assertEquals(3, $task['time_estimated']); } - + public function testGetProjectId() { $taskCreationModel = new TaskCreationModel($this->container); @@ -393,7 +327,7 @@ class SubtaskModelTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); - + $this->assertEquals(1, $subtaskModel->getProjectId(1)); $this->assertEquals(0, $subtaskModel->getProjectId(2)); } diff --git a/tests/units/Model/TaskCreationModelTest.php b/tests/units/Model/TaskCreationModelTest.php index f97c61dc..ce9996d9 100644 --- a/tests/units/Model/TaskCreationModelTest.php +++ b/tests/units/Model/TaskCreationModelTest.php @@ -17,7 +17,7 @@ class TaskCreationModelTest extends Base $event_data = $event->getAll(); $this->assertNotEmpty($event_data); $this->assertEquals(1, $event_data['task_id']); - $this->assertEquals('test', $event_data['title']); + $this->assertEquals('test', $event_data['task']['title']); } public function testNoTitle() diff --git a/tests/units/Model/TaskFinderModelTest.php b/tests/units/Model/TaskFinderModelTest.php new file mode 100644 index 00000000..72da3b6d --- /dev/null +++ b/tests/units/Model/TaskFinderModelTest.php @@ -0,0 +1,142 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\ColumnModel; +use Kanboard\Model\TaskCreationModel; +use Kanboard\Model\TaskFinderModel; +use Kanboard\Model\ProjectModel; + +class TaskFinderModelTest extends Base +{ + public function testGetTasksForDashboard() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $columnModel = new ColumnModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1))); + + $tasks = $taskFinderModel->getUserQuery(1)->findAll(); + $this->assertCount(2, $tasks); + + $this->assertTrue($columnModel->update(2, 'Test', 0, '', 1)); + + $tasks = $taskFinderModel->getUserQuery(1)->findAll(); + $this->assertCount(1, $tasks); + $this->assertEquals('Task #1', $tasks[0]['title']); + + $this->assertTrue($columnModel->update(2, 'Test', 0, '', 0)); + + $tasks = $taskFinderModel->getUserQuery(1)->findAll(); + $this->assertCount(2, $tasks); + } + + public function testGetOverdueTasks() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day')))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1))); + + $tasks = $taskFinderModel->getOverdueTasks(); + $this->assertNotEmpty($tasks); + $this->assertTrue(is_array($tasks)); + $this->assertCount(1, $tasks); + $this->assertEquals('Task #1', $tasks[0]['title']); + } + + public function testGetOverdueTasksByProject() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day')))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'date_due' => strtotime('-1 day')))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0))); + $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1))); + + $tasks = $taskFinderModel->getOverdueTasksByProject(1); + $this->assertNotEmpty($tasks); + $this->assertTrue(is_array($tasks)); + $this->assertCount(1, $tasks); + $this->assertEquals('Task #1', $tasks[0]['title']); + } + + public function testGetOverdueTasksByUser() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 1, 'date_due' => strtotime('-1 day')))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'owner_id' => 1, 'date_due' => strtotime('-1 day')))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0))); + $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1))); + + $tasks = $taskFinderModel->getOverdueTasksByUser(1); + $this->assertNotEmpty($tasks); + $this->assertTrue(is_array($tasks)); + $this->assertCount(2, $tasks); + + $this->assertEquals(1, $tasks[0]['id']); + $this->assertEquals('Task #1', $tasks[0]['title']); + $this->assertEquals(1, $tasks[0]['owner_id']); + $this->assertEquals(1, $tasks[0]['project_id']); + $this->assertEquals('Project #1', $tasks[0]['project_name']); + $this->assertEquals('admin', $tasks[0]['assignee_username']); + $this->assertEquals('', $tasks[0]['assignee_name']); + + $this->assertEquals('Task #2', $tasks[1]['title']); + } + + public function testCountByProject() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 2))); + + $this->assertEquals(1, $taskFinderModel->countByProjectId(1)); + $this->assertEquals(2, $taskFinderModel->countByProjectId(2)); + } + + public function testGetProjectToken() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); + + $this->assertTrue($projectModel->enablePublicAccess(1)); + + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2))); + + $project = $projectModel->getById(1); + $this->assertEquals($project['token'], $taskFinderModel->getProjectToken(1)); + $this->assertEmpty($taskFinderModel->getProjectToken(2)); + } +} diff --git a/tests/units/Model/TaskFinderTest.php b/tests/units/Model/TaskFinderTest.php deleted file mode 100644 index 46792baf..00000000 --- a/tests/units/Model/TaskFinderTest.php +++ /dev/null @@ -1,115 +0,0 @@ -<?php - -require_once __DIR__.'/../Base.php'; - -use Kanboard\Model\TaskCreationModel; -use Kanboard\Model\TaskFinderModel; -use Kanboard\Model\ProjectModel; - -class TaskFinderTest extends Base -{ - public function testGetOverdueTasks() - { - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day')))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #3', 'project_id' => 1))); - - $tasks = $tf->getOverdueTasks(); - $this->assertNotEmpty($tasks); - $this->assertTrue(is_array($tasks)); - $this->assertCount(1, $tasks); - $this->assertEquals('Task #1', $tasks[0]['title']); - } - - public function testGetOverdueTasksByProject() - { - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(2, $p->create(array('name' => 'Project #2'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day')))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 2, 'date_due' => strtotime('-1 day')))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0))); - $this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1))); - - $tasks = $tf->getOverdueTasksByProject(1); - $this->assertNotEmpty($tasks); - $this->assertTrue(is_array($tasks)); - $this->assertCount(1, $tasks); - $this->assertEquals('Task #1', $tasks[0]['title']); - } - - public function testGetOverdueTasksByUser() - { - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(2, $p->create(array('name' => 'Project #2'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 1, 'date_due' => strtotime('-1 day')))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 2, 'owner_id' => 1, 'date_due' => strtotime('-1 day')))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0))); - $this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1))); - - $tasks = $tf->getOverdueTasksByUser(1); - $this->assertNotEmpty($tasks); - $this->assertTrue(is_array($tasks)); - $this->assertCount(2, $tasks); - - $this->assertEquals(1, $tasks[0]['id']); - $this->assertEquals('Task #1', $tasks[0]['title']); - $this->assertEquals(1, $tasks[0]['owner_id']); - $this->assertEquals(1, $tasks[0]['project_id']); - $this->assertEquals('Project #1', $tasks[0]['project_name']); - $this->assertEquals('admin', $tasks[0]['assignee_username']); - $this->assertEquals('', $tasks[0]['assignee_name']); - - $this->assertEquals('Task #2', $tasks[1]['title']); - } - - public function testCountByProject() - { - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(2, $p->create(array('name' => 'Project #2'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 2))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 2))); - - $this->assertEquals(1, $tf->countByProjectId(1)); - $this->assertEquals(2, $tf->countByProjectId(2)); - } - - public function testGetProjectToken() - { - $taskCreationModel = new TaskCreationModel($this->container); - $taskFinderModel = new TaskFinderModel($this->container); - $projectModel = new ProjectModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); - - $this->assertTrue($projectModel->enablePublicAccess(1)); - - $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); - $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2))); - - $project = $projectModel->getById(1); - $this->assertEquals($project['token'], $taskFinderModel->getProjectToken(1)); - $this->assertEmpty($taskFinderModel->getProjectToken(2)); - } -} diff --git a/tests/units/Model/TaskModificationModelTest.php b/tests/units/Model/TaskModificationModelTest.php index c81f968b..f70561b3 100644 --- a/tests/units/Model/TaskModificationModelTest.php +++ b/tests/units/Model/TaskModificationModelTest.php @@ -18,7 +18,8 @@ class TaskModificationModelTest extends Base $event_data = $event->getAll(); $this->assertNotEmpty($event_data); $this->assertEquals(1, $event_data['task_id']); - $this->assertEquals('Task #1', $event_data['title']); + $this->assertEquals('After', $event_data['task']['title']); + $this->assertEquals('After', $event_data['changes']['title']); } public function onUpdate($event) @@ -28,7 +29,7 @@ class TaskModificationModelTest extends Base $event_data = $event->getAll(); $this->assertNotEmpty($event_data); $this->assertEquals(1, $event_data['task_id']); - $this->assertEquals('Task #1', $event_data['title']); + $this->assertEquals('After', $event_data['task']['title']); } public function onAssigneeChange($event) @@ -38,7 +39,7 @@ class TaskModificationModelTest extends Base $event_data = $event->getAll(); $this->assertNotEmpty($event_data); $this->assertEquals(1, $event_data['task_id']); - $this->assertEquals(1, $event_data['owner_id']); + $this->assertEquals(1, $event_data['changes']['owner_id']); } public function testThatNoEventAreFiredWhenNoChanges() @@ -66,19 +67,19 @@ class TaskModificationModelTest extends Base $taskFinderModel = new TaskFinderModel($this->container); $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Before', 'project_id' => 1))); $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, array($this, 'onCreateUpdate')); $this->container['dispatcher']->addListener(TaskModel::EVENT_UPDATE, array($this, 'onUpdate')); - $this->assertTrue($taskModificationModel->update(array('id' => 1, 'title' => 'Task #1'))); + $this->assertTrue($taskModificationModel->update(array('id' => 1, 'title' => 'After'))); $called = $this->container['dispatcher']->getCalledListeners(); $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.TaskModificationModelTest::onCreateUpdate', $called); $this->assertArrayHasKey(TaskModel::EVENT_UPDATE.'.TaskModificationModelTest::onUpdate', $called); $task = $taskFinderModel->getById(1); - $this->assertEquals('Task #1', $task['title']); + $this->assertEquals('After', $task['title']); } public function testChangeAssignee() diff --git a/tests/units/Model/TaskPositionTest.php b/tests/units/Model/TaskPositionModelTest.php index 7ab6950e..03caf7ed 100644 --- a/tests/units/Model/TaskPositionTest.php +++ b/tests/units/Model/TaskPositionModelTest.php @@ -11,57 +11,57 @@ use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; use Kanboard\Model\SwimlaneModel; -class TaskPositionTest extends Base +class TaskPositionModelTest extends Base { public function testGetTaskProgression() { - $t = new TaskModel($this->container); - $ts = new TaskStatusModel($this->container); - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); + $taskModel = new TaskModel($this->container); + $taskStatusModel = new TaskStatusModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); $columnModel = new ColumnModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(0, $t->getProgress($tf->getById(1), $columnModel->getList(1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(0, $taskModel->getProgress($taskFinderModel->getById(1), $columnModel->getList(1))); - $this->assertTrue($tp->movePosition(1, 1, 2, 1)); - $this->assertEquals(25, $t->getProgress($tf->getById(1), $columnModel->getList(1))); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 2, 1)); + $this->assertEquals(25, $taskModel->getProgress($taskFinderModel->getById(1), $columnModel->getList(1))); - $this->assertTrue($tp->movePosition(1, 1, 3, 1)); - $this->assertEquals(50, $t->getProgress($tf->getById(1), $columnModel->getList(1))); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 3, 1)); + $this->assertEquals(50, $taskModel->getProgress($taskFinderModel->getById(1), $columnModel->getList(1))); - $this->assertTrue($tp->movePosition(1, 1, 4, 1)); - $this->assertEquals(75, $t->getProgress($tf->getById(1), $columnModel->getList(1))); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 4, 1)); + $this->assertEquals(75, $taskModel->getProgress($taskFinderModel->getById(1), $columnModel->getList(1))); - $this->assertTrue($ts->close(1)); - $this->assertEquals(100, $t->getProgress($tf->getById(1), $columnModel->getList(1))); + $this->assertTrue($taskStatusModel->close(1)); + $this->assertEquals(100, $taskModel->getProgress($taskFinderModel->getById(1), $columnModel->getList(1))); } public function testMoveTaskToWrongPosition() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); // We move the task 2 to the position 0 - $this->assertFalse($tp->movePosition(1, 1, 3, 0)); + $this->assertFalse($taskPositionModel->movePosition(1, 1, 3, 0)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); @@ -69,26 +69,26 @@ class TaskPositionTest extends Base public function testMoveTaskToGreaterPosition() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); // We move the task 2 to the position 42 - $this->assertTrue($tp->movePosition(1, 1, 1, 42)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 1, 42)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); @@ -96,26 +96,26 @@ class TaskPositionTest extends Base public function testMoveTaskToEmptyColumn() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); // We move the task 1 to the column 3 - $this->assertTrue($tp->movePosition(1, 1, 3, 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 3, 1)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(3, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); @@ -123,62 +123,62 @@ class TaskPositionTest extends Base public function testMoveTaskToAnotherColumn() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 2))); - $this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 2))); - $this->assertEquals(6, $tc->create(array('title' => 'Task #6', 'project_id' => 1, 'column_id' => 2))); - $this->assertEquals(7, $tc->create(array('title' => 'Task #7', 'project_id' => 1, 'column_id' => 3))); - $this->assertEquals(8, $tc->create(array('title' => 'Task #8', 'project_id' => 1, 'column_id' => 1))); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(6, $taskCreationModel->create(array('title' => 'Task #6', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(7, $taskCreationModel->create(array('title' => 'Task #7', 'project_id' => 1, 'column_id' => 3))); + $this->assertEquals(8, $taskCreationModel->create(array('title' => 'Task #8', 'project_id' => 1, 'column_id' => 1))); // We move the task 3 to the column 3 - $this->assertTrue($tp->movePosition(1, 3, 3, 2)); + $this->assertTrue($taskPositionModel->movePosition(1, 3, 3, 2)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); - $task = $tf->getById(3); + $task = $taskFinderModel->getById(3); $this->assertEquals(3, $task['id']); $this->assertEquals(3, $task['column_id']); $this->assertEquals(2, $task['position']); - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(5); + $task = $taskFinderModel->getById(5); $this->assertEquals(5, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(2, $task['position']); - $task = $tf->getById(6); + $task = $taskFinderModel->getById(6); $this->assertEquals(6, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(3, $task['position']); - $task = $tf->getById(7); + $task = $taskFinderModel->getById(7); $this->assertEquals(7, $task['id']); $this->assertEquals(3, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(8); + $task = $taskFinderModel->getById(8); $this->assertEquals(8, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(3, $task['position']); @@ -186,37 +186,37 @@ class TaskPositionTest extends Base public function testMoveTaskTop() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); // Move the last task to the top - $this->assertTrue($tp->movePosition(1, 4, 1, 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 4, 1, 1)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(3, $task['position']); - $task = $tf->getById(3); + $task = $taskFinderModel->getById(3); $this->assertEquals(3, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(4, $task['position']); - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); @@ -224,37 +224,37 @@ class TaskPositionTest extends Base public function testMoveTaskBottom() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); // Move the first task to the bottom - $this->assertTrue($tp->movePosition(1, 1, 1, 4)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 1, 4)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(4, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(3); + $task = $taskFinderModel->getById(3); $this->assertEquals(3, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(3, $task['position']); @@ -262,12 +262,12 @@ class TaskPositionTest extends Base public function testMovePosition() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); $counter = 1; $task_per_column = 5; @@ -280,240 +280,240 @@ class TaskPositionTest extends Base 'owner_id' => 0, ); - $this->assertEquals($counter, $tc->create($task)); + $this->assertEquals($counter, $taskCreationModel->create($task)); - $task = $tf->getById($counter); + $task = $taskFinderModel->getById($counter); $this->assertNotEmpty($task); $this->assertEquals($i, $task['position']); } } // We move task id #4, column 1, position 4 to the column 2, position 3 - $this->assertTrue($tp->movePosition(1, 4, 2, 3)); + $this->assertTrue($taskPositionModel->movePosition(1, 4, 2, 3)); // We check the new position of the task - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(3, $task['position']); // The tasks before have the correct position - $task = $tf->getById(3); + $task = $taskFinderModel->getById(3); $this->assertEquals(3, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(3, $task['position']); - $task = $tf->getById(7); + $task = $taskFinderModel->getById(7); $this->assertEquals(7, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(2, $task['position']); // The tasks after have the correct position - $task = $tf->getById(5); + $task = $taskFinderModel->getById(5); $this->assertEquals(5, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(4, $task['position']); - $task = $tf->getById(8); + $task = $taskFinderModel->getById(8); $this->assertEquals(8, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(4, $task['position']); // The number of tasks per column - $this->assertEquals($task_per_column - 1, $tf->countByColumnId(1, 1)); - $this->assertEquals($task_per_column + 1, $tf->countByColumnId(1, 2)); - $this->assertEquals($task_per_column, $tf->countByColumnId(1, 3)); - $this->assertEquals($task_per_column, $tf->countByColumnId(1, 4)); + $this->assertEquals($task_per_column - 1, $taskFinderModel->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $taskFinderModel->countByColumnId(1, 2)); + $this->assertEquals($task_per_column, $taskFinderModel->countByColumnId(1, 3)); + $this->assertEquals($task_per_column, $taskFinderModel->countByColumnId(1, 4)); // We move task id #1, column 1, position 1 to the column 4, position 6 (last position) - $this->assertTrue($tp->movePosition(1, 1, 4, $task_per_column + 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 4, $task_per_column + 1)); // We check the new position of the task - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(4, $task['column_id']); $this->assertEquals($task_per_column + 1, $task['position']); // The tasks before have the correct position - $task = $tf->getById(20); + $task = $taskFinderModel->getById(20); $this->assertEquals(20, $task['id']); $this->assertEquals(4, $task['column_id']); $this->assertEquals($task_per_column, $task['position']); // The tasks after have the correct position - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); // The number of tasks per column - $this->assertEquals($task_per_column - 2, $tf->countByColumnId(1, 1)); - $this->assertEquals($task_per_column + 1, $tf->countByColumnId(1, 2)); - $this->assertEquals($task_per_column, $tf->countByColumnId(1, 3)); - $this->assertEquals($task_per_column + 1, $tf->countByColumnId(1, 4)); + $this->assertEquals($task_per_column - 2, $taskFinderModel->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $taskFinderModel->countByColumnId(1, 2)); + $this->assertEquals($task_per_column, $taskFinderModel->countByColumnId(1, 3)); + $this->assertEquals($task_per_column + 1, $taskFinderModel->countByColumnId(1, 4)); // Our previous moved task should stay at the same place - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(3, $task['position']); // Test wrong position number - $this->assertFalse($tp->movePosition(1, 2, 3, 0)); - $this->assertFalse($tp->movePosition(1, 2, 3, -2)); + $this->assertFalse($taskPositionModel->movePosition(1, 2, 3, 0)); + $this->assertFalse($taskPositionModel->movePosition(1, 2, 3, -2)); // Wrong column - $this->assertFalse($tp->movePosition(1, 2, 22, 2)); + $this->assertFalse($taskPositionModel->movePosition(1, 2, 22, 2)); // Test position greater than the last position - $this->assertTrue($tp->movePosition(1, 11, 1, 22)); + $this->assertTrue($taskPositionModel->movePosition(1, 11, 1, 22)); - $task = $tf->getById(11); + $task = $taskFinderModel->getById(11); $this->assertEquals(11, $task['id']); $this->assertEquals(1, $task['column_id']); - $this->assertEquals($tf->countByColumnId(1, 1), $task['position']); + $this->assertEquals($taskFinderModel->countByColumnId(1, 1), $task['position']); - $task = $tf->getById(5); + $task = $taskFinderModel->getById(5); $this->assertEquals(5, $task['id']); $this->assertEquals(1, $task['column_id']); - $this->assertEquals($tf->countByColumnId(1, 1) - 1, $task['position']); + $this->assertEquals($taskFinderModel->countByColumnId(1, 1) - 1, $task['position']); - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(3, $task['position']); - $this->assertEquals($task_per_column - 1, $tf->countByColumnId(1, 1)); - $this->assertEquals($task_per_column + 1, $tf->countByColumnId(1, 2)); - $this->assertEquals($task_per_column - 1, $tf->countByColumnId(1, 3)); - $this->assertEquals($task_per_column + 1, $tf->countByColumnId(1, 4)); + $this->assertEquals($task_per_column - 1, $taskFinderModel->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $taskFinderModel->countByColumnId(1, 2)); + $this->assertEquals($task_per_column - 1, $taskFinderModel->countByColumnId(1, 3)); + $this->assertEquals($task_per_column + 1, $taskFinderModel->countByColumnId(1, 4)); // Our previous moved task should stay at the same place - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(3, $task['position']); // Test moving task to position 1 - $this->assertTrue($tp->movePosition(1, 14, 1, 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 14, 1, 1)); - $task = $tf->getById(14); + $task = $taskFinderModel->getById(14); $this->assertEquals(14, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); - $this->assertEquals($task_per_column, $tf->countByColumnId(1, 1)); - $this->assertEquals($task_per_column + 1, $tf->countByColumnId(1, 2)); - $this->assertEquals($task_per_column - 2, $tf->countByColumnId(1, 3)); - $this->assertEquals($task_per_column + 1, $tf->countByColumnId(1, 4)); + $this->assertEquals($task_per_column, $taskFinderModel->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $taskFinderModel->countByColumnId(1, 2)); + $this->assertEquals($task_per_column - 2, $taskFinderModel->countByColumnId(1, 3)); + $this->assertEquals($task_per_column + 1, $taskFinderModel->countByColumnId(1, 4)); } public function testMoveTaskSwimlane() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - $s = new SwimlaneModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'test 1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 1))); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $swimlaneModel = new SwimlaneModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $swimlaneModel->create(array('project_id' => 1, 'name' => 'test 1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 1))); // Move the task to the swimlane - $this->assertTrue($tp->movePosition(1, 1, 2, 1, 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 2, 1, 1)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(1, $task['swimlane_id']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(0, $task['swimlane_id']); - $task = $tf->getById(3); + $task = $taskFinderModel->getById(3); $this->assertEquals(3, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); $this->assertEquals(0, $task['swimlane_id']); - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(3, $task['position']); $this->assertEquals(0, $task['swimlane_id']); // Move the task to the swimlane - $this->assertTrue($tp->movePosition(1, 2, 2, 1, 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 2, 2, 1, 1)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(2, $task['position']); $this->assertEquals(1, $task['swimlane_id']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(1, $task['swimlane_id']); - $task = $tf->getById(3); + $task = $taskFinderModel->getById(3); $this->assertEquals(3, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(0, $task['swimlane_id']); - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); $this->assertEquals(0, $task['swimlane_id']); // Move the task 5 to the last column - $this->assertTrue($tp->movePosition(1, 5, 4, 1, 0)); + $this->assertTrue($taskPositionModel->movePosition(1, 5, 4, 1, 0)); // Check tasks position - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(2, $task['position']); $this->assertEquals(1, $task['swimlane_id']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(1, $task['swimlane_id']); - $task = $tf->getById(3); + $task = $taskFinderModel->getById(3); $this->assertEquals(3, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(0, $task['swimlane_id']); - $task = $tf->getById(4); + $task = $taskFinderModel->getById(4); $this->assertEquals(4, $task['id']); $this->assertEquals(1, $task['column_id']); $this->assertEquals(2, $task['position']); $this->assertEquals(0, $task['swimlane_id']); - $task = $tf->getById(5); + $task = $taskFinderModel->getById(5); $this->assertEquals(5, $task['id']); $this->assertEquals(4, $task['column_id']); $this->assertEquals(1, $task['position']); @@ -522,73 +522,73 @@ class TaskPositionTest extends Base public function testEvents() { - $tp = new TaskPositionModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - $s = new SwimlaneModel($this->container); + $taskPositionModel = new TaskPositionModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $swimlaneModel = new SwimlaneModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'test 1'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $swimlaneModel->create(array('project_id' => 1, 'name' => 'test 1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2))); $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_COLUMN, array($this, 'onMoveColumn')); $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_POSITION, array($this, 'onMovePosition')); $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_SWIMLANE, array($this, 'onMoveSwimlane')); // We move the task 1 to the column 2 - $this->assertTrue($tp->movePosition(1, 1, 2, 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 2, 1)); - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(1, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(2, $task['position']); $called = $this->container['dispatcher']->getCalledListeners(); - $this->assertArrayHasKey(TaskModel::EVENT_MOVE_COLUMN.'.TaskPositionTest::onMoveColumn', $called); + $this->assertArrayHasKey(TaskModel::EVENT_MOVE_COLUMN.'.TaskPositionModelTest::onMoveColumn', $called); $this->assertEquals(1, count($called)); // We move the task 1 to the position 2 - $this->assertTrue($tp->movePosition(1, 1, 2, 2)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 2, 2)); - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(2, $task['position']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(1, $task['position']); $called = $this->container['dispatcher']->getCalledListeners(); - $this->assertArrayHasKey(TaskModel::EVENT_MOVE_POSITION.'.TaskPositionTest::onMovePosition', $called); + $this->assertArrayHasKey(TaskModel::EVENT_MOVE_POSITION.'.TaskPositionModelTest::onMovePosition', $called); $this->assertEquals(2, count($called)); // Move to another swimlane - $this->assertTrue($tp->movePosition(1, 1, 3, 1, 1)); + $this->assertTrue($taskPositionModel->movePosition(1, 1, 3, 1, 1)); - $task = $tf->getById(1); + $task = $taskFinderModel->getById(1); $this->assertEquals(1, $task['id']); $this->assertEquals(3, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(1, $task['swimlane_id']); - $task = $tf->getById(2); + $task = $taskFinderModel->getById(2); $this->assertEquals(2, $task['id']); $this->assertEquals(2, $task['column_id']); $this->assertEquals(1, $task['position']); $this->assertEquals(0, $task['swimlane_id']); $called = $this->container['dispatcher']->getCalledListeners(); - $this->assertArrayHasKey(TaskModel::EVENT_MOVE_SWIMLANE.'.TaskPositionTest::onMoveSwimlane', $called); + $this->assertArrayHasKey(TaskModel::EVENT_MOVE_SWIMLANE.'.TaskPositionModelTest::onMoveSwimlane', $called); $this->assertEquals(3, count($called)); } diff --git a/tests/units/Model/TaskProjectMoveModelTest.php b/tests/units/Model/TaskProjectMoveModelTest.php index c4282638..52f61b28 100644 --- a/tests/units/Model/TaskProjectMoveModelTest.php +++ b/tests/units/Model/TaskProjectMoveModelTest.php @@ -24,7 +24,7 @@ class TaskProjectMoveModelTest extends Base $event_data = $event->getAll(); $this->assertNotEmpty($event_data); $this->assertEquals(1, $event_data['task_id']); - $this->assertEquals('test', $event_data['title']); + $this->assertEquals('test', $event_data['task']['title']); } public function testMoveAnotherProject() |