summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md4
-rw-r--r--ChangeLog28
-rw-r--r--Dockerfile1
-rw-r--r--Makefile2
-rw-r--r--README.md15
-rw-r--r--app.json2
-rw-r--r--app/Action/Base.php13
-rw-r--r--app/Action/CommentCreationMoveTaskColumn.php12
-rw-r--r--app/Action/TaskAssignCategoryColor.php7
-rw-r--r--app/Action/TaskAssignColorCategory.php7
-rw-r--r--app/Action/TaskAssignColorColumn.php7
-rw-r--r--app/Action/TaskAssignColorPriority.php7
-rw-r--r--app/Action/TaskAssignColorUser.php7
-rw-r--r--app/Action/TaskAssignCurrentUserColumn.php7
-rw-r--r--app/Action/TaskAssignSpecificUser.php7
-rw-r--r--app/Action/TaskCloseColumn.php10
-rw-r--r--app/Action/TaskCloseNoActivityColumn.php96
-rw-r--r--app/Action/TaskCreation.php1
-rw-r--r--app/Action/TaskDuplicateAnotherProject.php14
-rw-r--r--app/Action/TaskEmail.php14
-rw-r--r--app/Action/TaskMoveAnotherProject.php8
-rw-r--r--app/Action/TaskMoveColumnAssigned.php19
-rw-r--r--app/Action/TaskMoveColumnCategoryChange.php19
-rw-r--r--app/Action/TaskMoveColumnUnAssigned.php19
-rw-r--r--app/Action/TaskUpdateStartDate.php7
-rw-r--r--app/Auth/TotpAuth.php6
-rw-r--r--app/Controller/ColumnController.php20
-rw-r--r--app/Controller/ProjectPermissionController.php2
-rw-r--r--app/Core/Base.php6
-rw-r--r--app/Core/Filter/LexerBuilder.php2
-rw-r--r--app/Core/Http/Request.php1
-rw-r--r--app/Core/Queue/JobHandler.php24
-rw-r--r--app/Core/Queue/QueueManager.php6
-rw-r--r--app/Event/FileEvent.php7
-rw-r--r--app/Event/ProjectFileEvent.php7
-rw-r--r--app/Event/TaskFileEvent.php7
-rw-r--r--app/EventBuilder/BaseEventBuilder.php23
-rw-r--r--app/EventBuilder/CommentEventBuilder.php48
-rw-r--r--app/EventBuilder/ProjectFileEventBuilder.php50
-rw-r--r--app/EventBuilder/SubtaskEventBuilder.php79
-rw-r--r--app/EventBuilder/TaskEventBuilder.php123
-rw-r--r--app/EventBuilder/TaskFileEventBuilder.php50
-rw-r--r--app/Filter/TaskPriorityFilter.php38
-rw-r--r--app/Formatter/TaskAutoCompleteFormatter.php9
-rw-r--r--app/Helper/TaskHelper.php2
-rw-r--r--app/Job/CommentEventJob.php50
-rw-r--r--app/Job/NotificationJob.php60
-rw-r--r--app/Job/ProjectFileEventJob.php45
-rw-r--r--app/Job/SubtaskEventJob.php48
-rw-r--r--app/Job/TaskEventJob.php75
-rw-r--r--app/Job/TaskFileEventJob.php45
-rw-r--r--app/Locale/bs_BA/translations.php11
-rw-r--r--app/Locale/cs_CZ/translations.php11
-rw-r--r--app/Locale/da_DK/translations.php11
-rw-r--r--app/Locale/de_DE/translations.php11
-rw-r--r--app/Locale/el_GR/translations.php11
-rw-r--r--app/Locale/es_ES/translations.php11
-rw-r--r--app/Locale/fi_FI/translations.php11
-rw-r--r--app/Locale/fr_FR/translations.php11
-rw-r--r--app/Locale/hu_HU/translations.php11
-rw-r--r--app/Locale/id_ID/translations.php11
-rw-r--r--app/Locale/it_IT/translations.php11
-rw-r--r--app/Locale/ja_JP/translations.php11
-rw-r--r--app/Locale/ko_KR/translations.php103
-rw-r--r--app/Locale/my_MY/translations.php11
-rw-r--r--app/Locale/nb_NO/translations.php11
-rw-r--r--app/Locale/nl_NL/translations.php11
-rw-r--r--app/Locale/pl_PL/translations.php11
-rw-r--r--app/Locale/pt_BR/translations.php11
-rw-r--r--app/Locale/pt_PT/translations.php13
-rw-r--r--app/Locale/ru_RU/translations.php11
-rw-r--r--app/Locale/sr_Latn_RS/translations.php11
-rw-r--r--app/Locale/sv_SE/translations.php11
-rw-r--r--app/Locale/th_TH/translations.php11
-rw-r--r--app/Locale/tr_TR/translations.php11
-rw-r--r--app/Locale/zh_CN/translations.php11
-rw-r--r--app/Model/ColumnModel.php12
-rw-r--r--app/Model/CommentModel.php9
-rw-r--r--app/Model/FileModel.php22
-rw-r--r--app/Model/GroupModel.php19
-rw-r--r--app/Model/NotificationModel.php10
-rw-r--r--app/Model/ProjectFileModel.php9
-rw-r--r--app/Model/SubtaskModel.php23
-rw-r--r--app/Model/TaskCreationModel.php32
-rw-r--r--app/Model/TaskFileModel.php23
-rw-r--r--app/Model/TaskModificationModel.php62
-rw-r--r--app/Model/TaskPositionModel.php28
-rw-r--r--app/Model/TaskProjectMoveModel.php7
-rw-r--r--app/Model/TaskStatusModel.php8
-rw-r--r--app/Schema/Sql/mysql.sql6
-rw-r--r--app/Schema/Sql/postgres.sql10
-rw-r--r--app/ServiceProvider/ActionProvider.php2
-rw-r--r--app/ServiceProvider/FilterProvider.php2
-rw-r--r--app/ServiceProvider/JobProvider.php57
-rw-r--r--app/ServiceProvider/QueueProvider.php6
-rw-r--r--app/Subscriber/BaseSubscriber.php24
-rw-r--r--app/Subscriber/NotificationSubscriber.php12
-rw-r--r--app/Subscriber/ProjectDailySummarySubscriber.php2
-rw-r--r--app/Subscriber/ProjectModificationDateSubscriber.php6
-rw-r--r--app/Template/column/create.php2
-rw-r--r--app/Template/column/edit.php2
-rw-r--r--app/Template/config/about.php2
-rw-r--r--app/Template/dashboard/tasks.php2
-rw-r--r--app/Template/event/comment_delete.php11
-rw-r--r--app/Template/event/comment_update.php3
-rw-r--r--app/Template/event/subtask_delete.php15
-rw-r--r--app/Template/notification/comment_delete.php7
-rw-r--r--app/Template/notification/subtask_delete.php11
-rw-r--r--app/Template/project_view/show.php2
-rw-r--r--app/Template/user_modification/show.php8
-rw-r--r--app/Template/user_view/sidebar.php68
-rw-r--r--app/User/OAuthUserProvider.php14
-rw-r--r--app/common.php1
-rw-r--r--app/constants.php2
-rw-r--r--assets/css/app.min.css2
-rw-r--r--assets/css/print.min.css2
-rw-r--r--assets/css/src/markdown.css4
-rw-r--r--assets/css/vendor.min.css2
-rw-r--r--assets/js/vendor.min.js5
-rw-r--r--composer.json2
-rw-r--r--composer.lock14
-rw-r--r--doc/docker.markdown2
-rw-r--r--doc/heroku.markdown4
-rw-r--r--doc/installation.markdown2
-rw-r--r--doc/plugin-authentication.markdown2
-rw-r--r--doc/plugin-group-provider.markdown2
-rw-r--r--doc/plugins.markdown2
-rw-r--r--doc/ru_RU/2fa.markdown37
-rw-r--r--doc/ru_RU/analytics-tasks.markdown37
-rw-r--r--doc/ru_RU/analytics.markdown95
-rw-r--r--doc/ru_RU/api-json-rpc.markdown78
-rw-r--r--doc/ru_RU/application-configuration.markdown54
-rw-r--r--doc/ru_RU/assets.markdown53
-rw-r--r--doc/ru_RU/automatic-actions.markdown128
-rw-r--r--doc/ru_RU/board-collapsed-expanded.markdown31
-rw-r--r--doc/ru_RU/board-configuration.markdown39
-rw-r--r--doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown19
-rw-r--r--doc/ru_RU/board-show-hide-columns.markdown25
-rw-r--r--doc/ru_RU/bruteforce-protection.markdown37
-rw-r--r--doc/ru_RU/calendar-configuration.markdown59
-rw-r--r--doc/ru_RU/calendar.markdown31
-rw-r--r--doc/ru_RU/centos-installation.markdown127
-rw-r--r--doc/ru_RU/cli.markdown331
-rw-r--r--doc/ru_RU/closing-tasks.markdown30
-rw-r--r--doc/ru_RU/cloudron.markdown45
-rw-r--r--doc/ru_RU/coding-standards.markdown64
-rw-r--r--doc/ru_RU/config.markdown523
-rw-r--r--doc/ru_RU/contributing.markdown96
-rw-r--r--doc/ru_RU/create-tasks-by-email.markdown61
-rw-r--r--doc/ru_RU/creating-projects.markdown62
-rw-r--r--doc/ru_RU/creating-tasks.markdown42
-rw-r--r--doc/ru_RU/cronjob.markdown41
-rw-r--r--doc/ru_RU/currency-rate.markdown43
-rw-r--r--doc/ru_RU/custom-filters.markdown36
-rw-r--r--doc/ru_RU/debian-installation.markdown104
-rw-r--r--doc/ru_RU/docker.markdown134
-rw-r--r--doc/ru_RU/duplicate-move-tasks.markdown79
-rw-r--r--doc/ru_RU/editing-projects.markdown25
-rw-r--r--doc/ru_RU/email-configuration.markdown156
-rw-r--r--doc/ru_RU/env.markdown21
-rw-r--r--doc/ru_RU/ext-search.markdown235
-rw-r--r--doc/ru_RU/faq.markdown162
-rw-r--r--doc/ru_RU/freebsd-installation.markdown187
-rw-r--r--doc/ru_RU/gantt-chart-projects.markdown60
-rw-r--r--doc/ru_RU/gantt-chart-tasks.markdown66
-rw-r--r--doc/ru_RU/genindex.markdown15
-rw-r--r--doc/ru_RU/groups.markdown35
-rw-r--r--doc/ru_RU/heroku.markdown72
-rw-r--r--doc/ru_RU/ical.markdown111
-rw-r--r--doc/ru_RU/index.markdown248
-rw-r--r--doc/ru_RU/installation.markdown117
-rw-r--r--doc/ru_RU/kanban-vs-todo-and-scrum.markdown75
-rw-r--r--doc/ru_RU/keyboard-shortcuts.markdown99
-rw-r--r--doc/ru_RU/ldap-authentication.markdown327
-rw-r--r--doc/ru_RU/ldap-configuration-examples.markdown438
-rw-r--r--doc/ru_RU/ldap-group-sync.markdown153
-rw-r--r--doc/ru_RU/ldap-parameters.markdown49
-rw-r--r--doc/ru_RU/ldap-profile-picture.markdown46
-rw-r--r--doc/ru_RU/link-labels.markdown23
-rw-r--r--doc/ru_RU/mysql-configuration.markdown128
-rw-r--r--doc/ru_RU/nice-urls.markdown233
-rw-r--r--doc/ru_RU/nitrous.markdown16
-rw-r--r--doc/ru_RU/notifications.markdown111
-rw-r--r--doc/ru_RU/plugin-directory.markdown38
-rw-r--r--doc/ru_RU/plugins.markdown167
-rw-r--r--doc/ru_RU/postgresql-configuration.markdown92
-rw-r--r--doc/ru_RU/project-configuration.markdown105
-rw-r--r--doc/ru_RU/project-permissions.markdown55
-rw-r--r--doc/ru_RU/project-types.markdown27
-rw-r--r--doc/ru_RU/project-views.markdown154
-rw-r--r--doc/ru_RU/recurring-tasks.markdown67
-rw-r--r--doc/ru_RU/requirements.markdown137
-rw-r--r--doc/ru_RU/reverse-proxy-authentication.markdown138
-rw-r--r--doc/ru_RU/roles.markdown44
-rw-r--r--doc/ru_RU/rss.markdown58
-rw-r--r--doc/ru_RU/screenshots.markdown74
-rw-r--r--doc/ru_RU/search.markdown24
-rw-r--r--doc/ru_RU/sharing-projects.markdown82
-rw-r--r--doc/ru_RU/sqlite-database.markdown96
-rw-r--r--doc/ru_RU/subtasks.markdown111
-rw-r--r--doc/ru_RU/suse-installation.markdown36
-rw-r--r--doc/ru_RU/swimlanes.markdown81
-rw-r--r--doc/ru_RU/syntax-guide.markdown246
-rw-r--r--doc/ru_RU/task-links.markdown93
-rw-r--r--doc/ru_RU/tests.markdown262
-rw-r--r--doc/ru_RU/time-tracking.markdown112
-rw-r--r--doc/ru_RU/transitions.markdown60
-rw-r--r--doc/ru_RU/translations.markdown155
-rw-r--r--doc/ru_RU/ubuntu-installation.markdown111
-rw-r--r--doc/ru_RU/update.markdown57
-rw-r--r--doc/ru_RU/usage-examples.markdown193
-rw-r--r--doc/ru_RU/user-management.markdown89
-rw-r--r--doc/ru_RU/user-mentions.markdown49
-rw-r--r--doc/ru_RU/user-types.markdown26
-rw-r--r--doc/ru_RU/vagrant.markdown51
-rw-r--r--doc/ru_RU/webhooks.markdown536
-rw-r--r--doc/ru_RU/what-is-kanban.markdown80
-rw-r--r--doc/ru_RU/windows-apache-installation.markdown253
-rw-r--r--doc/ru_RU/windows-iis-installation.markdown150
-rw-r--r--doc/update.markdown2
-rw-r--r--doc/web.config22
-rw-r--r--tests/units/Action/BaseActionTest.php10
-rw-r--r--tests/units/Action/CommentCreationMoveTaskColumnTest.php6
-rw-r--r--tests/units/Action/TaskAssignCategoryColorTest.php18
-rw-r--r--tests/units/Action/TaskAssignCategoryLinkTest.php43
-rw-r--r--tests/units/Action/TaskAssignColorCategoryTest.php18
-rw-r--r--tests/units/Action/TaskAssignColorColumnTest.php18
-rw-r--r--tests/units/Action/TaskAssignColorPriorityTest.php18
-rw-r--r--tests/units/Action/TaskAssignColorUserTest.php18
-rw-r--r--tests/units/Action/TaskAssignCurrentUserColumnTest.php26
-rw-r--r--tests/units/Action/TaskAssignSpecificUserTest.php17
-rw-r--r--tests/units/Action/TaskCloseColumnTest.php18
-rw-r--r--tests/units/Action/TaskCloseNoActivityColumnTest.php49
-rw-r--r--tests/units/Action/TaskCloseTest.php15
-rw-r--r--tests/units/Action/TaskDuplicateAnotherProjectTest.php18
-rw-r--r--tests/units/Action/TaskEmailTest.php19
-rw-r--r--tests/units/Action/TaskMoveAnotherProjectTest.php17
-rw-r--r--tests/units/Action/TaskMoveColumnAssignedTest.php18
-rw-r--r--tests/units/Action/TaskMoveColumnCategoryChangeTest.php31
-rw-r--r--tests/units/Action/TaskMoveColumnUnAssignedTest.php31
-rw-r--r--tests/units/Action/TaskOpenTest.php15
-rw-r--r--tests/units/Action/TaskUpdateStartDateTest.php17
-rw-r--r--tests/units/Auth/TotpAuthTest.php5
-rw-r--r--tests/units/Base.php1
-rw-r--r--tests/units/Core/Filter/LexerBuilderTest.php46
-rw-r--r--tests/units/Core/Filter/OrCriteriaTest.php5
-rw-r--r--tests/units/Core/Http/RequestTest.php3
-rw-r--r--tests/units/EventBuilder/CommentEventBuilderTest.php37
-rw-r--r--tests/units/EventBuilder/ProjectFileEventBuilderTest.php33
-rw-r--r--tests/units/EventBuilder/SubtaskEventBuilderTest.php62
-rw-r--r--tests/units/EventBuilder/TaskEventBuilderTest.php100
-rw-r--r--tests/units/EventBuilder/TaskFileEventBuilderTest.php36
-rw-r--r--tests/units/Filter/TaskPriorityFilterTest.php47
-rw-r--r--tests/units/Formatter/TaskAutoCompleteFormatterTest.php34
-rw-r--r--tests/units/Job/CommentEventJobTest.php52
-rw-r--r--tests/units/Job/ProjectFileEventJobTest.php43
-rw-r--r--tests/units/Job/SubtaskEventJobTest.php52
-rw-r--r--tests/units/Job/TaskEventJobTest.php189
-rw-r--r--tests/units/Job/TaskFileEventJobTest.php46
-rw-r--r--tests/units/Model/CommentModelTest.php (renamed from tests/units/Model/CommentTest.php)4
-rw-r--r--tests/units/Model/GroupModelTest.php (renamed from tests/units/Model/GroupTest.php)11
-rw-r--r--tests/units/Model/SubtaskModelTest.php70
-rw-r--r--tests/units/Model/TaskCreationModelTest.php2
-rw-r--r--tests/units/Model/TaskFinderModelTest.php142
-rw-r--r--tests/units/Model/TaskFinderTest.php115
-rw-r--r--tests/units/Model/TaskModificationModelTest.php13
-rw-r--r--tests/units/Model/TaskPositionModelTest.php (renamed from tests/units/Model/TaskPositionTest.php)394
-rw-r--r--tests/units/Model/TaskProjectMoveModelTest.php2
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)
diff --git a/ChangeLog b/ChangeLog
index 7e6662d6..a1e39436 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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
--------------
diff --git a/Dockerfile b/Dockerfile
index 518f4685..99e940bb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/Makefile b/Makefile
index 4595481d..a96846a1 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
index 49735475..d14982f0 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
Kanboard
========
-[![Build Status](https://travis-ci.org/fguillot/kanboard.svg)](https://travis-ci.org/fguillot/kanboard)
-[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/fguillot/kanboard/badges/quality-score.png?s=2b6490781608657cc8c43d02285bfafb4f489528)](https://scrutinizer-ci.com/g/fguillot/kanboard/)
-[![SensioLabsInsight](https://insight.sensiolabs.com/projects/5e50750e-fc62-4a1f-b02a-71991123a2a7/mini.png)](https://insight.sensiolabs.com/projects/5e50750e-fc62-4a1f-b02a-71991123a2a7)
-[![Gitter chat](https://badges.gitter.im/kanboard/gitter.png)](https://gitter.im/kanboard/kanboard)
+[![Build Status](https://travis-ci.org/kanboard/kanboard.svg?branch=master)](https://travis-ci.org/kanboard/kanboard)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/kanboard/kanboard/badges/quality-score.png?b=master)](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
------------------------
diff --git a/app.json b/app.json
index 4d41737b..76d9b8e0 100644
--- a/app.json
+++ b/app.json
@@ -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={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};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">&times;</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">&times;</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={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};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">&times;</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">&times;</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:
-[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/fguillot/kanboard)
+[![Deploy](https://www.herokucdn.com/deploy/button.png)](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. Секретный ключ сгенерируется для вас
+
+![2FA](https://kanboard.net/screenshots/documentation/2fa.png)
+
+Рисунок. Двухуровневая аутентификация.
+
+
+- Вы должны сохранить секретный ключ в вашей 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 @@
+Аналитика для задач
+===================
+
+На странице детального просмотра задачи, в левом боковом меню, для каждой задачи имеется раздел аналитики.
+
+Затраченное время и время цикла
+-------------------------------
+
+![Lead and cycle time](https://kanboard.net/screenshots/documentation/task-lead-cycle-time.png)
+
+Рисунок. Затраченное время и время цикла
+
+
+- Затраченное время - время между созданием задачи и датой завершения (закрытие задачи).
+- Время цикла - время между началом испольнения задачи и датой завершения.
+- Если задача не закрыта, то для расчета используется текущее время вместо даты завершения.
+- Если дата начала выполнения задачи не указана, то время цикла не может быть расчитано.
+
+
+**Заметка**: Вы можете настроить автоматическое создание даты начала выполения задачи, когда вы перемещаете задачу в определенную колонку.
+
+
+Время затраченное в каждой колонке
+----------------------------------
+
+![Time spent into each column](https://kanboard.net/screenshots/documentation/time-into-each-column.png)
+
+Рисунок. Время затраченное в каждой колонке
+
+
+
+- Этот график показывает сколько времени задача находилась в каждой колонке.
+- Затраченное время расчитывается до закрытия задачи.
+
+
+[Русская документация 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 @@
+Аналитика
+=========
+
+Каждый проект имеет анлитический раздел. В зависимости от того как вы используете Канборд, вы можете видеть подобные отчеты:
+
+Перераспределение(загрузка) пользователей
+-----------------------------------------
+
+![User repartition](https://kanboard.net/screenshots/documentation/user-repartition.png)
+
+Перераспределение(загрузка) пользователей
+
+
+Круговая диаграмма, представленная выше, показыает количество открытых задач назначенных определенным пользователям.
+
+
+Распределение задач
+-------------------
+
+![Task distribution](https://kanboard.net/screenshots/documentation/task-distribution.png)
+
+Рисунок. Распределение задач
+
+
+
+На рисунке выше, представлена круговая диаграмма, которая показывает количество открытых задач в определенных колонках.
+
+
+
+Накопительная диаграмма
+-----------------------
+
+![Cumulative flow diagram](https://kanboard.net/screenshots/documentation/cfd.png)
+
+Рисунок. Накопительная диаграмма
+
+
+- Эта диаграмма отображает количество задач выполненных в каждой колонке в определенный промежуток времени.
+- Счетчик задач записывается для каждой колонки каждый день.
+- Если вы хотите исключить закрытые задачи, измените [глобальные настройки проекта](project-configuration.markdown).
+
+
+Заметка: Для того чтобы увидеть этот график, вам нужно иметь, как минимум, данные за два дня.
+
+
+Диаграмма сгорания
+------------------
+
+![Burndown chart](https://kanboard.net/screenshots/documentation/burndown-chart.png)
+
+Рисунок. Диаграмма сгорания
+
+
+
+[Диаграмма сгорания](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) доступна для каждого проекта.
+
+
+- Эта диаграмма отображает время затраченное на выполнение работы.
+- Канборд использует историю задач для генерации этой диаграммы.
+- Сумма историй задач для каждой колонки пересчитывается каждый день.
+
+Среднее время затраченное в каждой колонке
+------------------------------------------
+
+![Average time spent into each column](https://kanboard.net/screenshots/documentation/average-time-spent-into-each-column.png)
+
+Рисунок. Среднее время затраченное в каждой колонке
+
+
+Этот график показывает среднее время затраченное в каждой колонке для последних 1000 задач.
+
+- Канборд использует для подсчета данных переходы задач между колонками.
+- Затраченное время подсчитывается до закрытия задачи.
+
+Среднее время выполнения и время цикла
+--------------------------------------
+
+![Average time spent into each column](https://kanboard.net/screenshots/documentation/average-lead-cycle-time.png)
+
+Рисунок. Среднее время затраченное в каждой колонке
+
+Эта диаграмма показывает Среднее время выполнения и цикла для последних 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 @@
+Настройки приложения
+====================
+
+Некоторые параметры для приложения могут быть изменены на странице настроек. Только администратор может сделать эти настройки.
+Выберите в правом выпадающем меню **Настройки**, затем в слева выберите **Настройки приложения**.
+
+![Application settings](https://kanboard.net/screenshots/documentation/application-settings.png)
+
+Рисунок. Настройки приложения
+
+
+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 "Ссылка на этот заголовок")
+---------------------------------------------------------------------------
+
+
+Нажмите на ссылку **Добавить новое действие**.
+
+![Automatique action](screenshots/automatic-action-creation.png)
+
+Рисунок. Автоматическое действие.
+
+
+- Выберете действие
+- Затем, выберете событие
+- И в завершении, задайте параметр
+
+
+Список доступных действий[¶](#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 "Ссылка на этот заголовок")
+-------------------------------------------------------------
+
+
+![Tasks collapsed](screenshots/board-collapsed-mode.png)
+
+Рисунок. Задачи представлены в компактном виде
+
+- Если для задачи назначен исполнитель, то инициалы исполнителя показываются рядом с номером задачи;
+- Если заголовок задачи слишком длинный, вы можете подвести курсор мышки над задачей и полный заголовок задачи отобразится во всплывающем окне.
+
+
+
+Развернутый вид[¶](#expanded-mode "Ссылка на этот заголовок")
+-------------------------------------------------------------
+
+
+![Tasks expanded](screenshots/board-expanded-mode.png)
+Рисунок. Развернутый вид
+
+
+
+
+[Русская документация 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 @@
+Настройка Доски
+===============
+
+
+В правом верхнем выпадающем меню выберите **Настройки**, затем, слева, выберите **Настройки Доски**.
+
+![Board settings](https://kanboard.net/screenshots/documentation/board-settings.png)
+
+Рисунок. Настройка Доски
+
+
+Подстветка задач[¶](#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 @@
+Горизонтальная прокрутка и компактный вид
+=========================================
+
+Когда ширины экрана не хватает для отображения всех колонок, то внизу появляется горизонтальная прокрутка.
+
+Однако, можно переключится на компактный вид доски для отображения всех колонок на вашем экране.
+
+
+![Switch to compact mode](screenshots/board-compact-mode.png)
+
+Рисунок. Переключение на компактное представление.
+
+Переключится между горизонтальной прокруткой и компактным видом можно с помощью горячей клавиши **“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 @@
+Показать и скрыть колонки на Доске
+==================================
+
+Вы можете показать и скрыть колонки на Доске очень просто:
+
+![Hide a column](screenshots/hide-column.png)
+
+Рисунок. Спрятать колонку.
+
+
+Чтобы скрыть (спрятать) колонку , откройте выпадающее меню колонки.
+
+![Show a column](screenshots/show-column.png)
+
+Рисунок.Показать колонку.
+
+
+Для отображения скрытой колонки нажмите “иконку плюс”
+
+
+
+
+
+[Русская документация 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 @@
+Настройки календаря
+===================
+
+В правом верхнем выпадающем меню выберите **Настройки**, затем, слева, выберите **Настройки календаря**.
+
+
+![Calendar settings](https://kanboard.net/screenshots/documentation/calendar-settings.png)
+
+Рисунок. Настройки календаря
+
+
+В Канборде имеется два вида Календаря:
+
+- Календарь проекта
+- Пользовательский календарь (доступен в левом меню Инфопанели)
+
+
+Календарь проекта[¶](#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 @@
+Календарь
+=========
+
+
+Календарь может быть представлен в двух видах:
+
+- Представление в проекте с использование фильтров (доступно на Доске)
+- Пользовательское представление (доступно в рабочей панели и в пользовательском разделе)
+
+В Календаре можно увидеть следующую информацию:
+
+- Задачи с “датой испольнения”, отображаются наверху. **Дата испольнения может быть изменена перемещением задачи на другой день**.
+- Задачи с датой создания или датой начала. **Эти события не могут быть изменены в календаре**.
+- Отслеживание времени подзадачи. Все записанные временные диапазоны будут отображены в Календаре.
+- Подсчеты, прогнозы затрачиваемого время на подзадачу.
+
+![Calendar](https://kanboard.net/screenshots/documentation/calendar.png)
+
+Рисунок. Календарь
+
+
+Настроки Календаря могут быть изменены на странице **Настройки**
+
+Заметка: Дата исполения не содержит информацию о времени.
+
+
+
+
+
+[Русская документация 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** в любой форме поиска или просто выбрать фильтр “Закрытые задачи” в выпадающем меню.
+
+Имеется два пути для закрытия задачи: - На Доске выбрать задачу и выпадающем меню выбрать **Закрыть задачу**
+
+![Close a task from drop-down menu](https://kanboard.net/screenshots/documentation/menu-close-task.png)
+
+Рисунок. Закрытие задачи, используя выпадающее меню.
+
+
+или - Используя детальное представление задачи, выбрать **Закрыть задачу** в меню боковой панели (слева)
+
+
+![Close task](https://kanboard.net/screenshots/documentation/closing-tasks.png)
+
+Рисунок. Закрытие задачи.
+
+
+
+**Заметка**: Когда вы закрываете задачу, у всех не выполненных подзадач будет изменен статус на “Выполнено”
+
+
+
+
+[Русская документация 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) приватный смартсервер, на котором вы можете установить веб приложения, такие как Канборд. Вы можете установить Канборд в определенном домене, при этом каждой инсталяции создавается резервная копия и поддерживается новая версия Канборда автоматически.
+
+
+
+[![Install](https://cloudron.io/img/button.svg)](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 "Ссылка на этот заголовок")
+------------------------------------------------------------------------------------------------------------------
+
+- Только пользователи с ролью администратор и менеджер могут создавать такие проекты
+- Можно добавлять к проекту пользователей и группы
+
+На рабочей панели нажмите ссылку **Новый проект**:
+
+![Project creation form](screenshots/new-project.png)
+
+Рисунок. Форма создания проекта.
+
+
+Теперь надо только добавить название для проекта! Легко, не правда ли?
+
+
+Создание приватного проекта[¶](#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 @@
+Создание задач
+==============
+
+
+На Доске нажмите значок плюс рядом с названием колонки:
+
+
+![Task creation from the board](https://kanboard.net/screenshots/documentation/task-creation-board.png)
+
+Рисунок. Создание задачи на Доске
+
+
+Далее появится форма создания задачи:
+
+![Task creation form](https://kanboard.net/screenshots/documentation/task-creation-form.png)
+
+Рисунок. Форма создания задачи.
+
+
+Только поле **Название** является обязательным полем для заполнения.
+
+
+Описание полей:
+
+- **Название**: Название вашей задачи, которое будет отображаться на доске.
+- **Описание**: Позволяет вам добавить больше информации о задаче, содержимое может содержать синтаксис [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 @@
+Курсы валют
+===========
+
+
+Каждый пользователь может иметь предопределенный ежечасный курс для разных валют. Если вы хотите вручную занести курсы валют, то вы можете указать ставку в соответсвии с курсом.
+
+Эта опция используются для расчета бюджета проекта.
+
+![Currency Rate](https://kanboard.net/screenshots/documentation/currency-rate.png)
+
+Рисунок. Курсы валют
+
+
+Для настроек курса валют выберите, справа вверху в выпадающем меню, **Настройки** -\> затем, слева, **Курсы валют**.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[Русская документация 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 "Ссылка на этот заголовок")
+----------------------------------------------------------------
+
+
+Перейдите в **Меню** -\> **Пользовательские фильтры** или **Меню** -\> **Настройки** -\> **Пользовательские фильтры**
+
+![Custom Filter Creation](https://kanboard.net/screenshots/documentation/custom-filter-creation.png)
+
+Рисунок. Создание пользовательского фильтра.
+
+
+
+Созданый фильтр появится на Доске рядом со стандартными фильтрами
+
+![Custom Filter Dropdown](https://kanboard.net/screenshots/documentation/custom-filter-dropdown.png)
+
+Рисунок. Выпадающий список - Пользовательский фильтр.
+
+
+
+
+
+
+
+
+[Русская документация 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 "Ссылка на этот заголовок")
+-------------------------------------------------------------------------------------------------------------
+
+
+Перейдите в детальное представление задачи и выберите в боковой панели (слева) **Клонировать**.
+
+![Task Duplication](https://kanboard.net/screenshots/documentation/task-duplication.png)
+
+Рисунок. Создание копии задачи.
+
+
+Новая задача будет создана с теми же свойствами как и у оригинальной задачи.
+
+
+Создание копии задачи в другой проект[¶](#duplicate-a-task-to-another-project "Ссылка на этот заголовок")
+---------------------------------------------------------------------------------------------------------
+
+
+Перейдите в детальное представление задачи и выберите в боковом меню (слева) **Клонировать в другой проект**.
+
+![Task Duplication Another Project](https://kanboard.net/screenshots/documentation/task-duplication-another-project.png)
+
+Рисунок. Создание копии задачи в другой проект.
+
+
+При выборе проекта в выпадающем списке, показываются только те проекты в которых вы являетесь участниками.
+
+Перед тем как скопировать задачу, Канборд просит вас указать свойства проекта (куда будет копироваться), потому что проекты могуг иметь разные столбцы, дорожки и т.д.
+
+Вам нужно указать:
+
+- Дорожку, в которую скопируется задача
+- Колонку
+- Категорию
+- Испольнителя
+
+Перемещение задачи в другой проект[¶](#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 @@
+Редактирование проектов
+=======================
+
+
+Проект может быть переименован и выключен в любое время
+
+Для переименования проекта нажмите на ссылку **“Изменить проект”** (для перехода выберите **Меню** -\> **Настройки**)
+
+
+![Project edition](screenshots/project-edition.png)
+
+Рисунок. Изменение проекта.
+
+- Дата начала и дата завершения используются при генерации диаграммы Ганта
+- Описание отображается как подсказка на Доске и на странице со списком проектов
+- Администраторы и менеджеры проекта могут сделать приватный проект доступным для других пользователей установив галочку в чекбоксе **“Приватный проект”**
+- Вы можете сделать публичный проект приватным.
+
+Внимание: Когда вы делаете приватный проект из публичного, все пользователи ранее присоединенные к проекту будут иметь доступ. Ограничьте список пользователей для вашего приватного проекта.
+
+
+
+
+[Русская документация 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 выглядит криво (&amp;)[¶](#page-not-found-and-the-url-seems-wrong-amp "Ссылка на этот заголовок")
+---------------------------------------------------------------------------------------------------------------------------
+
+- UTL выглядит как `/?controller=auth&amp;action=login&amp;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 @@
+Диаграмма Ганта для всех проектов
+=================================
+
+
+
+Цель диаграммы Ганта для проектов - показать прогресс проектов основанный на дате начала и дате завершения.
+
+
+
+- Диаграмма Ганта для проектов доступна из раздела **Управление проектами**
+
+
+
+- Только менеджеры проекта и администраторы имеют доступ в этот раздел
+
+
+
+- Менеджеры проекта могут видеть только те проекты, в которых они являются участниками
+
+
+
+- Приватные проекты не показывают этот график
+
+
+
+![Gantt Chart for all projects](https://kanboard.net/screenshots/documentation/gantt-chart-all-projects.png)
+
+Рисунок. Диаграмма Ганта для всех проектов
+
+
+
+- **Дата начала** и **дата завершения** проекта используются для рисования графика
+
+
+
+- Горизонтальные полосы (столбики) могут быть расширены (сжаты) и перемещены горизонтально с помощью мыши
+
+
+
+- Перемещение по вертикали невозможно
+
+
+
+- Полосы (столбики) проекта отображаются черным, когда проект не имеет дату начала и завершения
+
+
+
+- Информационная подсказка показывает список менеджеров и участников проекта
+
+
+
+
+
+
+
+
+
+
+[Русская документация 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 @@
+Диаграмма Ганта для задач
+=========================
+
+
+
+Цель диаграммы Ганта - показать время отведенное на задачу в заданном проекте.
+
+
+
+- Диаграмма Ганта доступна в рабочем окружении проекта
+
+
+
+- Только менеджеры проектов могут иметь доступ в этот раздел
+
+
+
+![Gantt Chart](https://kanboard.net/screenshots/documentation/gantt-chart-project.png)
+
+Рисунок. Диаграмма Ганта.
+
+
+
+- Дата начала и дата завершения задач используется для рисования диаграммы
+
+
+
+- Задача может быть расширена и перемещена горизонтально с помощью мыши
+
+
+
+- Перемещение по вертикали невозможно
+
+
+
+- Полоса (горизонтальный столбик) на диаграмме имеет такой же цвет как и задача
+
+
+
+- Каждая полоса отображает статус прогресса в процентах. Проценты подсчитываются с учетом позиции задачи в колонке на Доске.
+
+
+
+- Для соответсвия модели Kanban, задачи могут быть отсортированы в соответствии с позициями на доске или по дате начала
+
+
+
+- Новые задачи созданные через диаграмму Ганта будут показаны на Доске в первой колонке на первой позиции
+
+
+
+- Задачи отображаются черным цветом, если не указана дата начала или дата исполнения
+
+
+
+![Task not defined](https://kanboard.net/screenshots/documentation/gantt-chart-not-defined.png)
+
+Рисунок. Задача без указанных дат начала или завершения
+
+
+
+
+
+
+[Русская документация 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 @@
+Управление группами
+===================
+
+
+
+В Канборде каждый пользователь может быть членом одной или нескольких групп. Группа - это что-то вроде команды или организации.
+
+
+
+Только администраторы могут создавать новую группу и добавлять туда пользователей.
+
+
+
+Настройка групп доступна через **Управление пользователями** (выпадающее меню справа вверху) -\> **Просмотр всех пользователей**. Здесь вы можете создавать новые группы и добавлять пользователей в группы.
+
+
+
+![Group Management](screenshots/groups-management.png)
+
+Рисунок. Управление группами.
+
+
+
+Менеджеры проектов могут предоставлять доступ группам к проектам на [странице Разрешения проекта](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** и следовать руководству приведенному ниже:
+
+[![Deploy](https://www.herokucdn.com/deploy/button.png)](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 транслятор и вставьте
+
+![Add iCal subscription](https://kanboard.net/screenshots/documentation/apple-calendar-add-subscription.png)
+
+Рисунок. Добавление подписки на календарь.
+
+
+- Вы можете выбрать синхронизацию календаря с iCloud, чтобы иметь доступ к календарю с любых ваших устройств
+- Не забудьте указать частоту синхронизации
+
+
+![Edit iCal subscription](https://kanboard.net/screenshots/documentation/apple-calendar-edit-subscription.png)
+
+Рисунок. Редактирование подписки на календарь.
+
+
+Добавление вашего календаря из Канборд в Microsoft Outlook[¶](#adding-your-kanboard-calendar-to-microsoft-outlook "Ссылка на этот заголовок")
+---------------------------------------------------------------------------------------------------------------------------------------------
+
+![Outlook Add Internet Calendar](https://kanboard.net/screenshots/documentation/outlook-add-subscription.png)
+
+Рисунок. Добавление в Outlook календаря из интернет
+
+- Откройте Outlook
+- Выберите **Открыть календарь** -\> **Из интернета**
+- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте
+
+
+![Outlook Edit Internet Calendar](https://kanboard.net/screenshots/documentation/outlook-edit-subscription.png)
+
+Рисунок. Настройка интернет календаря в Outlook.
+
+
+Добавление вашего календаря из Канборд в Mozilla Thunderbird[¶](#adding-your-kanboard-calendar-to-mozilla-thunderbird "Ссылка на этот заголовок")
+-------------------------------------------------------------------------------------------------------------------------------------------------
+
+
+- Установите в Thunderbird Дополнение **Lightning**
+- Выберите **Файл** -\> **Новый календарь**
+- В диалоговом окне, выберите **Из сети**
+
+![Thunderbird Step 1](https://kanboard.net/screenshots/documentation/thunderbird-new-calendar-step1.png)
+
+Рисунок. Создание календаря в Thunderbird, шаг 1.
+
+
+
+- Выберите формат iCalendar
+- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте
+
+![Thunderbird Step 2](https://kanboard.net/screenshots/documentation/thunderbird-new-calendar-step2.png)
+
+Рисунок. Создание календаря в Thunderbird, шаг 2.
+
+- Выберите цвета и другие настройки и в завершении нажмите **Сохранить**.
+
+
+Добавление вашего календаря Канборд в календарь Google[¶](#adding-your-kanboard-calendar-to-google-calendar "Ссылка на этот заголовок")
+---------------------------------------------------------------------------------------------------------------------------------------
+
+- Нажмите иконку “треугольник” рядом с **Другие календари** (слева).
+- Вставьте ссылку на календарь из Канборд в поле “Добавить календарь друга”
+- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте
+
+
+![Google Calendar](https://kanboard.net/screenshots/documentation/google-calendar-add-subscription.png)
+
+Рисунок. Календарь 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 @@
+Настройки ссылки
+================
+
+
+Связи в задачах могут быть изменены в настройках приложения (**Настройки** -\> **Настройки ссылки**)
+
+![Link Labels](https://kanboard.net/screenshots/documentation/link-labels.png)
+
+Рисунок. Метки для ссылок.
+
+
+Каждая метка может иметь противоположное опеределение. Если нет противоположного значения, метка считается двунаправленная.
+
+![Link Label Creation](https://kanboard.net/screenshots/documentation/link-label-creation.png)
+
+Рисунок. Создание ссылки.
+
+
+
+
+
+[Русская документация 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), которая должна быть указана в вашем профиле, и Канборд должен быть настроен на отправку электронной почты.
+
+
+
+![Notifications](https://kanboard.net/screenshots/documentation/notifications.png)
+
+Рисунок. Уведомления
+
+
+
+Вы можете выбрать, удобный для вас, способ получения уведомлений:
+
+
+
+- Email
+
+
+
+- Веб (смотрите ниже)
+
+
+
+Для каждого проекта в котором вы являетесь участником, вы можете выбрать получение уведомления для:
+
+
+
+- Всех задач
+
+
+
+- Только для задач назначеных вам
+
+
+
+- Только для задач, которые создали вы
+
+
+
+- Только для задач, созданных вами и назначенных вам
+
+
+
+Также, вы можете выбрать проекты из которых хотите получать уведомления. По умолчанию - все проекты, в которых вы являетесь участником.
+
+
+
+Веб уведомления[¶](#web-notifications "Ссылка на этот заголовок")
+-----------------------------------------------------------------
+
+
+
+Веб уведомления доступны на рабочей панели **Мои уведомления** или вверху в виде иконки:
+
+
+
+![Web Notifications Icon](https://kanboard.net/screenshots/documentation/web-notifications-icon.png)
+
+Рисунок. Иконка веб уведомления.
+
+
+
+Уведомления отображаются списком. Вы можете выбрать действие **Пометить как прочитанное** для каждого сообщения или отметить сразу все.
+
+
+
+![Web Notifications](https://kanboard.net/screenshots/documentation/web-notifications.png)
+
+Рисунок. Веб уведомления.
+
+
+
+Таким образом, вы можете получать веб уведомления без использования электронной почты.
+
+
+
+
+
+
+
+
+
+[Русская документация 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 @@
+Настройки проекта
+=================
+
+
+
+В правом верхнем выпадающем меню выберите **Настройки**, затем выберите **Настройки проекта** слева.
+
+
+
+![Project settings](https://kanboard.net/screenshots/documentation/project-settings.png)
+
+Рисунок. Настройки проекта.
+
+
+
+Колонки по умолчанию для новых проектов[¶](#default-columns-for-new-projects "Ссылка на этот заголовок")
+--------------------------------------------------------------------------------------------------------
+
+
+
+Вы можете изменить колонки по умолчанию. Это удобно, когда вы создаете однотипные проекты с одними и теми же колонками.
+
+
+
+Название колонок должны быть разделены запятой.
+
+
+
+По умолчанию, в Канборде используются следующие колонки: Ожидающие, Готовые, В процессе, Выполнено
+
+
+
+Стандартные категории для новых проектов[¶](#default-categories-for-new-projects "Ссылка на этот заголовок")
+------------------------------------------------------------------------------------------------------------
+
+
+
+Категории не являются общими для приложения, но могут использоваться в проектах. Каждый проект может иметь разные категории.
+
+
+
+Однако, если вы постоянно создаете одни и теже категории для разных проектов, то вы можете задать список категорий, которые будут автоматически создаваться при создании проекта.
+
+
+
+Разрешена только одна подзадача в разработке одновременно для одного пользователя[¶](#allow-only-one-subtask-in-progress-at-the-same-time-for-a-user "Ссылка на этот заголовок")
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+
+
+Когда эта опция включена, пользователь может работать только с одной подзадачей одновременно.
+
+
+
+Если другая подзадача перейдет статус “В процессе”, то пользователь увидит следующее диалоговое окно:
+
+
+
+![Subtask user restriction](https://kanboard.net/screenshots/documentation/subtask-user-restriction.png)
+
+Рисунок. Ограничение пользовательских подзадач.
+
+
+
+Триггер автоматического отслеживания времени подзадач[¶](#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):
+
+
+
+- Менеджер проекта
+
+
+
+- Участник проекта
+
+
+
+- Наблюдатель проекта
+
+
+
+Только администратор может иметь полный доступ ко всему.
+
+
+
+Назначение ролей доступно через **Меню -\> Настройки -\> Разрешения**
+
+
+
+![Project Permissions](screenshots/project-permissions.png)
+
+Рисунок. Права доступа к проекту
+
+
+
+Если вы выберите **Разрешить любому**, то все пользователи Канборд будут считаться участниками Проекта. В таком случае, нет необходимости назначать роли. Потому что, разрешения, назначенные пользователям и группам, на доступ к Проекту не будут работать.
+
+
+
+Приватный проект не позволяет устанавливать разрешения.
+
+
+
+
+
+
+
+
+
+[Русская документация 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 "Ссылка на этот заголовок")
+----------------------------------------------------------------
+
+
+
+![Board view](screenshots/board-view.png)
+
+Рисунок. Представление зачад в виде доски
+
+
+
+- В этом представлении вы можете мышкой перемещать задачи между колонками.
+
+
+
+- Также, для перемещения задач на доске, можно использовать горячие клавиши **“v b”**.
+
+
+
+- Затемнения вокруг задачи показывает активную или измененную задачу.
+
+
+
+![Board Task Limit](screenshots/board-task-limit.png)
+
+Рисунок. Лимит задач на Доске
+
+
+
+Когда лимит задач для колонки достигнут, тогда фон колонки становится красный. Это означает, что слишком много задач выполняются одновременно.
+
+
+
+[Ознакомится с настройками Доски](board-configuration.markdown)
+
+
+
+Представление - Календарь[¶](#calendar-view "Ссылка на этот заголовок")
+-----------------------------------------------------------------------
+
+
+
+![Calendar view](screenshots/calendar-view.png)
+
+Рисунок. Представление в виде календаря
+
+
+
+- В этом представлении вы можете видеть задачи на конкретные даты.
+
+
+
+- Вы можете сделать настройки, которые позволят вам видеть задачи в работе.
+
+
+
+- Вы можете использовать горячие клавиши **“v c”** для перехода на представление Календарь.
+
+
+
+- [Ознакомится с настройками Календаря](calendar-configuration.markdown)
+
+
+
+Представление - Список[¶](#list-view "Ссылка на этот заголовок")
+----------------------------------------------------------------
+
+
+
+![List view](https://kanboard.net/screenshots/documentation/list-view.png)
+
+Рисунок. Представление списком.
+
+
+
+- С помощью этого представления все результаты отображаются в виде таблицы.
+
+
+
+- Для быстрого перехода на представление Список вы можете использовать горячие клавиши **“v l”**.
+
+
+
+Представление - Гант.[¶](#gantt-view "Ссылка на этот заголовок")
+----------------------------------------------------------------
+
+
+
+![Gantt view](screenshots/gantt-view.png)
+
+Рисунок. Представление диаграммой Ганта.
+
+
+
+- Представление Гант отображает задачи горизонтальными графиками
+
+
+
+- Для построения графика используется дата начала и срок выполнения
+
+
+
+- Для быстрого перехода к представлению Гант используйте горячие клавиши : **“v g”**
+
+
+
+Обзор Проекта[¶](#project-overview "Ссылка на этот заголовок")
+--------------------------------------------------------------
+
+
+
+![Project overview](screenshots/project-view.png)
+
+Рисунок. Представления проекта
+
+
+
+- Отображает описание проекта
+
+
+
+- Показывает прикрепленные и загруженные документы проекта
+
+
+
+- Показывает список участников проекта
+
+
+
+- Показывает последнюю активность в проекте
+
+
+
+
+
+
+
+
+
+
+
+
+
+[Русская документация 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 "Ссылка на этот заголовок")
+-------------------------------------------------------
+
+
+
+Перейдите на страницу детального представления задачи или используйте выпадающее меню на доске, выберите **Редактировать повторы**.
+
+
+
+![Recurring task](https://kanboard.net/screenshots/documentation/recurring-tasks.png)
+
+Рисунок. Редактировать повторы.
+
+
+
+В редактировании повторов имеется выбор 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 "Ссылка на этот заголовок")
+-------------------------------------------------------------------------------------------------------
+
+
+
+Перейдите в **Настройки проекта** -\> **Общий доступ**
+
+
+
+![Disable public access](https://kanboard.net/screenshots/documentation/project-disable-sharing.png)
+
+Рисунок. Выключение общего доступа.
+
+
+
+Включение/выключение 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 @@
+Добавление снимков экрана (скриншота)
+=====================================
+
+
+
+Для экономии времени вы можете копировать и вставлять изображения прямо в Канборде. Загруженные изображения прикрепляются к задаче.
+
+
+
+Например, очень удобно для решения проблемы прикрепить снимок экрана.
+
+
+
+Вы можете добавить снимок экрана прямо из Доски нажав на выпадающее меню задачи и выбрав **Прикрепить картинку** или на странице детального просмотра задачи.
+
+
+
+![Drop-down screenshot menu](https://kanboard.net/screenshots/documentation/dropdown-screenshot.png)
+
+
+
+Рисунок. Выпадающее меню задачи - **Прикрепить картинку**.
+
+
+
+Для добавления нового снимка экрана (скриншота), сделайте снимок экрана (нажмите клавиши Ctrl+PrtScn) и вставьте его используя сочетания клавиш CTRL+V или Command+V
+
+
+
+![Screenshot page](https://kanboard.net/screenshots/documentation/task-screenshot.png)
+
+Рисунок. Прикрепить картинку.
+
+
+
+В 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 "Ссылка на этот заголовок")
+-----------------------------------------------------------------------------
+
+
+
+Выберете ваш проект, затем нажмите на ссылку **“Общий доступ”** и в завершении нажмите на кнопку **“Включить общий доступ”**
+
+
+
+![Enable public access](screenshots/project-enable-sharing.png)
+
+Рисунок. Включение общего доступа
+
+
+
+Когда общий доступ к проекту включен, сгенерируется несколько ссылок:
+
+
+
+- Ссылка для просмотра
+
+
+
+- RSS лента
+
+
+
+- iCalendar данные
+
+
+
+![Disable public access](screenshots/project-disable-sharing.png)
+
+Рисунок. Отключить общий доступ.
+
+
+
+Вы можете выключить общий доступ к проекту в любой момент.
+
+
+
+Каждый раз, когда вы включаете или выключаете общий доступ, генерируется новый ключ. **Доступ по предыдущей ссылке будет невозможен**.
+
+
+
+
+
+
+
+
+
+[Русская документация 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 "Ссылка на этот заголовок")
+--------------------------------------------------------------------
+
+
+
+В детальном представлении задачи, в левой боковой панели нажмите **Добавить подзадачу**:
+
+
+
+![Add a subtask](https://kanboard.net/screenshots/documentation/add-subtask.png)
+
+Рисунок. Добавление подзадачи.
+
+
+
+Вы, также, можете быстро добавить подзадачу нажав на заголовок:
+
+
+
+![Add a subtask from the task view](https://kanboard.net/screenshots/documentation/add-subtask-shortcut.png)
+
+Рисунок. Добавление подзадачи на странице детального просмотра задачи.
+
+
+
+Изменение статуса подзадачи[¶](#change-subtask-status "Ссылка на этот заголовок")
+---------------------------------------------------------------------------------
+
+
+
+Когда вы нажимаете на заголовок подзадачи стату меняется:
+
+
+
+![Subtask in progress](https://kanboard.net/screenshots/documentation/subtask-status-inprogress.png)
+
+Рисунок. Выполнение подзадачи.
+
+
+
+Иконка перед названием подзадачи обновляется в соответсвии со статусом.
+
+
+
+![Subtask done](https://kanboard.net/screenshots/documentation/subtask-status-done.png)
+
+Рисунок. Подзадача выполнена.
+
+
+
+**Заметка**: Когда задача закрыта, то все подзадачи меняют статус на **Выполнена**.
+
+
+
+Таймер подзадачи[¶](#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 "Ссылка на этот заголовок")
+----------------------------------------------------------------------
+
+
+
+![Swimlanes](screenshots/swimlanes.png)
+
+Рисунок. Дорожки
+
+
+
+- Вы можете свернуть дорожку нажав на иконку слева
+
+
+
+- “Стандатная дорожка” всегда расположена сверху
+
+
+
+Управление дорожками[¶](#managing-swimlanes "Ссылка на этот заголовок")
+-----------------------------------------------------------------------
+
+
+
+- Все проекты имеют дорожку по умолчанию - **Стандартная дорожка**
+
+
+
+- Если имеется больше одной дорожки, то на Доске будут показаны все имеющиеся дорожки.
+
+
+
+- Вы можете перемещать мышкой задачи между дорожками.
+
+
+
+Для настройки дорожек перейдите на страницу **настройки проекта** (Меню -\> Настройки) и нажмите **Дорожки** (слева).
+
+
+
+![Swimlanes Configuration](screenshots/swimlane-configuration.png)
+
+Рисунок. Настройка Дорожек.
+
+
+
+Теперь вы можете добавить новую дорожку или переименовать стандартную дорожку. Также, вы можете выключить дорожку или изменить расположение любой дорожки.
+
+
+
+- Стандартная дорожка всегда расположена сверху, но вы можете ее выключить и она не будет отображаться на Доске.
+
+
+
+- Выключенные дорожки не отображаются на Доске.
+
+
+
+- **Удаление дорожки не влечет за собой удаление расположенных на этой дорожке задач**, эти задачи будут перемещены в “Стандартную дорожку”.
+
+
+
+
+
+
+
+
+
+
+
+[Русская документация 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 @@
+Ссылки на задачи
+================
+
+
+
+Задачи могут быть созданы вместе с предопределенными связями:
+
+
+
+![Task Links](https://kanboard.net/screenshots/documentation/task-links.png)
+
+Рисунок. Ссылки на задачи
+
+
+
+Связи по умолчанию:
+
+
+
+- **относится к**
+
+
+
+- **блокирована**| блокирует
+
+
+
+- **блокирует** | блокирована
+
+
+
+- **дублирована** | дублирует
+
+
+
+- **дублирует** | дублирована
+
+
+
+- **является продолжением** | является началом для
+
+
+
+- **является началом для** | является продолжением
+
+
+
+- **часть вехи** | является вехой для
+
+
+
+- **является вехой для** | часть вехи
+
+
+
+- **исправлено** | исправляет
+
+
+
+- **исправляет** | исправлено
+
+
+
+Эти названия могут быть быть изменены в настройках приложения.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[Русская документация 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 "Ссылка на этот заголовок")
+-----------------------------------------------------------------------------------------
+
+
+
+![Task time tracking](https://kanboard.net/screenshots/documentation/task-time-tracking.png)
+
+Рисунок. Отслеживание времени испольнения задач
+
+
+
+Задачи имеют два поля:
+
+
+
+- Запланировано времени
+
+
+
+- Затрачено времени
+
+
+
+Эти значения показывают время работы и могут быть установлены вручную
+
+
+
+Отслеживание времени подзадач[¶](#subtask-time-tracking "Ссылка на этот заголовок")
+-----------------------------------------------------------------------------------
+
+
+
+![Subtask time tracking](https://kanboard.net/screenshots/documentation/subtask-time-tracking.png)
+
+Рисунок. Отслеживание времени подзадач
+
+
+
+Подзадачи тоже имеют поля “Запланировано” и “Затрачено” время.
+
+
+
+Когда вы меняете значения в этих полях, **отслеживание времени задачи обновляется автоматически и формируется суммарное время всех подзадач**
+
+
+
+Канборд записывает время между изменениями статуса каждой подзадачи в отдельную таблицу.
+
+
+
+- При изменении статуса подзадачи с **“Для испольнения”** на **“В работе”**, записывается время начала
+
+
+
+- При изменении статуса подзадачи с **“В работе”** на **“Выполнено”**, записывается как время окончания и, при этом, обновляется **затраченное время** в подзадаче и в задаче.
+
+
+
+Анализ всех записей можно увидеть на странице детального просмотра задачи:
+
+
+
+![Task timesheet](https://kanboard.net/screenshots/documentation/task-timesheet.png)
+
+Рисунок. Таблица учета времени.
+
+
+
+Для каждой подзадачи, таймер может быть остановлен и запущен в любое время:
+
+
+
+![Subtask timer](https://kanboard.net/screenshots/documentation/subtask-timer.png)
+
+Рисунок. Таймер подзадач.
+
+
+
+- Таймер не зависит от статуса подзадачи
+
+
+
+- Вы можете запустить таймер для новой записи, созданной в таблице отслеживания задач, в любое время
+
+
+
+- Вы можете остановить учет времени даты завершения в таблице отслеживания задач, в любое время
+
+
+
+- Подсчет затраченного времени округляется до четверти часа
+
+
+
+
+
+
+
+
+
+
+[Русская документация 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 @@
+Перемещения задач
+=================
+
+
+
+Запись о перемещении отражает каждое движение задачи между колонками.
+
+
+
+![Transitions](https://kanboard.net/screenshots/documentation/transitions.png)
+
+Рисунок. Перемещения.
+
+
+
+Перемещение доступно в боковом меню в детальном представлении задачи (**Перемещения**). Вы можете увидеть следующую информацию:
+
+
+
+- Дата, когда было выполенено перемещение
+
+
+
+- Исходная колонка - колонка, из которой было сделано перемещение
+
+
+
+- Колонка назначения - колонка, в которую была перемещена задача
+
+
+
+- Исполнитель (пользователь, который переместил задачу)
+
+
+
+- Время проведенное в колонке (сколько времени было затрачено на выполнение задачи в указанной колонке)
+
+
+
+Данные о перемещении задач, также, могут быть экспортированы со страницы настроек проекта (**Меню** -\> **Экспорт**).
+
+
+
+![Transitions Export](https://kanboard.net/screenshots/documentation/transitions-export.png)
+
+Рисунок. Экспорт перемещений задач.
+
+
+
+Для указанного промежутка времени вы можете сформировать 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. При создании пользователя нужно заполнить форму и сохранить
+
+
+
+![New user](screenshots/new-user.png)
+
+Рисунок. Форма создания нового пользователя.
+
+
+
+При создании **Локального пользователя** вы должны, как минимум, заполнить следующие поля:
+
+
+
+- **Имя пользователя**: это поле является уникальным идентификатором вашего пользователя (логин)
+
+
+
+- **Пароль**: Пароль пользователя должен иметь минимум 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 @@
+Ссылка на пользователя
+======================
+
+
+
+В Канборде есть возможность посылать уведомления пользователю, если кто-то ссылается на него в тексте.
+
+
+
+Если вы хотите заострить внимание о ком-либо в комментарии или в задаче, то вы можете использовать символ @ и следом указать имя пользователя. Канборд автоматически предлагает список пользователей:
+
+
+
+![User Mention](screenshots/mention-autocomplete.png)
+
+Рисунок. Ссылка на пользователя.
+
+
+
+- В данный момент, добавлять ссылку на пользователя можно только в описании задачи и тексте комментария.
+
+
+
+- Ссылка на пользователя работает только в задачах и при создании комментария.
+
+
+
+- Для получения уведомления, пользователь, на которого ссылаются, должен быть участником проекта, в котором создается ссылка.
+
+
+
+- Если была создана ссылка на пользователя, то этот пользователь получит уведомление.
+
+
+
+- @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()