summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes1
-rw-r--r--.travis.yml1
-rw-r--r--CONTRIBUTORS.md5
-rw-r--r--ChangeLog34
-rw-r--r--Makefile33
-rw-r--r--README.md2
-rw-r--r--Vagrantfile96
-rw-r--r--app/Api/Me.php2
-rw-r--r--app/Api/Project.php4
-rw-r--r--app/Api/User.php1
-rw-r--r--app/Auth/LdapAuth.php6
-rw-r--r--app/Console/BaseCommand.php (renamed from app/Console/Base.php)4
-rw-r--r--app/Console/CronjobCommand.php (renamed from app/Console/Cronjob.php)2
-rw-r--r--app/Console/LocaleComparatorCommand.php (renamed from app/Console/LocaleComparator.php)2
-rw-r--r--app/Console/LocaleSyncCommand.php (renamed from app/Console/LocaleSync.php)2
-rw-r--r--app/Console/ProjectDailyColumnStatsExportCommand.php (renamed from app/Console/ProjectDailyColumnStatsExport.php)2
-rw-r--r--app/Console/ProjectDailyStatsCalculationCommand.php (renamed from app/Console/ProjectDailyStatsCalculation.php)2
-rw-r--r--app/Console/ResetPasswordCommand.php79
-rw-r--r--app/Console/ResetTwoFactorCommand.php38
-rw-r--r--app/Console/SubtaskExportCommand.php (renamed from app/Console/SubtaskExport.php)2
-rw-r--r--app/Console/TaskExportCommand.php (renamed from app/Console/TaskExport.php)2
-rw-r--r--app/Console/TaskOverdueNotificationCommand.php (renamed from app/Console/TaskOverdueNotification.php)2
-rw-r--r--app/Console/TaskTriggerCommand.php (renamed from app/Console/TaskTrigger.php)2
-rw-r--r--app/Console/TransitionExportCommand.php (renamed from app/Console/TransitionExport.php)2
-rw-r--r--app/Controller/Activity.php5
-rw-r--r--app/Controller/Analytic.php8
-rw-r--r--app/Controller/App.php2
-rw-r--r--app/Controller/AvatarFile.php92
-rw-r--r--app/Controller/Base.php56
-rw-r--r--app/Controller/Board.php21
-rw-r--r--app/Controller/Calendar.php46
-rw-r--r--app/Controller/Doc.php87
-rw-r--r--app/Controller/Feed.php4
-rw-r--r--app/Controller/FileViewer.php26
-rw-r--r--app/Controller/Gantt.php34
-rw-r--r--app/Controller/GroupHelper.php8
-rw-r--r--app/Controller/Ical.php59
-rw-r--r--app/Controller/Listing.php21
-rw-r--r--app/Controller/Oauth.php106
-rw-r--r--app/Controller/ProjectOverview.php21
-rw-r--r--app/Controller/ProjectPermission.php4
-rw-r--r--app/Controller/Search.php32
-rw-r--r--app/Controller/Task.php8
-rw-r--r--app/Controller/TaskHelper.php38
-rw-r--r--app/Controller/UserHelper.php13
-rw-r--r--app/Core/Action/ActionManager.php2
-rw-r--r--app/Core/Base.php33
-rw-r--r--app/Core/Cache/Base.php20
-rw-r--r--app/Core/ExternalLink/ExternalLinkManager.php2
-rw-r--r--app/Core/Filter/CriteriaInterface.php40
-rw-r--r--app/Core/Filter/FilterInterface.php56
-rw-r--r--app/Core/Filter/FormatterInterface.php31
-rw-r--r--app/Core/Filter/Lexer.php153
-rw-r--r--app/Core/Filter/LexerBuilder.php151
-rw-r--r--app/Core/Filter/OrCriteria.php68
-rw-r--r--app/Core/Filter/QueryBuilder.php103
-rw-r--r--app/Core/Helper.php29
-rw-r--r--app/Core/Http/OAuth2.php45
-rw-r--r--app/Core/Http/Response.php32
-rw-r--r--app/Core/Ldap/Client.php44
-rw-r--r--app/Core/Ldap/Query.php6
-rw-r--r--app/Core/Ldap/User.php3
-rw-r--r--app/Core/Lexer.php161
-rw-r--r--app/Core/Session/SessionStorage.php1
-rw-r--r--app/Core/Template.php40
-rw-r--r--app/Core/Thumbnail.php172
-rw-r--r--app/Core/Tool.php74
-rw-r--r--app/Core/User/Avatar/AvatarManager.php7
-rw-r--r--app/Core/User/UserSession.php13
-rw-r--r--app/Filter/BaseDateFilter.php103
-rw-r--r--app/Filter/BaseFilter.php75
-rw-r--r--app/Filter/ProjectActivityCreationDateFilter.php38
-rw-r--r--app/Filter/ProjectActivityCreatorFilter.php65
-rw-r--r--app/Filter/ProjectActivityProjectIdFilter.php38
-rw-r--r--app/Filter/ProjectActivityProjectIdsFilter.php43
-rw-r--r--app/Filter/ProjectActivityProjectNameFilter.php38
-rw-r--r--app/Filter/ProjectActivityTaskIdFilter.php38
-rw-r--r--app/Filter/ProjectActivityTaskStatusFilter.php43
-rw-r--r--app/Filter/ProjectActivityTaskTitleFilter.php25
-rw-r--r--app/Filter/ProjectGroupRoleProjectFilter.php38
-rw-r--r--app/Filter/ProjectGroupRoleUsernameFilter.php44
-rw-r--r--app/Filter/ProjectIdsFilter.php43
-rw-r--r--app/Filter/ProjectStatusFilter.php45
-rw-r--r--app/Filter/ProjectTypeFilter.php45
-rw-r--r--app/Filter/ProjectUserRoleProjectFilter.php38
-rw-r--r--app/Filter/ProjectUserRoleUsernameFilter.php41
-rw-r--r--app/Filter/TaskAssigneeFilter.php75
-rw-r--r--app/Filter/TaskCategoryFilter.php46
-rw-r--r--app/Filter/TaskColorFilter.php60
-rw-r--r--app/Filter/TaskColumnFilter.php44
-rw-r--r--app/Filter/TaskCommentFilter.php41
-rw-r--r--app/Filter/TaskCompletionDateFilter.php38
-rw-r--r--app/Filter/TaskCreationDateFilter.php38
-rw-r--r--app/Filter/TaskCreatorFilter.php74
-rw-r--r--app/Filter/TaskDescriptionFilter.php38
-rw-r--r--app/Filter/TaskDueDateFilter.php41
-rw-r--r--app/Filter/TaskDueDateRangeFilter.php39
-rw-r--r--app/Filter/TaskIdExclusionFilter.php38
-rw-r--r--app/Filter/TaskIdFilter.php38
-rw-r--r--app/Filter/TaskLinkFilter.php85
-rw-r--r--app/Filter/TaskModificationDateFilter.php38
-rw-r--r--app/Filter/TaskProjectFilter.php44
-rw-r--r--app/Filter/TaskProjectsFilter.php43
-rw-r--r--app/Filter/TaskReferenceFilter.php38
-rw-r--r--app/Filter/TaskStartDateFilter.php38
-rw-r--r--app/Filter/TaskStatusFilter.php43
-rw-r--r--app/Filter/TaskSubtaskAssigneeFilter.php140
-rw-r--r--app/Filter/TaskSwimlaneFilter.php50
-rw-r--r--app/Filter/TaskTitleFilter.php46
-rw-r--r--app/Filter/UserNameFilter.php35
-rw-r--r--app/Formatter/BaseFormatter.php37
-rw-r--r--app/Formatter/BaseTaskCalendarFormatter.php (renamed from app/Formatter/TaskFilterCalendarEvent.php)37
-rw-r--r--app/Formatter/BoardFormatter.php56
-rw-r--r--app/Formatter/FormatterInterface.php14
-rw-r--r--app/Formatter/GroupAutoCompleteFormatter.php28
-rw-r--r--app/Formatter/ProjectActivityEventFormatter.php61
-rw-r--r--app/Formatter/ProjectGanttFormatter.php39
-rw-r--r--app/Formatter/SubtaskTimeTrackingCalendarFormatter.php38
-rw-r--r--app/Formatter/TaskAutoCompleteFormatter.php (renamed from app/Formatter/TaskFilterAutoCompleteFormatter.php)12
-rw-r--r--app/Formatter/TaskCalendarFormatter.php (renamed from app/Formatter/TaskFilterCalendarFormatter.php)30
-rw-r--r--app/Formatter/TaskGanttFormatter.php (renamed from app/Formatter/TaskFilterGanttFormatter.php)14
-rw-r--r--app/Formatter/TaskICalFormatter.php (renamed from app/Formatter/TaskFilterICalendarFormatter.php)17
-rw-r--r--app/Formatter/UserAutoCompleteFormatter.php (renamed from app/Formatter/UserFilterAutoCompleteFormatter.php)6
-rw-r--r--app/Helper/AvatarHelper.php22
-rw-r--r--app/Helper/CalendarHelper.php112
-rw-r--r--app/Helper/ICalHelper.php38
-rw-r--r--app/Helper/ProjectActivityHelper.php105
-rw-r--r--app/Helper/ProjectHeaderHelper.php80
-rw-r--r--app/Helper/UserHelper.php2
-rw-r--r--app/Locale/bs_BA/translations.php68
-rw-r--r--app/Locale/cs_CZ/translations.php52
-rw-r--r--app/Locale/da_DK/translations.php52
-rw-r--r--app/Locale/de_DE/translations.php86
-rw-r--r--app/Locale/el_GR/translations.php52
-rw-r--r--app/Locale/es_ES/translations.php52
-rw-r--r--app/Locale/fi_FI/translations.php52
-rw-r--r--app/Locale/fr_FR/translations.php51
-rw-r--r--app/Locale/hu_HU/translations.php52
-rw-r--r--app/Locale/id_ID/translations.php52
-rw-r--r--app/Locale/it_IT/translations.php78
-rw-r--r--app/Locale/ja_JP/translations.php52
-rw-r--r--app/Locale/ko_KR/translations.php1170
-rw-r--r--app/Locale/my_MY/translations.php52
-rw-r--r--app/Locale/nb_NO/translations.php52
-rw-r--r--app/Locale/nl_NL/translations.php52
-rw-r--r--app/Locale/pl_PL/translations.php52
-rw-r--r--app/Locale/pt_BR/translations.php222
-rw-r--r--app/Locale/pt_PT/translations.php52
-rw-r--r--app/Locale/ru_RU/translations.php52
-rw-r--r--app/Locale/sr_Latn_RS/translations.php52
-rw-r--r--app/Locale/sv_SE/translations.php52
-rw-r--r--app/Locale/th_TH/translations.php52
-rw-r--r--app/Locale/tr_TR/translations.php52
-rw-r--r--app/Locale/zh_CN/translations.php52
-rw-r--r--app/Model/AvatarFile.php112
-rw-r--r--app/Model/Base.php24
-rw-r--r--app/Model/Comment.php7
-rw-r--r--app/Model/Config.php2
-rw-r--r--app/Model/File.php20
-rw-r--r--app/Model/Project.php14
-rw-r--r--app/Model/ProjectActivity.php152
-rw-r--r--app/Model/ProjectGroupRoleFilter.php89
-rw-r--r--app/Model/ProjectPermission.php18
-rw-r--r--app/Model/ProjectUserRole.php4
-rw-r--r--app/Model/ProjectUserRoleFilter.php88
-rw-r--r--app/Model/Setting.php1
-rw-r--r--app/Model/SubtaskTimeTracking.php88
-rw-r--r--app/Model/TaskFilter.php745
-rw-r--r--app/Model/TaskFinder.php23
-rw-r--r--app/Model/User.php9
-rw-r--r--app/Model/UserFilter.php80
-rw-r--r--app/Model/UserNotification.php19
-rw-r--r--app/Model/UserNotificationFilter.php9
-rw-r--r--app/Schema/Mysql.php18
-rw-r--r--app/Schema/Postgres.php7
-rw-r--r--app/Schema/Sql/mysql.sql17
-rw-r--r--app/Schema/Sql/postgres.sql67
-rw-r--r--app/Schema/Sqlite.php7
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php1
-rw-r--r--app/ServiceProvider/AvatarProvider.php2
-rw-r--r--app/ServiceProvider/ClassProvider.php13
-rw-r--r--app/ServiceProvider/DatabaseProvider.php4
-rw-r--r--app/ServiceProvider/FilterProvider.php174
-rw-r--r--app/ServiceProvider/HelperProvider.php4
-rw-r--r--app/ServiceProvider/RouteProvider.php3
-rw-r--r--app/Template/activity/filter_dropdown.php14
-rw-r--r--app/Template/activity/project.php40
-rw-r--r--app/Template/analytic/layout.php33
-rw-r--r--app/Template/app/layout.php2
-rw-r--r--app/Template/avatar_file/show.php20
-rw-r--r--app/Template/board/task_avatar.php1
-rw-r--r--app/Template/board/task_footer.php2
-rw-r--r--app/Template/board/view_private.php17
-rw-r--r--app/Template/calendar/show.php6
-rw-r--r--app/Template/comment/show.php2
-rw-r--r--app/Template/event/events.php3
-rw-r--r--app/Template/gantt/project.php7
-rw-r--r--app/Template/listing/show.php14
-rw-r--r--app/Template/project/layout.php27
-rw-r--r--app/Template/project_header/dropdown.php4
-rw-r--r--app/Template/project_header/header.php2
-rw-r--r--app/Template/project_header/views.php2
-rw-r--r--app/Template/project_overview/description.php2
-rw-r--r--app/Template/project_overview/show.php6
-rw-r--r--app/Template/search/activity.php39
-rw-r--r--app/Template/search/index.php4
-rw-r--r--app/Template/task/details.php7
-rw-r--r--app/Template/task/layout.php22
-rw-r--r--app/Template/task/menu.php78
-rw-r--r--app/Template/task/show.php1
-rw-r--r--app/Template/task/sidebar.php3
-rw-r--r--app/Template/task_creation/form.php4
-rw-r--r--app/Template/user/index.php5
-rw-r--r--app/Template/user/notifications.php3
-rw-r--r--app/Template/user/profile.php1
-rw-r--r--app/Template/user/sidebar.php3
-rw-r--r--app/User/Avatar/AvatarFileProvider.php42
-rw-r--r--app/User/Avatar/GravatarProvider.php5
-rw-r--r--app/User/Avatar/LetterAvatarProvider.php7
-rw-r--r--app/common.php1
-rw-r--r--assets/css/app.css2
-rw-r--r--assets/css/print.css2
-rw-r--r--assets/css/src/alert.css12
-rw-r--r--assets/css/src/filters.css12
-rw-r--r--assets/css/src/header.css7
-rw-r--r--assets/css/src/task.css5
-rw-r--r--assets/js/app.js2
-rw-r--r--assets/js/src/Gantt.js2
-rw-r--r--composer.json2
-rw-r--r--composer.lock64
-rw-r--r--doc/.htaccess7
-rw-r--r--doc/automatic-actions.markdown14
-rw-r--r--doc/board-collapsed-expanded.markdown5
-rw-r--r--doc/board-horizontal-scrolling-and-compact-view.markdown4
-rw-r--r--doc/board-show-hide-columns.markdown9
-rw-r--r--doc/cli.markdown17
-rw-r--r--doc/creating-projects.markdown13
-rw-r--r--doc/editing-projects.markdown2
-rw-r--r--doc/faq.markdown6
-rw-r--r--doc/fr/automatic-actions.markdown24
-rw-r--r--doc/fr/board-collapsed-expanded.markdown21
-rw-r--r--doc/fr/board-horizontal-scrolling-and-compact-view.markdown8
-rw-r--r--doc/fr/board-show-hide-columns.markdown10
-rw-r--r--doc/fr/captures/kanboard-acces-prive.pngbin14432 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-acces-public.pngbin26672 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-board.pngbin65587 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-creer-utilisateur.pngbin35907 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-nouveauprojet-prive.pngbin17942 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-nouveauprojet.pngbin21282 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-swimlanes.pngbin43399 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-vue-calendrier.pngbin31640 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-vue-gantt.pngbin42360 -> 0 bytes
-rw-r--r--doc/fr/captures/kanboard-vue-liste.pngbin38316 -> 0 bytes
-rw-r--r--doc/fr/creating-projects.markdown23
-rw-r--r--doc/fr/editing-projects.markdown5
-rw-r--r--doc/fr/index.markdown2
-rw-r--r--doc/fr/project-permissions.markdown55
-rw-r--r--doc/fr/project-types.markdown14
-rw-r--r--doc/fr/project-views.markdown51
-rw-r--r--doc/fr/roles.markdown24
-rw-r--r--doc/fr/screenshots/automatic-action-creation.pngbin0 -> 19900 bytes
-rw-r--r--doc/fr/screenshots/board-collapsed-mode.pngbin0 -> 7074 bytes
-rw-r--r--doc/fr/screenshots/board-compact-mode.pngbin0 -> 12765 bytes
-rw-r--r--doc/fr/screenshots/board-expanded-mode.pngbin0 -> 11783 bytes
-rw-r--r--doc/fr/screenshots/board-task-limit.pngbin0 -> 16848 bytes
-rw-r--r--doc/fr/screenshots/board-view.pngbin0 -> 24371 bytes
-rw-r--r--doc/fr/screenshots/calendar-view.pngbin0 -> 21691 bytes
-rw-r--r--doc/fr/screenshots/gantt-view.pngbin0 -> 29367 bytes
-rw-r--r--doc/fr/screenshots/hide-column.pngbin0 -> 9642 bytes
-rw-r--r--doc/fr/screenshots/list-view.pngbin0 -> 23457 bytes
-rw-r--r--doc/fr/screenshots/new-project.pngbin0 -> 20818 bytes
-rw-r--r--doc/fr/screenshots/new-user.pngbin0 -> 26286 bytes
-rw-r--r--doc/fr/screenshots/project-disable-sharing.pngbin0 -> 17706 bytes
-rw-r--r--doc/fr/screenshots/project-edition.pngbin0 -> 40230 bytes
-rw-r--r--doc/fr/screenshots/project-enable-sharing.pngbin0 -> 14297 bytes
-rw-r--r--doc/fr/screenshots/project-permissions.pngbin0 -> 32926 bytes
-rw-r--r--doc/fr/screenshots/project-view.pngbin0 -> 40868 bytes
-rw-r--r--doc/fr/screenshots/show-column.pngbin0 -> 17940 bytes
-rw-r--r--doc/fr/screenshots/swimlane-configuration.pngbin0 -> 14226 bytes
-rw-r--r--doc/fr/screenshots/swimlanes.pngbin0 -> 25290 bytes
-rw-r--r--doc/fr/sharing-projects.markdown10
-rw-r--r--doc/fr/swimlanes.markdown7
-rw-r--r--doc/fr/user-management.markdown60
-rw-r--r--doc/installation.markdown2
-rw-r--r--doc/plugin-hooks.markdown12
-rw-r--r--doc/plugin-overrides.markdown6
-rw-r--r--doc/project-views.markdown18
-rw-r--r--doc/roles.markdown1
-rw-r--r--doc/screenshots/automatic-action-creation.pngbin0 -> 16241 bytes
-rw-r--r--doc/screenshots/board-collapsed-mode.pngbin0 -> 6296 bytes
-rw-r--r--doc/screenshots/board-compact-mode.pngbin0 -> 13501 bytes
-rw-r--r--doc/screenshots/board-expanded-mode.pngbin0 -> 10680 bytes
-rw-r--r--doc/screenshots/board-task-limit.pngbin0 -> 22642 bytes
-rw-r--r--doc/screenshots/board-view.pngbin0 -> 24460 bytes
-rw-r--r--doc/screenshots/calendar-view.pngbin0 -> 20973 bytes
-rw-r--r--doc/screenshots/gantt-view.pngbin0 -> 27793 bytes
-rw-r--r--doc/screenshots/hide-column.pngbin0 -> 8553 bytes
-rw-r--r--doc/screenshots/list-view.pngbin0 -> 22769 bytes
-rw-r--r--doc/screenshots/new-project.pngbin0 -> 20808 bytes
-rw-r--r--doc/screenshots/new-user.pngbin0 -> 22387 bytes
-rw-r--r--doc/screenshots/project-disable-sharing.pngbin0 -> 15375 bytes
-rw-r--r--doc/screenshots/project-edition.pngbin0 -> 36345 bytes
-rw-r--r--doc/screenshots/project-enable-sharing.pngbin0 -> 12251 bytes
-rw-r--r--doc/screenshots/project-permissions.pngbin64960 -> 41833 bytes
-rw-r--r--doc/screenshots/project-view.pngbin0 -> 37215 bytes
-rw-r--r--doc/screenshots/show-column.pngbin0 -> 13581 bytes
-rw-r--r--doc/screenshots/swimlane-configuration.pngbin0 -> 13069 bytes
-rw-r--r--doc/screenshots/swimlanes.pngbin0 -> 25733 bytes
-rw-r--r--doc/search.markdown101
-rw-r--r--doc/sharing-projects.markdown6
-rw-r--r--doc/swimlanes.markdown13
-rw-r--r--doc/ubuntu-installation.markdown43
-rw-r--r--doc/update.markdown2
-rw-r--r--doc/user-management.markdown10
-rw-r--r--doc/vagrant.markdown64
-rwxr-xr-xkanboard44
-rw-r--r--pull_request_template.md8
-rw-r--r--tests/units/Base.php2
-rw-r--r--tests/units/Core/Filter/LexerBuilderTest.php106
-rw-r--r--tests/units/Core/Filter/LexerTest.php100
-rw-r--r--tests/units/Core/Filter/OrCriteriaTest.php58
-rw-r--r--tests/units/Core/Http/OAuth2Test.php7
-rw-r--r--tests/units/Core/LexerTest.php468
-rw-r--r--tests/units/Core/TemplateTest.php30
-rw-r--r--tests/units/Filter/ProjectActivityCreationDateFilterTest.php117
-rw-r--r--tests/units/Filter/ProjectActivityCreatorFilterTest.php91
-rw-r--r--tests/units/Filter/ProjectActivityProjectIdFilterTest.php35
-rw-r--r--tests/units/Filter/ProjectActivityProjectIdsFilterTest.php63
-rw-r--r--tests/units/Filter/ProjectActivityProjectNameFilterTest.php35
-rw-r--r--tests/units/Filter/ProjectActivityTaskIdFilterTest.php34
-rw-r--r--tests/units/Filter/ProjectActivityTaskStatusFilterTest.php49
-rw-r--r--tests/units/Filter/ProjectActivityTaskTitleFilterTest.php79
-rw-r--r--tests/units/Filter/TaskAssigneeFilterTest.php159
-rw-r--r--tests/units/Filter/TaskCommentFilterTest.php52
-rw-r--r--tests/units/Filter/TaskCreatorFilterTest.php159
-rw-r--r--tests/units/Formatter/TaskFilterCalendarFormatterTest.php21
-rw-r--r--tests/units/Formatter/TaskFilterGanttFormatterTest.php24
-rw-r--r--tests/units/Formatter/TaskFilterICalendarFormatterTest.php74
-rw-r--r--tests/units/Helper/ProjectActivityHelperTest.php97
-rw-r--r--tests/units/Helper/UserHelperTest.php1
-rw-r--r--tests/units/Model/ProjectActivityTest.php83
-rw-r--r--tests/units/Model/ProjectFileTest.php4
-rw-r--r--tests/units/Model/SubtaskTimeTrackingTest.php77
-rw-r--r--tests/units/Model/TaskFileTest.php4
-rw-r--r--tests/units/Model/TaskFilterTest.php624
-rw-r--r--tests/units/Model/UserNotificationFilterTest.php5
-rw-r--r--tests/units/User/Avatar/LetterAvatarProviderTest.php4
347 files changed, 8723 insertions, 4744 deletions
diff --git a/.gitattributes b/.gitattributes
index 94b64588..71b22659 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -7,6 +7,7 @@ app/constants.php export-subst
.scrutinizer.yml export-ignore
.travis.yml export-ignore
Dockerfile export-ignore
+docker-compose.yml export-ignore
Makefile export-ignore
README.md export-ignore
Vagrantfile export-ignore
diff --git a/.travis.yml b/.travis.yml
index 1c132a0b..40af3ca8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,7 +23,6 @@ before_script:
- phpenv config-rm xdebug.ini
- phpenv config-add tests/php.ini
- composer install
- - php -i
script:
- phpunit -c tests/units.$DB.xml
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f4576949..c93c92f1 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -29,7 +29,7 @@ Contributors:
- [Daniel Raknes](https://github.com/danielraknes)
- [David-Norris](https://github.com/David-Norris)
- [Dmitry](https://github.com/dmkcv)
-- [Djpadz](https://github.com/djpadz)
+- [Dj Padzensky](https://github.com/djpadz)
- [Draza (bdpsoft)](https://github.com/bdpsoft)
- [Eskiso](https://github.com/eSkiSo)
- [Esteban Monge](https://github.com/EstebanMonge)
@@ -120,6 +120,7 @@ Contributors:
- [Vladimir Babin](https://github.com/Chiliec)
- [Yannick Ihmels](https://github.com/ihmels)
- [Ybarc](https://github.com/ybarc)
+- [Yu Yongwoo](https://github.com/uyu423)
- [Yuichi Murata](https://github.com/yuichi1004)
-There is also many people who have reported bugs or proposed awesome ideas. \ No newline at end of file
+There is also many people who have reported bugs or proposed awesome ideas.
diff --git a/ChangeLog b/ChangeLog
index 54a28db7..398ec499 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,14 +1,41 @@
-Version 1.0.27 (unreleased)
+Version 1.0.28 (unreleased)
+--------------
+
+New features:
+
+* Search in activity stream
+* Search in comments
+* Search by task creator
+* Added command line utility to reset user password and to disable 2FA
+
+Improvements:
+
+* Filter/Lexer/QueryBuilder refactoring
+
+Bug fixes:
+
+* Removed PHP notices in comment suppression view
+
+Version 1.0.27
--------------
New features:
* Added Markdown editor
-* Added letter avatar provider
-* Added pluggable Avatar providers
+* Added user avatars with pluggable system
+ - Default is a letter based avatar
+ - Gravatar
+ - Avatar Image upload
+* Added Korean translation
Improvements:
+* Added more logging for LDAP client
+* Improve schema migration process
+* Improve notification configuration form
+* Handle state in OAuth2 client
+* Allow to use the original template in overridden templates
+* Unification of the project header
* Refactoring of Javascript code
* Improve comments design
* Improve task summary sections
@@ -27,6 +54,7 @@ Improvements:
Bug fixes:
+* Fix bad unique constraints in Mysql table user_has_notifications
* Force integer type for aggregated metrics (Burndown chart concat values instead of summing)
* Fixes cycle time calculation when the start date is defined in the future
* Access allowed to any tasks from the shared public board by changing the URL parameters
diff --git a/Makefile b/Makefile
index f44475f6..d3ccf485 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jqu
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Namespace App Dropdown Popover Notification Accordion Session Calendar AvgTimeColumnChart BurndownChart CompareHoursColumnChart CumulativeFlowDiagram LeadCycleTimeChart UserRepartitionChart TaskTimeColumnChart TaskRepartitionChart Gantt Column Markdown ProjectPermission ProjectCreation Screenshot FileUpload Search Task Subtask Swimlane BoardColumnView BoardColumnScrolling BoardHorizontalScrolling BoardCollapsedMode BoardDragAndDrop BoardTask BoardPolling Tooltip Bootstrap))
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min simplemde.min))
-JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
+JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja ko nl nb pl pt pt-br ru sv sr th tr zh-cn))
all: css js
@@ -56,6 +56,7 @@ archive:
@ rm -rf ${BUILD_DIR}/kanboard/Makefile
@ rm -rf ${BUILD_DIR}/kanboard/Vagrantfile
@ rm -rf ${BUILD_DIR}/kanboard/Dockerfile
+ @ rm -rf ${BUILD_DIR}/kanboard/docker-compose.yml
@ rm -rf ${BUILD_DIR}/kanboard/.*.yml
@ rm -rf ${BUILD_DIR}/kanboard/*.md
@ rm -rf ${BUILD_DIR}/kanboard/*.markdown
@@ -71,40 +72,12 @@ archive:
@ find ${BUILD_DIR}/kanboard/vendor -name .travis.yml -delete
@ find ${BUILD_DIR}/kanboard/vendor -name README.* -delete
@ find ${BUILD_DIR}/kanboard/vendor -name .gitignore -delete
- @ cd ${BUILD_DIR}/kanboard && sed -i.bak s/master/${version}/g app/constants.php && rm -f app/*.bak
+ @ cd ${BUILD_DIR}/kanboard && sed -i.bak 11s/.*/"define('APP_VERSION', '${version}');"/g app/constants.php && rm -f app/*.bak
@ cd ${BUILD_DIR} && zip -r kanboard-${version}.zip kanboard > /dev/null
@ cd ${BUILD_DIR} && mv kanboard-${version}.zip ${dst}
@ cd ${dst} && if [ -L kanboard-latest.zip ]; then unlink kanboard-latest.zip; ln -s kanboard-${version}.zip kanboard-latest.zip; fi
@ rm -rf ${BUILD_DIR}/kanboard
-test-archive:
- @ echo "Build archive with tests: 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}/kanboard && composer --prefer-dist --optimize-autoloader --quiet install
- @ rm -rf ${BUILD_DIR}/kanboard/data/*
- @ rm -rf ${BUILD_DIR}/kanboard/.git*
- @ rm -rf ${BUILD_DIR}/kanboard/.*.yml
- @ rm -rf ${BUILD_DIR}/kanboard/*.md
- @ rm -rf ${BUILD_DIR}/kanboard/*.markdown
- @ rm -rf ${BUILD_DIR}/kanboard/Dockerfile
- @ rm -rf ${BUILD_DIR}/kanboard/.docker
- @ rm -rf ${BUILD_DIR}/kanboard/Vagrantfile
- @ rm -rf ${BUILD_DIR}/kanboard/app.json
- @ rm -rf ${BUILD_DIR}/kanboard/plugins/.gitignore
- @ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +;
- @ cd ${BUILD_DIR}/kanboard && find ./vendor -name test -type d -exec rm -rf {} +;
- @ cd ${BUILD_DIR}/kanboard && find ./vendor -name tests -type d -exec rm -rf {} +;
- @ find ${BUILD_DIR}/kanboard/vendor -name composer.json -delete
- @ find ${BUILD_DIR}/kanboard/vendor -name phpunit.xml -delete
- @ find ${BUILD_DIR}/kanboard/vendor -name .travis.yml -delete
- @ find ${BUILD_DIR}/kanboard/vendor -name README.* -delete
- @ find ${BUILD_DIR}/kanboard/vendor -name .gitignore -delete
- @ cd ${BUILD_DIR}/kanboard && sed -i.bak s/master/${version}/g app/constants.php && rm -f app/*.bak
- @ cd ${BUILD_DIR} && zip -r kanboard-${version}.zip kanboard > /dev/null
- @ cd ${BUILD_DIR} && mv kanboard-${version}.zip ${dst}
- @ rm -rf ${BUILD_DIR}/kanboard
-
test-sqlite-coverage:
@ phpunit --coverage-html /tmp/coverage --whitelist app/ -c tests/units.sqlite.xml
diff --git a/README.md b/README.md
index 9ca0c616..42b80aa9 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ Official website: <http://kanboard.net>
- [Change Log](https://github.com/fguillot/kanboard/blob/master/ChangeLog)
- [Documentation](https://github.com/fguillot/kanboard/blob/master/doc/index.markdown)
-[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
+[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/fguillot/kanboard)
Authors
-------
diff --git a/Vagrantfile b/Vagrantfile
index 63846c3d..7db068e4 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -1,54 +1,16 @@
-$debian_script = <<SCRIPT
-apt-get update
-apt-get install -y apache2 php5 php5-gd php5-curl php5-sqlite php5-xdebug php5-ldap
-apt-get clean -y
+$script = <<SCRIPT
+apt-get install -y apache2 php5 php5-sqlite php5-mysql php5-pgsql php5-gd curl unzip && \
+apt-get clean && \
+echo "ServerName localhost" >> /etc/apache2/apache2.conf && \
+sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf && \
+a2enmod rewrite
-sudo sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf
-
-if [ -f /etc/apache2/sites-enabled/000-default ]; then
- sudo sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/sites-enabled/000-default
-fi
-
-sudo a2enmod rewrite
service apache2 restart
rm -f /var/www/html/index.html
date > /etc/vagrant_provisioned_at
-wget -q https://getcomposer.org/composer.phar
-chmod +x composer.phar
-sudo mv composer.phar /usr/local/bin/composer
-
-wget -q https://phar.phpunit.de/phpunit.phar
-chmod +x phpunit.phar
-sudo mv phpunit.phar /usr/local/bin/phpunit
-
-SCRIPT
-
-$centos_script = <<SCRIPT
-sudo yum update -y
-sudo yum install -y httpd php php-cli php-gd php-ldap php-mbstring php-mysql php-pdo php-pgsql php-xml wget
-
-sudo sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/httpd/conf/httpd.conf
-
-if [ -x /usr/bin/systemctl ]; then
- sudo systemctl restart httpd
- sudo systemctl enable httpd
- sudo chcon -R -t httpd_sys_content_rw_t /var/www/html/data
- sudo setsebool -P httpd_can_network_connect=1
-else
- sudo service httpd restart
- sudo chkconfig httpd on
-fi
-
-rm -f /var/www/html/index.html
-date > /etc/vagrant_provisioned_at
-
-wget -q https://getcomposer.org/composer.phar
-chmod +x composer.phar
-sudo mv composer.phar /usr/local/bin/composer
-
wget -q https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
@@ -59,52 +21,8 @@ Vagrant.configure("2") do |config|
config.vm.define "ubuntu" do |m|
m.vm.box = "ubuntu/trusty64"
- m.vm.provision "shell", inline: $debian_script
+ m.vm.provision "shell", inline: $script
m.vm.synced_folder ".", "/var/www/html", owner: "www-data", group: "www-data"
m.vm.network :forwarded_port, guest: 80, host: 8001
end
-
- config.vm.define "debian8" do |m|
- m.vm.box = "debian/jessie64"
- m.vm.provision "shell", inline: $debian_script
- m.vm.synced_folder ".", "/var/www/html", owner: "www-data", group: "www-data"
- m.vm.network :forwarded_port, guest: 80, host: 8002
- end
-
- config.vm.define "debian7" do |m|
- m.vm.box = "debian/wheezy64"
- m.vm.provision "shell", inline: $debian_script
- m.vm.synced_folder ".", "/var/www", owner: "www-data", group: "www-data"
- m.vm.network :forwarded_port, guest: 80, host: 8003
- end
-
- config.vm.define "debian6" do |m|
- m.vm.box = "bento/debian-6.0.10"
- m.vm.provision "shell", inline: $debian_script
- m.vm.synced_folder ".", "/var/www", owner: "www-data", group: "www-data"
- m.vm.network :forwarded_port, guest: 80, host: 8004
- end
-
- config.vm.define "centos7" do |m|
- m.vm.box = "centos/7"
- m.vm.provision "shell", inline: $centos_script
- m.vm.synced_folder ".", "/var/www/html", owner: "apache", group: "apache", type: "rsync",
- rsync__exclude: ".git/", rsync__auto: true
- m.vm.network :forwarded_port, guest: 80, host: 8005
- end
-
- config.vm.define "centos6" do |m|
- m.vm.box = "bento/centos-6.7"
- m.vm.provision "shell", inline: $centos_script
- m.vm.synced_folder ".", "/var/www/html", owner: "apache", group: "apache", type: "rsync",
- rsync__exclude: ".git/", rsync__auto: true
- m.vm.network :forwarded_port, guest: 80, host: 8006
- end
-
- config.vm.define "freebsd10" do |m|
- m.vm.box = "freebsd/FreeBSD-10.2-STABLE"
- m.vm.base_mac = "080027D14C66"
- m.ssh.shell = "sh"
- m.vm.network :forwarded_port, guest: 80, host: 8007
- end
end
diff --git a/app/Api/Me.php b/app/Api/Me.php
index ccc809ed..3d08626a 100644
--- a/app/Api/Me.php
+++ b/app/Api/Me.php
@@ -33,7 +33,7 @@ class Me extends Base
public function getMyActivityStream()
{
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
- return $this->projectActivity->getProjects($project_ids, 100);
+ return $this->helper->projectActivity->getProjectsEvents($project_ids, 100);
}
public function createMyPrivateProject($name, $description = null)
diff --git a/app/Api/Project.php b/app/Api/Project.php
index 8e311f7f..846d7046 100644
--- a/app/Api/Project.php
+++ b/app/Api/Project.php
@@ -53,13 +53,13 @@ class Project extends Base
public function getProjectActivities(array $project_ids)
{
- return $this->projectActivity->getProjects($project_ids);
+ return $this->helper->projectActivity->getProjectsEvents($project_ids);
}
public function getProjectActivity($project_id)
{
$this->checkProjectPermission($project_id);
- return $this->projectActivity->getProject($project_id);
+ return $this->helper->projectActivity->getProjectEvents($project_id);
}
public function createProject($name, $description = null)
diff --git a/app/Api/User.php b/app/Api/User.php
index 9b8081d6..6ee935a3 100644
--- a/app/Api/User.php
+++ b/app/Api/User.php
@@ -87,6 +87,7 @@ class User extends \Kanboard\Core\Base
try {
$ldap = LdapClient::connect();
+ $ldap->setLogger($this->logger);
$user = LdapUser::getUser($ldap, $username);
if ($user === null) {
diff --git a/app/Auth/LdapAuth.php b/app/Auth/LdapAuth.php
index b4efbb55..c9423580 100644
--- a/app/Auth/LdapAuth.php
+++ b/app/Auth/LdapAuth.php
@@ -63,10 +63,12 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
try {
$client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
+ $client->setLogger($this->logger);
+
$user = LdapUser::getUser($client, $this->username);
if ($user === null) {
- $this->logger->info('User not found in LDAP server');
+ $this->logger->info('User ('.$this->username.') not found in LDAP server');
return false;
}
@@ -74,6 +76,8 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
+ $this->logger->info('Authenticate user: '.$user->getDn());
+
if ($client->authenticate($user->getDn(), $this->password)) {
$this->userInfo = $user;
return true;
diff --git a/app/Console/Base.php b/app/Console/BaseCommand.php
index 25d48e44..23cdcc9c 100644
--- a/app/Console/Base.php
+++ b/app/Console/BaseCommand.php
@@ -11,6 +11,7 @@ use Symfony\Component\Console\Command\Command;
* @package console
* @author Frederic Guillot
*
+ * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
@@ -21,11 +22,12 @@ use Symfony\Component\Console\Command\Command;
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
* @property \Kanboard\Model\Task $task
* @property \Kanboard\Model\TaskFinder $taskFinder
+ * @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
-abstract class Base extends Command
+abstract class BaseCommand extends Command
{
/**
* Container instance
diff --git a/app/Console/Cronjob.php b/app/Console/CronjobCommand.php
index 3a5c5596..dae13af9 100644
--- a/app/Console/Cronjob.php
+++ b/app/Console/CronjobCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\NullOutput;
-class Cronjob extends Base
+class CronjobCommand extends BaseCommand
{
private $commands = array(
'projects:daily-stats',
diff --git a/app/Console/LocaleComparator.php b/app/Console/LocaleComparatorCommand.php
index 8e5e0904..de83714f 100644
--- a/app/Console/LocaleComparator.php
+++ b/app/Console/LocaleComparatorCommand.php
@@ -7,7 +7,7 @@ use RecursiveDirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class LocaleComparator extends Base
+class LocaleComparatorCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';
diff --git a/app/Console/LocaleSync.php b/app/Console/LocaleSyncCommand.php
index d62b40b5..11cfbde0 100644
--- a/app/Console/LocaleSync.php
+++ b/app/Console/LocaleSyncCommand.php
@@ -6,7 +6,7 @@ use DirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class LocaleSync extends Base
+class LocaleSyncCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';
diff --git a/app/Console/ProjectDailyColumnStatsExport.php b/app/Console/ProjectDailyColumnStatsExportCommand.php
index 2513fbf1..ced1a374 100644
--- a/app/Console/ProjectDailyColumnStatsExport.php
+++ b/app/Console/ProjectDailyColumnStatsExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class ProjectDailyColumnStatsExport extends Base
+class ProjectDailyColumnStatsExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/ProjectDailyStatsCalculation.php b/app/Console/ProjectDailyStatsCalculationCommand.php
index 9884cc1c..5b898f02 100644
--- a/app/Console/ProjectDailyStatsCalculation.php
+++ b/app/Console/ProjectDailyStatsCalculationCommand.php
@@ -6,7 +6,7 @@ use Kanboard\Model\Project;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class ProjectDailyStatsCalculation extends Base
+class ProjectDailyStatsCalculationCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/ResetPasswordCommand.php b/app/Console/ResetPasswordCommand.php
new file mode 100644
index 00000000..93dc3761
--- /dev/null
+++ b/app/Console/ResetPasswordCommand.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\Question;
+
+class ResetPasswordCommand extends BaseCommand
+{
+ protected function configure()
+ {
+ $this
+ ->setName('user:reset-password')
+ ->setDescription('Change user password')
+ ->addArgument('username', InputArgument::REQUIRED, 'Username')
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $helper = $this->getHelper('question');
+ $username = $input->getArgument('username');
+
+ $passwordQuestion = new Question('What is the new password for '.$username.'? (characters are not printed)'.PHP_EOL);
+ $passwordQuestion->setHidden(true);
+ $passwordQuestion->setHiddenFallback(false);
+
+ $password = $helper->ask($input, $output, $passwordQuestion);
+
+ $confirmationQuestion = new Question('Confirmation:'.PHP_EOL);
+ $confirmationQuestion->setHidden(true);
+ $confirmationQuestion->setHiddenFallback(false);
+
+ $confirmation = $helper->ask($input, $output, $confirmationQuestion);
+
+ if ($this->validatePassword($output, $password, $confirmation)) {
+ $this->resetPassword($output, $username, $password);
+ }
+ }
+
+ private function validatePassword(OutputInterface $output, $password, $confirmation)
+ {
+ list($valid, $errors) = $this->passwordResetValidator->validateModification(array(
+ 'password' => $password,
+ 'confirmation' => $confirmation,
+ ));
+
+ if (!$valid) {
+ foreach ($errors as $error_list) {
+ foreach ($error_list as $error) {
+ $output->writeln('<error>'.$error.'</error>');
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ private function resetPassword(OutputInterface $output, $username, $password)
+ {
+ $userId = $this->user->getIdByUsername($username);
+
+ if (empty($userId)) {
+ $output->writeln('<error>User not found</error>');
+ return false;
+ }
+
+ if (!$this->user->update(array('id' => $userId, 'password' => $password))) {
+ $output->writeln('<error>Unable to update password</error>');
+ return false;
+ }
+
+ $output->writeln('<info>Password updated successfully</info>');
+
+ return true;
+ }
+}
diff --git a/app/Console/ResetTwoFactorCommand.php b/app/Console/ResetTwoFactorCommand.php
new file mode 100644
index 00000000..3bf01e81
--- /dev/null
+++ b/app/Console/ResetTwoFactorCommand.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ResetTwoFactorCommand extends BaseCommand
+{
+ protected function configure()
+ {
+ $this
+ ->setName('user:reset-2fa')
+ ->setDescription('Remove two-factor authentication for a user')
+ ->addArgument('username', InputArgument::REQUIRED, 'Username');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $username = $input->getArgument('username');
+ $userId = $this->user->getIdByUsername($username);
+
+ if (empty($userId)) {
+ $output->writeln('<error>User not found</error>');
+ return false;
+ }
+
+ if (!$this->user->update(array('id' => $userId, 'twofactor_activated' => 0, 'twofactor_secret' => ''))) {
+ $output->writeln('<error>Unable to update user profile</error>');
+ return false;
+ }
+
+ $output->writeln('<info>Two-factor authentication disabled</info>');
+
+ return true;
+ }
+}
diff --git a/app/Console/SubtaskExport.php b/app/Console/SubtaskExportCommand.php
index aaa95276..986af1a4 100644
--- a/app/Console/SubtaskExport.php
+++ b/app/Console/SubtaskExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class SubtaskExport extends Base
+class SubtaskExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TaskExport.php b/app/Console/TaskExportCommand.php
index 4515bf95..789245bc 100644
--- a/app/Console/TaskExport.php
+++ b/app/Console/TaskExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class TaskExport extends Base
+class TaskExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TaskOverdueNotification.php b/app/Console/TaskOverdueNotificationCommand.php
index 43be4df8..7d176ab1 100644
--- a/app/Console/TaskOverdueNotification.php
+++ b/app/Console/TaskOverdueNotificationCommand.php
@@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-class TaskOverdueNotification extends Base
+class TaskOverdueNotificationCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TaskTrigger.php b/app/Console/TaskTriggerCommand.php
index 8d707211..9e9554f9 100644
--- a/app/Console/TaskTrigger.php
+++ b/app/Console/TaskTriggerCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Kanboard\Model\Task;
use Kanboard\Event\TaskListEvent;
-class TaskTrigger extends Base
+class TaskTriggerCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TransitionExport.php b/app/Console/TransitionExportCommand.php
index d9f805a4..265757b3 100644
--- a/app/Console/TransitionExport.php
+++ b/app/Console/TransitionExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class TransitionExport extends Base
+class TransitionExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php
index db520ebe..47a66e0a 100644
--- a/app/Controller/Activity.php
+++ b/app/Controller/Activity.php
@@ -20,7 +20,7 @@ class Activity extends Base
$project = $this->getProject();
$this->response->html($this->helper->layout->app('activity/project', array(
- 'events' => $this->projectActivity->getProject($project['id']),
+ 'events' => $this->helper->projectActivity->getProjectEvents($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
)));
@@ -38,7 +38,8 @@ class Activity extends Base
$this->response->html($this->helper->layout->task('activity/task', array(
'title' => $task['title'],
'task' => $task,
- 'events' => $this->projectActivity->getTask($task['id']),
+ 'project' => $this->project->getById($task['project_id']),
+ 'events' => $this->helper->projectActivity->getTaskEvents($task['id']),
)));
}
}
diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php
index 6ce062c4..35bc3048 100644
--- a/app/Controller/Analytic.php
+++ b/app/Controller/Analytic.php
@@ -2,6 +2,7 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -44,14 +45,15 @@ class Analytic extends Base
public function compareHours()
{
$project = $this->getProject();
- $params = $this->getProjectFilters('analytic', 'compareHours');
- $query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery();
$paginator = $this->paginator
->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
- ->setQuery($query)
+ ->setQuery($this->taskQuery
+ ->withFilter(new TaskProjectFilter($project['id']))
+ ->getQuery()
+ )
->calculate();
$this->response->html($this->helper->layout->analytic('analytic/compare_hours', array(
diff --git a/app/Controller/App.php b/app/Controller/App.php
index df1d3c90..01f733ff 100644
--- a/app/Controller/App.php
+++ b/app/Controller/App.php
@@ -157,7 +157,7 @@ class App extends Base
$this->response->html($this->helper->layout->dashboard('app/activity', array(
'title' => t('My activity stream'),
- 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100),
+ 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id']), 100),
'user' => $user,
)));
}
diff --git a/app/Controller/AvatarFile.php b/app/Controller/AvatarFile.php
new file mode 100644
index 00000000..a47cca66
--- /dev/null
+++ b/app/Controller/AvatarFile.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\ObjectStorage\ObjectStorageException;
+use Kanboard\Core\Thumbnail;
+
+/**
+ * Avatar File Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class AvatarFile extends Base
+{
+ /**
+ * Display avatar page
+ */
+ public function show()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->helper->layout->user('avatar_file/show', array(
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Upload Avatar
+ */
+ public function upload()
+ {
+ $user = $this->getUser();
+
+ if (! $this->avatarFile->uploadFile($user['id'], $this->request->getFileInfo('avatar'))) {
+ $this->flash->failure(t('Unable to upload the file.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Remove Avatar image
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $user = $this->getUser();
+ $this->avatarFile->remove($user['id']);
+ $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Show Avatar image (public)
+ */
+ public function image()
+ {
+ $user_id = $this->request->getIntegerParam('user_id');
+ $size = $this->request->getStringParam('size', 48);
+ $filename = $this->avatarFile->getFilename($user_id);
+ $etag = md5($filename.$size);
+
+ $this->response->cache(365 * 86400, $etag);
+ $this->response->contentType('image/jpeg');
+
+ if ($this->request->getHeader('If-None-Match') !== '"'.$etag.'"') {
+ $this->render($filename, $size);
+ } else {
+ $this->response->status(304);
+ }
+ }
+
+ /**
+ * Render thumbnail from object storage
+ *
+ * @access private
+ * @param string $filename
+ * @param integer $size
+ */
+ private function render($filename, $size)
+ {
+ try {
+ $blob = $this->objectStorage->get($filename);
+
+ Thumbnail::createFromString($blob)
+ ->resize($size, $size)
+ ->toOutput();
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+}
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index 2453be18..beb56909 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -287,60 +287,4 @@ abstract class Base extends \Kanboard\Core\Base
return $subtask;
}
-
- /**
- * Common method to get project filters
- *
- * @access protected
- * @param string $controller
- * @param string $action
- * @return array
- */
- protected function getProjectFilters($controller, $action)
- {
- $project = $this->getProject();
- $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
- $board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
- unset($board_selector[$project['id']]);
-
- $filters = array(
- 'controller' => $controller,
- 'action' => $action,
- 'project_id' => $project['id'],
- 'search' => urldecode($search),
- );
-
- $this->userSession->setFilters($project['id'], $filters['search']);
-
- return array(
- 'project' => $project,
- 'board_selector' => $board_selector,
- 'filters' => $filters,
- 'title' => $project['name'],
- 'description' => $this->getProjectDescription($project),
- );
- }
-
- /**
- * Get project description
- *
- * @access protected
- * @param array &$project
- * @return string
- */
- protected function getProjectDescription(array &$project)
- {
- if ($project['owner_id'] > 0) {
- $description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL;
-
- if (! empty($project['description'])) {
- $description .= '***'.PHP_EOL.PHP_EOL;
- $description .= $project['description'];
- }
- } else {
- $description = $project['description'];
- }
-
- return $description;
- }
}
diff --git a/app/Controller/Board.php b/app/Controller/Board.php
index 199f1703..67e99b81 100644
--- a/app/Controller/Board.php
+++ b/app/Controller/Board.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Formatter\BoardFormatter;
+
/**
* Board controller
*
@@ -47,16 +49,19 @@ class Board extends Base
*/
public function show()
{
- $params = $this->getProjectFilters('board', 'show');
+ $project = $this->getProject();
+ $search = $this->helper->projectHeader->getSearchQuery($project);
$this->response->html($this->helper->layout->app('board/view_private', array(
- 'categories_list' => $this->category->getList($params['project']['id'], false),
- 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
- 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
- 'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
- ) + $params));
+ 'swimlanes' => $this->taskLexer
+ ->build($search)
+ ->format(BoardFormatter::getInstance($this->container)->setProjectId($project['id']))
+ )));
}
/**
@@ -177,9 +182,11 @@ class Board extends Base
{
return $this->template->render('board/table_container', array(
'project' => $this->project->getById($project_id),
- 'swimlanes' => $this->taskFilter->search($this->userSession->getFilters($project_id))->getBoard($project_id),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
+ 'swimlanes' => $this->taskLexer
+ ->build($this->userSession->getFilters($project_id))
+ ->format(BoardFormatter::getInstance($this->container)->setProjectId($project_id))
));
}
}
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
index a0a25e41..2517286d 100644
--- a/app/Controller/Calendar.php
+++ b/app/Controller/Calendar.php
@@ -2,6 +2,9 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskStatusFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -20,9 +23,14 @@ class Calendar extends Base
*/
public function show()
{
+ $project = $this->getProject();
+
$this->response->html($this->helper->layout->app('calendar/show', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'check_interval' => $this->config->get('board_private_refresh_interval'),
- ) + $this->getProjectFilters('calendar', 'show')));
+ )));
}
/**
@@ -35,21 +43,11 @@ class Calendar extends Base
$project_id = $this->request->getIntegerParam('project_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
+ $search = $this->userSession->getFilters($project_id);
+ $queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id));
- // Common filter
- $filter = $this->taskFilterCalendarFormatter
- ->search($this->userSession->getFilters($project_id))
- ->filterByProject($project_id);
-
- // Tasks
- if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
- $events = $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format();
- } else {
- $events = $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format();
- }
-
- // Tasks with due date
- $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format());
+ $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
+ $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
$events = $this->hook->merge('controller:calendar:project:events', $events, array(
'project_id' => $project_id,
@@ -70,21 +68,15 @@ class Calendar extends Base
$user_id = $this->request->getIntegerParam('user_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
- $filter = $this->taskFilterCalendarFormatter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN);
-
- // Task with due date
- $events = $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format();
+ $queryBuilder = $this->taskQuery
+ ->withFilter(new TaskAssigneeFilter($user_id))
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN));
- // Tasks
- if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') {
- $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format());
- } else {
- $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format());
- }
+ $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
+ $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
- // Subtasks time tracking
if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) {
- $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
+ $events = array_merge($events, $this->helper->calendar->getSubtaskTimeTrackingEvents($user_id, $start, $end));
}
$events = $this->hook->merge('controller:calendar:user:events', $events, array(
diff --git a/app/Controller/Doc.php b/app/Controller/Doc.php
index f85326ac..00b9e585 100644
--- a/app/Controller/Doc.php
+++ b/app/Controller/Doc.php
@@ -5,54 +5,32 @@ namespace Kanboard\Controller;
use Parsedown;
/**
- * Documentation controller
+ * Documentation Viewer
*
* @package controller
* @author Frederic Guillot
*/
class Doc extends Base
{
- private function readFile($filename)
- {
- $url = $this->helper->url;
- $data = file_get_contents($filename);
- list($title, ) = explode("\n", $data, 2);
-
- $replaceUrl = function (array $matches) use ($url) {
- return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
- };
-
- $content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data);
-
- return array(
- 'content' => Parsedown::instance()->text($content),
- 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
- );
- }
-
public function show()
{
$page = $this->request->getStringParam('file', 'index');
- if (! preg_match('/^[a-z0-9\-]+/', $page)) {
+ if (!preg_match('/^[a-z0-9\-]+/', $page)) {
$page = 'index';
}
- $filenames = array(__DIR__.'/../../doc/'.$page.'.markdown');
- $filename = __DIR__.'/../../doc/index.markdown';
-
if ($this->config->getCurrentLanguage() === 'fr_FR') {
- array_unshift($filenames, __DIR__.'/../../doc/fr/'.$page.'.markdown');
+ $filename = __DIR__.'/../../doc/fr/' . $page . '.markdown';
+ } else {
+ $filename = __DIR__ . '/../../doc/' . $page . '.markdown';
}
- foreach ($filenames as $file) {
- if (file_exists($file)) {
- $filename = $file;
- break;
- }
+ if (!file_exists($filename)) {
+ $filename = __DIR__.'/../../doc/index.markdown';
}
- $this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename)));
+ $this->response->html($this->helper->layout->app('doc/show', $this->render($filename)));
}
/**
@@ -62,4 +40,53 @@ class Doc extends Base
{
$this->response->html($this->template->render('config/keyboard_shortcuts'));
}
+
+ /**
+ * Prepare Markdown file
+ *
+ * @access private
+ * @param string $filename
+ * @return array
+ */
+ private function render($filename)
+ {
+ $data = file_get_contents($filename);
+ $content = preg_replace_callback('/\((.*.markdown)\)/', array($this, 'replaceMarkdownUrl'), $data);
+ $content = preg_replace_callback('/\((screenshots.*\.png)\)/', array($this, 'replaceImageUrl'), $content);
+
+ list($title, ) = explode("\n", $data, 2);
+
+ return array(
+ 'content' => Parsedown::instance()->text($content),
+ 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
+ );
+ }
+
+ /**
+ * Regex callback to replace Markdown links
+ *
+ * @access public
+ * @param array $matches
+ * @return string
+ */
+ public function replaceMarkdownUrl(array $matches)
+ {
+ return '('.$this->helper->url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
+ }
+
+ /**
+ * Regex callback to replace image links
+ *
+ * @access public
+ * @param array $matches
+ * @return string
+ */
+ public function replaceImageUrl(array $matches)
+ {
+ if ($this->config->getCurrentLanguage() === 'fr_FR') {
+ return '('.$this->helper->url->base().'doc/fr/'.$matches[1].')';
+ }
+
+ return '('.$this->helper->url->base().'doc/'.$matches[1].')';
+ }
}
diff --git a/app/Controller/Feed.php b/app/Controller/Feed.php
index 8457c383..f8b3d320 100644
--- a/app/Controller/Feed.php
+++ b/app/Controller/Feed.php
@@ -26,7 +26,7 @@ class Feed extends Base
}
$this->response->xml($this->template->render('feed/user', array(
- 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])),
+ 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id'])),
'user' => $user,
)));
}
@@ -47,7 +47,7 @@ class Feed extends Base
}
$this->response->xml($this->template->render('feed/project', array(
- 'events' => $this->projectActivity->getProject($project['id']),
+ 'events' => $this->helper->projectActivity->getProjectEvents($project['id']),
'project' => $project,
)));
}
diff --git a/app/Controller/FileViewer.php b/app/Controller/FileViewer.php
index bc91c3d8..3be4ea14 100644
--- a/app/Controller/FileViewer.php
+++ b/app/Controller/FileViewer.php
@@ -66,9 +66,16 @@ class FileViewer extends Base
*/
public function image()
{
+ $file = $this->getFile();
+ $etag = md5($file['path']);
+ $this->response->contentType($this->helper->file->getImageMimeType($file['name']));
+ $this->response->cache(5 * 86400, $etag);
+
+ if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
+ return $this->response->status(304);
+ }
+
try {
- $file = $this->getFile();
- $this->response->contentType($this->helper->file->getImageMimeType($file['name']));
$this->objectStorage->output($file['path']);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
@@ -82,12 +89,21 @@ class FileViewer extends Base
*/
public function thumbnail()
{
+ $file = $this->getFile();
+ $model = $file['model'];
+ $filename = $this->$model->getThumbnailPath($file['path']);
+ $etag = md5($filename);
+
+ $this->response->cache(5 * 86400, $etag);
$this->response->contentType('image/jpeg');
+ if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
+ return $this->response->status(304);
+ }
+
try {
- $file = $this->getFile();
- $model = $file['model'];
- $this->objectStorage->output($this->$model->getThumbnailPath($file['path']));
+
+ $this->objectStorage->output($filename);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php
index 9ffa277f..5e9ad55e 100644
--- a/app/Controller/Gantt.php
+++ b/app/Controller/Gantt.php
@@ -2,7 +2,14 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\ProjectIdsFilter;
+use Kanboard\Filter\ProjectStatusFilter;
+use Kanboard\Filter\ProjectTypeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Formatter\ProjectGanttFormatter;
+use Kanboard\Formatter\TaskGanttFormatter;
use Kanboard\Model\Task as TaskModel;
+use Kanboard\Model\Project as ProjectModel;
/**
* Gantt controller
@@ -17,14 +24,16 @@ class Gantt extends Base
*/
public function projects()
{
- if ($this->userSession->isAdmin()) {
- $project_ids = $this->project->getAllIds();
- } else {
- $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
- }
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $filter = $this->projectQuery
+ ->withFilter(new ProjectTypeFilter(ProjectModel::TYPE_TEAM))
+ ->withFilter(new ProjectStatusFilter(ProjectModel::ACTIVE))
+ ->withFilter(new ProjectIdsFilter($project_ids));
+
+ $filter->getQuery()->asc(ProjectModel::TABLE.'.start_date');
$this->response->html($this->helper->layout->app('gantt/projects', array(
- 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
+ 'projects' => $filter->format(new ProjectGanttFormatter($this->container)),
'title' => t('Gantt chart for all projects'),
)));
}
@@ -54,9 +63,10 @@ class Gantt extends Base
*/
public function project()
{
- $params = $this->getProjectFilters('gantt', 'project');
- $filter = $this->taskFilterGanttFormatter->search($params['filters']['search'])->filterByProject($params['project']['id']);
+ $project = $this->getProject();
+ $search = $this->helper->projectHeader->getSearchQuery($project);
$sorting = $this->request->getStringParam('sorting', 'board');
+ $filter = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project['id']));
if ($sorting === 'date') {
$filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation');
@@ -64,10 +74,12 @@ class Gantt extends Base
$filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position');
}
- $this->response->html($this->helper->layout->app('gantt/project', $params + array(
- 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
+ $this->response->html($this->helper->layout->app('gantt/project', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'sorting' => $sorting,
- 'tasks' => $filter->format(),
+ 'tasks' => $filter->format(new TaskGanttFormatter($this->container)),
)));
}
diff --git a/app/Controller/GroupHelper.php b/app/Controller/GroupHelper.php
index 34f522a6..429614c2 100644
--- a/app/Controller/GroupHelper.php
+++ b/app/Controller/GroupHelper.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Formatter\GroupAutoCompleteFormatter;
+
/**
* Group Helper
*
@@ -11,14 +13,14 @@ namespace Kanboard\Controller;
class GroupHelper extends Base
{
/**
- * Group autocompletion (Ajax)
+ * Group auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $groups = $this->groupManager->find($search);
- $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format());
+ $formatter = new GroupAutoCompleteFormatter($this->groupManager->find($search));
+ $this->response->json($formatter->format());
}
}
diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php
index f1ea6d8f..8fe97b46 100644
--- a/app/Controller/Ical.php
+++ b/app/Controller/Ical.php
@@ -2,7 +2,11 @@
namespace Kanboard\Controller;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskStatusFilter;
+use Kanboard\Formatter\TaskICalFormatter;
use Kanboard\Model\Task as TaskModel;
use Eluceo\iCal\Component\Calendar as iCalendar;
@@ -30,10 +34,11 @@ class Ical extends Base
}
// Common filter
- $filter = $this->taskFilterICalendarFormatter
- ->create()
- ->filterByStatus(TaskModel::STATUS_OPEN)
- ->filterByOwner($user['id']);
+ $queryBuilder = new QueryBuilder();
+ $queryBuilder
+ ->withQuery($this->taskFinder->getICalQuery())
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
+ ->withFilter(new TaskAssigneeFilter($user['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@@ -41,7 +46,7 @@ class Ical extends Base
$calendar->setDescription($user['name'] ?: $user['username']);
$calendar->setPublishedTTL('PT1H');
- $this->renderCalendar($filter, $calendar);
+ $this->renderCalendar($queryBuilder, $calendar);
}
/**
@@ -60,10 +65,11 @@ class Ical extends Base
}
// Common filter
- $filter = $this->taskFilterICalendarFormatter
- ->create()
- ->filterByStatus(TaskModel::STATUS_OPEN)
- ->filterByProject($project['id']);
+ $queryBuilder = new QueryBuilder();
+ $queryBuilder
+ ->withQuery($this->taskFinder->getICalQuery())
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
+ ->withFilter(new TaskProjectFilter($project['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@@ -71,7 +77,7 @@ class Ical extends Base
$calendar->setDescription($project['name']);
$calendar->setPublishedTTL('PT1H');
- $this->renderCalendar($filter, $calendar);
+ $this->renderCalendar($queryBuilder, $calendar);
}
/**
@@ -79,37 +85,14 @@ class Ical extends Base
*
* @access private
*/
- private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
+ private function renderCalendar(QueryBuilder $queryBuilder, iCalendar $calendar)
{
$start = $this->request->getStringParam('start', strtotime('-2 month'));
$end = $this->request->getStringParam('end', strtotime('+6 months'));
- // Tasks
- if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
- $filter
- ->copy()
- ->filterByCreationDateRange($start, $end)
- ->setColumns('date_creation', 'date_completed')
- ->setCalendar($calendar)
- ->addDateTimeEvents();
- } else {
- $filter
- ->copy()
- ->filterByStartDateRange($start, $end)
- ->setColumns('date_started', 'date_completed')
- ->setCalendar($calendar)
- ->addDateTimeEvents($calendar);
- }
-
- // Tasks with due date
- $filter
- ->copy()
- ->filterByDueDateRange($start, $end)
- ->setColumns('date_due')
- ->setCalendar($calendar)
- ->addFullDayEvents($calendar);
+ $this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end);
- $this->response->contentType('text/calendar; charset=utf-8');
- echo $filter->setCalendar($calendar)->format();
+ $formatter = new TaskICalFormatter($this->container);
+ $this->response->ical($formatter->setCalendar($calendar)->format());
}
}
diff --git a/app/Controller/Listing.php b/app/Controller/Listing.php
index c784dd50..2024ff03 100644
--- a/app/Controller/Listing.php
+++ b/app/Controller/Listing.php
@@ -2,6 +2,7 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -19,22 +20,26 @@ class Listing extends Base
*/
public function show()
{
- $params = $this->getProjectFilters('listing', 'show');
- $query = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id'])->getQuery();
+ $project = $this->getProject();
+ $search = $this->helper->projectHeader->getSearchQuery($project);
$paginator = $this->paginator
- ->setUrl('listing', 'show', array('project_id' => $params['project']['id']))
+ ->setUrl('listing', 'show', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
->setDirection('DESC')
- ->setQuery($query)
+ ->setQuery($this->taskLexer
+ ->build($search)
+ ->withFilter(new TaskProjectFilter($project['id']))
+ ->getQuery()
+ )
->calculate();
- $this->response->html($this->helper->layout->app('listing/show', $params + array(
+ $this->response->html($this->helper->layout->app('listing/show', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'paginator' => $paginator,
- 'categories_list' => $this->category->getList($params['project']['id'], false),
- 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
- 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
)));
}
}
diff --git a/app/Controller/Oauth.php b/app/Controller/Oauth.php
index 452faecd..12b91144 100644
--- a/app/Controller/Oauth.php
+++ b/app/Controller/Oauth.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
+
/**
* OAuth controller
*
@@ -11,25 +13,6 @@ namespace Kanboard\Controller;
class Oauth extends Base
{
/**
- * Unlink external account
- *
- * @access public
- */
- public function unlink()
- {
- $backend = $this->request->getStringParam('backend');
- $this->checkCSRFParam();
-
- if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) {
- $this->flash->success(t('Your external account is not linked anymore to your profile.'));
- } else {
- $this->flash->failure(t('Unable to unlink your external account.'));
- }
-
- $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
- }
-
- /**
* Redirect to the provider if no code received
*
* @access private
@@ -38,9 +21,10 @@ class Oauth extends Base
protected function step1($provider)
{
$code = $this->request->getStringParam('code');
+ $state = $this->request->getStringParam('state');
if (! empty($code)) {
- $this->step2($provider, $code);
+ $this->step2($provider, $code, $state);
} else {
$this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl());
}
@@ -50,34 +34,44 @@ class Oauth extends Base
* Link or authenticate the user
*
* @access protected
- * @param string $provider
+ * @param string $providerName
* @param string $code
+ * @param string $state
*/
- protected function step2($provider, $code)
+ protected function step2($providerName, $code, $state)
{
- $this->authenticationManager->getProvider($provider)->setCode($code);
+ $provider = $this->authenticationManager->getProvider($providerName);
+ $provider->setCode($code);
+ $hasValidState = $provider->getService()->isValidateState($state);
if ($this->userSession->isLogged()) {
- $this->link($provider);
+ if ($hasValidState) {
+ $this->link($provider);
+ } else {
+ $this->flash->failure(t('The OAuth2 state parameter is invalid'));
+ $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
+ }
+ } else {
+ if ($hasValidState) {
+ $this->authenticate($providerName);
+ } else {
+ $this->authenticationFailure(t('The OAuth2 state parameter is invalid'));
+ }
}
-
- $this->authenticate($provider);
}
/**
* Link the account
*
* @access protected
- * @param string $provider
+ * @param OAuthAuthenticationProviderInterface $provider
*/
- protected function link($provider)
+ protected function link(OAuthAuthenticationProviderInterface $provider)
{
- $authProvider = $this->authenticationManager->getProvider($provider);
-
- if (! $authProvider->authenticate()) {
+ if (! $provider->authenticate()) {
$this->flash->failure(t('External authentication failed'));
} else {
- $this->userProfile->assign($this->userSession->getId(), $authProvider->getUser());
+ $this->userProfile->assign($this->userSession->getId(), $provider->getUser());
$this->flash->success(t('Your external account is linked to your profile successfully.'));
}
@@ -85,22 +79,52 @@ class Oauth extends Base
}
/**
+ * Unlink external account
+ *
+ * @access public
+ */
+ public function unlink()
+ {
+ $backend = $this->request->getStringParam('backend');
+ $this->checkCSRFParam();
+
+ if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) {
+ $this->flash->success(t('Your external account is not linked anymore to your profile.'));
+ } else {
+ $this->flash->failure(t('Unable to unlink your external account.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
+ }
+
+ /**
* Authenticate the account
*
* @access protected
- * @param string $provider
+ * @param string $providerName
*/
- protected function authenticate($provider)
+ protected function authenticate($providerName)
{
- if ($this->authenticationManager->oauthAuthentication($provider)) {
+ if ($this->authenticationManager->oauthAuthentication($providerName)) {
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
- $this->response->html($this->helper->layout->app('auth/index', array(
- 'errors' => array('login' => t('External authentication failed')),
- 'values' => array(),
- 'no_layout' => true,
- 'title' => t('Login')
- )));
+ $this->authenticationFailure(t('External authentication failed'));
}
}
+
+ /**
+ * Show login failure page
+ *
+ * @access protected
+ * @param string $message
+ */
+ protected function authenticationFailure($message)
+ {
+ $this->response->html($this->helper->layout->app('auth/index', array(
+ 'errors' => array('login' => $message),
+ 'values' => array(),
+ 'no_layout' => true,
+ 'title' => t('Login')
+ )));
+ }
}
diff --git a/app/Controller/ProjectOverview.php b/app/Controller/ProjectOverview.php
index b0687ed3..b2bb33d6 100644
--- a/app/Controller/ProjectOverview.php
+++ b/app/Controller/ProjectOverview.php
@@ -15,15 +15,18 @@ class ProjectOverview extends Base
*/
public function show()
{
- $params = $this->getProjectFilters('ProjectOverview', 'show');
- $params['users'] = $this->projectUserRole->getAllUsersGroupedByRole($params['project']['id']);
- $params['roles'] = $this->role->getProjectRoles();
- $params['events'] = $this->projectActivity->getProject($params['project']['id'], 10);
- $params['images'] = $this->projectFile->getAllImages($params['project']['id']);
- $params['files'] = $this->projectFile->getAllDocuments($params['project']['id']);
+ $project = $this->getProject();
+ $this->project->getColumnStats($project);
- $this->project->getColumnStats($params['project']);
-
- $this->response->html($this->helper->layout->app('project_overview/show', $params));
+ $this->response->html($this->helper->layout->app('project_overview/show', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
+ 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']),
+ 'roles' => $this->role->getProjectRoles(),
+ 'events' => $this->helper->projectActivity->getProjectEvents($project['id'], 10),
+ 'images' => $this->projectFile->getAllImages($project['id']),
+ 'files' => $this->projectFile->getAllDocuments($project['id']),
+ )));
}
}
diff --git a/app/Controller/ProjectPermission.php b/app/Controller/ProjectPermission.php
index 800da02f..e203c0db 100644
--- a/app/Controller/ProjectPermission.php
+++ b/app/Controller/ProjectPermission.php
@@ -83,7 +83,9 @@ class ProjectPermission extends Base
$project = $this->getProject();
$values = $this->request->getValues();
- if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
+ if (empty($values['user_id'])) {
+ $this->flash->failure(t('User not found.'));
+ } elseif ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
diff --git a/app/Controller/Search.php b/app/Controller/Search.php
index 9b9b9e65..a42e9d3d 100644
--- a/app/Controller/Search.php
+++ b/app/Controller/Search.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectsFilter;
+
/**
* Search controller
*
@@ -23,14 +25,12 @@ class Search extends Base
->setDirection('DESC');
if ($search !== '' && ! empty($projects)) {
- $query = $this
- ->taskFilter
- ->search($search)
- ->filterByProjects(array_keys($projects))
- ->getQuery();
-
$paginator
- ->setQuery($query)
+ ->setQuery($this->taskLexer
+ ->build($search)
+ ->withFilter(new TaskProjectsFilter(array_keys($projects)))
+ ->getQuery()
+ )
->calculate();
$nb_tasks = $paginator->getTotal();
@@ -46,4 +46,22 @@ class Search extends Base
'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
+
+ public function activity()
+ {
+ $search = urldecode($this->request->getStringParam('search'));
+ $events = $this->helper->projectActivity->searchEvents($search);
+ $nb_events = count($events);
+
+ $this->response->html($this->helper->layout->app('search/activity', array(
+ 'values' => array(
+ 'search' => $search,
+ 'controller' => 'search',
+ 'action' => 'activity',
+ ),
+ 'title' => t('Search in activity stream').($nb_events > 0 ? ' ('.$nb_events.')' : ''),
+ 'nb_events' => $nb_events,
+ 'events' => $events,
+ )));
+ }
}
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index dc10604e..902a32d6 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -71,17 +71,16 @@ class Task extends Base
$values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT));
$this->response->html($this->helper->layout->task('task/show', array(
+ 'task' => $task,
'project' => $this->project->getById($task['project_id']),
+ 'values' => $values,
'files' => $this->taskFile->getAllDocuments($task['id']),
'images' => $this->taskFile->getAllImages($task['id']),
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()),
'subtasks' => $subtasks,
'internal_links' => $this->taskLink->getAllGroupedByLabel($task['id']),
'external_links' => $this->taskExternalLink->getAll($task['id']),
- 'task' => $task,
- 'values' => $values,
'link_label_list' => $this->link->getList(0, false),
- 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false),
)));
}
@@ -96,6 +95,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/analytics', array(
'task' => $task,
+ 'project' => $this->project->getById($task['project_id']),
'lead_time' => $this->taskAnalytic->getLeadTime($task),
'cycle_time' => $this->taskAnalytic->getCycleTime($task),
'time_spent_columns' => $this->taskAnalytic->getTimeSpentByColumn($task),
@@ -121,6 +121,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/time_tracking_details', array(
'task' => $task,
+ 'project' => $this->project->getById($task['project_id']),
'subtask_paginator' => $subtask_paginator,
)));
}
@@ -136,6 +137,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/transitions', array(
'task' => $task,
+ 'project' => $this->project->getById($task['project_id']),
'transitions' => $this->transition->getAllByTask($task['id']),
)));
}
diff --git a/app/Controller/TaskHelper.php b/app/Controller/TaskHelper.php
index 7e340a6a..6835ab2b 100644
--- a/app/Controller/TaskHelper.php
+++ b/app/Controller/TaskHelper.php
@@ -2,6 +2,12 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskIdExclusionFilter;
+use Kanboard\Filter\TaskIdFilter;
+use Kanboard\Filter\TaskProjectsFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Formatter\TaskAutoCompleteFormatter;
+
/**
* Task Ajax Helper
*
@@ -11,31 +17,33 @@ namespace Kanboard\Controller;
class TaskHelper extends Base
{
/**
- * Task autocompletion (Ajax)
+ * Task auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $exclude_task_id = $this->request->getIntegerParam('exclude_task_id');
- if (empty($projects)) {
+ if (empty($project_ids)) {
$this->response->json(array());
- }
+ } else {
- $filter = $this->taskFilterAutoCompleteFormatter
- ->create()
- ->filterByProjects($projects)
- ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
+ $filter = $this->taskQuery->withFilter(new TaskProjectsFilter($project_ids));
- // Search by task id or by title
- if (ctype_digit($search)) {
- $filter->filterById($search);
- } else {
- $filter->filterByTitle($search);
- }
+ if (! empty($exclude_task_id)) {
+ $filter->withFilter(new TaskIdExclusionFilter(array($exclude_task_id)));
+ }
+
+ if (ctype_digit($search)) {
+ $filter->withFilter(new TaskIdFilter($search));
+ } else {
+ $filter->withFilter(new TaskTitleFilter($search));
+ }
- $this->response->json($filter->format());
+ $this->response->json($filter->format(new TaskAutoCompleteFormatter($this->container)));
+ }
}
}
diff --git a/app/Controller/UserHelper.php b/app/Controller/UserHelper.php
index 041ed2c8..47bbe554 100644
--- a/app/Controller/UserHelper.php
+++ b/app/Controller/UserHelper.php
@@ -2,6 +2,10 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\UserNameFilter;
+use Kanboard\Formatter\UserAutoCompleteFormatter;
+use Kanboard\Model\User as UserModel;
+
/**
* User Helper
*
@@ -11,19 +15,20 @@ namespace Kanboard\Controller;
class UserHelper extends Base
{
/**
- * User autocompletion (Ajax)
+ * User auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
- $this->response->json($users);
+ $filter = $this->userQuery->withFilter(new UserNameFilter($search));
+ $filter->getQuery()->asc(UserModel::TABLE.'.name')->asc(UserModel::TABLE.'.username');
+ $this->response->json($filter->format(new UserAutoCompleteFormatter($this->container)));
}
/**
- * User mention autocompletion (Ajax)
+ * User mention auto-completion (Ajax)
*
* @access public
*/
diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php
index f1ea8abe..dfa5a140 100644
--- a/app/Core/Action/ActionManager.php
+++ b/app/Core/Action/ActionManager.php
@@ -18,7 +18,7 @@ class ActionManager extends Base
* List of automatic actions
*
* @access private
- * @var array
+ * @var ActionBase[]
*/
private $actions = array();
diff --git a/app/Core/Base.php b/app/Core/Base.php
index f87f271a..2b619af5 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -48,18 +48,11 @@ use Pimple\Container;
* @property \Kanboard\Core\User\UserSession $userSession
* @property \Kanboard\Core\DateParser $dateParser
* @property \Kanboard\Core\Helper $helper
- * @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
- * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter
- * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter
- * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
- * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter
- * @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter
- * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
- * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
* @property \Kanboard\Model\ActionParameter $actionParameter
+ * @property \Kanboard\Model\AvatarFile $avatarFile
* @property \Kanboard\Model\Board $board
* @property \Kanboard\Model\Category $category
* @property \Kanboard\Model\Color $color
@@ -84,7 +77,6 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission
* @property \Kanboard\Model\ProjectUserRole $projectUserRole
- * @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter
* @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
@@ -98,7 +90,6 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskDuplication $taskDuplication
* @property \Kanboard\Model\TaskExternalLink $taskExternalLink
* @property \Kanboard\Model\TaskFinder $taskFinder
- * @property \Kanboard\Model\TaskFilter $taskFilter
* @property \Kanboard\Model\TaskLink $taskLink
* @property \Kanboard\Model\TaskModification $taskModification
* @property \Kanboard\Model\TaskPermission $taskPermission
@@ -136,6 +127,14 @@ use Pimple\Container;
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectActivityQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $userQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery
+ * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer
+ * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer
* @property \Psr\Log\LoggerInterface $logger
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
@@ -172,4 +171,18 @@ abstract class Base
{
return $this->container[$name];
}
+
+ /**
+ * Get object instance
+ *
+ * @static
+ * @access public
+ * @param Container $container
+ * @return static
+ */
+ public static function getInstance(Container $container)
+ {
+ $self = new static($container);
+ return $self;
+ }
}
diff --git a/app/Core/Cache/Base.php b/app/Core/Cache/Base.php
index 2879f1f1..d62b8507 100644
--- a/app/Core/Cache/Base.php
+++ b/app/Core/Cache/Base.php
@@ -11,26 +11,6 @@ namespace Kanboard\Core\Cache;
abstract class Base
{
/**
- * Fetch value from cache
- *
- * @abstract
- * @access public
- * @param string $key
- * @return mixed Null when not found, cached value otherwise
- */
- abstract public function get($key);
-
- /**
- * Save a new value in the cache
- *
- * @abstract
- * @access public
- * @param string $key
- * @param mixed $value
- */
- abstract public function set($key, $value);
-
- /**
* Proxy cache
*
* Note: Arguments must be scalar types
diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php
index 1fa423c2..804e6b34 100644
--- a/app/Core/ExternalLink/ExternalLinkManager.php
+++ b/app/Core/ExternalLink/ExternalLinkManager.php
@@ -23,7 +23,7 @@ class ExternalLinkManager extends Base
* Registered providers
*
* @access private
- * @var array
+ * @var ExternalLinkProviderInterface[]
*/
private $providers = array();
diff --git a/app/Core/Filter/CriteriaInterface.php b/app/Core/Filter/CriteriaInterface.php
new file mode 100644
index 00000000..009c4bd3
--- /dev/null
+++ b/app/Core/Filter/CriteriaInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Criteria Interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface CriteriaInterface
+{
+ /**
+ * Set the Query
+ *
+ * @access public
+ * @param Table $query
+ * @return CriteriaInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Set filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return CriteriaInterface
+ */
+ public function withFilter(FilterInterface $filter);
+
+ /**
+ * Apply condition
+ *
+ * @access public
+ * @return CriteriaInterface
+ */
+ public function apply();
+}
diff --git a/app/Core/Filter/FilterInterface.php b/app/Core/Filter/FilterInterface.php
new file mode 100644
index 00000000..7b66ec28
--- /dev/null
+++ b/app/Core/Filter/FilterInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Filter Interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface FilterInterface
+{
+ /**
+ * BaseFilter constructor
+ *
+ * @access public
+ * @param mixed $value
+ */
+ public function __construct($value = null);
+
+ /**
+ * Set the value
+ *
+ * @access public
+ * @param string $value
+ * @return FilterInterface
+ */
+ public function withValue($value);
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FilterInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes();
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply();
+}
diff --git a/app/Core/Filter/FormatterInterface.php b/app/Core/Filter/FormatterInterface.php
new file mode 100644
index 00000000..b7c04c51
--- /dev/null
+++ b/app/Core/Filter/FormatterInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Formatter interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface FormatterInterface
+{
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return mixed
+ */
+ public function format();
+}
diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php
new file mode 100644
index 00000000..041b58d3
--- /dev/null
+++ b/app/Core/Filter/Lexer.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+/**
+ * Lexer
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class Lexer
+{
+ /**
+ * Current position
+ *
+ * @access private
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * Token map
+ *
+ * @access private
+ * @var array
+ */
+ private $tokenMap = array(
+ "/^(\s+)/" => 'T_WHITESPACE',
+ '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
+ '/^(yesterday|tomorrow|today)/' => 'T_DATE',
+ '/^("(.*?)")/' => 'T_STRING',
+ "/^(\w+)/" => 'T_STRING',
+ "/^(#\d+)/" => 'T_STRING',
+ );
+
+ /**
+ * Default token
+ *
+ * @access private
+ * @var string
+ */
+ private $defaultToken = '';
+
+ /**
+ * Add token
+ *
+ * @access public
+ * @param string $regex
+ * @param string $token
+ * @return $this
+ */
+ public function addToken($regex, $token)
+ {
+ $this->tokenMap = array($regex => $token) + $this->tokenMap;
+ return $this;
+ }
+
+ /**
+ * Set default token
+ *
+ * @access public
+ * @param string $token
+ * @return $this
+ */
+ public function setDefaultToken($token)
+ {
+ $this->defaultToken = $token;
+ return $this;
+ }
+
+ /**
+ * Tokenize input string
+ *
+ * @access public
+ * @param string $input
+ * @return array
+ */
+ public function tokenize($input)
+ {
+ $tokens = array();
+ $this->offset = 0;
+
+ while (isset($input[$this->offset])) {
+ $result = $this->match(substr($input, $this->offset));
+
+ if ($result === false) {
+ return array();
+ }
+
+ $tokens[] = $result;
+ }
+
+ return $this->map($tokens);
+ }
+
+ /**
+ * Find a token that match and move the offset
+ *
+ * @access protected
+ * @param string $string
+ * @return array|boolean
+ */
+ protected function match($string)
+ {
+ foreach ($this->tokenMap as $pattern => $name) {
+ if (preg_match($pattern, $string, $matches)) {
+ $this->offset += strlen($matches[1]);
+
+ return array(
+ 'match' => trim($matches[1], '"'),
+ 'token' => $name,
+ );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Build map of tokens and matches
+ *
+ * @access protected
+ * @param array $tokens
+ * @return array
+ */
+ protected function map(array $tokens)
+ {
+ $map = array();
+ $leftOver = '';
+
+ while (false !== ($token = current($tokens))) {
+ if ($token['token'] === 'T_STRING' || $token['token'] === 'T_WHITESPACE') {
+ $leftOver .= $token['match'];
+ } else {
+ $next = next($tokens);
+
+ if ($next !== false && in_array($next['token'], array('T_STRING', 'T_DATE'))) {
+ $map[$token['token']][] = $next['match'];
+ }
+ }
+
+ next($tokens);
+ }
+
+ $leftOver = trim($leftOver);
+
+ if ($this->defaultToken !== '' && $leftOver !== '') {
+ $map[$this->defaultToken] = array($leftOver);
+ }
+
+ return $map;
+ }
+}
diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php
new file mode 100644
index 00000000..7a9a714f
--- /dev/null
+++ b/app/Core/Filter/LexerBuilder.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Lexer Builder
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class LexerBuilder
+{
+ /**
+ * Lexer object
+ *
+ * @access protected
+ * @var Lexer
+ */
+ protected $lexer;
+
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * List of filters
+ *
+ * @access protected
+ * @var FilterInterface[]
+ */
+ protected $filters;
+
+ /**
+ * QueryBuilder object
+ *
+ * @access protected
+ * @var QueryBuilder
+ */
+ protected $queryBuilder;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ */
+ public function __construct()
+ {
+ $this->lexer = new Lexer;
+ $this->queryBuilder = new QueryBuilder();
+ }
+
+ /**
+ * Add a filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @param bool $default
+ * @return LexerBuilder
+ */
+ public function withFilter(FilterInterface $filter, $default = false)
+ {
+ $attributes = $filter->getAttributes();
+
+ foreach ($attributes as $attribute) {
+ $this->filters[$attribute] = $filter;
+ $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute);
+
+ if ($default) {
+ $this->lexer->setDefaultToken($attribute);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the query
+ *
+ * @access public
+ * @param Table $query
+ * @return LexerBuilder
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ $this->queryBuilder->withQuery($this->query);
+ return $this;
+ }
+
+ /**
+ * Parse the input and build the query
+ *
+ * @access public
+ * @param string $input
+ * @return QueryBuilder
+ */
+ public function build($input)
+ {
+ $tokens = $this->lexer->tokenize($input);
+
+ foreach ($tokens as $token => $values) {
+ if (isset($this->filters[$token])) {
+ $this->applyFilters($this->filters[$token], $values);
+ }
+ }
+
+ return $this->queryBuilder;
+ }
+
+ /**
+ * Apply filters to the query
+ *
+ * @access protected
+ * @param FilterInterface $filter
+ * @param array $values
+ */
+ protected function applyFilters(FilterInterface $filter, array $values)
+ {
+ $len = count($values);
+
+ if ($len > 1) {
+ $criteria = new OrCriteria();
+ $criteria->withQuery($this->query);
+
+ foreach ($values as $value) {
+ $currentFilter = clone($filter);
+ $criteria->withFilter($currentFilter->withValue($value));
+ }
+
+ $this->queryBuilder->withCriteria($criteria);
+ } elseif ($len === 1) {
+ $this->queryBuilder->withFilter($filter->withValue($values[0]));
+ }
+ }
+
+ /**
+ * Clone object with deep copy
+ */
+ public function __clone()
+ {
+ $this->lexer = clone $this->lexer;
+ $this->query = clone $this->query;
+ $this->queryBuilder = clone $this->queryBuilder;
+ }
+}
diff --git a/app/Core/Filter/OrCriteria.php b/app/Core/Filter/OrCriteria.php
new file mode 100644
index 00000000..174b8458
--- /dev/null
+++ b/app/Core/Filter/OrCriteria.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * OR criteria
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class OrCriteria implements CriteriaInterface
+{
+ /**
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * @var FilterInterface[]
+ */
+ protected $filters = array();
+
+ /**
+ * Set the Query
+ *
+ * @access public
+ * @param Table $query
+ * @return CriteriaInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return CriteriaInterface
+ */
+ public function withFilter(FilterInterface $filter)
+ {
+ $this->filters[] = $filter;
+ return $this;
+ }
+
+ /**
+ * Apply condition
+ *
+ * @access public
+ * @return CriteriaInterface
+ */
+ public function apply()
+ {
+ $this->query->beginOr();
+
+ foreach ($this->filters as $filter) {
+ $filter->withQuery($this->query)->apply();
+ }
+
+ $this->query->closeOr();
+ return $this;
+ }
+}
diff --git a/app/Core/Filter/QueryBuilder.php b/app/Core/Filter/QueryBuilder.php
new file mode 100644
index 00000000..3de82b63
--- /dev/null
+++ b/app/Core/Filter/QueryBuilder.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Class QueryBuilder
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class QueryBuilder
+{
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * Set the query
+ *
+ * @access public
+ * @param Table $query
+ * @return QueryBuilder
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set a filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return QueryBuilder
+ */
+ public function withFilter(FilterInterface $filter)
+ {
+ $filter->withQuery($this->query)->apply();
+ return $this;
+ }
+
+ /**
+ * Set a criteria
+ *
+ * @access public
+ * @param CriteriaInterface $criteria
+ * @return QueryBuilder
+ */
+ public function withCriteria(CriteriaInterface $criteria)
+ {
+ $criteria->withQuery($this->query)->apply();
+ return $this;
+ }
+
+ /**
+ * Set a formatter
+ *
+ * @access public
+ * @param FormatterInterface $formatter
+ * @return string|array
+ */
+ public function format(FormatterInterface $formatter)
+ {
+ return $formatter->withQuery($this->query)->format();
+ }
+
+ /**
+ * Get the query result as array
+ *
+ * @access public
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->query->findAll();
+ }
+
+ /**
+ * Get Query object
+ *
+ * @access public
+ * @return Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Clone object with deep copy
+ */
+ public function __clone()
+ {
+ $this->query = clone $this->query;
+ }
+}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
index 3764a67c..66f8d429 100644
--- a/app/Core/Helper.php
+++ b/app/Core/Helper.php
@@ -10,18 +10,23 @@ use Pimple\Container;
* @package core
* @author Frederic Guillot
*
- * @property \Kanboard\Helper\AppHelper $app
- * @property \Kanboard\Helper\AssetHelper $asset
- * @property \Kanboard\Helper\DateHelper $dt
- * @property \Kanboard\Helper\FileHelper $file
- * @property \Kanboard\Helper\FormHelper $form
- * @property \Kanboard\Helper\ModelHelper $model
- * @property \Kanboard\Helper\SubtaskHelper $subtask
- * @property \Kanboard\Helper\TaskHelper $task
- * @property \Kanboard\Helper\TextHelper $text
- * @property \Kanboard\Helper\UrlHelper $url
- * @property \Kanboard\Helper\UserHelper $user
- * @property \Kanboard\Helper\LayoutHelper $layout
+ * @property \Kanboard\Helper\AppHelper $app
+ * @property \Kanboard\Helper\AssetHelper $asset
+ * @property \Kanboard\Helper\CalendarHelper $calendar
+ * @property \Kanboard\Helper\DateHelper $dt
+ * @property \Kanboard\Helper\FileHelper $file
+ * @property \Kanboard\Helper\FormHelper $form
+ * @property \Kanboard\Helper\HookHelper $hook
+ * @property \Kanboard\Helper\ICalHelper $ical
+ * @property \Kanboard\Helper\ModelHelper $model
+ * @property \Kanboard\Helper\SubtaskHelper $subtask
+ * @property \Kanboard\Helper\TaskHelper $task
+ * @property \Kanboard\Helper\TextHelper $text
+ * @property \Kanboard\Helper\UrlHelper $url
+ * @property \Kanboard\Helper\UserHelper $user
+ * @property \Kanboard\Helper\LayoutHelper $layout
+ * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader
+ * @property \Kanboard\Helper\ProjectActivityHelper $projectActivity
*/
class Helper
{
diff --git a/app/Core/Http/OAuth2.php b/app/Core/Http/OAuth2.php
index 6fa1fb0a..211ca5b4 100644
--- a/app/Core/Http/OAuth2.php
+++ b/app/Core/Http/OAuth2.php
@@ -12,14 +12,14 @@ use Kanboard\Core\Base;
*/
class OAuth2 extends Base
{
- private $clientId;
- private $secret;
- private $callbackUrl;
- private $authUrl;
- private $tokenUrl;
- private $scopes;
- private $tokenType;
- private $accessToken;
+ protected $clientId;
+ protected $secret;
+ protected $callbackUrl;
+ protected $authUrl;
+ protected $tokenUrl;
+ protected $scopes;
+ protected $tokenType;
+ protected $accessToken;
/**
* Create OAuth2 service
@@ -46,6 +46,33 @@ class OAuth2 extends Base
}
/**
+ * Generate OAuth2 state and return the token value
+ *
+ * @access public
+ * @return string
+ */
+ public function getState()
+ {
+ if (! isset($this->sessionStorage->oauthState) || empty($this->sessionStorage->oauthState)) {
+ $this->sessionStorage->oauthState = $this->token->getToken();
+ }
+
+ return $this->sessionStorage->oauthState;
+ }
+
+ /**
+ * Check the validity of the state (CSRF token)
+ *
+ * @access public
+ * @param string $state
+ * @return bool
+ */
+ public function isValidateState($state)
+ {
+ return $state === $this->getState();
+ }
+
+ /**
* Get authorization url
*
* @access public
@@ -58,6 +85,7 @@ class OAuth2 extends Base
'client_id' => $this->clientId,
'redirect_uri' => $this->callbackUrl,
'scope' => implode(' ', $this->scopes),
+ 'state' => $this->getState(),
);
return $this->authUrl.'?'.http_build_query($params);
@@ -94,6 +122,7 @@ class OAuth2 extends Base
'client_secret' => $this->secret,
'redirect_uri' => $this->callbackUrl,
'grant_type' => 'authorization_code',
+ 'state' => $this->getState(),
);
$response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true);
diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php
index d098f519..996fc58d 100644
--- a/app/Core/Http/Response.php
+++ b/app/Core/Http/Response.php
@@ -14,6 +14,24 @@ use Kanboard\Core\Csv;
class Response extends Base
{
/**
+ * Send headers to cache a resource
+ *
+ * @access public
+ * @param integer $duration
+ * @param string $etag
+ */
+ public function cache($duration, $etag = '')
+ {
+ header('Pragma: cache');
+ header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT');
+ header('Cache-Control: public, max-age=' . $duration);
+
+ if ($etag) {
+ header('ETag: "' . $etag . '"');
+ }
+ }
+
+ /**
* Send no cache headers
*
* @access public
@@ -214,6 +232,20 @@ class Response extends Base
}
/**
+ * Send a iCal response
+ *
+ * @access public
+ * @param string $data Raw data
+ * @param integer $status_code HTTP status code
+ */
+ public function ical($data, $status_code = 200)
+ {
+ $this->status($status_code);
+ $this->contentType('text/calendar; charset=utf-8');
+ echo $data;
+ }
+
+ /**
* Send the security header: Content-Security-Policy
*
* @access public
diff --git a/app/Core/Ldap/Client.php b/app/Core/Ldap/Client.php
index 05658190..867d67fe 100644
--- a/app/Core/Ldap/Client.php
+++ b/app/Core/Ldap/Client.php
@@ -3,6 +3,7 @@
namespace Kanboard\Core\Ldap;
use LogicException;
+use Psr\Log\LoggerInterface;
/**
* LDAP Client
@@ -21,6 +22,14 @@ class Client
protected $ldap;
/**
+ * Logger instance
+ *
+ * @access private
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
* Establish LDAP connection
*
* @static
@@ -165,4 +174,39 @@ class Client
{
return LDAP_PASSWORD;
}
+
+ /**
+ * Set logger
+ *
+ * @access public
+ * @param LoggerInterface $logger
+ * @return Client
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ return $this;
+ }
+
+ /**
+ * Get logger
+ *
+ * @access public
+ * @return LoggerInterface
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * Test if a logger is defined
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasLogger()
+ {
+ return $this->logger !== null;
+ }
}
diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php
index 1779fa61..7c1524ca 100644
--- a/app/Core/Ldap/Query.php
+++ b/app/Core/Ldap/Query.php
@@ -48,6 +48,12 @@ class Query
*/
public function execute($baseDn, $filter, array $attributes)
{
+ if (DEBUG && $this->client->hasLogger()) {
+ $this->client->getLogger()->debug('BaseDN='.$baseDn);
+ $this->client->getLogger()->debug('Filter='.$filter);
+ $this->client->getLogger()->debug('Attributes='.implode(', ', $attributes));
+ }
+
$sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes);
if ($sr === false) {
return $this;
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
index 52283434..d23ec07e 100644
--- a/app/Core/Ldap/User.php
+++ b/app/Core/Ldap/User.php
@@ -44,8 +44,7 @@ class User
*/
public static function getUser(Client $client, $username)
{
- $className = get_called_class();
- $self = new $className(new Query($client));
+ $self = new static(new Query($client));
return $self->find($self->getLdapUserPattern($username));
}
diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php
deleted file mode 100644
index df2d90ae..00000000
--- a/app/Core/Lexer.php
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-
-namespace Kanboard\Core;
-
-/**
- * Lexer
- *
- * @package core
- * @author Frederic Guillot
- */
-class Lexer
-{
- /**
- * Current position
- *
- * @access private
- * @var integer
- */
- private $offset = 0;
-
- /**
- * Token map
- *
- * @access private
- * @var array
- */
- private $tokenMap = array(
- "/^(assignee:)/" => 'T_ASSIGNEE',
- "/^(color:)/" => 'T_COLOR',
- "/^(due:)/" => 'T_DUE',
- "/^(updated:)/" => 'T_UPDATED',
- "/^(modified:)/" => 'T_UPDATED',
- "/^(created:)/" => 'T_CREATED',
- "/^(status:)/" => 'T_STATUS',
- "/^(description:)/" => 'T_DESCRIPTION',
- "/^(category:)/" => 'T_CATEGORY',
- "/^(column:)/" => 'T_COLUMN',
- "/^(project:)/" => 'T_PROJECT',
- "/^(swimlane:)/" => 'T_SWIMLANE',
- "/^(ref:)/" => 'T_REFERENCE',
- "/^(reference:)/" => 'T_REFERENCE',
- "/^(link:)/" => 'T_LINK',
- "/^(\s+)/" => 'T_WHITESPACE',
- '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
- '/^(yesterday|tomorrow|today)/' => 'T_DATE',
- '/^("(.*?)")/' => 'T_STRING',
- "/^(\w+)/" => 'T_STRING',
- "/^(#\d+)/" => 'T_STRING',
- );
-
- /**
- * Tokenize input string
- *
- * @access public
- * @param string $input
- * @return array
- */
- public function tokenize($input)
- {
- $tokens = array();
- $this->offset = 0;
-
- while (isset($input[$this->offset])) {
- $result = $this->match(substr($input, $this->offset));
-
- if ($result === false) {
- return array();
- }
-
- $tokens[] = $result;
- }
-
- return $tokens;
- }
-
- /**
- * Find a token that match and move the offset
- *
- * @access public
- * @param string $string
- * @return array|boolean
- */
- public function match($string)
- {
- foreach ($this->tokenMap as $pattern => $name) {
- if (preg_match($pattern, $string, $matches)) {
- $this->offset += strlen($matches[1]);
-
- return array(
- 'match' => trim($matches[1], '"'),
- 'token' => $name,
- );
- }
- }
-
- return false;
- }
-
- /**
- * Change the output of tokenizer to be easily parsed by the database filter
- *
- * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title']
- *
- * @access public
- * @param array $tokens
- * @return array
- */
- public function map(array $tokens)
- {
- $map = array(
- 'T_TITLE' => '',
- );
-
- while (false !== ($token = current($tokens))) {
- switch ($token['token']) {
- case 'T_ASSIGNEE':
- case 'T_COLOR':
- case 'T_CATEGORY':
- case 'T_COLUMN':
- case 'T_PROJECT':
- case 'T_SWIMLANE':
- case 'T_LINK':
- $next = next($tokens);
-
- if ($next !== false && $next['token'] === 'T_STRING') {
- $map[$token['token']][] = $next['match'];
- }
-
- break;
-
- case 'T_STATUS':
- case 'T_DUE':
- case 'T_UPDATED':
- case 'T_CREATED':
- case 'T_DESCRIPTION':
- case 'T_REFERENCE':
- $next = next($tokens);
-
- if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) {
- $map[$token['token']] = $next['match'];
- }
-
- break;
-
- default:
- $map['T_TITLE'] .= $token['match'];
- break;
- }
-
- next($tokens);
- }
-
- $map['T_TITLE'] = trim($map['T_TITLE']);
-
- if (empty($map['T_TITLE'])) {
- unset($map['T_TITLE']);
- }
-
- return $map;
- }
-}
diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php
index 667d9253..6e2f9660 100644
--- a/app/Core/Session/SessionStorage.php
+++ b/app/Core/Session/SessionStorage.php
@@ -21,6 +21,7 @@ namespace Kanboard\Core\Session;
* @property bool $boardCollapsed
* @property bool $twoFactorBeforeCodeCalled
* @property string $twoFactorSecret
+ * @property string $oauthState
*/
class SessionStorage
{
diff --git a/app/Core/Template.php b/app/Core/Template.php
index f85c7f28..1874d44a 100644
--- a/app/Core/Template.php
+++ b/app/Core/Template.php
@@ -7,6 +7,21 @@ namespace Kanboard\Core;
*
* @package core
* @author Frederic Guillot
+ *
+ * @property \Kanboard\Helper\AppHelper $app
+ * @property \Kanboard\Helper\AssetHelper $asset
+ * @property \Kanboard\Helper\DateHelper $dt
+ * @property \Kanboard\Helper\FileHelper $file
+ * @property \Kanboard\Helper\FormHelper $form
+ * @property \Kanboard\Helper\HookHelper $hook
+ * @property \Kanboard\Helper\ModelHelper $model
+ * @property \Kanboard\Helper\SubtaskHelper $subtask
+ * @property \Kanboard\Helper\TaskHelper $task
+ * @property \Kanboard\Helper\TextHelper $text
+ * @property \Kanboard\Helper\UrlHelper $url
+ * @property \Kanboard\Helper\UserHelper $user
+ * @property \Kanboard\Helper\LayoutHelper $layout
+ * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader
*/
class Template
{
@@ -84,25 +99,26 @@ class Template
/**
* Find template filename
*
- * Core template name: 'task/show'
- * Plugin template name: 'myplugin:task/show'
+ * Core template: 'task/show' or 'kanboard:task/show'
+ * Plugin template: 'myplugin:task/show'
*
* @access public
- * @param string $template_name
+ * @param string $template
* @return string
*/
- public function getTemplateFile($template_name)
+ public function getTemplateFile($template)
{
- $template_name = isset($this->overrides[$template_name]) ? $this->overrides[$template_name] : $template_name;
+ $plugin = '';
+ $template = isset($this->overrides[$template]) ? $this->overrides[$template] : $template;
+
+ if (strpos($template, ':') !== false) {
+ list($plugin, $template) = explode(':', $template);
+ }
- if (strpos($template_name, ':') !== false) {
- list($plugin, $template) = explode(':', $template_name);
- $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins';
- $path .= DIRECTORY_SEPARATOR.ucfirst($plugin).DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template.'.php';
- } else {
- $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template_name.'.php';
+ if ($plugin !== 'kanboard' && $plugin !== '') {
+ return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', '..', 'plugins', ucfirst($plugin), 'Template', $template.'.php'));
}
- return $path;
+ return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php'));
}
}
diff --git a/app/Core/Thumbnail.php b/app/Core/Thumbnail.php
new file mode 100644
index 00000000..733d3a3c
--- /dev/null
+++ b/app/Core/Thumbnail.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Kanboard\Core;
+
+/**
+ * Thumbnail Generator
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+class Thumbnail
+{
+ protected $metadata = array();
+ protected $srcImage;
+ protected $dstImage;
+
+ /**
+ * Create a thumbnail from a local file
+ *
+ * @static
+ * @access public
+ * @param string $filename
+ * @return Thumbnail
+ */
+ public static function createFromFile($filename)
+ {
+ $self = new static();
+ $self->fromFile($filename);
+ return $self;
+ }
+
+ /**
+ * Create a thumbnail from a string
+ *
+ * @static
+ * @access public
+ * @param string $blob
+ * @return Thumbnail
+ */
+ public static function createFromString($blob)
+ {
+ $self = new static();
+ $self->fromString($blob);
+ return $self;
+ }
+
+ /**
+ * Load the local image file in memory with GD
+ *
+ * @access public
+ * @param string $filename
+ * @return Thumbnail
+ */
+ public function fromFile($filename)
+ {
+ $this->metadata = getimagesize($filename);
+ $this->srcImage = imagecreatefromstring(file_get_contents($filename));
+ return $this;
+ }
+
+ /**
+ * Load the image blob in memory with GD
+ *
+ * @access public
+ * @param string $blob
+ * @return Thumbnail
+ */
+ public function fromString($blob)
+ {
+ if (!function_exists('getimagesizefromstring')) {
+ $uri = 'data://application/octet-stream;base64,' . base64_encode($blob);
+ $this->metadata = getimagesize($uri);
+ } else {
+ $this->metadata = getimagesizefromstring($blob);
+ }
+
+ $this->srcImage = imagecreatefromstring($blob);
+ return $this;
+ }
+
+ /**
+ * Resize the image
+ *
+ * @access public
+ * @param int $width
+ * @param int $height
+ * @return Thumbnail
+ */
+ public function resize($width = 250, $height = 100)
+ {
+ $srcWidth = $this->metadata[0];
+ $srcHeight = $this->metadata[1];
+ $dstX = 0;
+ $dstY = 0;
+
+ if ($width == 0 && $height == 0) {
+ $width = 100;
+ $height = 100;
+ }
+
+ if ($width > 0 && $height == 0) {
+ $dstWidth = $width;
+ $dstHeight = floor($srcHeight * ($width / $srcWidth));
+ $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
+ } elseif ($width == 0 && $height > 0) {
+ $dstWidth = floor($srcWidth * ($height / $srcHeight));
+ $dstHeight = $height;
+ $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
+ } else {
+ $srcRatio = $srcWidth / $srcHeight;
+ $resizeRatio = $width / $height;
+
+ if ($srcRatio <= $resizeRatio) {
+ $dstWidth = $width;
+ $dstHeight = floor($srcHeight * ($width / $srcWidth));
+ $dstY = ($dstHeight - $height) / 2 * (-1);
+ } else {
+ $dstWidth = floor($srcWidth * ($height / $srcHeight));
+ $dstHeight = $height;
+ $dstX = ($dstWidth - $width) / 2 * (-1);
+ }
+
+ $this->dstImage = imagecreatetruecolor($width, $height);
+ }
+
+ imagecopyresampled($this->dstImage, $this->srcImage, $dstX, $dstY, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
+
+ return $this;
+ }
+
+ /**
+ * Save the thumbnail to a local file
+ *
+ * @access public
+ * @param string $filename
+ * @return Thumbnail
+ */
+ public function toFile($filename)
+ {
+ imagejpeg($this->dstImage, $filename);
+ imagedestroy($this->dstImage);
+ imagedestroy($this->srcImage);
+ return $this;
+ }
+
+ /**
+ * Return the thumbnail as a string
+ *
+ * @access public
+ * @return string
+ */
+ public function toString()
+ {
+ ob_start();
+ imagejpeg($this->dstImage, null);
+ imagedestroy($this->dstImage);
+ imagedestroy($this->srcImage);
+ return ob_get_clean();
+ }
+
+ /**
+ * Output the thumbnail directly to the browser or stdout
+ *
+ * @access public
+ */
+ public function toOutput()
+ {
+ imagejpeg($this->dstImage, null);
+ imagedestroy($this->dstImage);
+ imagedestroy($this->srcImage);
+ }
+}
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index db2445a1..3423998d 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -75,78 +75,4 @@ class Tool
return $container;
}
-
- /**
- * Generate a jpeg thumbnail from an image
- *
- * @static
- * @access public
- * @param string $src_file Source file image
- * @param string $dst_file Destination file image
- * @param integer $resize_width Desired image width
- * @param integer $resize_height Desired image height
- */
- public static function generateThumbnail($src_file, $dst_file, $resize_width = 250, $resize_height = 100)
- {
- $metadata = getimagesize($src_file);
- $src_width = $metadata[0];
- $src_height = $metadata[1];
- $dst_y = 0;
- $dst_x = 0;
-
- if (empty($metadata['mime'])) {
- return;
- }
-
- if ($resize_width == 0 && $resize_height == 0) {
- $resize_width = 100;
- $resize_height = 100;
- }
-
- if ($resize_width > 0 && $resize_height == 0) {
- $dst_width = $resize_width;
- $dst_height = floor($src_height * ($resize_width / $src_width));
- $dst_image = imagecreatetruecolor($dst_width, $dst_height);
- } elseif ($resize_width == 0 && $resize_height > 0) {
- $dst_width = floor($src_width * ($resize_height / $src_height));
- $dst_height = $resize_height;
- $dst_image = imagecreatetruecolor($dst_width, $dst_height);
- } else {
- $src_ratio = $src_width / $src_height;
- $resize_ratio = $resize_width / $resize_height;
-
- if ($src_ratio <= $resize_ratio) {
- $dst_width = $resize_width;
- $dst_height = floor($src_height * ($resize_width / $src_width));
-
- $dst_y = ($dst_height - $resize_height) / 2 * (-1);
- } else {
- $dst_width = floor($src_width * ($resize_height / $src_height));
- $dst_height = $resize_height;
-
- $dst_x = ($dst_width - $resize_width) / 2 * (-1);
- }
-
- $dst_image = imagecreatetruecolor($resize_width, $resize_height);
- }
-
- switch ($metadata['mime']) {
- case 'image/jpeg':
- case 'image/jpg':
- $src_image = imagecreatefromjpeg($src_file);
- break;
- case 'image/png':
- $src_image = imagecreatefrompng($src_file);
- break;
- case 'image/gif':
- $src_image = imagecreatefromgif($src_file);
- break;
- default:
- return;
- }
-
- imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height);
- imagejpeg($dst_image, $dst_file);
- imagedestroy($dst_image);
- }
}
diff --git a/app/Core/User/Avatar/AvatarManager.php b/app/Core/User/Avatar/AvatarManager.php
index 71bd8aa5..5b61cbdb 100644
--- a/app/Core/User/Avatar/AvatarManager.php
+++ b/app/Core/User/Avatar/AvatarManager.php
@@ -32,23 +32,25 @@ class AvatarManager
}
/**
- * Render avatar html element
+ * Render avatar HTML element
*
* @access public
* @param string $user_id
* @param string $username
* @param string $name
* @param string $email
+ * @param string $avatar_path
* @param int $size
* @return string
*/
- public function render($user_id, $username, $name, $email, $size)
+ public function render($user_id, $username, $name, $email, $avatar_path, $size)
{
$user = array(
'id' => $user_id,
'username' => $username,
'name' => $name,
'email' => $email,
+ 'avatar_path' => $avatar_path,
);
krsort($this->providers);
@@ -80,6 +82,7 @@ class AvatarManager
'username' => '',
'name' => '?',
'email' => '',
+ 'avatar_path' => '',
);
return $provider->render($user, $size);
diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php
index e494e7b4..0034c47a 100644
--- a/app/Core/User/UserSession.php
+++ b/app/Core/User/UserSession.php
@@ -14,6 +14,19 @@ use Kanboard\Core\Security\Role;
class UserSession extends Base
{
/**
+ * Refresh current session if necessary
+ *
+ * @access public
+ * @param integer $user_id
+ */
+ public function refresh($user_id)
+ {
+ if ($this->getId() == $user_id) {
+ $this->initialize($this->user->getById($user_id));
+ }
+ }
+
+ /**
* Update user session
*
* @access public
diff --git a/app/Filter/BaseDateFilter.php b/app/Filter/BaseDateFilter.php
new file mode 100644
index 00000000..56fb2d78
--- /dev/null
+++ b/app/Filter/BaseDateFilter.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\DateParser;
+
+/**
+ * Base date filter class
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+abstract class BaseDateFilter extends BaseFilter
+{
+ /**
+ * DateParser object
+ *
+ * @access protected
+ * @var DateParser
+ */
+ protected $dateParser;
+
+ /**
+ * Set DateParser object
+ *
+ * @access public
+ * @param DateParser $dateParser
+ * @return $this
+ */
+ public function setDateParser(DateParser $dateParser)
+ {
+ $this->dateParser = $dateParser;
+ return $this;
+ }
+
+ /**
+ * Parse operator in the input string
+ *
+ * @access protected
+ * @return string
+ */
+ protected function parseOperator()
+ {
+ $operators = array(
+ '<=' => 'lte',
+ '>=' => 'gte',
+ '<' => 'lt',
+ '>' => 'gt',
+ );
+
+ foreach ($operators as $operator => $method) {
+ if (strpos($this->value, $operator) === 0) {
+ $this->value = substr($this->value, strlen($operator));
+ return $method;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Apply a date filter
+ *
+ * @access protected
+ * @param string $field
+ */
+ protected function applyDateFilter($field)
+ {
+ $method = $this->parseOperator();
+ $timestamp = $this->dateParser->getTimestampFromIsoFormat($this->value);
+
+ if ($method !== '') {
+ $this->query->$method($field, $this->getTimestampFromOperator($method, $timestamp));
+ } else {
+ $this->query->gte($field, $timestamp);
+ $this->query->lte($field, $timestamp + 86399);
+ }
+ }
+
+ /**
+ * Get timestamp from the operator
+ *
+ * @access public
+ * @param string $method
+ * @param integer $timestamp
+ * @return integer
+ */
+ protected function getTimestampFromOperator($method, $timestamp)
+ {
+ switch ($method) {
+ case 'lte':
+ return $timestamp + 86399;
+ case 'lt':
+ return $timestamp;
+ case 'gte':
+ return $timestamp;
+ case 'gt':
+ return $timestamp + 86400;
+ }
+
+ return $timestamp;
+ }
+}
diff --git a/app/Filter/BaseFilter.php b/app/Filter/BaseFilter.php
new file mode 100644
index 00000000..79a664be
--- /dev/null
+++ b/app/Filter/BaseFilter.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Base filter class
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+abstract class BaseFilter
+{
+ /**
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * BaseFilter constructor
+ *
+ * @access public
+ * @param mixed $value
+ */
+ public function __construct($value = null)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Get object instance
+ *
+ * @static
+ * @access public
+ * @param mixed $value
+ * @return static
+ */
+ public static function getInstance($value = null)
+ {
+ $self = new static($value);
+ return $self;
+ }
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return \Kanboard\Core\Filter\FilterInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set the value
+ *
+ * @access public
+ * @param string $value
+ * @return \Kanboard\Core\Filter\FilterInterface
+ */
+ public function withValue($value)
+ {
+ $this->value = $value;
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityCreationDateFilter.php b/app/Filter/ProjectActivityCreationDateFilter.php
new file mode 100644
index 00000000..d0b7f754
--- /dev/null
+++ b/app/Filter/ProjectActivityCreationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by creation date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityCreationDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('created');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(ProjectActivity::TABLE.'.date_creation');
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityCreatorFilter.php b/app/Filter/ProjectActivityCreatorFilter.php
new file mode 100644
index 00000000..c95569d6
--- /dev/null
+++ b/app/Filter/ProjectActivityCreatorFilter.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by creator
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityCreatorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('creator');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if ($this->value === 'me') {
+ $this->query->eq(ProjectActivity::TABLE . '.creator_id', $this->currentUserId);
+ } else {
+ $this->query->beginOr();
+ $this->query->ilike('uc.username', '%'.$this->value.'%');
+ $this->query->ilike('uc.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+}
diff --git a/app/Filter/ProjectActivityProjectIdFilter.php b/app/Filter/ProjectActivityProjectIdFilter.php
new file mode 100644
index 00000000..bb4d8bd1
--- /dev/null
+++ b/app/Filter/ProjectActivityProjectIdFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by projectId
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityProjectIdFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project_id');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectActivity::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityProjectIdsFilter.php b/app/Filter/ProjectActivityProjectIdsFilter.php
new file mode 100644
index 00000000..47cf0c25
--- /dev/null
+++ b/app/Filter/ProjectActivityProjectIdsFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by projectIds
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityProjectIdsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('projects');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (empty($this->value)) {
+ $this->query->eq(ProjectActivity::TABLE.'.project_id', 0);
+ } else {
+ $this->query->in(ProjectActivity::TABLE.'.project_id', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityProjectNameFilter.php b/app/Filter/ProjectActivityProjectNameFilter.php
new file mode 100644
index 00000000..0cf73657
--- /dev/null
+++ b/app/Filter/ProjectActivityProjectNameFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter activity events by project name
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityProjectNameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Project::TABLE.'.name', '%'.$this->value.'%');
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskIdFilter.php b/app/Filter/ProjectActivityTaskIdFilter.php
new file mode 100644
index 00000000..e99efe09
--- /dev/null
+++ b/app/Filter/ProjectActivityTaskIdFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by taskId
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityTaskIdFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('task_id');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectActivity::TABLE.'.task_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskStatusFilter.php b/app/Filter/ProjectActivityTaskStatusFilter.php
new file mode 100644
index 00000000..69e2c52d
--- /dev/null
+++ b/app/Filter/ProjectActivityTaskStatusFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter activity events by task status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityTaskStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if ($this->value === 'open') {
+ $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN);
+ } elseif ($this->value === 'closed') {
+ $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_CLOSED);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskTitleFilter.php b/app/Filter/ProjectActivityTaskTitleFilter.php
new file mode 100644
index 00000000..bf2afa30
--- /dev/null
+++ b/app/Filter/ProjectActivityTaskTitleFilter.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+
+/**
+ * Filter activity events by task title
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityTaskTitleFilter extends TaskTitleFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('title');
+ }
+}
diff --git a/app/Filter/ProjectGroupRoleProjectFilter.php b/app/Filter/ProjectGroupRoleProjectFilter.php
new file mode 100644
index 00000000..b0950868
--- /dev/null
+++ b/app/Filter/ProjectGroupRoleProjectFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectGroupRole;
+
+/**
+ * Filter ProjectGroupRole users by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectGroupRoleProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectGroupRoleUsernameFilter.php b/app/Filter/ProjectGroupRoleUsernameFilter.php
new file mode 100644
index 00000000..c10855bc
--- /dev/null
+++ b/app/Filter/ProjectGroupRoleUsernameFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\GroupMember;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\User;
+
+/**
+ * Filter ProjectGroupRole users by username
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectGroupRoleUsernameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->ilike(User::TABLE.'.username', $this->value.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectIdsFilter.php b/app/Filter/ProjectIdsFilter.php
new file mode 100644
index 00000000..641f7f18
--- /dev/null
+++ b/app/Filter/ProjectIdsFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectIdsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project_ids');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (empty($this->value)) {
+ $this->query->eq(Project::TABLE.'.id', 0);
+ } else {
+ $this->query->in(Project::TABLE.'.id', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectStatusFilter.php b/app/Filter/ProjectStatusFilter.php
new file mode 100644
index 00000000..a994600c
--- /dev/null
+++ b/app/Filter/ProjectStatusFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Project::TABLE.'.is_active', $this->value);
+ } elseif ($this->value === 'inactive' || $this->value === 'closed' || $this->value === 'disabled') {
+ $this->query->eq(Project::TABLE.'.is_active', 0);
+ } else {
+ $this->query->eq(Project::TABLE.'.is_active', 1);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectTypeFilter.php b/app/Filter/ProjectTypeFilter.php
new file mode 100644
index 00000000..e085e2f6
--- /dev/null
+++ b/app/Filter/ProjectTypeFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by type
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectTypeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('type');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Project::TABLE.'.is_private', $this->value);
+ } elseif ($this->value === 'private') {
+ $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_PRIVATE);
+ } else {
+ $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_TEAM);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectUserRoleProjectFilter.php b/app/Filter/ProjectUserRoleProjectFilter.php
new file mode 100644
index 00000000..3b880df5
--- /dev/null
+++ b/app/Filter/ProjectUserRoleProjectFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectUserRole;
+
+/**
+ * Filter ProjectUserRole users by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectUserRoleProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectUserRole::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectUserRoleUsernameFilter.php b/app/Filter/ProjectUserRoleUsernameFilter.php
new file mode 100644
index 00000000..c00493a3
--- /dev/null
+++ b/app/Filter/ProjectUserRoleUsernameFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\User;
+
+/**
+ * Filter ProjectUserRole users by username
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectUserRoleUsernameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query
+ ->join(User::TABLE, 'id', 'user_id')
+ ->ilike(User::TABLE.'.username', $this->value.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskAssigneeFilter.php b/app/Filter/TaskAssigneeFilter.php
new file mode 100644
index 00000000..783d6a12
--- /dev/null
+++ b/app/Filter/TaskAssigneeFilter.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+use Kanboard\Model\User;
+
+/**
+ * Filter tasks by assignee
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('assignee');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.owner_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $this->query->eq(Task::TABLE.'.owner_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $this->query->eq(Task::TABLE.'.owner_id', 0);
+ break;
+ default:
+ $this->query->beginOr();
+ $this->query->ilike(User::TABLE.'.username', '%'.$this->value.'%');
+ $this->query->ilike(User::TABLE.'.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+ }
+}
diff --git a/app/Filter/TaskCategoryFilter.php b/app/Filter/TaskCategoryFilter.php
new file mode 100644
index 00000000..517f24d9
--- /dev/null
+++ b/app/Filter/TaskCategoryFilter.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Category;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by category
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCategoryFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('category');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.category_id', $this->value);
+ } elseif ($this->value === 'none') {
+ $this->query->eq(Task::TABLE.'.category_id', 0);
+ } else {
+ $this->query->eq(Category::TABLE.'.name', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskColorFilter.php b/app/Filter/TaskColorFilter.php
new file mode 100644
index 00000000..784162d4
--- /dev/null
+++ b/app/Filter/TaskColorFilter.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Color;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by color
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskColorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Color object
+ *
+ * @access private
+ * @var Color
+ */
+ private $colorModel;
+
+ /**
+ * Set color model object
+ *
+ * @access public
+ * @param Color $colorModel
+ * @return TaskColorFilter
+ */
+ public function setColorModel(Color $colorModel)
+ {
+ $this->colorModel = $colorModel;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('color', 'colour');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.color_id', $this->colorModel->find($this->value));
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskColumnFilter.php b/app/Filter/TaskColumnFilter.php
new file mode 100644
index 00000000..9a4d4253
--- /dev/null
+++ b/app/Filter/TaskColumnFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Column;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by column
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskColumnFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('column');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.column_id', $this->value);
+ } else {
+ $this->query->eq(Column::TABLE.'.title', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCommentFilter.php b/app/Filter/TaskCommentFilter.php
new file mode 100644
index 00000000..455098c2
--- /dev/null
+++ b/app/Filter/TaskCommentFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Comment;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by comment
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCommentFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('comment');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Comment::TABLE.'.comment', '%'.$this->value.'%');
+ $this->query->join(Comment::TABLE, 'task_id', 'id', Task::TABLE);
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCompletionDateFilter.php b/app/Filter/TaskCompletionDateFilter.php
new file mode 100644
index 00000000..f206a3e2
--- /dev/null
+++ b/app/Filter/TaskCompletionDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by completion date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCompletionDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('completed');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_completed');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCreationDateFilter.php b/app/Filter/TaskCreationDateFilter.php
new file mode 100644
index 00000000..bb6efad6
--- /dev/null
+++ b/app/Filter/TaskCreationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by creation date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCreationDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('created');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_creation');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCreatorFilter.php b/app/Filter/TaskCreatorFilter.php
new file mode 100644
index 00000000..af35e6bc
--- /dev/null
+++ b/app/Filter/TaskCreatorFilter.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by creator
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCreatorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('creator');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.creator_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $this->query->eq(Task::TABLE.'.creator_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $this->query->eq(Task::TABLE.'.creator_id', 0);
+ break;
+ default:
+ $this->query->beginOr();
+ $this->query->ilike('uc.username', '%'.$this->value.'%');
+ $this->query->ilike('uc.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+ }
+}
diff --git a/app/Filter/TaskDescriptionFilter.php b/app/Filter/TaskDescriptionFilter.php
new file mode 100644
index 00000000..6dda58ae
--- /dev/null
+++ b/app/Filter/TaskDescriptionFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by description
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDescriptionFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('description', 'desc');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Task::TABLE.'.description', '%'.$this->value.'%');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskDueDateFilter.php b/app/Filter/TaskDueDateFilter.php
new file mode 100644
index 00000000..e36efdd0
--- /dev/null
+++ b/app/Filter/TaskDueDateFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by due date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDueDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('due');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->neq(Task::TABLE.'.date_due', 0);
+ $this->query->notNull(Task::TABLE.'.date_due');
+ $this->applyDateFilter(Task::TABLE.'.date_due');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskDueDateRangeFilter.php b/app/Filter/TaskDueDateRangeFilter.php
new file mode 100644
index 00000000..10deb0d3
--- /dev/null
+++ b/app/Filter/TaskDueDateRangeFilter.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by due date range
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDueDateRangeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->gte(Task::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0]));
+ $this->query->lte(Task::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1]));
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskIdExclusionFilter.php b/app/Filter/TaskIdExclusionFilter.php
new file mode 100644
index 00000000..8bfefb2b
--- /dev/null
+++ b/app/Filter/TaskIdExclusionFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Exclude task ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskIdExclusionFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('exclude');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->notin(Task::TABLE.'.id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskIdFilter.php b/app/Filter/TaskIdFilter.php
new file mode 100644
index 00000000..87bac794
--- /dev/null
+++ b/app/Filter/TaskIdFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by id
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskIdFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('id');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskLinkFilter.php b/app/Filter/TaskLinkFilter.php
new file mode 100644
index 00000000..18a13a09
--- /dev/null
+++ b/app/Filter/TaskLinkFilter.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Link;
+use Kanboard\Model\Task;
+use Kanboard\Model\TaskLink;
+use PicoDb\Database;
+use PicoDb\Table;
+
+/**
+ * Filter tasks by link name
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskLinkFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskLinkFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('link');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ $task_ids = $this->getSubQuery()->findAllByColumn('task_id');
+
+ if (! empty($task_ids)) {
+ $this->query->in(Task::TABLE.'.id', $task_ids);
+ } else {
+ $this->query->eq(Task::TABLE.'.id', 0); // No match
+ }
+ }
+
+ /**
+ * Get subquery
+ *
+ * @access protected
+ * @return Table
+ */
+ protected function getSubQuery()
+ {
+ return $this->db->table(TaskLink::TABLE)
+ ->columns(
+ TaskLink::TABLE.'.task_id',
+ Link::TABLE.'.label'
+ )
+ ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE)
+ ->ilike(Link::TABLE.'.label', $this->value);
+ }
+}
diff --git a/app/Filter/TaskModificationDateFilter.php b/app/Filter/TaskModificationDateFilter.php
new file mode 100644
index 00000000..5036e9c1
--- /dev/null
+++ b/app/Filter/TaskModificationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by modification date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskModificationDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('updated', 'modified');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_modification');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskProjectFilter.php b/app/Filter/TaskProjectFilter.php
new file mode 100644
index 00000000..e432efee
--- /dev/null
+++ b/app/Filter/TaskProjectFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.project_id', $this->value);
+ } else {
+ $this->query->ilike(Project::TABLE.'.name', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskProjectsFilter.php b/app/Filter/TaskProjectsFilter.php
new file mode 100644
index 00000000..47636b1d
--- /dev/null
+++ b/app/Filter/TaskProjectsFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by project ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskProjectsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('projects');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (empty($this->value)) {
+ $this->query->eq(Task::TABLE.'.project_id', 0);
+ } else {
+ $this->query->in(Task::TABLE.'.project_id', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskReferenceFilter.php b/app/Filter/TaskReferenceFilter.php
new file mode 100644
index 00000000..4ad47dd5
--- /dev/null
+++ b/app/Filter/TaskReferenceFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by reference
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskReferenceFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('reference', 'ref');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.reference', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskStartDateFilter.php b/app/Filter/TaskStartDateFilter.php
new file mode 100644
index 00000000..dd30762b
--- /dev/null
+++ b/app/Filter/TaskStartDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by start date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskStartDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('started');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_started');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskStatusFilter.php b/app/Filter/TaskStatusFilter.php
new file mode 100644
index 00000000..0ba4361e
--- /dev/null
+++ b/app/Filter/TaskStatusFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if ($this->value === 'open' || $this->value === 'closed') {
+ $this->query->eq(Task::TABLE.'.is_active', $this->value === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED);
+ } else {
+ $this->query->eq(Task::TABLE.'.is_active', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskSubtaskAssigneeFilter.php b/app/Filter/TaskSubtaskAssigneeFilter.php
new file mode 100644
index 00000000..4c757315
--- /dev/null
+++ b/app/Filter/TaskSubtaskAssigneeFilter.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Subtask;
+use Kanboard\Model\Task;
+use Kanboard\Model\User;
+use PicoDb\Database;
+use PicoDb\Table;
+
+/**
+ * Filter tasks by subtasks assignee
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskSubtaskAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskSubtaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskSubtaskAssigneeFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('subtask:assignee');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ $task_ids = $this->getSubQuery()->findAllByColumn('task_id');
+
+ if (! empty($task_ids)) {
+ $this->query->in(Task::TABLE.'.id', $task_ids);
+ } else {
+ $this->query->eq(Task::TABLE.'.id', 0); // No match
+ }
+ }
+
+ /**
+ * Get subquery
+ *
+ * @access protected
+ * @return Table
+ */
+ protected function getSubQuery()
+ {
+ $subquery = $this->db->table(Subtask::TABLE)
+ ->columns(
+ Subtask::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ User::TABLE.'.name',
+ User::TABLE.'.username'
+ )
+ ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
+ ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
+
+ return $this->applySubQueryFilter($subquery);
+ }
+
+ /**
+ * Apply subquery filter
+ *
+ * @access protected
+ * @param Table $subquery
+ * @return Table
+ */
+ protected function applySubQueryFilter(Table $subquery)
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $subquery->eq(Subtask::TABLE.'.user_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $subquery->eq(Subtask::TABLE.'.user_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $subquery->eq(Subtask::TABLE.'.user_id', 0);
+ break;
+ default:
+ $subquery->beginOr();
+ $subquery->ilike(User::TABLE.'.username', $this->value.'%');
+ $subquery->ilike(User::TABLE.'.name', '%'.$this->value.'%');
+ $subquery->closeOr();
+ }
+ }
+
+ return $subquery;
+ }
+}
diff --git a/app/Filter/TaskSwimlaneFilter.php b/app/Filter/TaskSwimlaneFilter.php
new file mode 100644
index 00000000..4e030244
--- /dev/null
+++ b/app/Filter/TaskSwimlaneFilter.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+use Kanboard\Model\Swimlane;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by swimlane
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskSwimlaneFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('swimlane');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.swimlane_id', $this->value);
+ } elseif ($this->value === 'default') {
+ $this->query->eq(Task::TABLE.'.swimlane_id', 0);
+ } else {
+ $this->query->beginOr();
+ $this->query->ilike(Swimlane::TABLE.'.name', $this->value);
+ $this->query->ilike(Project::TABLE.'.default_swimlane', $this->value);
+ $this->query->closeOr();
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskTitleFilter.php b/app/Filter/TaskTitleFilter.php
new file mode 100644
index 00000000..9853369c
--- /dev/null
+++ b/app/Filter/TaskTitleFilter.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by title
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskTitleFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('title');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (ctype_digit($this->value) || (strlen($this->value) > 1 && $this->value{0} === '#' && ctype_digit(substr($this->value, 1)))) {
+ $this->query->beginOr();
+ $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $this->value));
+ $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
+ $this->query->closeOr();
+ } else {
+ $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/UserNameFilter.php b/app/Filter/UserNameFilter.php
new file mode 100644
index 00000000..dfb07fdd
--- /dev/null
+++ b/app/Filter/UserNameFilter.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+
+class UserNameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('name');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->beginOr()
+ ->ilike('username', '%'.$this->value.'%')
+ ->ilike('name', '%'.$this->value.'%')
+ ->closeOr();
+
+ return $this;
+ }
+}
diff --git a/app/Formatter/BaseFormatter.php b/app/Formatter/BaseFormatter.php
new file mode 100644
index 00000000..a9f0ad15
--- /dev/null
+++ b/app/Formatter/BaseFormatter.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\FormatterInterface;
+use PicoDb\Table;
+
+/**
+ * Class BaseFormatter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+abstract class BaseFormatter extends Base
+{
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+}
diff --git a/app/Formatter/TaskFilterCalendarEvent.php b/app/Formatter/BaseTaskCalendarFormatter.php
index 12ea8687..8fab3e9a 100644
--- a/app/Formatter/TaskFilterCalendarEvent.php
+++ b/app/Formatter/BaseTaskCalendarFormatter.php
@@ -2,7 +2,7 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
* Common class to handle calendar events
@@ -10,7 +10,7 @@ use Kanboard\Model\TaskFilter;
* @package formatter
* @author Frederic Guillot
*/
-abstract class TaskFilterCalendarEvent extends TaskFilter
+abstract class BaseTaskCalendarFormatter extends BaseFormatter
{
/**
* Column used for event start date
@@ -29,20 +29,12 @@ abstract class TaskFilterCalendarEvent extends TaskFilter
protected $endColumn = 'date_completed';
/**
- * Full day event flag
- *
- * @access private
- * @var boolean
- */
- private $fullDay = false;
-
- /**
* Transform results to calendar events
*
* @access public
* @param string $start_column Column name for the start date
* @param string $end_column Column name for the end date
- * @return TaskFilterCalendarEvent
+ * @return FormatterInterface
*/
public function setColumns($start_column, $end_column = '')
{
@@ -50,27 +42,4 @@ abstract class TaskFilterCalendarEvent extends TaskFilter
$this->endColumn = $end_column ?: $start_column;
return $this;
}
-
- /**
- * When called calendar events will be full day
- *
- * @access public
- * @return TaskFilterCalendarEvent
- */
- public function setFullDay()
- {
- $this->fullDay = true;
- return $this;
- }
-
- /**
- * Return true if the events are full day
- *
- * @access public
- * @return boolean
- */
- public function isFullDay()
- {
- return $this->fullDay;
- }
}
diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php
new file mode 100644
index 00000000..6a96b3e6
--- /dev/null
+++ b/app/Formatter/BoardFormatter.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Board Formatter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+class BoardFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Project id
+ *
+ * @access protected
+ * @var integer
+ */
+ protected $projectId;
+
+ /**
+ * Set ProjectId
+ *
+ * @access public
+ * @param integer $projectId
+ * @return $this
+ */
+ public function setProjectId($projectId)
+ {
+ $this->projectId = $projectId;
+ return $this;
+ }
+
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $tasks = $this->query
+ ->eq(Task::TABLE.'.project_id', $this->projectId)
+ ->asc(Task::TABLE.'.position')
+ ->findAll();
+
+ return $this->board->getBoard($this->projectId, function ($project_id, $column_id, $swimlane_id) use ($tasks) {
+ return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) {
+ return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id;
+ });
+ });
+ }
+}
diff --git a/app/Formatter/FormatterInterface.php b/app/Formatter/FormatterInterface.php
deleted file mode 100644
index 0bb61292..00000000
--- a/app/Formatter/FormatterInterface.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Kanboard\Formatter;
-
-/**
- * Formatter Interface
- *
- * @package formatter
- * @author Frederic Guillot
- */
-interface FormatterInterface
-{
- public function format();
-}
diff --git a/app/Formatter/GroupAutoCompleteFormatter.php b/app/Formatter/GroupAutoCompleteFormatter.php
index 7023e367..4d552886 100644
--- a/app/Formatter/GroupAutoCompleteFormatter.php
+++ b/app/Formatter/GroupAutoCompleteFormatter.php
@@ -2,8 +2,12 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
+use Kanboard\Core\Group\GroupProviderInterface;
+use PicoDb\Table;
+
/**
- * Autocomplete formatter for groups
+ * Auto-complete formatter for groups
*
* @package formatter
* @author Frederic Guillot
@@ -14,25 +18,35 @@ class GroupAutoCompleteFormatter implements FormatterInterface
* Groups found
*
* @access private
- * @var array
+ * @var GroupProviderInterface[]
*/
private $groups;
/**
- * Format groups for the ajax autocompletion
+ * Format groups for the ajax auto-completion
*
* @access public
- * @param array $groups
- * @return GroupAutoCompleteFormatter
+ * @param GroupProviderInterface[] $groups
*/
- public function setGroups(array $groups)
+ public function __construct(array $groups)
{
$this->groups = $groups;
+ }
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query)
+ {
return $this;
}
/**
- * Format groups for the ajax autocompletion
+ * Format groups for the ajax auto-completion
*
* @access public
* @return array
diff --git a/app/Formatter/ProjectActivityEventFormatter.php b/app/Formatter/ProjectActivityEventFormatter.php
new file mode 100644
index 00000000..ae80e5e7
--- /dev/null
+++ b/app/Formatter/ProjectActivityEventFormatter.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+
+class ProjectActivityEventFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $events = $this->query->findAll();
+
+ foreach ($events as &$event) {
+ $event += $this->unserializeEvent($event['data']);
+ unset($event['data']);
+
+ $event['author'] = $event['author_name'] ?: $event['author_username'];
+ $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event);
+ $event['event_content'] = $this->renderEvent($event);
+ }
+
+ return $events;
+ }
+
+ /**
+ * Decode event data, supports unserialize() and json_decode()
+ *
+ * @access protected
+ * @param string $data Serialized data
+ * @return array
+ */
+ protected function unserializeEvent($data)
+ {
+ if ($data{0} === 'a') {
+ return unserialize($data);
+ }
+
+ return json_decode($data, true) ?: array();
+ }
+
+ /**
+ * Get the event html content
+ *
+ * @access protected
+ * @param array $params Event properties
+ * @return string
+ */
+ protected function renderEvent(array $params)
+ {
+ return $this->template->render(
+ 'event/'.str_replace('.', '_', $params['event_name']),
+ $params
+ );
+ }
+}
diff --git a/app/Formatter/ProjectGanttFormatter.php b/app/Formatter/ProjectGanttFormatter.php
index 4f73e217..aee1f27f 100644
--- a/app/Formatter/ProjectGanttFormatter.php
+++ b/app/Formatter/ProjectGanttFormatter.php
@@ -2,7 +2,7 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\Project;
+use Kanboard\Core\Filter\FormatterInterface;
/**
* Gantt chart formatter for projects
@@ -10,41 +10,9 @@ use Kanboard\Model\Project;
* @package formatter
* @author Frederic Guillot
*/
-class ProjectGanttFormatter extends Project implements FormatterInterface
+class ProjectGanttFormatter extends BaseFormatter implements FormatterInterface
{
/**
- * List of projects
- *
- * @access private
- * @var array
- */
- private $projects = array();
-
- /**
- * Filter projects to generate the Gantt chart
- *
- * @access public
- * @param int[] $project_ids
- * @return ProjectGanttFormatter
- */
- public function filter(array $project_ids)
- {
- if (empty($project_ids)) {
- $this->projects = array();
- } else {
- $this->projects = $this->db
- ->table(self::TABLE)
- ->asc('start_date')
- ->in('id', $project_ids)
- ->eq('is_active', self::ACTIVE)
- ->eq('is_private', 0)
- ->findAll();
- }
-
- return $this;
- }
-
- /**
* Format projects to be displayed in the Gantt chart
*
* @access public
@@ -52,10 +20,11 @@ class ProjectGanttFormatter extends Project implements FormatterInterface
*/
public function format()
{
+ $projects = $this->query->findAll();
$colors = $this->color->getDefaultColors();
$bars = array();
- foreach ($this->projects as $project) {
+ foreach ($projects as $project) {
$start = empty($project['start_date']) ? time() : strtotime($project['start_date']);
$end = empty($project['end_date']) ? $start : strtotime($project['end_date']);
$color = next($colors) ?: reset($colors);
diff --git a/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php
new file mode 100644
index 00000000..c5d4e2be
--- /dev/null
+++ b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+
+class SubtaskTimeTrackingCalendarFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Format calendar events
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $events = array();
+
+ foreach ($this->query->findAll() as $row) {
+ $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
+
+ $events[] = array(
+ 'id' => $row['id'],
+ 'subtask_id' => $row['subtask_id'],
+ 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
+ 'start' => date('Y-m-d\TH:i:s', $row['start']),
+ 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
+ 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
+ 'borderColor' => $this->color->getBorderColor($row['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
+ 'editable' => false,
+ );
+ }
+
+ return $events;
+ }
+}
diff --git a/app/Formatter/TaskFilterAutoCompleteFormatter.php b/app/Formatter/TaskAutoCompleteFormatter.php
index c9af4654..480ee797 100644
--- a/app/Formatter/TaskFilterAutoCompleteFormatter.php
+++ b/app/Formatter/TaskAutoCompleteFormatter.php
@@ -2,19 +2,19 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
use Kanboard\Model\Task;
-use Kanboard\Model\TaskFilter;
/**
- * Autocomplete formatter for task filter
+ * Task AutoComplete Formatter
*
- * @package formatter
- * @author Frederic Guillot
+ * @package formatter
+ * @author Frederic Guillot
*/
-class TaskFilterAutoCompleteFormatter extends TaskFilter implements FormatterInterface
+class TaskAutoCompleteFormatter extends BaseFormatter implements FormatterInterface
{
/**
- * Format the tasks for the ajax autocompletion
+ * Apply formatter
*
* @access public
* @return array
diff --git a/app/Formatter/TaskFilterCalendarFormatter.php b/app/Formatter/TaskCalendarFormatter.php
index 1b5d6ca4..60b9a062 100644
--- a/app/Formatter/TaskFilterCalendarFormatter.php
+++ b/app/Formatter/TaskCalendarFormatter.php
@@ -2,15 +2,37 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
+
/**
* Calendar event formatter for task filter
*
* @package formatter
* @author Frederic Guillot
*/
-class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface
+class TaskCalendarFormatter extends BaseTaskCalendarFormatter implements FormatterInterface
{
/**
+ * Full day event flag
+ *
+ * @access private
+ * @var boolean
+ */
+ private $fullDay = false;
+
+ /**
+ * When called calendar events will be full day
+ *
+ * @access public
+ * @return FormatterInterface
+ */
+ public function setFullDay()
+ {
+ $this->fullDay = true;
+ return $this;
+ }
+
+ /**
* Transform tasks to calendar events
*
* @access public
@@ -31,8 +53,8 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For
'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]),
'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()),
- 'editable' => $this->isFullDay(),
- 'allday' => $this->isFullDay(),
+ 'editable' => $this->fullDay,
+ 'allday' => $this->fullDay,
);
}
@@ -47,6 +69,6 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For
*/
private function getDateTimeFormat()
{
- return $this->isFullDay() ? 'Y-m-d' : 'Y-m-d\TH:i:s';
+ return $this->fullDay ? 'Y-m-d' : 'Y-m-d\TH:i:s';
}
}
diff --git a/app/Formatter/TaskFilterGanttFormatter.php b/app/Formatter/TaskGanttFormatter.php
index a4eef1ee..3209aa37 100644
--- a/app/Formatter/TaskFilterGanttFormatter.php
+++ b/app/Formatter/TaskGanttFormatter.php
@@ -2,15 +2,15 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * Gantt chart formatter for task filter
+ * Task Gantt Formatter
*
- * @package formatter
- * @author Frederic Guillot
+ * @package formatter
+ * @author Frederic Guillot
*/
-class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface
+class TaskGanttFormatter extends BaseFormatter implements FormatterInterface
{
/**
* Local cache for project columns
@@ -19,9 +19,9 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface
* @var array
*/
private $columns = array();
-
+
/**
- * Format tasks to be displayed in the Gantt chart
+ * Apply formatter
*
* @access public
* @return array
diff --git a/app/Formatter/TaskFilterICalendarFormatter.php b/app/Formatter/TaskICalFormatter.php
index 25b3aea0..a149f725 100644
--- a/app/Formatter/TaskFilterICalendarFormatter.php
+++ b/app/Formatter/TaskICalFormatter.php
@@ -6,14 +6,15 @@ use DateTime;
use Eluceo\iCal\Component\Calendar;
use Eluceo\iCal\Component\Event;
use Eluceo\iCal\Property\Event\Attendees;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * iCal event formatter for task filter
+ * iCal event formatter for tasks
*
* @package formatter
* @author Frederic Guillot
*/
-class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface
+class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterInterface
{
/**
* Calendar object
@@ -39,7 +40,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
*
* @access public
* @param \Eluceo\iCal\Component\Calendar $vCalendar
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function setCalendar(Calendar $vCalendar)
{
@@ -48,10 +49,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Transform results to ical events
+ * Transform results to iCal events
*
* @access public
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function addDateTimeEvents()
{
@@ -73,10 +74,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Transform results to all day ical events
+ * Transform results to all day iCal events
*
* @access public
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function addFullDayEvents()
{
@@ -96,7 +97,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Get common events for task ical events
+ * Get common events for task iCal events
*
* @access protected
* @param array $task
diff --git a/app/Formatter/UserFilterAutoCompleteFormatter.php b/app/Formatter/UserAutoCompleteFormatter.php
index b98e0d69..c46a24d0 100644
--- a/app/Formatter/UserFilterAutoCompleteFormatter.php
+++ b/app/Formatter/UserAutoCompleteFormatter.php
@@ -3,15 +3,15 @@
namespace Kanboard\Formatter;
use Kanboard\Model\User;
-use Kanboard\Model\UserFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * Autocomplete formatter for user filter
+ * Auto-complete formatter for user filter
*
* @package formatter
* @author Frederic Guillot
*/
-class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface
+class UserAutoCompleteFormatter extends BaseFormatter implements FormatterInterface
{
/**
* Format the tasks for the ajax autocompletion
diff --git a/app/Helper/AvatarHelper.php b/app/Helper/AvatarHelper.php
index c4e27ed9..a36d9b4a 100644
--- a/app/Helper/AvatarHelper.php
+++ b/app/Helper/AvatarHelper.php
@@ -20,16 +20,17 @@ class AvatarHelper extends Base
* @param string $username
* @param string $name
* @param string $email
+ * @param string $avatar_path
* @param string $css
* @param int $size
* @return string
*/
- public function render($user_id, $username, $name, $email, $css = 'avatar-left', $size = 48)
+ public function render($user_id, $username, $name, $email, $avatar_path, $css = 'avatar-left', $size = 48)
{
if (empty($user_id) && empty($username)) {
$html = $this->avatarManager->renderDefault($size);
} else {
- $html = $this->avatarManager->render($user_id, $username, $name, $email, $size);
+ $html = $this->avatarManager->render($user_id, $username, $name, $email, $avatar_path, $size);
}
return '<div class="avatar avatar-'.$size.' '.$css.'">'.$html.'</div>';
@@ -39,26 +40,29 @@ class AvatarHelper extends Base
* Render small user avatar
*
* @access public
- * @param string $user_id
- * @param string $username
- * @param string $name
- * @param string $email
+ * @param string $user_id
+ * @param string $username
+ * @param string $name
+ * @param string $email
+ * @param string $avatar_path
+ * @param string $css
* @return string
*/
- public function small($user_id, $username, $name, $email, $css = '')
+ public function small($user_id, $username, $name, $email, $avatar_path, $css = '')
{
- return $this->render($user_id, $username, $name, $email, $css, 20);
+ return $this->render($user_id, $username, $name, $email, $avatar_path, $css, 20);
}
/**
* Get a small avatar for the current user
*
* @access public
+ * @param string $css
* @return string
*/
public function currentUserSmall($css = '')
{
$user = $this->userSession->getAll();
- return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $css);
+ return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css);
}
}
diff --git a/app/Helper/CalendarHelper.php b/app/Helper/CalendarHelper.php
new file mode 100644
index 00000000..d5f4af21
--- /dev/null
+++ b/app/Helper/CalendarHelper.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskDueDateRangeFilter;
+use Kanboard\Formatter\SubtaskTimeTrackingCalendarFormatter;
+use Kanboard\Formatter\TaskCalendarFormatter;
+
+/**
+ * Calendar Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class CalendarHelper extends Base
+{
+ /**
+ * Get formatted calendar task due events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getTaskDateDueEvents(QueryBuilder $queryBuilder, $start, $end)
+ {
+ $formatter = new TaskCalendarFormatter($this->container);
+ $formatter->setFullDay();
+ $formatter->setColumns('date_due');
+
+ return $queryBuilder
+ ->withFilter(new TaskDueDateRangeFilter(array($start, $end)))
+ ->format($formatter);
+ }
+
+ /**
+ * Get formatted calendar task events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getTaskEvents(QueryBuilder $queryBuilder, $start, $end)
+ {
+ $startColumn = $this->config->get('calendar_project_tasks', 'date_started');
+
+ $queryBuilder->getQuery()->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ $startColumn,
+ 'date_due'
+ ));
+
+ $formatter = new TaskCalendarFormatter($this->container);
+ $formatter->setColumns($startColumn, 'date_due');
+
+ return $queryBuilder->format($formatter);
+ }
+
+ /**
+ * Get formatted calendar subtask time tracking events
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getSubtaskTimeTrackingEvents($user_id, $start, $end)
+ {
+ $formatter = new SubtaskTimeTrackingCalendarFormatter($this->container);
+ return $formatter
+ ->withQuery($this->subtaskTimeTracking->getUserQuery($user_id)
+ ->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ 'start',
+ 'end'
+ ))
+ )
+ ->format();
+ }
+
+ /**
+ * Build SQL condition for a given time range
+ *
+ * @access public
+ * @param string $start_time Start timestamp
+ * @param string $end_time End timestamp
+ * @param string $start_column Start column name
+ * @param string $end_column End column name
+ * @return string
+ */
+ public function getCalendarCondition($start_time, $end_time, $start_column, $end_column)
+ {
+ $start_column = $this->db->escapeIdentifier($start_column);
+ $end_column = $this->db->escapeIdentifier($end_column);
+
+ $conditions = array(
+ "($start_column >= '$start_time' AND $start_column <= '$end_time')",
+ "($start_column <= '$start_time' AND $end_column >= '$start_time')",
+ "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
+ );
+
+ return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
+ }
+}
diff --git a/app/Helper/ICalHelper.php b/app/Helper/ICalHelper.php
new file mode 100644
index 00000000..dc399bf8
--- /dev/null
+++ b/app/Helper/ICalHelper.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskDueDateRangeFilter;
+use Kanboard\Formatter\TaskICalFormatter;
+use Eluceo\iCal\Component\Calendar as iCalendar;
+
+/**
+ * ICal Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class ICalHelper extends Base
+{
+ /**
+ * Get formatted calendar task due events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param iCalendar $calendar
+ * @param string $start
+ * @param string $end
+ */
+ public function addTaskDateDueEvents(QueryBuilder $queryBuilder, iCalendar $calendar, $start, $end)
+ {
+ $queryBuilder->withFilter(new TaskDueDateRangeFilter(array($start, $end)));
+
+ $formatter = new TaskICalFormatter($this->container);
+ $formatter->setColumns('date_due');
+ $formatter->setCalendar($calendar);
+ $formatter->withQuery($queryBuilder->getQuery());
+ $formatter->addFullDayEvents();
+ }
+}
diff --git a/app/Helper/ProjectActivityHelper.php b/app/Helper/ProjectActivityHelper.php
new file mode 100644
index 00000000..0638a978
--- /dev/null
+++ b/app/Helper/ProjectActivityHelper.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Filter\ProjectActivityProjectIdFilter;
+use Kanboard\Filter\ProjectActivityProjectIdsFilter;
+use Kanboard\Filter\ProjectActivityTaskIdFilter;
+use Kanboard\Formatter\ProjectActivityEventFormatter;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Project Activity Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class ProjectActivityHelper extends Base
+{
+ /**
+ * Search events
+ *
+ * @access public
+ * @param string $search
+ * @return array
+ */
+ public function searchEvents($search)
+ {
+ $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
+ $events = array();
+
+ if ($search !== '') {
+ $queryBuilder = $this->projectActivityLexer->build($search);
+ $queryBuilder
+ ->withFilter(new ProjectActivityProjectIdsFilter(array_keys($projects)))
+ ->getQuery()
+ ->desc(ProjectActivity::TABLE.'.id')
+ ->limit(500)
+ ;
+
+ $events = $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get project activity events
+ *
+ * @access public
+ * @param integer $project_id
+ * @param int $limit
+ * @return array
+ */
+ public function getProjectEvents($project_id, $limit = 50)
+ {
+ $queryBuilder = $this->projectActivityQuery
+ ->withFilter(new ProjectActivityProjectIdFilter($project_id));
+
+ $queryBuilder->getQuery()
+ ->desc(ProjectActivity::TABLE.'.id')
+ ->limit($limit)
+ ;
+
+ return $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+
+ /**
+ * Get projects activity events
+ *
+ * @access public
+ * @param int[] $project_ids
+ * @param int $limit
+ * @return array
+ */
+ public function getProjectsEvents(array $project_ids, $limit = 50)
+ {
+ $queryBuilder = $this->projectActivityQuery
+ ->withFilter(new ProjectActivityProjectIdsFilter($project_ids));
+
+ $queryBuilder->getQuery()
+ ->desc(ProjectActivity::TABLE.'.id')
+ ->limit($limit)
+ ;
+
+ return $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+
+ /**
+ * Get task activity events
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getTaskEvents($task_id)
+ {
+ $queryBuilder = $this->projectActivityQuery
+ ->withFilter(new ProjectActivityTaskIdFilter($task_id));
+
+ $queryBuilder->getQuery()->desc(ProjectActivity::TABLE.'.id');
+
+ return $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+}
diff --git a/app/Helper/ProjectHeaderHelper.php b/app/Helper/ProjectHeaderHelper.php
new file mode 100644
index 00000000..19570059
--- /dev/null
+++ b/app/Helper/ProjectHeaderHelper.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+
+/**
+ * Project Header Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class ProjectHeaderHelper extends Base
+{
+ /**
+ * Get current search query
+ *
+ * @access public
+ * @param array $project
+ * @return string
+ */
+ public function getSearchQuery(array $project)
+ {
+ $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
+ $this->userSession->setFilters($project['id'], $search);
+ return urldecode($search);
+ }
+
+ /**
+ * Render project header (views switcher and search box)
+ *
+ * @access public
+ * @param array $project
+ * @param string $controller
+ * @param string $action
+ * @param bool $boardView
+ * @return string
+ */
+ public function render(array $project, $controller, $action, $boardView = false)
+ {
+ $filters = array(
+ 'controller' => $controller,
+ 'action' => $action,
+ 'project_id' => $project['id'],
+ 'search' => $this->getSearchQuery($project),
+ );
+
+ return $this->template->render('project_header/header', array(
+ 'project' => $project,
+ 'filters' => $filters,
+ 'categories_list' => $this->category->getList($project['id'], false),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], false),
+ 'custom_filters_list' => $this->customFilter->getAll($project['id'], $this->userSession->getId()),
+ 'board_view' => $boardView,
+ ));
+ }
+
+ /**
+ * Get project description
+ *
+ * @access public
+ * @param array &$project
+ * @return string
+ */
+ public function getDescription(array &$project)
+ {
+ if ($project['owner_id'] > 0) {
+ $description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL;
+
+ if (! empty($project['description'])) {
+ $description .= '***'.PHP_EOL.PHP_EOL;
+ $description .= $project['description'];
+ }
+ } else {
+ $description = $project['description'];
+ }
+
+ return $description;
+ }
+}
diff --git a/app/Helper/UserHelper.php b/app/Helper/UserHelper.php
index ee7d8ba5..c3369dfd 100644
--- a/app/Helper/UserHelper.php
+++ b/app/Helper/UserHelper.php
@@ -34,7 +34,7 @@ class UserHelper extends Base
{
$initials = '';
- foreach (explode(' ', $name) as $string) {
+ foreach (explode(' ', $name, 2) as $string) {
$initials .= mb_substr($string, 0, 1);
}
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index c9abcd08..fadf0a1b 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Svi projekti',
'Add a new column' => 'Dodaj novu kolonu',
'Title' => 'Naslov',
- 'Nobody assigned' => 'Niko nije dodijeljen',
'Assigned to %s' => 'Dodijeljen korisniku %s',
'Remove a column' => 'Ukloni kolonu',
'Remove a column from a board' => 'Ukloni kolonu sa table',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Broj zadataka',
'User' => 'Korisnik',
'Comments' => 'Komentari',
- 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown',
'Leave a comment' => 'Ostavi komentar',
'Comment is required' => 'Komentar je obavezan',
'Leave a description' => 'Dodaj opis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Praćenje vremena:',
'New sub-task' => 'Novi pod-zadatak',
'New attachment added "%s"' => 'Ubačen novi prilog "%s"',
- 'Comment updated' => 'Komentar ažuriran',
'New comment posted by %s' => '%s ostavio novi komentar',
'New attachment' => 'Novi prilog',
'New comment' => 'Novi komentar',
+ 'Comment updated' => 'Komentar ažuriran',
'New subtask' => 'Novi pod-zadatak',
'Subtask updated' => 'Pod-zadatak ažuriran',
'Task updated' => 'Zadatak ažuriran',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvijek prihvatljiv, primjer: "%s", "%s"',
'New private project' => 'Novi privatni projekat',
'This project is private' => 'Ovaj projekat je privatan',
- 'Type here to create a new sub-task' => 'Piši ovdje za kreiranje novog pod-zadatka',
'Add' => 'Dodaj',
'Start date' => 'Datum početka',
'Time estimated' => 'Procijenjeno vrijeme',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Izvoz zbirnog pregleda po danima za "%s"',
'Exports' => 'Izvozi',
'This export contains the number of tasks per column grouped per day.' => 'Ovaj izvoz sadržava broj zadataka po koloni grupisanih po danima.',
- 'Nothing to preview...' => 'Ništa za pokazati...',
- 'Preview' => 'Pregled',
- 'Write' => 'Piši',
'Active swimlanes' => 'Aktivne swimline trake',
'Add a new swimlane' => 'Dodaj novu swimline traku',
'Change default swimlane' => 'Preimenuj podrazumijevanu swimline traku',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Trajanje zadatka u danima',
'Days in this column' => 'Dani u ovoj koloni',
'%dd' => '%dd',
- 'Add a link' => 'Dodaj vezu',
'Add a new link' => 'Dodaj novu vezu',
'Do you really want to remove this link: "%s"?' => 'Da li zaista želite ukloniti ovu vezu: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Da li zaista želite ukloniti ovu vezu sa zadatkom #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Tok mojih aktivnosti',
'My calendar' => 'Moj kalendar',
'Search tasks' => 'Pretraga zadataka',
- 'Back to the calendar' => 'Vrati na kalendar',
- 'Filters' => 'Filteri',
'Reset filters' => 'Vrati filtere na početno',
'My tasks due tomorrow' => 'Moji zadaci koje treba završiti sutra',
'Tasks due today' => 'Zadaci koje treba završiti danas',
@@ -772,14 +763,14 @@ return array(
'List' => 'Lista',
'Filter' => 'Filter',
'Advanced search' => 'Napredna pretraga',
- 'Example of query: ' => 'Primjer za upit',
- 'Search by project: ' => 'Pretraga po projektu',
- 'Search by column: ' => 'Pretraga po koloni',
- 'Search by assignee: ' => 'Pretraga po izvršiocu',
- 'Search by color: ' => 'Pretraga po boji',
- 'Search by category: ' => 'Pretraga po kategoriji',
- 'Search by description: ' => 'Pretraga po opisu',
- 'Search by due date: ' => 'Pretraga po datumu završetka',
+ 'Example of query: ' => 'Primjer za upit: ',
+ 'Search by project: ' => 'Pretraga po projektu: ',
+ 'Search by column: ' => 'Pretraga po koloni: ',
+ 'Search by assignee: ' => 'Pretraga po izvršiocu: ',
+ 'Search by color: ' => 'Pretraga po boji: ',
+ 'Search by category: ' => 'Pretraga po kategoriji: ',
+ 'Search by description: ' => 'Pretraga po opisu: ',
+ 'Search by due date: ' => 'Pretraga po datumu završetka: ',
'Lead and Cycle time for "%s"' => 'Vrijeme upravljanje i vremenski ciklus za "%s"',
'Average time spent into each column for "%s"' => 'Prosjek utrošenog vremena u svakoj koloni za "%s"',
'Average time spent into each column' => 'Prosjek utrošenog vrmena u svakoj koloni',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Datum završetka:',
'There is no start date or end date for this project.' => 'Nema početnog ili krajnjeg datuma za ovaj projekat.',
'Projects Gantt chart' => 'Gantogram projekata',
- 'Link type' => 'Tip veze',
'Change task color when using a specific task link' => 'Promijeni boju zadatka kada se koristi određena veza na zadatku',
'Task link creation or modification' => 'Veza na zadatku je napravljena ili izmijenjena',
'Milestone' => 'Prekretnica',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Podijeljeno',
'Owner' => 'Vlasnik',
'Unread notifications' => 'Nepročitana obavještenja',
- 'My filters' => 'Moji filteri',
'Notification methods:' => 'Metode obavještenja:',
'Import tasks from CSV file' => 'Uvezi zadatke putem CSV fajla',
'Unable to read your file' => 'Nemoguće pročitati fajl',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Korisničko ime mora biti malim slovima i jedinstveno',
'Passwords will be encrypted if present' => 'Šifra će biti kriptovana',
'%s attached a new file to the task %s' => '%s je dodano novi fajl u zadatak %s',
+ 'Link type' => 'Tip veze',
'Assign automatically a category based on a link' => 'Automatsko pridruživanje kategorije bazirano na vezi',
'BAM - Konvertible Mark' => 'BAM - Konvertibilna marka',
'Assignee Username' => 'Pridruži korisničko ime',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Zatvori zadatak kada nema aktivnosti',
'Duration in days' => 'Dužina trajanja u danima',
'Send email when there is no activity on a task' => 'Pošalji email kada nema aktivnosti na zadatku',
- 'List of external links' => 'Lista vanjskih veza',
'Unable to fetch link information.' => 'Ne mogu da pribavim informacije o vezi.',
'Daily background job for tasks' => 'Dnevni pozadinski poslovi na zadacima',
'Auto' => 'Automatski',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Vanjska veza',
'Copy and paste your link here...' => 'Kopiraj i zalijepi svoju vezu ovdje...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Trenutno nema vanjskih veza.',
'Internal links' => 'Unutrašnje veze',
- 'There is no internal link for the moment.' => 'Trenutno nema unutrašnjih veza.',
'Assign to me' => 'Dodijeli meni',
'Me' => 'Za mene',
'Do not duplicate anything' => 'Ništa ne dupliciraj',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Menadžment korisnika',
'Groups management' => 'Menadžment grupa',
'Create from another project' => 'Napravi iz drugog projekta',
- 'There is no subtask at the moment.' => 'Trenutno nema pod-zadataka.',
'open' => 'otvoreno',
'closed' => 'zatvoreno',
'Priority:' => 'Prioritet:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Početo:',
'Moved:' => 'Pomjereno:',
'Task #%d' => 'Zadatak #%d',
- 'Sub-tasks' => 'Pod-zadaci',
'Date and time format' => 'Format za datum i vrijeme',
'Time format' => 'Format za vrijeme',
'Start date: ' => 'Početni datum:',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Korisnički filteri',
'Category filters' => 'Kategorija filtera',
'Upload a file' => 'Dodaj fajl',
- 'There is no attachment at the moment.' => 'Trenutno nema priloga.',
'View file' => 'Pregled fajla',
'Last activity' => 'Posljednja aktivnost',
'Change subtask position' => 'Promijeni poziciju pod-zadatka',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Trenutno nema akcija.',
'Import actions from another project' => 'Uvezi akcije iz drugog projekta',
'There is no available project.' => 'Trenutno nema dostupnih projekata.',
+ 'Local File' => 'Lokalni fajl',
+ 'Configuration' => 'Konfiguracija',
+ 'PHP version:' => 'Verzija PHP-a:',
+ 'PHP SAPI:' => 'Verzija SAPI-a:',
+ 'OS version:' => 'Verzija OS-a:',
+ 'Database version:' => 'Verzija baze podataka:',
+ 'Browser:' => 'Pretraživač:',
+ 'Task view' => 'Pregled zadatka',
+ 'Edit task' => 'Uredi zadatak',
+ 'Edit description' => 'Uredi opis',
+ 'New internal link' => 'Nova unutrašnja veza',
+ 'Display list of keyboard shortcuts' => 'Prikaži listu prečica na tastaturi',
+ 'Menu' => 'Meni',
+ 'Set start date' => 'Postavi početni datum',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Dodaj sliku za moj avatar',
+ 'Remove my image' => 'Ukloni moju sliku',
+ 'The OAuth2 state parameter is invalid' => 'OAuth2 status parametar nije validan',
+ 'User not found.' => 'Korisnik nije pronađen.',
+ 'Search in activity stream' => 'Pretraži aktivnosti',
+ 'My activities' => 'Moje aktivnosti',
+ 'Activity until yesterday' => 'Aktivnosti do jučer',
+ 'Activity until today' => 'Aktivnosti do danas',
+ 'Search by creator: ' => 'Pretraga po kreatoru: ',
+ 'Search by creation date: ' => 'Pretraga po datumu kreiranja: ',
+ 'Search by task status: ' => 'Pretraga po statusu zadatka: ',
+ 'Search by task title: ' => 'Pretraga po naslovu zadatka: ',
+ 'Activity stream search' => 'Pretraga aktivnosti',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index 08836dec..777e9b42 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Všechny projekty',
'Add a new column' => 'Přidat nový sloupec',
'Title' => 'Název',
- 'Nobody assigned' => 'Nepřiřazena žádná osoba',
'Assigned to %s' => 'Přiřazeno uživateli: %s',
'Remove a column' => 'Vyjmout sloupec',
'Remove a column from a board' => 'Vyjmout sloupec z nástěnky',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Počet úkolů',
'User' => 'Uživatel',
'Comments' => 'Komentáře',
- 'Write your text in Markdown' => 'Můžete použít i Markdown-syntaxi',
'Leave a comment' => 'Zanechte komentář',
'Comment is required' => 'Komentář je vyžadován',
'Leave a description' => 'Vložte popis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Sledování času:',
'New sub-task' => 'Nový dílčí úkol',
'New attachment added "%s"' => 'Byla přidána nová příloha "%s".',
- 'Comment updated' => 'Komentář byl aktualizován.',
'New comment posted by %s' => 'Nový komentář publikovaný uživatelem %s',
'New attachment' => 'Nová příloha',
'New comment' => 'Nový komentář',
+ 'Comment updated' => 'Komentář byl aktualizován.',
'New subtask' => 'Nový dílčí úkol',
'Subtask updated' => 'Dílčí úkol byl aktualizován',
'Task updated' => 'Úkol byl aktualizován',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formát je vždy akceptován, například: "%s" a "%s"',
'New private project' => 'Nový soukromý projekt',
'This project is private' => 'Tento projekt je soukromuý',
- 'Type here to create a new sub-task' => 'Uveďte zde pro vytvoření nového dílčího úkolu',
'Add' => 'Přidat',
'Start date' => 'Počáteční datum',
'Time estimated' => 'Odhadovaný čas',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export denních přehledů pro "%s"',
'Exports' => 'Exporty',
'This export contains the number of tasks per column grouped per day.' => 'Tento export obsahuje počet úkolů pro jednotlivé sloupce seskupených podle dní.',
- 'Nothing to preview...' => 'Žádná položka k zobrazení ...',
- 'Preview' => 'Náhled',
- 'Write' => 'Režim psaní',
'Active swimlanes' => 'Aktive Swimlane',
'Add a new swimlane' => 'Přidat nový řádek',
'Change default swimlane' => 'Standard Swimlane ändern',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Doba trvání úkolu ve dnech',
'Days in this column' => 'Dní v tomto sloupci',
'%dd' => '%d d',
- 'Add a link' => 'Přidat odkaz',
'Add a new link' => 'Přidat nový odkaz',
'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?',
'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Přehled mých aktivit',
'My calendar' => 'Můj kalendář',
'Search tasks' => 'Hledání úkolů',
- 'Back to the calendar' => 'Zpět do kalendáře',
- 'Filters' => 'Filtry',
'Reset filters' => 'Resetovat filtry',
'My tasks due tomorrow' => 'Moje zítřejší úkoly',
'Tasks due today' => 'Dnešní úkoly',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 8e686ee4..7c255561 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle Projekter',
'Add a new column' => 'Tilføj en ny kolonne',
'Title' => 'Titel',
- 'Nobody assigned' => 'Ingen ansvarlig',
'Assigned to %s' => 'Ansvarlig: %s',
'Remove a column' => 'Fjern en kolonne',
'Remove a column from a board' => 'Fjern en kolonne fra et board',
@@ -166,7 +165,6 @@ return array(
// 'Task count' => '',
'User' => 'Bruger',
'Comments' => 'Kommentarer',
- 'Write your text in Markdown' => 'Skriv din tekst i markdown',
'Leave a comment' => 'Efterlad en kommentar',
'Comment is required' => 'Kommentar er krævet',
'Leave a description' => 'Efterlad en beskrivelse...',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tidsmåling:',
'New sub-task' => 'Ny under-opgave',
'New attachment added "%s"' => 'Ny vedhæftning tilføjet "%s"',
- 'Comment updated' => 'Kommentar opdateret',
'New comment posted by %s' => 'Ny kommentar af %s',
// 'New attachment' => '',
// 'New comment' => '',
+ 'Comment updated' => 'Kommentar opdateret',
// 'New subtask' => '',
// 'Subtask updated' => '',
// 'Task updated' => '',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er altid accepteret, eksempelvis: "%s" og "%s"',
'New private project' => 'Nyt privat projekt',
'This project is private' => 'Dette projekt er privat',
- 'Type here to create a new sub-task' => 'Skriv her for at tilføje en ny under-opgave',
'Add' => 'Tilføj',
'Start date' => 'Start dato',
'Time estimated' => 'Tid estimeret',
@@ -483,9 +480,6 @@ return array(
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
- // 'Nothing to preview...' => '',
- // 'Preview' => '',
- // 'Write' => '',
// 'Active swimlanes' => '',
// 'Add a new swimlane' => '',
// 'Change default swimlane' => '',
@@ -539,7 +533,6 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
- // 'Add a link' => '',
// 'Add a new link' => '',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 59c16e11..43b80561 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle Projekte',
'Add a new column' => 'Neue Spalte hinzufügen',
'Title' => 'Titel',
- 'Nobody assigned' => 'Nicht zugeordnet',
'Assigned to %s' => 'Zuständig: %s',
'Remove a column' => 'Spalte löschen',
'Remove a column from a board' => 'Spalte einer Pinnwand löschen',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Aufgabenanzahl',
'User' => 'Benutzer',
'Comments' => 'Kommentare',
- 'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax',
'Leave a comment' => 'Kommentar eingeben',
'Comment is required' => 'Ein Kommentar wird benötigt',
'Leave a description' => 'Beschreibung eingeben',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Zeittracking',
'New sub-task' => 'Neue Teilaufgabe',
'New attachment added "%s"' => 'Neuer Anhang "%s" wurde hinzugefügt.',
- 'Comment updated' => 'Kommentar wurde aktualisiert',
'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s',
'New attachment' => 'Neuer Anhang',
'New comment' => 'Neuer Kommentar',
+ 'Comment updated' => 'Kommentar wurde aktualisiert',
'New subtask' => 'Neue Teilaufgabe',
'Subtask updated' => 'Teilaufgabe aktualisiert',
'Task updated' => 'Aufgabe aktualisiert',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"',
'New private project' => 'Neues privates Projekt',
'This project is private' => 'Dieses Projekt ist privat',
- 'Type here to create a new sub-task' => 'Hier tippen, um eine neue Teilaufgabe zu erstellen',
'Add' => 'Hinzufügen',
'Start date' => 'Startdatum',
'Time estimated' => 'Geschätzte Zeit',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export der täglichen Projektzusammenfassung für "%s"',
'Exports' => 'Exporte',
'This export contains the number of tasks per column grouped per day.' => 'Dieser Export enthält die Anzahl der Aufgaben pro Spalte nach Tagen gruppiert.',
- 'Nothing to preview...' => 'Nichts in der Vorschau anzuzeigen ...',
- 'Preview' => 'Vorschau',
- 'Write' => 'Ändern',
'Active swimlanes' => 'Aktive Swimlane',
'Add a new swimlane' => 'Eine neue Swimlane hinzufügen',
'Change default swimlane' => 'Standard-Swimlane ändern',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Aufgabenalter in Tagen',
'Days in this column' => 'Tage in dieser Spalte',
'%dd' => '%dT',
- 'Add a link' => 'Verbindung hinzufügen',
'Add a new link' => 'Neue Verbindung hinzufügen',
'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?',
'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?',
@@ -627,9 +620,9 @@ return array(
'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.',
'Code' => 'Code',
'Two factor authentication' => 'Zwei-Faktor-Authentifizierung',
- 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI',
+ 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI: ',
'Check my code' => 'Überprüfe meinen Code',
- 'Secret key: ' => 'Geheimer Schlüssel',
+ 'Secret key: ' => 'Geheimer Schlüssel: ',
'Test your device' => 'Teste dein Gerät',
'Assign a color when the task is moved to a specific column' => 'Weise eine Farbe zu, wenn die Aufgabe zu einer bestimmten Spalte bewegt wird',
'%s via Kanboard' => '%s via Kanboard',
@@ -660,15 +653,15 @@ return array(
'Timeframe to calculate new due date' => 'Zeitfenster zur Berechnung für neues Ablaufdatum',
'Base date to calculate new due date' => 'Basisdatum zur Berechnung für neues Ablaufdatum',
'Action date' => 'Aktionsdatum',
- 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum:',
- 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt:',
+ 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum: ',
+ 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt: ',
'Day(s)' => 'Tag(e)',
'Existing due date' => 'Existierendes Ablaufdatum',
- 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum',
+ 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum: ',
'Month(s)' => 'Monat(e)',
'Recurrence' => 'Wiederholung',
- 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von:',
- 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt',
+ 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von: ',
+ 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt ',
'Timeframe to calculate new due date: ' => 'Zeitfenster zur Berechnung für neues Ablaufdatum: ',
'Trigger to generate recurrent task: ' => 'Auslöser für wiederkehrende Aufgabe: ',
'When task is closed' => 'Wenn Aufgabe geshlossen wird',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aktivitätsstream',
'My calendar' => 'Mein Kalender',
'Search tasks' => 'Suche nach Aufgaben',
- 'Back to the calendar' => 'Zurück zum Kalender',
- 'Filters' => 'Filter',
'Reset filters' => 'Filter zurücksetzen',
'My tasks due tomorrow' => 'Meine morgen fälligen Aufgaben',
'Tasks due today' => 'Heute fällige Aufgaben',
@@ -786,15 +777,15 @@ return array(
'Average time spent' => 'Durchschnittlicher Zeitverbrauch',
'This chart show the average time spent into each column for the last %d tasks.' => 'Dieses Diagramm zeigt die durchschnittliche Zeit in jeder Spalte der letzten %d Aufgaben.',
'Average Lead and Cycle time' => 'Durchschnittliche Zyklus- und Durchlaufzeit',
- 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit:',
- 'Average cycle time: ' => 'Durchschnittliche Zykluszeit:',
+ 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit: ',
+ 'Average cycle time: ' => 'Durchschnittliche Zykluszeit: ',
'Cycle Time' => 'Zykluszeit',
'Lead Time' => 'Durchlaufzeit',
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Das Diagramm zeigt die durchschnittliche Durchlauf- und Zykluszeit der letzten %d Aufgaben über die Zeit an.',
'Average time into each column' => 'Durchschnittzeit in jeder Spalte',
'Lead and cycle time' => 'Durchlauf- und Zykluszeit',
- 'Lead time: ' => 'Durchlaufzeit:',
- 'Cycle time: ' => 'Zykluszeit:',
+ 'Lead time: ' => 'Durchlaufzeit: ',
+ 'Cycle time: ' => 'Zykluszeit: ',
'Time spent into each column' => 'zeit verbracht in jeder Spalte',
'The lead time is the duration between the task creation and the completion.' => 'Die Durchlaufzeit ist die Dauer zwischen Erstellung und Fertigstellung.',
'The cycle time is the duration between the start date and the completion.' => 'Die Zykluszeit ist die Dauer zwischen Start und Fertigstellung.',
@@ -802,7 +793,7 @@ return array(
'Set automatically the start date' => 'Setze Startdatum automatisch',
'Edit Authentication' => 'Authentifizierung bearbeiten',
'Remote user' => 'Remote-Benutzer',
- 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Google und Github Accounts',
+ 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel: LDAP, Google und Github Accounts',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.',
'New remote user' => 'Neuer Remote-Benutzer',
'New local user' => 'Neuer lokaler Benutzer',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Endedatum:',
'There is no start date or end date for this project.' => 'Es gibt kein Startdatum oder Endedatum für dieses Projekt',
'Projects Gantt chart' => 'Projekt Gantt Diagramm',
- 'Link type' => 'Verbindungstyp',
'Change task color when using a specific task link' => 'Aufgabefarbe ändern bei bestimmter Aufgabenverbindung',
'Task link creation or modification' => 'Aufgabenverbindung erstellen oder bearbeiten',
'Milestone' => 'Meilenstein',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Geteilt',
'Owner' => 'Eigentümer',
'Unread notifications' => 'Ungelesene Benachrichtigungen',
- 'My filters' => 'Meine Filter',
'Notification methods:' => 'Benachrichtigungs-Methoden:',
'Import tasks from CSV file' => 'Importiere Aufgaben aus CSV Datei',
'Unable to read your file' => 'Die Datei kann nicht gelesen werden',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Benutzernamen müssen in Kleinbuschstaben und eindeutig sein',
'Passwords will be encrypted if present' => 'Passwörter werden verschlüsselt wenn vorhanden',
'%s attached a new file to the task %s' => '%s hat eine neue Datei zur Aufgabe %s hinzufgefügt',
+ 'Link type' => 'Verbindungstyp',
'Assign automatically a category based on a link' => 'Linkbasiert eine Kategorie automatisch zuordnen',
'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
'Assignee Username' => 'Benutzername des Zuständigen',
@@ -1032,7 +1022,7 @@ return array(
'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Kein Plugin hat eine Projekt-Benachrichtigungsmethode registriert. Sie können individuelle Meldungen in Ihrem Benutzerprofil konfigurieren',
'My dashboard' => 'Mein Dashboard',
'My profile' => 'Mein Profil',
- 'Project owner: ' => 'Projekt-Besitzer:',
+ 'Project owner: ' => 'Projekt-Besitzer: ',
'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'Die Projekt-Kennung ist optional und muss alphanumerisch sein, beispielsweise: MYPROJECT.',
'Project owner' => 'Projekt-Besitzer',
'Those dates are useful for the project Gantt chart.' => 'Diese Daten sind nützlich für das Gantt-Diagramm.',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Schliesse eine Aufgabe, wenn keine Aktivitäten vorhanden sind',
'Duration in days' => 'Dauer in Tagen',
'Send email when there is no activity on a task' => 'Versende eine Email, wenn keine Aktivitäten an einer Aufgabe vorhanden sind',
- 'List of external links' => 'Liste der externen Verbindungen',
'Unable to fetch link information.' => 'Kann keine Informationen über Verbindungen holen',
'Daily background job for tasks' => 'Tägliche Hintergrundarbeit für Aufgaben',
'Auto' => 'Auto',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Externe Verbindung',
'Copy and paste your link here...' => 'Kopieren Sie Ihren Link hier...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Es gibt im Moment keine externe Verbindung.',
'Internal links' => 'Interne Verbindungen',
- 'There is no internal link for the moment.' => 'Es gibt im Moment keine interne Verbindung.',
'Assign to me' => 'Mir zuweisen',
'Me' => 'Mich',
'Do not duplicate anything' => 'Nichts duplizieren',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Benutzermanagement',
'Groups management' => 'Gruppenmanagement',
'Create from another project' => 'Von einem anderen Projekt erstellen',
- 'There is no subtask at the moment.' => 'Es gibt im Moment keine Teilaufgabe',
'open' => 'offen',
'closed' => 'geschlossen',
'Priority:' => 'Priorität:',
@@ -1096,13 +1082,12 @@ return array(
'Started:' => 'Gestarted:',
'Moved:' => 'Verschoben:',
'Task #%d' => 'Aufgabe #%d',
- 'Sub-tasks' => 'Teilaufgaben',
'Date and time format' => 'Datums- und Zeitformat',
'Time format' => 'Zeitformat',
- 'Start date: ' => 'Anfangsdatum:',
- 'End date: ' => 'Enddatum:',
- 'New due date: ' => 'Neues Fälligkeitsdatum',
- 'Start date changed: ' => 'Anfangsdatum geändert:',
+ 'Start date: ' => 'Anfangsdatum: ',
+ 'End date: ' => 'Enddatum: ',
+ 'New due date: ' => 'Neues Fälligkeitsdatum: ',
+ 'Start date changed: ' => 'Anfangsdatum geändert: ',
'Disable private projects' => 'Private Projekte deaktivieren',
'Do you really want to remove this custom filter: "%s"?' => 'Wollen Sie diesen benutzerdefinierten Filter wirklich entfernen: "%s"?',
'Remove a custom filter' => 'Benutzerdefinierten Filter entfernen',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Benutzer-Filter',
'Category filters' => 'Kategorie-Filter',
'Upload a file' => 'Eine Datei hochladen',
- 'There is no attachment at the moment.' => 'Es gibt zur Zeit keine Anhänge',
'View file' => 'Datei ansehen',
'Last activity' => 'Letzte Aktivität',
'Change subtask position' => 'Position der Unteraufgabe ändern',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Es gibt zur Zeit keine Aktionen.',
'Import actions from another project' => 'Aktionen von einem anderen Projekt importieren',
'There is no available project.' => 'Es ist kein Projekt verfügbar.',
+ 'Local File' => 'Lokale Datei',
+ 'Configuration' => 'Konfiguration',
+ 'PHP version:' => 'PHP Version:',
+ 'PHP SAPI:' => 'PHP SAPI:',
+ 'OS version:' => 'OS Version:',
+ 'Database version:' => 'Datenbank Version:',
+ 'Browser:' => 'Browser:',
+ 'Task view' => 'Aufgaben Ansicht',
+ 'Edit task' => 'Aufgabe bearbeiten',
+ 'Edit description' => 'Beschreibung bearbeiten',
+ 'New internal link' => 'Neue interne Verbindung',
+ 'Display list of keyboard shortcuts' => 'Liste der Tastaturkürzel anzeigen',
+ 'Menu' => 'Menü',
+ 'Set start date' => 'Anfangsdatum setzen',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Mein Avatar Bild hochladen',
+ 'Remove my image' => 'Mein Bild entfernen',
+ 'The OAuth2 state parameter is invalid' => 'Der OAuth2 Statusparameter ist ungültig',
+ 'User not found.' => 'Benutzer nicht gefunden',
+ 'Search in activity stream' => 'Im Aktivitätenstrom suchen',
+ 'My activities' => 'Meine Aktivitäten',
+ 'Activity until yesterday' => 'Aktivitäten bis gestern',
+ 'Activity until today' => 'Aktivitäten bis heute',
+ 'Search by creator: ' => 'nach Ersteller suchen',
+ 'Search by creation date: ' => 'nach Datum suchen',
+ 'Search by task status: ' => 'nach Aufgabenstatus suchen',
+ 'Search by task title: ' => 'nach Titel suchen',
+ 'Activity stream search' => 'Im Aktivitätenstrom suchen',
+ 'Projects where "%s" is manager' => 'Projekte in denen "%s" Manager ist',
+ 'Projects where "%s" is member' => 'Projekte in denen "%s" Mitglied ist',
+ 'Open tasks assigned to "%s"' => 'Offene Aufgaben, die "%s" zugeteilt sind',
+ 'Closed tasks assigned to "%s"' => 'Geschlossene Aufgaben, die "%s" zugeteilt sind',
);
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
index 1f58fb69..664bf328 100644
--- a/app/Locale/el_GR/translations.php
+++ b/app/Locale/el_GR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Όλα τα έργα',
'Add a new column' => 'Πρόσθήκη στήλης',
'Title' => 'Τίτλος',
- 'Nobody assigned' => 'Δεν έχει ανατεθεί',
'Assigned to %s' => 'Ανατιθεμένο στον %s',
'Remove a column' => 'Αφαίρεση στήλης',
'Remove a column from a board' => 'Αφαίρεση στήλης από τον πίνακα',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Αρίθμηση εργασιών',
'User' => 'Χρήστης',
'Comments' => 'Σχόλια',
- 'Write your text in Markdown' => 'Δυνατότητα γραφής και σε Markdown',
'Leave a comment' => 'Αφήστε ένα σχόλιο',
'Comment is required' => 'Το σχόλιο απαιτείται',
'Leave a description' => 'Αφήστε μια περιγραφή',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Παρακολούθηση του χρόνου:',
'New sub-task' => 'Νέα υπο-εργασία',
'New attachment added "%s"' => 'Νέα επικόλληση προστέθηκε « %s »',
- 'Comment updated' => 'Το σχόλιο ενημερώθηκε',
'New comment posted by %s' => 'Νέο σχόλιο από τον χρήστη « %s »',
'New attachment' => 'New attachment',
'New comment' => 'Νέο σχόλιο',
+ 'Comment updated' => 'Το σχόλιο ενημερώθηκε',
'New subtask' => 'Νέα υπο-εργασία',
'Subtask updated' => 'Η Υπο-Εργασία ενημερώθηκε',
'Task updated' => 'Η εργασία ενημερώθηκε',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format είναι πάντα αποδεκτό, π.χ.: « %s » και « %s »',
'New private project' => 'Νέο ιδιωτικό έργο',
'This project is private' => 'Αυτό το έργο είναι ιδιωτικό',
- 'Type here to create a new sub-task' => 'Πληκτρολογήστε εδώ για να δημιουργήσετε μια νέα υπο-εργασία',
'Add' => 'Προσθήκη',
'Start date' => 'Ημερομηνία έναρξης',
'Time estimated' => 'Εκτιμώμενος χρόνος',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Εξαγωγή της καθημερινής περίληψης του έργου « %s »',
'Exports' => 'Εξαγωγές',
'This export contains the number of tasks per column grouped per day.' => 'Αυτή η κατάσταση περιέχει τον αριθμό των εργασιών ανά στήλη ομαδοποιημένα ανά ημέρα.',
- 'Nothing to preview...' => 'Τίποτα για προεπισκόπηση...',
- 'Preview' => 'Προεπισκόπηση',
- 'Write' => 'Write',
'Active swimlanes' => 'Ενεργές λωρίδες',
'Add a new swimlane' => 'Προσθήκη λωρίδας',
'Change default swimlane' => 'Αλλαγή της εξ\' ορισμού λωρίδας',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Χρόνος εργασίας σε μέρες',
'Days in this column' => 'Μέρες σε αυτή την στήλη',
'%dd' => '%dημ',
- 'Add a link' => 'Προσθήκη ενός link',
'Add a new link' => 'Προσθήκη ενός νέου link',
'Do you really want to remove this link: "%s"?' => 'Θέλετε σίγουρα να αφαιρέσετε αυτό το link : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Θέλετε σίγουρα να αφαιρέσετε αυτό το link του έργου n°%d ?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Η ροή δραστηριοτήτων μου',
'My calendar' => 'Το ημερολόγιο μου',
'Search tasks' => 'Αναζήτηση εργασιών',
- 'Back to the calendar' => 'Πίσω στο ημερολόγιο',
- 'Filters' => 'Φίλτρα',
'Reset filters' => 'Επαναφορά φίλτρων',
'My tasks due tomorrow' => 'Οι εργασίες καθηκόντων μου αύριο',
'Tasks due today' => 'Οι εργασίες καθηκόντων μου αύριο',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Ημερομηνία λήξης :',
'There is no start date or end date for this project.' => 'Δεν υπάρχει ημερομηνία έναρξης ή λήξης για το έργο αυτό.',
'Projects Gantt chart' => 'Διάγραμμα Gantt έργων',
- 'Link type' => 'Τύπος συνδέσμου',
'Change task color when using a specific task link' => 'Αλλαγή χρώματος εργασίας χρησιμοποιώντας συγκεκριμένο σύνδεσμο εργασίας',
'Task link creation or modification' => 'Σύνδεσμος δημιουργίας ή τροποποίησης εργασίας',
'Milestone' => 'Ορόσημο',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Διαμοιρασμένα',
'Owner' => 'Ιδιοκτήτης',
'Unread notifications' => 'Αδιάβαστες ειδοποιήσεις',
- 'My filters' => 'Τα φίλτρα μου',
'Notification methods:' => 'Μέθοδοι ειδοποίησης:',
'Import tasks from CSV file' => 'Εισαγωγή εργασιών μέσω αρχείου CSV',
'Unable to read your file' => 'Δεν είναι δυνατή η ανάγνωση του αρχείου',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Οι ονομασίες χρηστών πρέπει να είναι σε μικρά γράμματα (lowercase) και μοναδικά',
'Passwords will be encrypted if present' => 'Οι κωδικοί πρόσβασης κρυπτογραφούνται, αν υπάρχουν',
'%s attached a new file to the task %s' => '%s νέο συνημμένο αρχείο της εργασίας %s',
+ 'Link type' => 'Τύπος συνδέσμου',
'Assign automatically a category based on a link' => 'Ανατίθεται αυτόματα κατηγορία, βασισμένη στον σύνδεσμο',
'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
'Assignee Username' => 'Δικαιοδόχο όνομα χρήστη',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Κλείσιμο εργασίας όταν δεν υπάρχει δραστηριότητα',
'Duration in days' => 'Διάρκεια σε ημέρες',
'Send email when there is no activity on a task' => 'Αποστολή email όταν δεν υπάρχει δραστηριότητα σε εργασία',
- 'List of external links' => 'Λίστα εξωτερικών συνδέσμων',
'Unable to fetch link information.' => 'Δεν είναι δυνατή η ανάλυση της πληροφορίας συνδεσμου',
'Daily background job for tasks' => 'Ημερήσια παρασκηνιακή δουλειά για τις εργασίες',
'Auto' => 'Αυτόματο',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Εξωτερικός σύνδεσμος',
'Copy and paste your link here...' => 'Κάντε αντιγραφή και επικόλληση εδώ',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Προς το παρών, δεν υπάρχουν εξωτερικοί σύνδεσμοι.',
'Internal links' => 'Εσωτερικοί σύνδεσμοι',
- 'There is no internal link for the moment.' => 'Προς το παρών, δεν υπάρχουν εσωτερικοί σύνδεσμοι.',
'Assign to me' => 'Αναττίθεται σε εμένα',
'Me' => 'Σε μένα',
'Do not duplicate anything' => 'Να μην γίνει κλωνοποίηση από άλλο έργο',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Διαχείριση χρηστών',
'Groups management' => 'Διαχείριση ομάδων',
'Create from another project' => 'Δημιουργία από άλλο έργο',
- 'There is no subtask at the moment.' => 'Προς το παρών, δεν υπάρχει καμία υπο-εργασία.',
'open' => 'Ανοικτό',
'closed' => 'Κλειστό',
'Priority:' => 'Προτεραιότητα:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Ξεκίνησε:',
'Moved:' => 'Μετακινήθηκε:',
'Task #%d' => 'Εργασία #%d',
- 'Sub-tasks' => 'Υπο-εργασίες',
'Date and time format' => 'Μορφή ημερομηνίας και ώρας',
'Time format' => 'Μορφή ώρας',
'Start date: ' => 'Ημερομηνία έναρξης: ',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Φίλτρα οριζόμενα από τον χρήστη',
'Category filters' => 'Κατηγορία φίλτρων',
'Upload a file' => 'Ανέβασμα αρχείου',
- 'There is no attachment at the moment.' => 'Προς το παρόν, δεν υπάρχουν συνημμένα',
'View file' => 'Προβολή αρχείου',
'Last activity' => 'Τελευταία δραστηριότητα',
'Change subtask position' => 'Αλλαγή θέσης υπο-εργασίας',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index bda20e61..6b4dda42 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Todos los proyectos',
'Add a new column' => 'Añadir una nueva columna',
'Title' => 'Título',
- 'Nobody assigned' => 'Nadie asignado',
'Assigned to %s' => 'Asignada a %s',
'Remove a column' => 'Suprimir esta columna',
'Remove a column from a board' => 'Suprimir una columna de un tablero',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Contador de tareas',
'User' => 'Usuario',
'Comments' => 'Comentarios',
- 'Write your text in Markdown' => 'Redacta el texto en Markdown',
'Leave a comment' => 'Dejar un comentario',
'Comment is required' => 'El comentario es obligatorio',
'Leave a description' => 'Dejar una descripción',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Control de tiempo:',
'New sub-task' => 'Nueva subtarea',
'New attachment added "%s"' => 'Nuevo adjunto agregado "%s"',
- 'Comment updated' => 'Comentario actualizado',
'New comment posted by %s' => 'Nuevo comentario agregado por %s',
'New attachment' => 'Nuevo adjunto',
'New comment' => 'Nuevo comentario',
+ 'Comment updated' => 'Comentario actualizado',
'New subtask' => 'Nueva subtarea',
'Subtask updated' => 'Subtarea actualizada',
'Task updated' => 'Tarea actualizada',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'El formato ISO siempre es aceptado, ejemplo: "%s" y "%s"',
'New private project' => 'Nuevo proyecto privado',
'This project is private' => 'Este proyecto es privado',
- 'Type here to create a new sub-task' => 'Escriba aquí para crear una nueva sub-tarea',
'Add' => 'Añadir',
'Start date' => 'Fecha de inicio',
'Time estimated' => 'Tiempo estimado',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Exportar sumario diario del proyecto para "%s"',
'Exports' => 'Exportaciones',
'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tereas por columna agrupada por día.',
- 'Nothing to preview...' => 'Nada que previsualizar...',
- 'Preview' => 'Previsualizar',
- 'Write' => 'Grabar',
'Active swimlanes' => 'Calles activas',
'Add a new swimlane' => 'Añadir nueva calle',
'Change default swimlane' => 'Cambiar la calle por defecto',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Edad de la tarea en días',
'Days in this column' => 'Días en esta columna',
'%dd' => '%dd',
- 'Add a link' => 'Añadir enlace',
'Add a new link' => 'Añadir nuevo enlace',
'Do you really want to remove this link: "%s"?' => '¿Realmente quiere quitar este enlace: "%s"?',
'Do you really want to remove this link with task #%d?' => '¿Realmente quiere quitar este enlace con esta tarea: #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Mi flujo de actividad',
'My calendar' => 'Mi calendario',
'Search tasks' => 'Buscar tareas',
- 'Back to the calendar' => 'Volver al calendario',
- 'Filters' => 'Filtros',
'Reset filters' => 'Limpiar filtros',
'My tasks due tomorrow' => 'Mis tareas a entregar mañana',
'Tasks due today' => 'Tareas a antregar hoy',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Fecha final',
'There is no start date or end date for this project.' => 'No existe fecha de inicio o de fin para este proyecto.',
'Projects Gantt chart' => 'Diagramas de Gantt de los proyectos',
- 'Link type' => 'Tipo de enlace',
'Change task color when using a specific task link' => 'Cambiar colo de la tarea al usar un enlace específico a tarea',
'Task link creation or modification' => 'Creación o modificación de enlace a tarea',
'Milestone' => 'Hito',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Compartido',
'Owner' => 'Dueño',
'Unread notifications' => 'Notificaciones sin leer',
- 'My filters' => 'Mis filtros',
'Notification methods:' => 'Métodos de notificación',
'Import tasks from CSV file' => 'Importar tareas desde archivo CSV',
'Unable to read your file' => 'No es posible leer el archivo',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Los nombres de usuario deben ser únicos y contener sólo minúsculas',
'Passwords will be encrypted if present' => 'Las contraseñas serán cifradas si es que existen',
'%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo a la tarea %s',
+ 'Link type' => 'Tipo de enlace',
'Assign automatically a category based on a link' => 'Asignar una categoría automáticamente basado en un enlace',
'BAM - Konvertible Mark' => 'BAM - marco convertible',
'Assignee Username' => 'Nombre de usuario del concesionario',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Cerrar tarea cuando no haya actividad',
'Duration in days' => 'Duración en días',
'Send email when there is no activity on a task' => 'Enviar correo cuando no haya actividad en una tarea',
- 'List of external links' => 'Lista de enlaces externos',
'Unable to fetch link information.' => 'No es posible obtener información sobre el enlace',
'Daily background job for tasks' => 'Tarea de fondo diaria para las tareas',
'Auto' => 'Automático',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Enlace externo',
'Copy and paste your link here...' => 'Copia y pega tu enlace aquí...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'No existe un enlace externo por el momento',
'Internal links' => 'Enlaces internos',
- 'There is no internal link for the moment.' => 'No existe un enlace interno por el momento',
'Assign to me' => 'Asignar a mí',
'Me' => 'Yo',
'Do not duplicate anything' => 'No duplicar nada',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Administración de usuarios',
'Groups management' => 'Administración de grupos',
'Create from another project' => 'Crear de otro proyecto',
- 'There is no subtask at the moment.' => 'No existe subtarea por el momento',
'open' => 'abierto',
'closed' => 'cerrado',
'Priority:' => 'Prioridad',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Iniciado',
'Moved:' => 'Movido',
'Task #%d' => 'Tarea #%d',
- 'Sub-tasks' => 'Subtareas',
'Date and time format' => 'Formato de hora y fecha',
'Time format' => 'Formato de hora',
'Start date: ' => 'Fecha de inicio',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Usar filtros',
'Category filters' => 'Categoría y filtros',
'Upload a file' => 'Subir archivo',
- 'There is no attachment at the moment.' => 'No existe ningún adjunto por el momento',
'View file' => 'Ver archivo',
'Last activity' => 'Última actividad',
'Change subtask position' => 'Cambiar posición de la subtarea',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'No hay ninguna acción en este momento.',
'Import actions from another project' => 'Importar acciones de otro proyecto',
'There is no available project.' => 'No hay proyectos disponibles.',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 48b6d659..f30b7b4c 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Kaikki projektit',
'Add a new column' => 'Lisää uusi sarake',
'Title' => 'Nimi',
- 'Nobody assigned' => 'Ei suorittajaa',
'Assigned to %s' => 'Tekijä: %s',
'Remove a column' => 'Poista sarake',
'Remove a column from a board' => 'Poista sarake taulusta',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Tehtävien määrä',
'User' => 'Käyttäjät',
'Comments' => 'Kommentit',
- 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla',
'Leave a comment' => 'Lisää kommentti',
'Comment is required' => 'Kommentti vaaditaan',
'Leave a description' => 'Lisää kuvaus',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Ajan seuranta:',
'New sub-task' => 'Uusi alitehtävä',
'New attachment added "%s"' => 'Uusi liite lisätty "%s"',
- 'Comment updated' => 'Kommentti päivitetty',
'New comment posted by %s' => '%s lisäsi uuden kommentin',
// 'New attachment' => '',
// 'New comment' => '',
+ 'Comment updated' => 'Kommentti päivitetty',
// 'New subtask' => '',
// 'Subtask updated' => '',
// 'Task updated' => '',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-muoto on aina hyväksytty, esimerkiksi %s ja %s',
'New private project' => 'Uusi yksityinen projekti',
'This project is private' => 'Tämä projekti on yksityinen',
- 'Type here to create a new sub-task' => 'Kirjoita tähän luodaksesi uuden alitehtävän',
'Add' => 'Lisää',
'Start date' => 'Aloituspäivä',
'Time estimated' => 'Arvioitu aika',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Päivittäisen yhteenvedon vienti kohteeseen "%s"',
'Exports' => 'Viennit',
'This export contains the number of tasks per column grouped per day.' => 'Tämä tiedosto sisältää tehtäviä sarakkeisiin päiväkohtaisesti ryhmilteltyinä',
- 'Nothing to preview...' => 'Ei esikatselua...',
- 'Preview' => 'Ei esikatselua',
- 'Write' => 'Kirjoita',
'Active swimlanes' => 'Aktiiviset kaistat',
'Add a new swimlane' => 'Lisää uusi kaista',
'Change default swimlane' => 'Vaihda oletuskaistaa',
@@ -539,7 +533,6 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
- // 'Add a link' => '',
// 'Add a new link' => '',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index ec8a36bd..ed4638cd 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Tous les projets',
'Add a new column' => 'Ajouter une nouvelle colonne',
'Title' => 'Titre',
- 'Nobody assigned' => 'Personne assigné',
'Assigned to %s' => 'Assigné à %s',
'Remove a column' => 'Supprimer une colonne',
'Remove a column from a board' => 'Supprimer une colonne d\'un tableau',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Nombre de tâches',
'User' => 'Utilisateur',
'Comments' => 'Commentaires',
- 'Write your text in Markdown' => 'Écrivez votre texte en Markdown',
'Leave a comment' => 'Laissez un commentaire',
'Comment is required' => 'Le commentaire est obligatoire',
'Leave a description' => 'Laissez une description',
@@ -329,14 +327,12 @@ return array(
'Time tracking:' => 'Gestion du temps :',
'New sub-task' => 'Nouvelle sous-tâche',
'New attachment added "%s"' => 'Nouvelle pièce-jointe ajoutée « %s »',
- 'Comment updated' => 'Commentaire ajouté',
'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »',
'New attachment' => 'Nouveau document',
'New comment' => 'Nouveau commentaire',
'Comment updated' => 'Commentaire mis à jour',
'New subtask' => 'Nouvelle sous-tâche',
'Subtask updated' => 'Sous-tâche mise à jour',
- 'New task' => 'Nouvelle tâche',
'Task updated' => 'Tâche mise à jour',
'Task closed' => 'Tâche fermée',
'Task opened' => 'Tâche ouverte',
@@ -434,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Le format ISO est toujours accepté, exemple : « %s » et « %s »',
'New private project' => 'Nouveau projet privé',
'This project is private' => 'Ce projet est privé',
- 'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici',
'Add' => 'Ajouter',
'Start date' => 'Date de début',
'Time estimated' => 'Temps estimé',
@@ -485,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export du résumé quotidien du projet pour « %s »',
'Exports' => 'Exports',
'This export contains the number of tasks per column grouped per day.' => 'Cet export contient le nombre de tâches par colonne groupé par jour.',
- 'Nothing to preview...' => 'Rien à prévisualiser...',
- 'Preview' => 'Prévisualiser',
- 'Write' => 'Écrire',
'Active swimlanes' => 'Swimlanes actives',
'Add a new swimlane' => 'Ajouter une nouvelle swimlane',
'Change default swimlane' => 'Modifier la swimlane par défaut',
@@ -541,7 +533,6 @@ return array(
'Task age in days' => 'Âge de la tâche en jours',
'Days in this column' => 'Jours dans cette colonne',
'%dd' => '%dj',
- 'Add a link' => 'Ajouter un lien',
'Add a new link' => 'Ajouter un nouveau lien',
'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%d ?',
@@ -751,8 +742,6 @@ return array(
'My activity stream' => 'Mon flux d\'activité',
'My calendar' => 'Mon agenda',
'Search tasks' => 'Rechercher des tâches',
- 'Back to the calendar' => 'Retour au calendrier',
- 'Filters' => 'Filtres',
'Reset filters' => 'Réinitialiser les filtres',
'My tasks due tomorrow' => 'Mes tâches qui arrivent à échéance demain',
'Tasks due today' => 'Tâches qui arrivent à échéance aujourd\'hui',
@@ -852,7 +841,6 @@ return array(
'End date:' => 'Date de fin :',
'There is no start date or end date for this project.' => 'Il n\'y a pas de date de début ou de date de fin pour ce projet.',
'Projects Gantt chart' => 'Diagramme de Gantt des projets',
- 'Link type' => 'Type de lien',
'Change task color when using a specific task link' => 'Changer la couleur de la tâche lorsqu\'un lien spécifique est utilisé',
'Task link creation or modification' => 'Création ou modification d\'un lien sur une tâche',
'Milestone' => 'Étape importante',
@@ -904,7 +892,6 @@ return array(
'Shared' => 'Partagé',
'Owner' => 'Propriétaire',
'Unread notifications' => 'Notifications non lus',
- 'My filters' => 'Mes filtres',
'Notification methods:' => 'Méthodes de notifications :',
'Import tasks from CSV file' => 'Importer les tâches depuis un fichier CSV',
'Unable to read your file' => 'Impossible de lire votre fichier',
@@ -1052,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Fermer une tâche sans activité',
'Duration in days' => 'Durée en jours',
'Send email when there is no activity on a task' => 'Envoyer un email lorsqu\'il n\'y a pas d\'activité sur une tâche',
- 'List of external links' => 'Liste des liens externes',
'Unable to fetch link information.' => 'Impossible de récupérer les informations sur le lien.',
'Daily background job for tasks' => 'Tâche planifiée quotidienne pour les tâches',
'Auto' => 'Auto',
@@ -1070,9 +1056,7 @@ return array(
'External link' => 'Lien externe',
'Copy and paste your link here...' => 'Copier-coller vôtre lien ici...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Il n\'y a pas de lien externe pour le moment.',
'Internal links' => 'Liens internes',
- 'There is no internal link for the moment.' => 'Il n\'y a pas de lien interne pour le moment.',
'Assign to me' => 'Assigner à moi',
'Me' => 'Moi',
'Do not duplicate anything' => 'Ne rien dupliquer',
@@ -1080,7 +1064,6 @@ return array(
'Users management' => 'Gestion des utilisateurs',
'Groups management' => 'Gestion des groupes',
'Create from another project' => 'Créer depuis un autre projet',
- 'There is no subtask at the moment.' => 'Il n\'y a aucune sous-tâche pour le moment.',
'open' => 'ouvert',
'closed' => 'fermé',
'Priority:' => 'Priorité :',
@@ -1099,7 +1082,6 @@ return array(
'Started:' => 'Commençé le :',
'Moved:' => 'Déplacé le : ',
'Task #%d' => 'Tâche n°%d',
- 'Sub-tasks' => 'Sous-tâches',
'Date and time format' => 'Format de la date et de l\'heure',
'Time format' => 'Format de l\'heure',
'Start date: ' => 'Date de début : ',
@@ -1140,7 +1122,6 @@ return array(
'User filters' => 'Filtres des utilisateurs',
'Category filters' => 'Filtres des catégories',
'Upload a file' => 'Uploader un fichier',
- 'There is no attachment at the moment.' => 'Il n\'y a aucune pièce-jointe pour le moment.',
'View file' => 'Voir le fichier',
'Last activity' => 'Dernières activités',
'Change subtask position' => 'Changer la position de la sous-tâche',
@@ -1154,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Il n\'y a aucune action pour le moment.',
'Import actions from another project' => 'Importer les actions depuis un autre projet',
'There is no available project.' => 'Il n\'y a pas de projet disponible.',
+ 'Local File' => 'Fichier local',
+ 'Configuration' => 'Configuration',
+ 'PHP version:' => 'Version de PHP :',
+ 'PHP SAPI:' => 'PHP SAPI :',
+ 'OS version:' => 'Version du système d\'exploitation :',
+ 'Database version:' => 'Version de la base de donnée :',
+ 'Browser:' => 'Navigateur web :',
+ 'Task view' => 'Vue détaillée d\'une tâche',
+ 'Edit task' => 'Modifier la tâche',
+ 'Edit description' => 'Modifier la description',
+ 'New internal link' => 'Nouveau lien interne',
+ 'Display list of keyboard shortcuts' => 'Afficher la liste des raccourcis claviers',
+ 'Menu' => 'Menu',
+ 'Set start date' => 'Définir la date de début',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Uploader mon image d\'avatar',
+ 'Remove my image' => 'Supprimer mon image',
+ 'The OAuth2 state parameter is invalid' => 'Le paramètre "state" de OAuth2 est invalide',
+ 'User not found.' => 'Utilisateur introuvable.',
+ 'Search in activity stream' => 'Chercher dans le flux d\'activité',
+ 'My activities' => 'Mes activités',
+ 'Activity until yesterday' => 'Activités jusqu\'à hier',
+ 'Activity until today' => 'Activités jusqu\'à aujourd\'hui',
+ 'Search by creator: ' => 'Rechercher par créateur : ',
+ 'Search by creation date: ' => 'Rechercher par date de création : ',
+ 'Search by task status: ' => 'Rechercher par le statut des tâches : ',
+ 'Search by task title: ' => 'Rechercher par le titre des tâches : ',
+ 'Activity stream search' => 'Recherche dans le flux d\'activité',
+ 'Projects where "%s" is manager' => 'Projets où « %s » est gestionnaire',
+ 'Projects where "%s" is member' => 'Projets où « %s » est membre du projet',
+ 'Open tasks assigned to "%s"' => 'Tâches ouvertes assignées à « %s »',
+ 'Closed tasks assigned to "%s"' => 'Tâches fermées assignées à « %s »',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 53e8ace5..394f89a0 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Minden projekt',
'Add a new column' => 'Új oszlop',
'Title' => 'Cím',
- 'Nobody assigned' => 'Nincs felelős',
'Assigned to %s' => 'Felelős: %s',
'Remove a column' => 'Oszlop törlése',
'Remove a column from a board' => 'Oszlop törlése a tábláról',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Feladatok száma',
'User' => 'Felhasználó',
'Comments' => 'Hozzászólások',
- 'Write your text in Markdown' => 'Írja be a szöveget Markdown szintaxissal',
'Leave a comment' => 'Írjon hozzászólást ...',
'Comment is required' => 'A hozzászólás mező kötelező',
'Leave a description' => 'Írjon leírást ...',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Idő követés:',
'New sub-task' => 'Új részfeladat',
'New attachment added "%s"' => 'Új melléklet "%s" hozzáadva.',
- 'Comment updated' => 'Megjegyzés frissítve',
'New comment posted by %s' => 'Új megjegyzés %s',
'New attachment' => 'Új melléklet',
'New comment' => 'Új megjegyzés',
+ 'Comment updated' => 'Megjegyzés frissítve',
'New subtask' => 'Új részfeladat',
'Subtask updated' => 'Részfeladat frissítve',
'Task updated' => 'Feladat frissítve',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formátum mindig elfogadott, pl: "%s" és "%s"',
'New private project' => 'Új privát projekt',
'This project is private' => 'Ez egy privát projekt',
- 'Type here to create a new sub-task' => 'Ide írva létrehozhat egy új részfeladatot',
'Add' => 'Hozzáadás',
'Start date' => 'Kezdés dátuma',
'Time estimated' => 'Becsült időtartam',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása: %s',
'Exports' => 'Exportálások',
'This export contains the number of tasks per column grouped per day.' => 'Ez az export tartalmazza a feladatok számát oszloponként összesítve, napokra lebontva.',
- 'Nothing to preview...' => 'Nincs semmi az előnézetben ...',
- 'Preview' => 'Előnézet',
- 'Write' => 'Szerkesztés',
'Active swimlanes' => 'Aktív folyamatok',
'Add a new swimlane' => 'Új folyamat',
'Change default swimlane' => 'Alapértelmezett folyamat változtatás',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Feladat életkora napokban',
'Days in this column' => 'Napok ebben az oszlopban',
'%dd' => '%dd',
- 'Add a link' => 'Hivatkozás hozzáadása',
'Add a new link' => 'Új hivatkozás hozzáadása',
'Do you really want to remove this link: "%s"?' => 'Biztos törölni akarja a hivatkozást: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Biztos törölni akarja a(z) #%d. feladatra mutató hivatkozást?',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index b4b8dcd4..bd1dd684 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Semua proyek',
'Add a new column' => 'Tambah kolom baru',
'Title' => 'Judul',
- 'Nobody assigned' => 'Tidak ada yang ditugaskan',
'Assigned to %s' => 'Ditugaskan ke %s',
'Remove a column' => 'Hapus kolom',
'Remove a column from a board' => 'Hapus kolom dari papan',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Jumlah tugas',
'User' => 'Pengguna',
'Comments' => 'Komentar',
- 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown',
'Leave a comment' => 'Tinggalkan komentar',
'Comment is required' => 'Komentar diperlukan',
'Leave a description' => 'Tinggalkan deskripsi',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Pelacakan waktu :',
'New sub-task' => 'Sub-tugas baru',
'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »',
- 'Comment updated' => 'Komentar diperbaharui',
'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »',
'New attachment' => 'Lampirkan baru',
'New comment' => 'Komentar baru',
+ 'Comment updated' => 'Komentar diperbaharui',
'New subtask' => 'Sub-tugas baru',
'Subtask updated' => 'Sub-tugas diperbaharui',
'Task updated' => 'Tugas diperbaharui',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalu diterima, contoh : « %s » et « %s »',
'New private project' => 'Proyek pribadi baru',
'This project is private' => 'Proyek ini adalah pribadi',
- 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru',
'Add' => 'Tambah',
'Start date' => 'Tanggal mulai',
'Time estimated' => 'Perkiraan waktu',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Ekspor ringkasan proyek harian untuk « %s »',
'Exports' => 'Ekspor',
'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.',
- 'Nothing to preview...' => 'Tidak ada yang dapat dilihat...',
- 'Preview' => 'Preview',
- 'Write' => 'Tulis',
'Active swimlanes' => 'Swimlanes aktif',
'Add a new swimlane' => 'Tambah swimlane baru',
'Change default swimlane' => 'Modifikasi standar swimlane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Usia tugas dalam hari',
'Days in this column' => 'Hari dalam kolom ini',
'%dd' => '%dj',
- 'Add a link' => 'Menambahkan tautan',
'Add a new link' => 'Tambah tautan baru',
'Do you really want to remove this link: "%s"?' => 'Apakah anda yakin akan menghapus tautan ini : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Apakah anda yakin akan menghapus tautan ini dengan tugas n°%d ?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aliran kegiatan saya',
'My calendar' => 'Kalender saya',
'Search tasks' => 'Cari tugas',
- 'Back to the calendar' => 'Kembali ke kalender',
- 'Filters' => 'Filter',
'Reset filters' => 'Reset ulang filter',
'My tasks due tomorrow' => 'Tugas saya yang berakhir besok',
'Tasks due today' => 'Tugas yang berakhir hari ini',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Waktu berakhir :',
'There is no start date or end date for this project.' => 'Tidak ada waktu mulai atau waktu berakhir untuk proyek ini',
'Projects Gantt chart' => 'Proyek grafik Gantt',
- 'Link type' => 'Tipe tautan',
'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan tautan tugas yang spesifik',
'Task link creation or modification' => 'Tautan pembuatan atau modifikasi tugas ',
'Milestone' => 'Milestone',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Tipe tautan',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index bba629cb..cee1c16a 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Tutti i progetti',
'Add a new column' => 'Aggiungi una nuova colonna',
'Title' => 'Titolo',
- 'Nobody assigned' => 'Nessuno assegnato',
'Assigned to %s' => 'Assegnato a %s',
'Remove a column' => 'Cancella questa colonna',
'Remove a column from a board' => 'Cancella una colonna da una bacheca',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Numero di task',
'User' => 'Utente',
'Comments' => 'Commenti',
- 'Write your text in Markdown' => 'Scrivi il testo in Markdown',
'Leave a comment' => 'Lascia un commento',
'Comment is required' => 'Si richiede un commento',
'Leave a description' => 'Lascia una descrizione',
@@ -329,10 +327,10 @@ return array(
// 'Time tracking:' => '',
'New sub-task' => 'Nuovo sotto-task',
'New attachment added "%s"' => 'Nuovo allegato aggiunto "%s"',
- 'Comment updated' => 'Commento aggiornato',
'New comment posted by %s' => 'Nuovo commento aggiunto da "%s"',
'New attachment' => 'Nuovo allegato',
'New comment' => 'Nuovo commento',
+ 'Comment updated' => 'Commento aggiornato',
'New subtask' => 'Nuovo sotto-task',
'Subtask updated' => 'Sotto-task aggiornato',
'Task updated' => 'Task aggiornato',
@@ -432,13 +430,12 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Il formato ISO è sempre accettato, esempio: "%s" e "%s"',
'New private project' => 'Nuovo progetto privato',
'This project is private' => 'Questo progetto è privato',
- 'Type here to create a new sub-task' => 'Scrivi qui per creare un sotto-task',
'Add' => 'Aggiungi',
'Start date' => 'Data di inizio',
'Time estimated' => 'Tempo stimato',
'There is nothing assigned to you.' => 'Non c\'è nulla assegnato a te.',
'My tasks' => 'I miei task',
- 'Activity stream' => 'Flusso di attività',
+ 'Activity stream' => 'Flusso attività',
'Dashboard' => 'Bacheca',
'Confirmation' => 'Conferma',
'Allow everybody to access to this project' => 'Abilita tutti ad accedere a questo progetto',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export del sommario giornaliero del progetto per "%s"',
'Exports' => 'Esporta',
'This export contains the number of tasks per column grouped per day.' => 'Questo export contiene il numero di task per colonna raggruppati per giorno',
- 'Nothing to preview...' => 'Nessuna anteprima...',
- 'Preview' => 'Anteprima',
- 'Write' => 'Scrivi',
'Active swimlanes' => 'Corsie attive',
'Add a new swimlane' => 'Aggiungi una corsia',
'Change default swimlane' => 'Cambia la corsia predefinita',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Anzianità del task in giorni',
'Days in this column' => 'Giorni in questa colonna',
'%dd' => '%dg',
- 'Add a link' => 'Aggiungi una relazione',
'Add a new link' => 'Aggiungi una nuova relazione',
'Do you really want to remove this link: "%s"?' => 'Vuoi davvero rimuovere la seguente relazione: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Vuoi davvero rimuovere questa relazione dal task #%d?',
@@ -746,11 +739,9 @@ return array(
'Start timer' => 'Avvia il timer',
'Add project member' => 'Aggiungi un membro di progetto',
'Enable notifications' => 'Abilita notifiche',
- 'My activity stream' => 'Il mio flusso di attività',
+ 'My activity stream' => 'Il mio flusso attività',
'My calendar' => 'Il mio calendario',
'Search tasks' => 'Ricerca task',
- 'Back to the calendar' => 'Torna al calendario',
- 'Filters' => 'Filtri',
'Reset filters' => 'Annulla filtri',
'My tasks due tomorrow' => 'I miei task da completare per domani',
'Tasks due today' => 'Task da completare oggi',
@@ -771,7 +762,7 @@ return array(
'Keyboard shortcut: "%s"' => 'Scorciatoia da tastiera: "%s"',
'List' => 'Lista',
'Filter' => 'Filtro',
- 'Advanced search' => 'Riceca avanzata',
+ 'Advanced search' => 'Ricerca avanzata',
'Example of query: ' => 'Esempio di query: ',
'Search by project: ' => 'Ricerca per progetto: ',
'Search by column: ' => 'Ricerca per colonna: ',
@@ -842,7 +833,7 @@ return array(
'Users overview' => 'Panoramica utenti',
'Members' => 'Membri',
'Shared project' => 'Progetto condiviso',
- 'Project managers' => 'Manager del progetto',
+ 'Project managers' => 'Manager di progetto',
'Gantt chart for all projects' => 'Grafico Gantt per tutti i progetti',
'Projects list' => 'Elenco progetti',
'Gantt chart for this project' => 'Grafico Gantt per questo progetto',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data di fine:',
'There is no start date or end date for this project.' => 'Non è prevista una data di inzio o fine per questo progetto.',
'Projects Gantt chart' => 'Grafico Gantt dei progetti',
- 'Link type' => 'Tipo di link',
'Change task color when using a specific task link' => 'Cambia colore del task quando si un utilizza una determinata relazione di task',
'Task link creation or modification' => 'Creazione o modifica di relazione di task',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Condiviso',
'Owner' => 'Proprietario',
'Unread notifications' => 'Notifiche non lette',
- 'My filters' => 'I miei filtri',
'Notification methods:' => 'Metodi di notifica',
'Import tasks from CSV file' => 'Importa task da file CSV',
'Unable to read your file' => 'Impossibile leggere il file',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'I nomi utente devono essere in minuscolo e univoci',
'Passwords will be encrypted if present' => 'Se presenti, le password verranno criptate',
'%s attached a new file to the task %s' => '%s ha allegato un nuovo file al task %s',
+ 'Link type' => 'Tipo di link',
'Assign automatically a category based on a link' => 'Assegna automaticamente una categoria sulla base di una relazione',
'BAM - Konvertible Mark' => 'BAM - Marco bosniaco',
'Assignee Username' => 'Nome utente dell\'assegnatario',
@@ -990,7 +980,7 @@ return array(
'Group Name' => 'Nome del gruppo',
'Enter group name...' => 'Inserisci il nome del gruppo...',
'Role:' => 'Ruolo:',
- 'Project members' => 'Membri del progetto',
+ 'Project members' => 'Membri di progetto',
'Compare hours for "%s"' => 'Confronta le ore per "%s"',
'%s mentioned you in the task #%d' => '%s ti ha menzionato nel task #%d',
'%s mentioned you in a comment on the task #%d' => '%s ti ha menzionato in un commento del task #%d',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Chiudi un task quando non c\'è nessuna attività',
'Duration in days' => 'Durata in giorni',
'Send email when there is no activity on a task' => 'Invia un\'email quando non c\'è attività sul task',
- 'List of external links' => 'Elenco dei link esterni',
'Unable to fetch link information.' => 'Impossibile recuperare informazioni sul link.',
'Daily background job for tasks' => 'Job giornaliero in background per i task',
// 'Auto' => '',
@@ -1067,17 +1056,14 @@ return array(
'External link' => 'Link esterno',
'Copy and paste your link here...' => 'Copia e incolla il tuo link qui...',
// 'URL' => '',
- 'There is no external link for the moment.' => 'Nessun link esterno presente al momento.',
'Internal links' => 'Link interni',
- 'There is no internal link for the moment.' => 'Nessun link interno presente al momento.',
'Assign to me' => 'Assegna a me',
- // 'Me' => '',
+ 'Me' => 'Me stesso',
'Do not duplicate anything' => 'Non duplicare nulla',
'Projects management' => 'Gestione progetti',
'Users management' => 'Gestione utenti',
'Groups management' => 'Gestione gruppi',
'Create from another project' => 'Crea da un\'altro progetto',
- 'There is no subtask at the moment.' => 'Nessun sotto-task presente al momento.',
'open' => 'aperto',
'closed' => 'chiuso',
'Priority:' => 'Priorità:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Iniziato:',
'Moved:' => 'Spostato:',
// 'Task #%d' => '',
- 'Sub-tasks' => 'Sotto-task',
'Date and time format' => 'Formato data e ora',
'Time format' => 'Formato ora',
'Start date: ' => 'Data di inizio: ',
@@ -1137,18 +1122,49 @@ return array(
'User filters' => 'Filtri utente',
'Category filters' => 'Filtri di categorie',
'Upload a file' => 'Carica un file',
- 'There is no attachment at the moment.' => 'Nessun allegato presente.',
'View file' => 'Visualizza file',
'Last activity' => 'Attività recente',
'Change subtask position' => 'Cambia la posizione del sotto-task',
'This value must be greater than %d' => 'Questo valore deve essere magiore di %d',
'Another swimlane with the same name exists in the project' => 'Un\'altra corsia con lo stesso nome è già esistente in questo progetto',
'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Esempio: http://example.kanboard.net/ (usato per generare URL assolute)',
- // 'Actions duplicated successfully.' => '',
- // 'Unable to duplicate actions.' => '',
- // 'Add a new action' => '',
- // 'Import from another project' => '',
- // 'There is no action at the moment.' => '',
- // 'Import actions from another project' => '',
- // 'There is no available project.' => '',
+ 'Actions duplicated successfully.' => 'Azioni duplicate con successo.',
+ 'Unable to duplicate actions.' => 'Impossibile duplicare le azioni.',
+ 'Add a new action' => 'Aggiungi una nuova azione',
+ 'Import from another project' => 'Importa da un altro progetto',
+ 'There is no action at the moment.' => 'Nessuna azione disponibile al momento.',
+ 'Import actions from another project' => 'Importa azioni da un altro progetto',
+ 'There is no available project.' => 'Nessun progetto disponibile.',
+ 'Local File' => 'File locale',
+ 'Configuration' => 'Configurazione',
+ 'PHP version:' => 'Versione PHP:',
+ // 'PHP SAPI:' => '',
+ 'OS version:' => 'Versione OS:',
+ 'Database version:' => 'Versione database:',
+ // 'Browser:' => '',
+ 'Task view' => 'Vista dei task',
+ 'Edit task' => 'Modifica task',
+ 'Edit description' => 'Modifica descrizione',
+ 'New internal link' => 'Nuovo link interno',
+ 'Display list of keyboard shortcuts' => 'Mostra una lista di scorciatoie da tastiera',
+ // 'Menu' => '',
+ 'Set start date' => 'Imposta la data di inzio',
+ // 'Avatar' => '',
+ 'Upload my avatar image' => 'Carica l\'immagine del mio avatar',
+ 'Remove my image' => 'Rimuovi la mia immagine',
+ 'The OAuth2 state parameter is invalid' => 'Il parametro di stato OAuth2 non è valido.',
+ 'User not found.' => 'Utente non trovato.',
+ 'Search in activity stream' => 'Ricerca nel mio flusso attività',
+ 'My activities' => 'Le mie attività',
+ 'Activity until yesterday' => 'Attività ad oggi',
+ 'Activity until today' => 'Attività fino a ieri',
+ 'Search by creator: ' => 'Ricerca per creatore: ',
+ 'Search by creation date: ' => 'Ricerca per data di creazione: ',
+ 'Search by task status: ' => 'Ricerca per stato del task: ',
+ 'Search by task title: ' => 'Ricerca per titolo del task: ',
+ 'Activity stream search' => 'Ricerca nel flusso attività',
+ 'Projects where "%s" is manager' => 'Progetti all\'interno dei quali "%s" è manager',
+ 'Projects where "%s" is member' => 'Progetti all\'interno dei quali "%s" è membro',
+ 'Open tasks assigned to "%s"' => 'Task aperti assegnati a "%s"',
+ 'Closed tasks assigned to "%s"' => 'Task chiusi assegnati a "%s"',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index e4b41c93..89769edd 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'すべてのプロジェクト',
'Add a new column' => 'カラムの追加',
'Title' => 'タイトル',
- 'Nobody assigned' => '担当なし',
'Assigned to %s' => '%sが担当',
'Remove a column' => 'カラムの削除',
'Remove a column from a board' => 'ボードからカラムの削除',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'タスク数',
'User' => 'ユーザ',
'Comments' => 'コメント',
- 'Write your text in Markdown' => 'Markdown 記法で書く',
'Leave a comment' => 'コメントを書く',
'Comment is required' => 'コメントを入力してください',
'Leave a description' => '説明を書く',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => '時間計測:',
'New sub-task' => '新しいサブタスク',
'New attachment added "%s"' => '添付ファイル「%s」が追加されました',
- 'Comment updated' => 'コメントが更新されました',
'New comment posted by %s' => '「%s」の新しいコメントが追加されました',
'New attachment' => '新しい添付ファイル',
'New comment' => '新しいコメント',
+ 'Comment updated' => 'コメントが更新されました',
'New subtask' => '新しいサブタスク',
'Subtask updated' => 'サブタスクの更新',
'Task updated' => 'タスクの更新',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO フォーマットが入力できます(例: %s または %s)',
'New private project' => '非公開プロジェクトを作る',
'This project is private' => 'このプロジェクトは非公開です',
- 'Type here to create a new sub-task' => 'サブタスクを追加するにはここに入力してください',
'Add' => '追加',
'Start date' => '開始時間',
'Time estimated' => '予想時間',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => '「%s」の日時プロジェクトサマリーの出力',
'Exports' => '出力',
'This export contains the number of tasks per column grouped per day.' => 'この出力は日時のカラムごとのタスク数を集計したものです',
- 'Nothing to preview...' => 'プレビューがありません',
- 'Preview' => 'プレビュー',
- 'Write' => '書く',
'Active swimlanes' => 'アクティブなスイムレーン',
'Add a new swimlane' => '新しいスイムレーン',
'Change default swimlane' => 'デフォルトスイムレーンの変更',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'タスクの経過日数',
'Days in this column' => 'カラムでの経過日数',
'%dd' => '%d 日',
- 'Add a link' => 'リンクの追加',
'Add a new link' => '新しいリンクの追加',
'Do you really want to remove this link: "%s"?' => 'リンク「%s」を本当に削除しますか?',
'Do you really want to remove this link with task #%d?' => 'このリンクとタスク#%dを削除しますか?',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php
new file mode 100644
index 00000000..ed9e3b86
--- /dev/null
+++ b/app/Locale/ko_KR/translations.php
@@ -0,0 +1,1170 @@
+<?php
+
+return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
+ 'None' => '없음',
+ 'edit' => '수정',
+ 'Edit' => '수정',
+ 'remove' => '삭제',
+ 'Remove' => '삭제',
+ 'Yes' => '예',
+ 'No' => '아니오',
+ 'cancel' => '취소',
+ 'or' => '또는',
+ 'Yellow' => '노랑',
+ 'Blue' => '파랑',
+ 'Green' => '초록',
+ 'Purple' => '보라',
+ 'Red' => '빨강',
+ 'Orange' => '주황',
+ 'Grey' => '회색',
+ 'Brown' => '브라운',
+ // 'Deep Orange' => '',
+ // 'Dark Grey' => '',
+ // 'Pink' => '',
+ // 'Teal' => '',
+ // 'Cyan' => '',
+ // 'Lime' => '',
+ // 'Light Green' => '',
+ // 'Amber' => '',
+ 'Save' => '저장',
+ 'Login' => '로그인',
+ 'Official website:' => '공식 웹사이트:',
+ 'Unassigned' => '담당자 없음',
+ 'View this task' => '이 할일 보기',
+ 'Remove user' => '사용자 삭제',
+ 'Do you really want to remove this user: "%s"?' => '사용자 "%s"를 정말로 삭제하시겠습니까?',
+ 'New user' => '사용자를 추가하는 ',
+ 'All users' => '모든 사용자',
+ 'Username' => '사용자 이름',
+ 'Password' => '패스워드',
+ 'Administrator' => '관리자',
+ 'Sign in' => '로그인',
+ 'Users' => '사용자',
+ 'No user' => '사용자가 없습니다',
+ 'Forbidden' => '접근 거부',
+ 'Access Forbidden' => '접속이 거부되었습니다',
+ 'Edit user' => '사용자를 변경하는 ',
+ 'Logout' => '로그아웃',
+ 'Bad username or password' => '사용자 이름 또는 패스워드가 다릅니다.',
+ 'Edit project' => '프로젝트 수정',
+ 'Name' => '이름',
+ 'Projects' => '프로젝트',
+ 'No project' => '프로젝트가 없습니다',
+ 'Project' => '프로젝트',
+ 'Status' => '상태',
+ 'Tasks' => '할일',
+ 'Board' => '보드',
+ 'Actions' => 'Actions',
+ 'Inactive' => '무효',
+ 'Active' => '유효',
+ '%d tasks on the board' => '%d개의 할일',
+ '%d tasks in total' => '총 %d개의 할일',
+ 'Unable to update this board.' => '보드를 갱신할 수 없었습니다',
+ 'Edit board' => '보드를 변경하는 ',
+ 'Disable' => '비활성화',
+ 'Enable' => '유효하게 한다',
+ 'New project' => '새 프로젝트',
+ // 'Do you really want to remove this project: "%s"?' => '',
+ 'Remove project' => '프로젝트의 삭제',
+ // 'Edit the board for "%s"' => '',
+ 'All projects' => '모든 프로젝트',
+ 'Add a new column' => '칼럼의 추가',
+ 'Title' => '제목',
+ 'Assigned to %s' => '담당자 %s',
+ 'Remove a column' => '칼럼 삭제',
+ 'Remove a column from a board' => '보드에서 칼럼 삭제',
+ 'Unable to remove this column.' => '(※)컬럼을 삭제할 수 없었습니다.',
+ // 'Do you really want to remove this column: "%s"?' => '',
+ 'This action will REMOVE ALL TASKS associated to this column!' => '이 조작은 이 컬럼에 할당된 『 모든 할일을 삭제 』합니다!',
+ 'Settings' => '설정',
+ 'Application settings' => '애플리케이션의 설정',
+ 'Language' => '언어',
+ 'Webhook token:' => 'Webhook토큰:',
+ 'API token:' => 'API토큰:',
+ 'Database size:' => '데이터베이스의 사이즈:',
+ 'Download the database' => '데이터베이스의 다운로드',
+ 'Optimize the database' => '데이터베이스 최적화',
+ '(VACUUM command)' => '(VACUUM명령)',
+ '(Gzip compressed Sqlite file)' => '(GZip명령으로 압축된 Sqlite파일)',
+ 'Close a task' => '할일 마치기',
+ 'Edit a task' => '할일 수정',
+ 'Column' => '칼럼',
+ 'Color' => '색',
+ 'Assignee' => '담당자',
+ 'Create another task' => '다른 할일 추가',
+ 'New task' => '새로운 할일',
+ 'Open a task' => '할일 열기',
+ // 'Do you really want to open this task: "%s"?' => '',
+ 'Back to the board' => '보드로 돌아가기',
+ 'There is nobody assigned' => '담당자가 없습니다',
+ 'Column on the board:' => '칼럼:',
+ 'Close this task' => '할일 마치기',
+ 'Open this task' => '할일을 열다',
+ 'There is no description.' => '설명이 없다',
+ 'Add a new task' => '할일을 추가하는 ',
+ 'The username is required' => '사용자 이름이 필요합니다',
+ // 'The maximum length is %d characters' => '',
+ // 'The minimum length is %d characters' => '',
+ 'The password is required' => '패스워드가 필요합니다',
+ 'This value must be an integer' => '정수로 입력하세요',
+ 'The username must be unique' => '사용자 이름이 이미 사용되고 있습니다',
+ 'The user id is required' => '사용자 ID가 필요합니다',
+ 'Passwords don\'t match' => '패스워드가 일치하지 않습니다',
+ 'The confirmation is required' => '확인용 패스워드를 입력하세요',
+ 'The project is required' => '프로젝트가 필요합니다',
+ 'The id is required' => 'ID가 필요합니다',
+ 'The project id is required' => '프로젝트 ID가 필요합니다',
+ 'The project name is required' => '프로젝트 이름이 필요합니다',
+ 'The title is required' => '제목이 필요합니다',
+ 'Settings saved successfully.' => '설정을 저장하였습니다',
+ 'Unable to save your settings.' => '설정의 보존에 실패했습니다.',
+ 'Database optimization done.' => '데이터베이스 최적화가 끝났습니다.',
+ 'Your project have been created successfully.' => '프로젝트를 작성했습니다.',
+ 'Unable to create your project.' => '프로젝트의 작성에 실패했습니다.',
+ 'Project updated successfully.' => '프로젝트를 갱신했습니다.',
+ 'Unable to update this project.' => '프로젝트의 갱신에 실패했습니다.',
+ 'Unable to remove this project.' => '프로젝트의 삭제에 실패했습니다.',
+ 'Project removed successfully.' => '프로젝트를 삭제했습니다.',
+ 'Project activated successfully.' => '프로젝트를 유효로 했습니다.',
+ 'Unable to activate this project.' => '프로젝트의 유효하게 못했어요.',
+ 'Project disabled successfully.' => '프로젝트를 무효로 했습니다.',
+ 'Unable to disable this project.' => '프로젝트의 무효화할 수 없었습니다.',
+ 'Unable to open this task.' => '할일의 오픈에 실패했습니다.',
+ 'Task opened successfully.' => '할일을 오픈했습니다.',
+ 'Unable to close this task.' => '할일의 클로즈에 실패했습니다.',
+ 'Task closed successfully.' => '할일을 마쳤습니다.',
+ 'Unable to update your task.' => '할일의 갱신에 실패했습니다.',
+ 'Task updated successfully.' => '할일을 갱신했습니다.',
+ 'Unable to create your task.' => '할일의 추가에 실패했습니다.',
+ 'Task created successfully.' => '할일을 추가했습니다.',
+ 'User created successfully.' => '사용자를 추가했습니다.',
+ 'Unable to create your user.' => '사용자의 추가에 실패했습니다.',
+ 'User updated successfully.' => '사용자를 갱신했습니다.',
+ 'Unable to update your user.' => '사용자의 갱신에 실패했습니다.',
+ 'User removed successfully.' => '사용자를 삭제했습니다.',
+ 'Unable to remove this user.' => '사용자 삭제에 실패했습니다.',
+ 'Board updated successfully.' => '보드를 갱신했습니다.',
+ 'Ready' => '준비완료',
+ 'Backlog' => '요구사항',
+ 'Work in progress' => '진행중',
+ 'Done' => '완료',
+ 'Application version:' => '애플리케이션의 버전:',
+ 'Id' => 'ID',
+ '%d closed tasks' => '%d개의 마친 할일',
+ 'No task for this project' => '이 프로젝트에 할일이 없습니다',
+ 'Public link' => '공개 접속 링크',
+ 'Change assignee' => '담당자 변경',
+ 'Change assignee for the task "%s"' => '할일 "%s"의 담당자를 변경',
+ 'Timezone' => '시간대',
+ 'Sorry, I didn\'t find this information in my database!' => '데이터베이스에서 정보가 발견되지 않았습니다!',
+ 'Page not found' => '페이지가 발견되지 않는다',
+ 'Complexity' => '복잡도',
+ 'Task limit' => '할일 수 제한',
+ 'Task count' => '할일 수',
+ 'User' => '사용자',
+ 'Comments' => '댓글',
+ 'Leave a comment' => '댓글 남기기',
+ 'Comment is required' => '댓글을 입력하세요',
+ 'Leave a description' => '설명을 입력하세요',
+ 'Comment added successfully.' => '의견을 추가했습니다.',
+ 'Unable to create your comment.' => '댓글의 추가에 실패했습니다.',
+ 'Edit this task' => '할일 수정',
+ 'Due Date' => '마감일',
+ 'Invalid date' => '날짜가 무효입니다',
+ 'Automatic actions' => '자동액션 관리',
+ 'Your automatic action have been created successfully.' => '자동 액션을 작성했습니다.',
+ 'Unable to create your automatic action.' => '자동 액션의 작성에 실패했습니다.',
+ 'Remove an action' => '자동 액션의 삭제',
+ 'Unable to remove this action.' => '자동 액션의 삭제에 실패했습니다.',
+ 'Action removed successfully.' => '자동 액션의 삭제에 성공했어요.',
+ // 'Automatic actions for the project "%s"' => '',
+ 'Add an action' => '자동 액션 추가',
+ 'Event name' => '이벤트 이름',
+ 'Action name' => '액션 이름',
+ 'Action parameters' => '액션의 바로미터',
+ 'Action' => '액션',
+ 'Event' => '이벤트',
+ 'When the selected event occurs execute the corresponding action.' => '선택된 이벤트가 발생했을 때 대응하는 액션을 실행한다.',
+ 'Next step' => '다음 단계',
+ 'Define action parameters' => '액션의 바로미터',
+ // 'Do you really want to remove this action: "%s"?' => '',
+ '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' => '할일을 다른 칼럼에 이동하는 ',
+ 'Task modification' => '할일 변경',
+ 'Task creation' => '할일을 만들',
+ 'Closing a task' => '할일을 닫혔다',
+ 'Assign a color to a specific user' => '색을 사용자에 할당',
+ 'Column title' => '칼럼의 제목',
+ 'Position' => '위치',
+ 'Duplicate to another project' => '다른 프로젝트에 복사',
+ 'Duplicate' => '복사',
+ 'link' => '링크',
+ 'Comment updated successfully.' => '댓글을 갱신했습니다.',
+ 'Unable to update your comment.' => '댓글의 갱신에 실패했습니다.',
+ 'Remove a comment' => '댓글 삭제',
+ 'Comment removed successfully.' => '댓글을 삭제했습니다.',
+ 'Unable to remove this comment.' => '댓글의 삭제에 실패했습니다.',
+ 'Do you really want to remove this comment?' => '댓글을 삭제합니까?',
+ 'Current password for the user "%s"' => '사용자 "%s"의 현재 패스워드',
+ 'The current password is required' => '현재의 패스워드를 입력하세요',
+ 'Wrong password' => '패스워드가 다릅니다',
+ 'Unknown' => '불명',
+ 'Last logins' => '마지막 로그인',
+ 'Login date' => '로그인 일시',
+ 'Authentication method' => '인증 방법',
+ 'IP address' => 'IP 주소',
+ 'User agent' => '사용자 에이전트',
+ 'Persistent connections' => '세션',
+ 'No session.' => '세션 없음',
+ 'Expiration date' => '유효기간',
+ 'Remember Me' => '자동 로그인',
+ 'Creation date' => '작성일',
+ 'Everybody' => '모두',
+ 'Open' => '열림',
+ 'Closed' => '닫힘',
+ 'Search' => '검색',
+ 'Nothing found.' => '결과가 없습니다',
+ 'Due date' => '마감일',
+ 'Others formats accepted: %s and %s' => ' 다른 서식: %s 또는 %s',
+ 'Description' => '설명',
+ '%d comments' => '%d개의 댓글',
+ '%d comment' => '%d개의 댓글',
+ 'Email address invalid' => '메일 주소가 올바르지 않습니다.',
+ // 'Your external account is not linked anymore to your profile.' => '',
+ // 'Unable to unlink your external account.' => '',
+ // 'External authentication failed' => '',
+ // 'Your external account is linked to your profile successfully.' => '',
+ 'Email' => '이메일',
+ 'Task removed successfully.' => '할일을 삭제했습니다.',
+ 'Unable to remove this task.' => '할일 삭제에 실패했습니다.',
+ 'Remove a task' => '할일 삭제',
+ // 'Do you really want to remove this task: "%s"?' => '',
+ 'Assign automatically a color based on a category' => '카테고리에 바탕을 두고 색을 바꾸고',
+ 'Assign automatically a category based on a color' => '색에 바탕을 두고 카테고리를 바꾸었다',
+ 'Task creation or modification' => '할일의 작성 또는 변경',
+ 'Category' => '카테고리',
+ 'Category:' => '카테고리:',
+ 'Categories' => '카테고리',
+ 'Category not found.' => '카테고리가 발견되지 않습니다',
+ 'Your category have been created successfully.' => '카테고리를 작성했습니다.',
+ 'Unable to create your category.' => '카테고리의 작성에 실패했습니다.',
+ 'Your category have been updated successfully.' => '카테고리를 갱신했습니다.',
+ 'Unable to update your category.' => '카테고리의 갱신에 실패했습니다.',
+ 'Remove a category' => '카테고리의 삭제',
+ 'Category removed successfully.' => '카테고리를 삭제했습니다.',
+ 'Unable to remove this category.' => '카테고리를 삭제할 수 없었습니다.',
+ // 'Category modification for the project "%s"' => '',
+ 'Category Name' => '카테고리 이름',
+ 'Add a new category' => '카테고리의 추가',
+ // 'Do you really want to remove this category: "%s"?' => '',
+ 'All categories' => '모든 카테고리',
+ 'No category' => '카테고리 없음',
+ 'The name is required' => '이름을 입력하십시오',
+ 'Remove a file' => '파일 삭제',
+ 'Unable to remove this file.' => '파일 삭제에 실패했습니다.',
+ 'File removed successfully.' => '파일을 삭제했습니다.',
+ 'Attach a document' => '문서 첨부',
+ 'Do you really want to remove this file: "%s"?' => '파일 "%s" 을 삭제할까요?',
+ 'Attachments' => '첨부',
+ 'Edit the task' => '할일 수정',
+ 'Edit the description' => '설명 수정',
+ 'Add a comment' => '댓글 추가',
+ 'Edit a comment' => '댓글 수정',
+ 'Summary' => '개요',
+ 'Time tracking' => '시간 추적',
+ 'Estimate:' => '예측:',
+ 'Spent:' => '경과:',
+ 'Do you really want to remove this sub-task?' => '서브 할일을 삭제합니까?',
+ 'Remaining:' => '나머지:',
+ 'hours' => '시간',
+ 'spent' => '경과',
+ 'estimated' => '예측',
+ 'Sub-Tasks' => '서브 할일',
+ 'Add a sub-task' => '서브 할일 추가',
+ 'Original estimate' => '최초 예측시간',
+ 'Create another sub-task' => '다음 서브 할일 추가',
+ 'Time spent' => '경과시간',
+ 'Edit a sub-task' => '서브 할일을 변경하는 ',
+ 'Remove a sub-task' => '서브 할일을 삭제하는 ',
+ 'The time must be a numeric value' => '시간은 숫자로 입력하세요',
+ 'Todo' => '할일 예정',
+ 'In progress' => '할일 중',
+ 'Sub-task removed successfully.' => '서브 할일을 삭제했습니다.',
+ 'Unable to remove this sub-task.' => '서브 할일의 삭제가 실패했습니다.',
+ 'Sub-task updated successfully.' => '서브 할일을 갱신했습니다.',
+ 'Unable to update your sub-task.' => '서브 할일의 경신에 실패했습니다.',
+ 'Unable to create your sub-task.' => '서브 할일의 추가에 실패했습니다.',
+ 'Sub-task added successfully.' => '서브 할일을 추가했습니다.',
+ 'Maximum size: ' => '최대: ',
+ 'Unable to upload the file.' => '파일 업로드에 실패했습니다.',
+ 'Display another project' => '프로젝트 보기',
+ 'Created by %s' => '작성자 %s',
+ 'Tasks Export' => '할일 내보내기',
+ // 'Tasks exportation for "%s"' => '',
+ 'Start Date' => '시작일',
+ 'End Date' => '종료일',
+ 'Execute' => '실행',
+ 'Task Id' => '할일 ID',
+ 'Creator' => '작성자',
+ 'Modification date' => '변경 일',
+ 'Completion date' => '완료일',
+ 'Clone' => '복사',
+ 'Project cloned successfully.' => '프로젝트를 복제했습니다.',
+ 'Unable to clone this project.' => '프로젝트의 복제에 실패했습니다.',
+ 'Enable email notifications' => '이메일 알림 설정',
+ 'Task position:' => '할일 위치:',
+ // 'The task #%d have been opened.' => '',
+ // 'The task #%d have been closed.' => '',
+ 'Sub-task updated' => '서브 할일 갱신',
+ 'Title:' => '제목:',
+ 'Status:' => '상태:',
+ 'Assignee:' => '담당:',
+ 'Time tracking:' => '시간 계측:',
+ 'New sub-task' => '새로운 서브 할일',
+ // 'New attachment added "%s"' => '',
+ 'New comment posted by %s' => '"%s"님이 댓글을 추가하였습니다',
+ 'New attachment' => ' 새로운 첨부 파일',
+ 'New comment' => ' 새로운 댓글',
+ 'Comment updated' => '댓글가 갱신되었습니다',
+ 'New subtask' => ' 새로운 서브 할일',
+ 'Subtask updated' => '서브 할일 갱신',
+ 'Task updated' => '할일 업데이트',
+ 'Task closed' => '할일 마침',
+ 'Task opened' => '할일 시작',
+ 'I want to receive notifications only for those projects:' => '다음 프로젝트의 알림만 받겠습니다:',
+ 'view the task on Kanboard' => 'Kanboard에서 할일을 본다',
+ 'Public access' => '공개 접속 설정',
+ 'Active tasks' => '활성화된 할일',
+ 'Disable public access' => '공개 접속 비활성화',
+ 'Enable public access' => '공개 접속 활성화',
+ 'Public access disabled' => '공개 접속 불가',
+ // 'Do you really want to disable this project: "%s"?' => '',
+ // 'Do you really want to enable this project: "%s"?' => '',
+ 'Project activation' => '프로젝트의 액티베ー션',
+ 'Move the task to another project' => '할일별 프로젝트에 옮기',
+ 'Move to another project' => '다른 프로젝트로 이동',
+ 'Do you really want to duplicate this task?' => '할일을 복제합니까?',
+ 'Duplicate a task' => '할일 복사',
+ 'External accounts' => '외부 계정',
+ 'Account type' => '계정종류',
+ 'Local' => '로컬',
+ 'Remote' => '원격',
+ 'Enabled' => '활성화',
+ 'Disabled' => '비활성화',
+ 'Username:' => '사용자명',
+ 'Name:' => '이름:',
+ 'Email:' => '이메일:',
+ 'Notifications:' => '알림:',
+ 'Notifications' => '알림',
+ 'Account type:' => '계정종류:',
+ 'Edit profile' => '프로필 변경',
+ 'Change password' => '패스워드 변경',
+ 'Password modification' => '패스워드 변경',
+ 'External authentications' => '외부 인증',
+ 'Never connected.' => '접속기록없음',
+ 'No external authentication enabled.' => '외부 인증이 설정되어 있지 않습니다.',
+ 'Password modified successfully.' => '패스워드를 변경했습니다.',
+ 'Unable to change the password.' => '비밀 번호가 변경할 수 없었습니다.',
+ 'Change category for the task "%s"' => '할일 "%s"의 카테고리의 변경',
+ 'Change category' => '카테고리 수정',
+ '%s updated the task %s' => '%s이 할일 %s을 업데이트했습니다',
+ // '%s opened the task %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" 로 옮겼습니다',
+ '%s created the task %s' => '%s이 할일%s을 추가했습니다',
+ '%s closed the task %s' => '%s이 할일%s을 마쳤습니다',
+ '%s created a subtask for the task %s' => '%s이 할일%s의 서브 할일을 추가했습니다',
+ '%s updated a subtask for the task %s' => '%s이 할일%s의 서브 할일을 갱신했습니다',
+ 'Assigned to %s with an estimate of %s/%sh' => '담당자 %s에게 예상 %s/%sh로 할당되었습니다',
+ // 'Not assigned, estimate of %sh' => '',
+ '%s updated a comment on the task %s' => '%s이 할일%s의 댓글을 수정했습니다',
+ '%s commented the task %s' => '%s이 할일%s에 댓글을 남겼습니다',
+ '%s\'s activity' => '%s의 활동',
+ 'RSS feed' => 'RSS피드',
+ // '%s updated a comment on the task #%d' => '',
+ // '%s commented on the task #%d' => '',
+ // '%s updated a subtask for the task #%d' => '',
+ // '%s created a subtask for the task #%d' => '',
+ '%s updated the task #%d' => '%s이 할일#%d을 갱신했습니다',
+ '%s created the task #%d' => '%s이 할일#%d을 추가했습니다',
+ '%s closed the task #%d' => '%s이 할일#%d을 닫혔습니다',
+ '%s open the task #%d' => '%s이 할일#%d를 오픈했습니다',
+ '%s moved the task #%d to the column "%s"' => '%s이 할일#%d을 칼럼"%s"로 옮겼습니다',
+ // '%s moved the task #%d to the position %d in the column "%s"' => '',
+ 'Activity' => '활동',
+ // 'Default values are "%s"' => '',
+ // 'Default columns for new projects (Comma-separated)' => '',
+ 'Task assignee change' => '담당자의 변경',
+ // '%s change the assignee of the task #%d to %s' => '',
+ '%s changed the assignee of the task %s to %s' => '%s이 할일 %s의 담당을 %s로 변경했습니다',
+ 'New password for the user "%s"' => '사용자 "%s"의 새로운 패스워드',
+ 'Choose an event' => '행사의 선택',
+ 'Create a task from an external provider' => '할일을 외부 서비스로부터 작성하는 ',
+ 'Change the assignee based on an external username' => '담당자를 외부 서비스에 바탕을 두고 변경하는 ',
+ 'Change the category based on an external label' => '카테고리를 외부 서비스에 바탕을 두고 변경하는 ',
+ 'Reference' => '참조',
+ 'Label' => '라벨',
+ 'Database' => '데이터베이스',
+ 'About' => '정보',
+ 'Database driver:' => '데이터베이스 드라이버:',
+ 'Board settings' => '기본 설정',
+ 'URL and token' => 'URL와 토큰',
+ 'Webhook settings' => 'Webhook의 설정',
+ 'URL for task creation:' => 'Task작성의 URL:',
+ 'Reset token' => '토큰 리셋',
+ 'API endpoint:' => 'API엔드 포인트:',
+ 'Refresh interval for private board' => '비공개 보드의 갱신 빈도',
+ 'Refresh interval for public board' => '공개 보드의 갱신 빈도',
+ 'Task highlight period' => '할일의 하이라이트 기간',
+ // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
+ // 'Frequency in second (60 seconds by default)' => '',
+ // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
+ 'Application URL' => '애플리케이션의 URL',
+ 'Token regenerated.' => '토큰이 다시 생성되었습니다.',
+ 'Date format' => '데이터 포맷',
+ // 'ISO format is always accepted, example: "%s" and "%s"' => '',
+ 'New private project' => '새 비공개 프로젝트',
+ 'This project is private' => '이 프로젝트는 비공개입니다',
+ 'Add' => '추가',
+ 'Start date' => '시작시간',
+ 'Time estimated' => '예상시간',
+ 'There is nothing assigned to you.' => '할일이 없습니다. 옆사람의 일을 도와주면 어떨까요?',
+ 'My tasks' => '내 할일',
+ 'Activity stream' => '활동기록',
+ 'Dashboard' => '대시보드',
+ 'Confirmation' => '확인',
+ 'Allow everybody to access to this project' => '모든 사람이 이 프로젝트에 접근할 수 있도록 합니다',
+ 'Everybody have access to this project.' => '누구나 이 프로젝트에 액세스 할 수 있습니다',
+ 'Webhooks' => 'Webhook',
+ 'API' => 'API',
+ 'Create a comment from an external provider' => '외부 서비스로부터 의견을 작성한다',
+ 'Project management' => '프로젝트 관리',
+ 'My projects' => '내 프로젝트',
+ 'Columns' => '칼럼',
+ 'Task' => '할일',
+ 'Your are not member of any project.' => '어떤 프로젝트에도 속하지 않습니다.',
+ 'Percentage' => '비중',
+ 'Number of tasks' => '할일 수',
+ 'Task distribution' => '할일 분포',
+ 'Reportings' => '리포트',
+ // 'Task repartition for "%s"' => '',
+ 'Analytics' => '분석',
+ 'Subtask' => '서브 할일',
+ 'My subtasks' => '내 서브 할일',
+ 'User repartition' => '담당자 분포',
+ // 'User repartition for "%s"' => '',
+ 'Clone this project' => '이 프로젝트를 복제하는 ',
+ 'Column removed successfully.' => '(※)컬럼을 삭제했습니다',
+ 'Not enough data to show the graph.' => '그래프를 선묘화하려면 나왔지만 부족합니다',
+ 'Previous' => ' 돌아가',
+ 'The id must be an integer' => 'id은 숫자가 아니면 안 됩니다',
+ 'The project id must be an integer' => 'project id은 숫자가 아니면 안 됩니다',
+ 'The status must be an integer' => 'status는 숫자지 않으면 안 됩니다',
+ 'The subtask id is required' => 'subtask id가 필요합니다',
+ 'The subtask id must be an integer' => 'subtask id은 숫자가 아니면 안 됩니다',
+ 'The task id is required' => 'task id가 필요합니다',
+ 'The task id must be an integer' => 'task id은 숫자가 아니면 안 됩니다',
+ 'The user id must be an integer' => 'user id은 숫자가 아니면 안 됩니다',
+ 'This value is required' => '이 값이 필요합니다',
+ 'This value must be numeric' => '이 값은 숫자가 아니면 안 됩니다',
+ 'Unable to create this task.' => '이 할일을 작성할 수 없었습니다',
+ 'Cumulative flow diagram' => '축적 플로',
+ // 'Cumulative flow diagram for "%s"' => '',
+ 'Daily project summary' => '일시 프로젝트 개요',
+ 'Daily project summary export' => '일시 프로젝트 개요의 출력',
+ // 'Daily project summary export for "%s"' => '',
+ 'Exports' => '출력',
+ 'This export contains the number of tasks per column grouped per day.' => '이 출력은 날짜의 칼람별 할일 수를 집계한 것입니다',
+ 'Active swimlanes' => '액티브한 스윔레인',
+ 'Add a new swimlane' => ' 새로운 스윔레인',
+ 'Change default swimlane' => '기본 스윔레인의 변경',
+ 'Default swimlane' => '기본 스윔레인',
+ // 'Do you really want to remove this swimlane: "%s"?' => '',
+ 'Inactive swimlanes' => '인터랙티브한 스윔레인',
+ 'Remove a swimlane' => '스윔레인의 삭제',
+ 'Show default swimlane' => '기본 스윔레인의 표시',
+ // 'Swimlane modification for the project "%s"' => '',
+ 'Swimlane not found.' => '스윔레인이 발견되지 않습니다.',
+ 'Swimlane removed successfully.' => '스윔레인을 삭제했습니다.',
+ 'Swimlanes' => '스윔레인',
+ 'Swimlane updated successfully.' => '스윔레인을 갱신했습니다.',
+ 'The default swimlane have been updated successfully.' => '기본 스윔레인을 갱신했습니다.',
+ 'Unable to remove this swimlane.' => '스윔레인을 삭제할 수 없었습니다.',
+ 'Unable to update this swimlane.' => '스윔레인을 갱신할 수 없었습니다.',
+ 'Your swimlane have been created successfully.' => '스윔레인이 작성되었습니다.',
+ // 'Example: "Bug, Feature Request, Improvement"' => '',
+ // 'Default categories for new projects (Comma-separated)' => '',
+ 'Integrations' => '연계',
+ 'Integration with third-party services' => '외부 서비스 연계',
+ 'Subtask Id' => '서브 할일 Id',
+ 'Subtasks' => '서브 할일',
+ 'Subtasks Export' => '서브 할일 출력',
+ // 'Subtasks exportation for "%s"' => '',
+ 'Task Title' => '할일 제목',
+ 'Untitled' => '제목 없음',
+ 'Application default' => '애플리케이션 기본',
+ 'Language:' => '언어:',
+ 'Timezone:' => '시간대:',
+ 'All columns' => '모든 칼럼',
+ 'Calendar' => '달력',
+ 'Next' => '다음에 ',
+ '#%d' => '#%d',
+ 'All swimlanes' => '모든 스윔레인',
+ 'All colors' => '모든 색',
+ // 'Moved to column %s' => '',
+ 'Change description' => '설명 수정',
+ 'User dashboard' => '대시보드',
+ 'Allow only one subtask in progress at the same time for a user' => '한 사용자에 대한 하나의 할일만 진행 중에 가능합니다',
+ // 'Edit column "%s"' => '',
+ // 'Select the new status of the subtask: "%s"' => '',
+ 'Subtask timesheet' => '서브 할일 타임시트',
+ 'There is nothing to show.' => '기록이 없습니다',
+ 'Time Tracking' => '타임 트레킹',
+ 'You already have one subtask in progress' => '이미 진행 중인 서브 할일가 있습니다.',
+ 'Which parts of the project do you want to duplicate?' => '프로젝트의 무엇을 복제합니까?',
+ // 'Disallow login form' => '',
+ 'Start' => '시작',
+ 'End' => '종료',
+ 'Task age in days' => '할일이 생긴 시간',
+ 'Days in this column' => '이 칼럼에 있는 시간',
+ '%dd' => '%d일',
+ 'Add a new link' => ' 새로운 링크 추가',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ 'Field required' => '필드가 필요합니다',
+ 'Link added successfully.' => '링크를 추가했습니다.',
+ 'Link updated successfully.' => '링크를 갱신했습니다.',
+ 'Link removed successfully.' => '링크를 삭제했습니다.',
+ 'Link labels' => '링크 라벨',
+ 'Link modification' => '링크의 변경',
+ 'Links' => '링크',
+ 'Link settings' => '링크 설정',
+ 'Opposite label' => '반대의 라벨',
+ 'Remove a link' => '라벨의 삭제',
+ 'Task\'s links' => '할일의 라벨',
+ 'The labels must be different' => ' 다른 라벨을 지정하세요',
+ 'There is no link.' => '링크가 없습니다',
+ 'This label must be unique' => '라벨은 독특할 필요가 있습니다',
+ 'Unable to create your link.' => '링크를 작성할 수 없었습니다.',
+ 'Unable to update your link.' => '링크를 갱신할 수 없었습니다.',
+ 'Unable to remove this link.' => '링크를 삭제할 수 없었습니다.',
+ 'relates to' => '연관 링크',
+ 'blocks' => '다음을 딜레이하는',
+ 'is blocked by' => '다음 때문에 딜레이되는',
+ 'duplicates' => '다음과 중복하는',
+ 'is duplicated by' => '다음에 중복되는',
+ 'is a child of' => '다음의 하위 할일',
+ 'is a parent of' => '다음의 상위 할일',
+ 'targets milestone' => '다음의 이정표를 목표로 하는',
+ 'is a milestone of' => '다음의 이정표인',
+ 'fixes' => '다음을 수정하는',
+ 'is fixed by' => '다음에 의해 수정되는',
+ 'This task' => '이 할일의 ',
+ '<1h' => '<1시간',
+ '%dh' => '%d시간',
+ 'Expand tasks' => '할일 크게',
+ 'Collapse tasks' => '할일 작게',
+ 'Expand/collapse tasks' => '할일 크게/작게',
+ 'Close dialog box' => '다이얼로그를 닫습니다',
+ 'Submit a form' => '제출',
+ 'Board view' => '보드 뷰',
+ 'Keyboard shortcuts' => '키보드 숏 컷',
+ 'Open board switcher' => '보드 전환을 열',
+ 'Application' => '애플리케이션',
+ 'Compact view' => '컴팩트 뷰',
+ 'Horizontal scrolling' => '세로 스크롤',
+ 'Compact/wide view' => '컴팩트/와이드 뷰',
+ 'No results match:' => '결과가 일치하지 않았습니다',
+ 'Currency' => '통화',
+ 'Private project' => '개인 프로젝트',
+ // 'AUD - Australian Dollar' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ 'Custom Stylesheet' => '커스텀 스타일 시트',
+ 'download' => '다운로드',
+ // 'EUR - Euro' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'USD - US Dollar' => '',
+ 'Destination column' => '이동 후 칼럼',
+ 'Move the task to another column when assigned to a user' => '사용자의 할당을 하면 할일을 다른 칼럼에 이동',
+ 'Move the task to another column when assignee is cleared' => '사용자의 할당이 없어지면 할일을 다른 칼럼에 이동',
+ 'Source column' => '이동 전 칼럼',
+ 'Transitions' => '이력',
+ 'Executer' => '실행자',
+ 'Time spent in the column' => '칼럼에 있던 시간',
+ 'Task transitions' => '할일 천이',
+ 'Task transitions export' => '할일 천이를 출력',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '이 리포트는 할일의 칼럼 간 이동을 시간, 유저, 경과 시간과 함께 기록한 것입니다.',
+ 'Currency rates' => '환율',
+ 'Rate' => '레이트',
+ 'Change reference currency' => '현재의 기축 통화',
+ 'Add a new currency rate' => ' 새로운 통화 환율을 추가',
+ 'Reference currency' => '기축 통화',
+ // 'The currency rate have been added successfully.' => '',
+ 'Unable to add this currency rate.' => '이 통화 환율을 추가할 수 없습니다.',
+ 'Webhook URL' => 'Webhook URL',
+ '%s remove the assignee of the task %s' => '%s이 할일 %s의 담당을 삭제했습니다',
+ 'Enable Gravatar images' => 'Gravatar이미지를 활성화',
+ 'Information' => '정보',
+ 'Check two factor authentication code' => '2단 인증을 체크한다',
+ 'The two factor authentication code is not valid.' => '2단 인증 코드는 무효입니다.',
+ 'The two factor authentication code is valid.' => '2단 인증 코드는 유효합니다.',
+ 'Code' => '코드',
+ 'Two factor authentication' => '2단 인증',
+ // 'This QR code contains the key URI: ' => '',
+ 'Check my code' => '코드 체크',
+ // 'Secret key: ' => '',
+ 'Test your device' => '디바이스 테스트',
+ // 'Assign a color when the task is moved to a specific column' => '',
+ '%s via Kanboard' => '%s via E-board',
+ // 'Burndown chart for "%s"' => '',
+ // 'Burndown chart' => '',
+ // 'This chart show the task complexity over the time (Work Remaining).' => '',
+ 'Screenshot taken %s' => '스크린샷_%s',
+ 'Add a screenshot' => '스크린샷 추가',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '스크린샷을 CTRL+V 혹은 ⌘+V를 눌러 붙여넣기',
+ 'Screenshot uploaded successfully.' => '스크린샷을 업로드하였습니다',
+ // 'SEK - Swedish Krona' => '',
+ // 'Identifier' => '',
+ // 'Disable two factor authentication' => '',
+ // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
+ // 'Edit link' => '',
+ 'Start to type task title...' => '할일 제목을 처음부터 입력해주세요',
+ // 'A task cannot be linked to itself' => '',
+ // 'The exact same link already exists' => '',
+ // 'Recurrent task is scheduled to be generated' => '',
+ // 'Score' => '',
+ // 'The identifier must be unique' => '',
+ // 'This linked task id doesn\'t exists' => '',
+ // 'This value must be alphanumeric' => '',
+ 'Edit recurrence' => '반복 수정',
+ 'Generate recurrent task' => '반복 할일 만들기',
+ 'Trigger to generate recurrent task' => '반복 할일 트리거 만들기',
+ 'Factor to calculate new due date' => '새로운 종료날짜 계산',
+ 'Timeframe to calculate new due date' => '종료날짜 계산 단위',
+ 'Base date to calculate new due date' => '새로운 기본 종료날짜 계산',
+ 'Action date' => '시작날짜',
+ 'Base date to calculate new due date: ' => '새로운 기본 종료날짜 계산: ',
+ // 'This task has created this child task: ' => '',
+ 'Day(s)' => '일',
+ 'Existing due date' => '기존 종료날짜',
+ 'Factor to calculate new due date: ' => '새로운 종료날짜 계산: ',
+ 'Month(s)' => '월',
+ // 'Recurrence' => '',
+ // 'This task has been created by: ' => '',
+ // 'Recurrent task has been generated:' => '',
+ 'Timeframe to calculate new due date: ' => '종료날짜 계산 단위',
+ // 'Trigger to generate recurrent task: ' => '',
+ 'When task is closed' => '할일을 마쳤을때',
+ 'When task is moved from first column' => '할일이 첫번째 칼럼으로 옮겨졌을때',
+ 'When task is moved to last column' => '할일이 마지막 칼럼으로 옮겨졌을때',
+ 'Year(s)' => '년',
+ // 'Calendar settings' => '',
+ // 'Project calendar view' => '',
+ 'Project settings' => '프로젝트 설정',
+ // 'Show subtasks based on the time tracking' => '',
+ // 'Show tasks based on the creation date' => '',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+ // 'User calendar view' => '',
+ // 'Automatically update the start date' => '',
+ // 'iCal feed' => '',
+ // 'Preferences' => '',
+ // 'Security' => '',
+ 'Two factor authentication disabled' => '2단 인증 비활성화',
+ // 'Two factor authentication enabled' => '',
+ // 'Unable to update this user.' => '',
+ // 'There is no user management for private projects.' => '',
+ // 'User that will receive the email' => '',
+ 'Email subject' => '이메일 제목',
+ 'Date' => '날짜',
+ // 'Add a comment log when moving the task between columns' => '',
+ // 'Move the task to another column when the category is changed' => '',
+ 'Send a task by email to someone' => '할일을 이메일로 보내기',
+ 'Reopen a task' => '할일 다시 시작',
+ 'Column change' => '칼럼 이동',
+ 'Position change' => '위치 이동',
+ 'Swimlane change' => '스윔레인 변경',
+ 'Assignee change' => '담당자 변경',
+ '[%s] Overdue tasks' => '[%s] 마감시간 지남',
+ 'Notification' => '알림',
+ // '%s moved the task #%d to the first swimlane' => '',
+ // '%s moved the task #%d to the swimlane "%s"' => '',
+ 'Swimlane' => '스윔레인',
+ // 'Gravatar' => '',
+ // '%s moved the task %s to the first swimlane' => '',
+ // '%s moved the task %s to the swimlane "%s"' => '',
+ // 'This report contains all subtasks information for the given date range.' => '',
+ // 'This report contains all tasks information for the given date range.' => '',
+ // 'Project activities for %s' => '',
+ // 'view the board on Kanboard' => '',
+ // 'The task have been moved to the first swimlane' => '',
+ // 'The task have been moved to another swimlane:' => '',
+ // 'Overdue tasks for the project "%s"' => '',
+ 'New title: %s' => '제목 변경: %s',
+ 'The task is not assigned anymore' => '담당자 없음',
+ 'New assignee: %s' => '담당자 변경: %s',
+ 'There is no category now' => '카테고리 없음',
+ 'New category: %s' => '카테고리 변경: %s',
+ 'New color: %s' => '색깔 변경: %s',
+ 'New complexity: %d' => '복잡도 변경: %d',
+ 'The due date have been removed' => '마감날짜 삭제',
+ 'There is no description anymore' => '설명 없음',
+ 'Recurrence settings have been modified' => '반복할일 설정 수정',
+ 'Time spent changed: %sh' => '경과시간 변경: %s시간',
+ 'Time estimated changed: %sh' => '%s시간으로 예상시간 변경',
+ // 'The field "%s" have been updated' => '',
+ // '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:' => '다음의 알림을 받기를 원합니다:',
+ 'All tasks' => '모든 할일',
+ 'Only for tasks assigned to me' => '내가 담당자인 일',
+ 'Only for tasks created by me' => '내가 만든 일',
+ 'Only for tasks created by me and assigned to me' => '내가 만들었거나 내가 담당자인 일',
+ // '%%Y-%%m-%%d' => '',
+ // 'Total for all columns' => '',
+ // 'You need at least 2 days of data to show the chart.' => '',
+ '<15m' => '<15분',
+ '<30m' => '<30분',
+ // 'Stop timer' => '',
+ 'Start timer' => '타이머 시작',
+ // 'Add project member' => '',
+ 'Enable notifications' => '알림 켜기',
+ 'My activity stream' => '내 활동기록',
+ 'My calendar' => '내 캘린더',
+ // 'Search tasks' => '',
+ 'Reset filters' => '필터 리셋',
+ 'My tasks due tomorrow' => '내일까지 내 할일',
+ 'Tasks due today' => '오늘까지 할일',
+ 'Tasks due tomorrow' => '내일까지 할일',
+ 'Tasks due yesterday' => '어제까지 할일',
+ 'Closed tasks' => '닫힌 할일',
+ 'Open tasks' => '열린 할일',
+ 'Not assigned' => '담당자가 없는 일',
+ 'View advanced search syntax' => '추가 검색 문법보기',
+ 'Overview' => '개요',
+ // 'Board/Calendar/List view' => '',
+ // 'Switch to the board view' => '',
+ // 'Switch to the calendar view' => '',
+ // 'Switch to the list view' => '',
+ // 'Go to the search/filter box' => '',
+ 'There is no activity yet.' => '활동이 없습니다',
+ // 'No tasks found.' => '',
+ // 'Keyboard shortcut: "%s"' => '',
+ 'List' => '목록',
+ // 'Filter' => '',
+ 'Advanced search' => '검색 문법',
+ 'Example of query: ' => '문법 예제 ',
+ 'Search by project: ' => '프로젝트로 찾기 ',
+ 'Search by column: ' => '칼럼으로 찾기 ',
+ 'Search by assignee: ' => '담당자로 찾기 ',
+ 'Search by color: ' => '색깔로 찾기 ',
+ 'Search by category: ' => '카테고리로 찾기 ',
+ 'Search by description: ' => '설명으로 찾기 ',
+ 'Search by due date: ' => '마감날짜로 찾기 ',
+ // 'Lead and Cycle time for "%s"' => '',
+ // 'Average time spent into each column for "%s"' => '',
+ // 'Average time spent into each column' => '',
+ // 'Average time spent' => '',
+ // 'This chart show the average time spent into each column for the last %d tasks.' => '',
+ // 'Average Lead and Cycle time' => '',
+ // 'Average lead time: ' => '',
+ // 'Average cycle time: ' => '',
+ 'Cycle Time' => '사이클 타임',
+ 'Lead Time' => '리드 타임',
+ // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
+ // 'Average time into each column' => '',
+ // 'Lead and cycle time' => '',
+ 'Lead time: ' => '리드 타임: ',
+ 'Cycle time: ' => '사이클 타임: ',
+ 'Time spent into each column' => '각 칼럼에서 걸린 시간',
+ // 'The lead time is the duration between the task creation and the completion.' => '',
+ // 'The cycle time is the duration between the start date and the completion.' => '',
+ // 'If the task is not closed the current time is used instead of the completion date.' => '',
+ // 'Set automatically the start date' => '',
+ 'Edit Authentication' => '계정 수정',
+ // 'Remote user' => '',
+ // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
+ // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
+ 'New remote user' => '새로운 원격유저',
+ 'New local user' => '새로운 유저',
+ // 'Default task color' => '',
+ 'This feature does not work with all browsers.' => '이 기능은 일부 브라우저에서 작동하지 않습니다',
+ // 'There is no destination project available.' => '',
+ // 'Trigger automatically subtask time tracking' => '',
+ // 'Include closed tasks in the cumulative flow diagram' => '',
+ // 'Current swimlane: %s' => '',
+ // 'Current column: %s' => '',
+ // 'Current category: %s' => '',
+ // 'no category' => '',
+ // 'Current assignee: %s' => '',
+ // 'not assigned' => '',
+ // 'Author:' => '',
+ // 'contributors' => '',
+ // 'License:' => '',
+ // 'License' => '',
+ // 'Enter the text below' => '',
+ // 'Gantt chart for %s' => '',
+ // 'Sort by position' => '',
+ // 'Sort by date' => '',
+ // 'Add task' => '',
+ // 'Start date:' => '',
+ // 'Due date:' => '',
+ // 'There is no start date or due date for this task.' => '',
+ // 'Moving or resizing a task will change the start and due date of the task.' => '',
+ // 'There is no task in your project.' => '',
+ 'Gantt chart' => '간트차트',
+ 'People who are project managers' => '프로젝트 매니저',
+ 'People who are project members' => '프로젝트 멤버',
+ // 'NOK - Norwegian Krone' => '',
+ 'Show this column' => '칼럼 보이기',
+ 'Hide this column' => '칼럼 숨기기',
+ 'open file' => '열기',
+ 'End date' => '종료 날짜',
+ 'Users overview' => '유저 전체보기',
+ 'Members' => '멤버',
+ // 'Shared project' => '',
+ 'Project managers' => '프로젝트 매니저',
+ // 'Gantt chart for all projects' => '',
+ 'Projects list' => '프로젝트 리스트',
+ 'Gantt chart for this project' => '이 프로젝트 간트차트',
+ 'Project board' => '프로젝트 보드',
+ // 'End date:' => '',
+ 'There is no start date or end date for this project.' => '이 프로젝트에는 시작날짜와 종료날짜가 없습니다',
+ 'Projects Gantt chart' => '프로젝트 간트차트',
+ // 'Change task color when using a specific task link' => '',
+ // 'Task link creation or modification' => '',
+ // 'Milestone' => '',
+ // 'Documentation: %s' => '',
+ // 'Switch to the Gantt chart view' => '',
+ // 'Reset the search/filter box' => '',
+ // 'Documentation' => '',
+ // 'Table of contents' => '',
+ 'Gantt' => '간트',
+ // 'Author' => '',
+ // 'Version' => '',
+ // 'Plugins' => '',
+ // 'There is no plugin loaded.' => '',
+ 'Set maximum column height' => '최대 칼럼 높이 제한하기',
+ 'Remove maximum column height' => '최대 칼럼 높이 없애기',
+ 'My notifications' => '내 알림',
+ 'Custom filters' => '사용자 정의 필터',
+ // 'Your custom filter have been created successfully.' => '',
+ // 'Unable to create your custom filter.' => '',
+ // 'Custom filter removed successfully.' => '',
+ // 'Unable to remove this custom filter.' => '',
+ // 'Edit custom filter' => '',
+ // 'Your custom filter have been updated successfully.' => '',
+ // 'Unable to update custom filter.' => '',
+ 'Web' => '웹',
+ // 'New attachment on task #%d: %s' => '',
+ 'New comment on task #%d' => '할일 #%d에 새로운 댓글이 달렸습니다',
+ 'Comment updated on task #%d' => '할일 #%d에 댓글이 업데이트되었습니다',
+ 'New subtask on task #%d' => '서브 할일 #%d이 업데이트되었습니다',
+ 'Subtask updated on task #%d' => '서브 할일 #%d가 업데이트되었습니다',
+ 'New task #%d: %s' => '할일 #%d: %s이 추가되었습니다',
+ 'Task updated #%d' => '할일 #%d이 업데이트되었습니다',
+ 'Task #%d closed' => '할일 #%d를 마쳤습니다',
+ // 'Task #%d opened' => '',
+ 'Column changed for task #%d' => '할일 #%d의 칼럼이 변경되었습니다',
+ 'New position for task #%d' => '할일 #%d이 새로운 위치에 등록되었습니다',
+ // 'Swimlane changed for task #%d' => '',
+ // 'Assignee changed on task #%d' => '',
+ // '%d overdue tasks' => '',
+ // 'Task #%d is overdue' => '',
+ 'No new notifications.' => '알림이 없습니다',
+ 'Mark all as read' => '모두 읽음',
+ 'Mark as read' => '읽음',
+ // 'Total number of tasks in this column across all swimlanes' => '',
+ // 'Collapse swimlane' => '',
+ // 'Expand swimlane' => '',
+ // 'Add a new filter' => '',
+ // 'Share with all project members' => '',
+ // 'Shared' => '',
+ // 'Owner' => '',
+ 'Unread notifications' => '읽지않은 알림',
+ 'Notification methods:' => '알림 방법',
+ // 'Import tasks from CSV file' => '',
+ // 'Unable to read your file' => '',
+ // '%d task(s) have been imported successfully.' => '',
+ // 'Nothing have been imported!' => '',
+ // 'Import users from CSV file' => '',
+ // '%d user(s) have been imported successfully.' => '',
+ // 'Comma' => '',
+ // 'Semi-colon' => '',
+ // 'Tab' => '',
+ // 'Vertical bar' => '',
+ // 'Double Quote' => '',
+ // 'Single Quote' => '',
+ // '%s attached a file to the task #%d' => '',
+ // 'There is no column or swimlane activated in your project!' => '',
+ // 'Append filter (instead of replacement)' => '',
+ // 'Append/Replace' => '',
+ // 'Append' => '',
+ // 'Replace' => '',
+ 'Import' => '가져오기',
+ // 'change sorting' => '',
+ // 'Tasks Importation' => '',
+ // 'Delimiter' => '',
+ // 'Enclosure' => '',
+ // 'CSV File' => '',
+ // 'Instructions' => '',
+ // 'Your file must use the predefined CSV format' => '',
+ // 'Your file must be encoded in UTF-8' => '',
+ // 'The first row must be the header' => '',
+ // 'Duplicates are not verified for you' => '',
+ // 'The due date must use the ISO format: YYYY-MM-DD' => '',
+ // 'Download CSV template' => '',
+ 'No external integration registered.' => '설정이 되어있지 않습니다',
+ // 'Duplicates are not imported' => '',
+ // 'Usernames must be lowercase and unique' => '',
+ // 'Passwords will be encrypted if present' => '',
+ '%s attached a new file to the task %s' => '%s이 새로운 파일을 할일 %s에 추가했습니다',
+ // 'Link type' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ 'Manager' => '매니저',
+ 'Project Manager' => '프로젝트 매니저',
+ 'Project Member' => '프로젝트 멤버',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ 'View all groups' => '모든그룹보기',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ 'Add group member' => '멤버추가',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ 'Permissions' => '권한',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ 'Role' => '역할',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ 'Role:' => '역할: ',
+ 'Project members' => '프로젝트 멤버',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ 'You were mentioned in a comment on the task #%d' => '할일 #%d의 댓글에서 언급되었습니다',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ 'Time-based One-time Password Algorithm' => '시간에 기반한 1회용 패스워드 알고리즘',
+ 'Two-Factor Provider: ' => '2단 인증: ',
+ // 'Disable two-factor authentication' => '',
+ 'Enable two-factor authentication' => '2단 인증 활성화',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ 'Forgot password?' => '비밀번호 찾기',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ 'Last Password Reset' => '비밀번호 초기화',
+ 'The password has never been reinitialized.' => '비밀번호가 초기화되지 않았습니다',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ 'Password reset history' => '비밀번호 초기화 기록',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ 'Close all tasks of this column' => '칼럼의 모든 할일 마치기',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ 'My dashboard' => '대시보드',
+ 'My profile' => '프로필',
+ // 'Project owner: ' => '',
+ // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '',
+ // 'Project owner' => '',
+ // 'Those dates are useful for the project Gantt chart.' => '',
+ // 'Private projects do not have users and groups management.' => '',
+ // 'There is no project member.' => '',
+ // 'Priority' => '',
+ // 'Task priority' => '',
+ // 'General' => '',
+ // 'Dates' => '',
+ // 'Default priority' => '',
+ // 'Lowest priority' => '',
+ // 'Highest priority' => '',
+ // 'If you put zero to the low and high priority, this feature will be disabled.' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'Internal links' => '',
+ // 'Assign to me' => '',
+ // 'Me' => '',
+ // 'Do not duplicate anything' => '',
+ // 'Projects management' => '',
+ // 'Users management' => '',
+ // 'Groups management' => '',
+ // 'Create from another project' => '',
+ // 'open' => '',
+ // 'closed' => '',
+ // 'Priority:' => '',
+ // 'Reference:' => '',
+ // 'Complexity:' => '',
+ // 'Swimlane:' => '',
+ // 'Column:' => '',
+ // 'Position:' => '',
+ // 'Creator:' => '',
+ // 'Time estimated:' => '',
+ // '%s hours' => '',
+ // 'Time spent:' => '',
+ // 'Created:' => '',
+ // 'Modified:' => '',
+ // 'Completed:' => '',
+ // 'Started:' => '',
+ // 'Moved:' => '',
+ // 'Task #%d' => '',
+ // 'Date and time format' => '',
+ // 'Time format' => '',
+ // 'Start date: ' => '',
+ // 'End date: ' => '',
+ // 'New due date: ' => '',
+ // 'Start date changed: ' => '',
+ // 'Disable private projects' => '',
+ // 'Do you really want to remove this custom filter: "%s"?' => '',
+ // 'Remove a custom filter' => '',
+ // 'User activated successfully.' => '',
+ // 'Unable to enable this user.' => '',
+ // 'User disabled successfully.' => '',
+ // 'Unable to disable this user.' => '',
+ // 'All files have been uploaded successfully.' => '',
+ // 'View uploaded files' => '',
+ // 'The maximum allowed file size is %sB.' => '',
+ // 'Choose files again' => '',
+ // 'Drag and drop your files here' => '',
+ // 'choose files' => '',
+ // 'View profile' => '',
+ // 'Two Factor' => '',
+ // 'Disable user' => '',
+ // 'Do you really want to disable this user: "%s"?' => '',
+ // 'Enable user' => '',
+ // 'Do you really want to enable this user: "%s"?' => '',
+ // 'Download' => '',
+ // 'Uploaded: %s' => '',
+ // 'Size: %s' => '',
+ // 'Uploaded by %s' => '',
+ // 'Filename' => '',
+ // 'Size' => '',
+ // 'Column created successfully.' => '',
+ // 'Another column with the same name exists in the project' => '',
+ // 'Default filters' => '',
+ // 'Your board doesn\'t have any column!' => '',
+ // 'Change column position' => '',
+ // 'Switch to the project overview' => '',
+ // 'User filters' => '',
+ // 'Category filters' => '',
+ // 'Upload a file' => '',
+ // 'View file' => '',
+ // 'Last activity' => '',
+ // 'Change subtask position' => '',
+ // 'This value must be greater than %d' => '',
+ // 'Another swimlane with the same name exists in the project' => '',
+ // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '',
+ // 'Actions duplicated successfully.' => '',
+ // 'Unable to duplicate actions.' => '',
+ // 'Add a new action' => '',
+ // 'Import from another project' => '',
+ // 'There is no action at the moment.' => '',
+ // 'Import actions from another project' => '',
+ // 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
+);
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
index fc1447e9..4537f38c 100644
--- a/app/Locale/my_MY/translations.php
+++ b/app/Locale/my_MY/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Semua projek',
'Add a new column' => 'Tambah kolom baru',
'Title' => 'Judul',
- 'Nobody assigned' => 'Tidak ada yang ditugaskan',
'Assigned to %s' => 'Ditugaskan ke %s',
'Remove a column' => 'Hapus kolom',
'Remove a column from a board' => 'Hapus kolom dari papan',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Jumlah tugas',
'User' => 'Pengguna',
'Comments' => 'Komentar',
- 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown',
'Leave a comment' => 'Tinggalkan komentar',
'Comment is required' => 'Komentar diperlukan',
'Leave a description' => 'Tinggalkan deskripsi',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Pelacakan waktu :',
'New sub-task' => 'Sub-tugas baru',
'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »',
- 'Comment updated' => 'Komentar diperbaharui',
'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »',
'New attachment' => 'Lampirkan baru',
'New comment' => 'Komentar baru',
+ 'Comment updated' => 'Komentar diperbaharui',
'New subtask' => 'Sub-tugas baru',
'Subtask updated' => 'Sub-tugas diperbaharui',
'Task updated' => 'Tugas diperbaharui',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalunya diterima, contoh: « %s » et « %s »',
'New private project' => 'Projek peribadi baharu',
'This project is private' => 'projek ini adalah peribadi',
- 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru',
'Add' => 'Tambah',
'Start date' => 'Tarikh mula',
'Time estimated' => 'Anggaran masa',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Ekspor ringkasan projek harian untuk « %s »',
'Exports' => 'Ekspor',
'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.',
- 'Nothing to preview...' => 'Tiada yang dapat diintai...',
- 'Preview' => 'Intai',
- 'Write' => 'Tulis',
'Active swimlanes' => 'Swimlanes aktif',
'Add a new swimlane' => 'Tambah swimlane baharu',
'Change default swimlane' => 'Tukar piawai swimlane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Usia tugas dalam bentuk harian',
'Days in this column' => 'Hari dalam kolom ini',
'%dd' => '%dj',
- 'Add a link' => 'Menambahkan pautan',
'Add a new link' => 'Tambah Pautan baru',
'Do you really want to remove this link: "%s"?' => 'Anda yakin akan menghapus Pautan ini : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Anda yakin akan menghapus Pautan ini dengan tugas n°%d ?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aliran kegiatan saya',
'My calendar' => 'Kalender saya',
'Search tasks' => 'Cari tugas',
- 'Back to the calendar' => 'Kembali ke kalender',
- 'Filters' => 'Filter',
'Reset filters' => 'Reset ulang filter',
'My tasks due tomorrow' => 'Tugas saya yang berakhir besok',
'Tasks due today' => 'Tugas yang berakhir hari ini',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Waktu berakhir :',
'There is no start date or end date for this project.' => 'Tidak ada waktu mula atau waktu berakhir pada projek ini',
'Projects Gantt chart' => 'projekkan carta Gantt',
- 'Link type' => 'Jenis pautan',
'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan Pautan tugas yang spesifik',
'Task link creation or modification' => 'Pautan tugas pada penciptaan atau penyuntingan',
'Milestone' => 'Batu Tanda',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Jenis pautan',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 003ca856..8c6a56f2 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle prosjekter',
'Add a new column' => 'Legg til en ny kolonne',
'Title' => 'Tittel',
- 'Nobody assigned' => 'Ikke tildelt',
'Assigned to %s' => 'Tildelt: %s',
'Remove a column' => 'Fjern en kolonne',
'Remove a column from a board' => 'Fjern en kolonne fra et board',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Antall oppgaver',
'User' => 'Bruker',
'Comments' => 'Kommentarer',
- 'Write your text in Markdown' => 'Skriv din tekst i markdown',
'Leave a comment' => 'Legg inn en kommentar',
'Comment is required' => 'Kommentar må legges inn',
'Leave a description' => 'Legg inn en beskrivelse...',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tidsmåling:',
'New sub-task' => 'Ny deloppgave',
'New attachment added "%s"' => 'Nytt vedlegg er lagt tilet "%s"',
- 'Comment updated' => 'Kommentar oppdatert',
'New comment posted by %s' => 'Ny kommentar fra %s',
'New attachment' => 'Nytt vedlegg',
'New comment' => 'Ny kommentar',
+ 'Comment updated' => 'Kommentar oppdatert',
'New subtask' => 'Ny deloppgave',
'Subtask updated' => 'Deloppgave oppdatert',
'Task updated' => 'Oppgave oppdatert',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er alltid akseptert, eksempelvis: "%s" og "%s"',
'New private project' => 'Nytt privat prosjekt',
'This project is private' => 'Dette projektet er privat',
- 'Type here to create a new sub-task' => 'Skriv her for ø opprette en ny deloppgave',
'Add' => 'Legg til',
'Start date' => 'Start dato',
'Time estimated' => 'Tid estimert',
@@ -483,9 +480,6 @@ return array(
// 'Daily project summary export for "%s"' => '',
'Exports' => 'Eksporter',
// 'This export contains the number of tasks per column grouped per day.' => '',
- 'Nothing to preview...' => 'Ingenting å forhåndsvise',
- 'Preview' => 'Forhåndsvisning',
- 'Write' => 'Skriv',
'Active swimlanes' => 'Aktive svømmebaner',
'Add a new swimlane' => 'Legg til en ny svømmebane',
'Change default swimlane' => 'Endre standard svømmebane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Dager siden oppgaven ble opprettet',
'Days in this column' => 'Dager siden oppgaven ble lagt i denne kolonnen',
// '%dd' => '',
- 'Add a link' => 'Legg til en relasjon',
'Add a new link' => 'Legg til en ny relasjon',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aktivitetslogg',
'My calendar' => 'Min kalender',
'Search tasks' => 'Søk oppgave',
- 'Back to the calendar' => 'Tilbake til kalender',
- 'Filters' => 'Filtere',
'Reset filters' => 'Nullstill filter',
'My tasks due tomorrow' => 'Mine oppgaver med frist i morgen',
'Tasks due today' => 'Oppgaver med frist i dag',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Sluttdato:',
// 'There is no start date or end date for this project.' => '',
'Projects Gantt chart' => 'Gantt skjema for prosjekter',
- 'Link type' => 'Relasjonstype',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
'Milestone' => 'Milepæl',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Relasjonstype',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index 4fce1c33..18155816 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle projecten',
'Add a new column' => 'Kolom toevoegen',
'Title' => 'Titel',
- 'Nobody assigned' => 'Niemand toegewezen',
'Assigned to %s' => 'Toegewezen aan %s',
'Remove a column' => 'Kolom verwijderen',
'Remove a column from a board' => 'Kolom verwijderen van het bord',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Aantal taken',
'User' => 'Gebruiker',
'Comments' => 'Commentaar',
- 'Write your text in Markdown' => 'Schrijf uw tekst in Markdown',
'Leave a comment' => 'Schrijf een commentaar',
'Comment is required' => 'Commentaar is verplicht',
'Leave a description' => 'Schrijf een omschrijving',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tijdschrijven :',
'New sub-task' => 'Nieuwe subtaak',
'New attachment added "%s"' => 'Nieuwe bijlage toegevoegd « %s »',
- 'Comment updated' => 'Commentaar aangepast',
'New comment posted by %s' => 'Nieuw commentaar geplaatst door « %s »',
'New attachment' => 'Nieuwe bijlage',
'New comment' => 'Nieuw commentaar',
+ 'Comment updated' => 'Commentaar aangepast',
'New subtask' => 'Nieuwe subtaak',
'Subtask updated' => 'Subtaak aangepast',
'Task updated' => 'Taak aangepast',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formaat is altijd geaccepteerd, bijvoorbeeld : « %s » et « %s »',
'New private project' => 'Nieuw privé project',
'This project is private' => 'Dit project is privé',
- 'Type here to create a new sub-task' => 'Typ hier om een nieuwe subtaak aan te maken',
'Add' => 'Toevoegen',
'Start date' => 'Startdatum',
'Time estimated' => 'Geschatte tijd',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Dagelijkse project samenvatting voor « %s »',
'Exports' => 'Exports',
'This export contains the number of tasks per column grouped per day.' => 'Dit rapport bevat het aantal taken per kolom gegroupeerd per dag.',
- 'Nothing to preview...' => 'Niets om te previewen...',
- 'Preview' => 'Preview',
- 'Write' => 'Schrijf',
'Active swimlanes' => 'Actieve swinlanes',
'Add a new swimlane' => 'Nieuwe swimlane toevoegen',
'Change default swimlane' => 'Standaard swimlane aapassen',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Leeftijd taak in dagen',
'Days in this column' => 'Dagen in deze kolom',
'%dd' => '%dj',
- 'Add a link' => 'Link toevoegen',
'Add a new link' => 'Nieuwe link toevoegen',
'Do you really want to remove this link: "%s"?' => 'Weet u zeker dat u deze link wil verwijderen : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Weet u zeker dat u deze link met taak %d wil verwijderen?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Mijn activiteiten',
'My calendar' => 'Mijn kalender',
'Search tasks' => 'Zoek taken',
- 'Back to the calendar' => 'Terug naar de kalender',
- 'Filters' => 'Filters',
'Reset filters' => 'Reset filters',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
'Milestone' => 'Mijlpaal',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index 87201bd0..d9427d80 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Wszystkie projekty',
'Add a new column' => 'Dodaj nową kolumnę',
'Title' => 'Nazwa',
- 'Nobody assigned' => 'Nikt nie przypisany',
'Assigned to %s' => 'Przypisane do %s',
'Remove a column' => 'Usuń kolumnę',
'Remove a column from a board' => 'Usuń kolumnę z tablicy',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Liczba zadań',
'User' => 'Użytkownik',
'Comments' => 'Komentarze',
- 'Write your text in Markdown' => 'Zobacz jak formatować tekst z użyciem Markdown',
'Leave a comment' => 'Wstaw komentarz',
'Comment is required' => 'Komentarz jest wymagany',
'Leave a description' => 'Dodaj opis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Śledzenie czasu: ',
'New sub-task' => 'Nowe Pod-zadanie',
'New attachment added "%s"' => 'Nowy załącznik dodany "%s"',
- 'Comment updated' => 'Komentarz zaktualizowany',
'New comment posted by %s' => 'Nowy komentarz dodany przez %s',
'New attachment' => 'Nowy załącznik',
'New comment' => 'Nowy Komentarz',
+ 'Comment updated' => 'Komentarz zaktualizowany',
'New subtask' => 'Nowe pod-zadanie',
'Subtask updated' => 'Zaktualizowane pod-zadanie',
'Task updated' => 'Zaktualizowane zadanie',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO jest zawsze akceptowany, przykłady: "%s", "%s"',
'New private project' => 'Nowy projekt prywatny',
'This project is private' => 'Ten projekt jest prywatny',
- 'Type here to create a new sub-task' => 'Nazwa podzadania',
'Add' => 'Dodaj',
'Start date' => 'Data rozpoczęcia',
'Time estimated' => 'Szacowany czas',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Wygeneruj dzienny raport dla projektu: "%s"',
'Exports' => 'Eksporty',
'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień',
- 'Nothing to preview...' => 'Nic do podejrzenia...',
- 'Preview' => 'Podgląd',
- 'Write' => 'Edycja',
'Active swimlanes' => 'Aktywne procesy',
'Add a new swimlane' => 'Dodaj proces',
'Change default swimlane' => 'Zmień domyślny proces',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Wiek zadania w dniach',
'Days in this column' => 'Dni w tej kolumnie',
// '%dd' => '',
- 'Add a link' => 'Dodaj link',
'Add a new link' => 'Dodaj nowy link',
'Do you really want to remove this link: "%s"?' => 'Czy na pewno chcesz usunąć ten link: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Czy na pewno chcesz usunąć ten link razem z zadaniem nr %d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Moja aktywność',
'My calendar' => 'Mój kalendarz',
'Search tasks' => 'Szukaj zadań',
- 'Back to the calendar' => 'Wróć do kalendarza',
- 'Filters' => 'Filtry',
'Reset filters' => 'Resetuj zastosowane filtry',
'My tasks due tomorrow' => 'Moje zadania do jutra',
'Tasks due today' => 'Zadania do dzisiaj',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data zakończenia:',
'There is no start date or end date for this project.' => 'Nie zdefiniowano czasu trwania projektu',
'Projects Gantt chart' => 'Wykres Gantta dla projektów',
- 'Link type' => 'Rodzaj link\'u',
'Change task color when using a specific task link' => 'Zmień kolor zadania używając specjalnego adresu URL',
'Task link creation or modification' => 'Adres URL do utworzenia zadania lub modyfikacji',
'Milestone' => 'Kamień milowy',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
'Owner' => 'Właściciel',
'Unread notifications' => 'Nieprzeczytane powiadomienia',
- 'My filters' => 'Moje filtry',
'Notification methods:' => 'Metody powiadomień:',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Rodzaj link\'u',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
'Auto' => 'Automatyczny',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Link zewnętrzny',
'Copy and paste your link here...' => 'Skopiuj i wklej link tutaj ...',
// 'URL' => '',
- 'There is no external link for the moment.' => 'Brak linków zewnętrznych.',
'Internal links' => 'Linki do innych zadań',
- 'There is no internal link for the moment.' => 'Brak powiązań do innych zadań.',
// 'Assign to me' => '',
'Me' => 'JA',
'Do not duplicate anything' => 'Nie kopiuj żadnego projektu',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Zarządzanie użytkownikami',
'Groups management' => 'Zarządzanie grupami',
'Create from another project' => 'Utwórz na podstawie innego projektu',
- 'There is no subtask at the moment.' => 'Brak podzadań.',
'open' => 'otwarty',
'closed' => 'zamknięty',
'Priority:' => 'Priorytet:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Rozpoczęte:',
'Moved:' => 'Przesunięcie:',
'Task #%d' => 'Zadanie #%d',
- 'Sub-tasks' => 'Podzadania',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
'Upload a file' => 'Prześlij plik',
- 'There is no attachment at the moment.' => 'Brak załączników.',
'View file' => 'Wyświetl plik',
'Last activity' => 'Ostatnia aktywność',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 2c9412b6..e0cdb17d 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Todos os projetos',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
- 'Nobody assigned' => 'Ninguém designado',
'Assigned to %s' => 'Designado para %s',
'Remove a column' => 'Remover uma coluna',
'Remove a column from a board' => 'Remover uma coluna do board',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Número de tarefas',
'User' => 'Usuário',
'Comments' => 'Comentários',
- 'Write your text in Markdown' => 'Escreva seu texto em Markdown',
'Leave a comment' => 'Deixe um comentário',
'Comment is required' => 'Comentário é obrigatório',
'Leave a description' => 'Deixe uma descrição',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Controle de tempo:',
'New sub-task' => 'Nova subtarefa',
'New attachment added "%s"' => 'Novo anexo adicionado "%s"',
- 'Comment updated' => 'Comentário atualizado',
'New comment posted by %s' => 'Novo comentário postado por %s',
'New attachment' => 'Novo anexo',
'New comment' => 'Novo comentário',
+ 'Comment updated' => 'Comentário atualizado',
'New subtask' => 'Nova subtarefa',
'Subtask updated' => 'Subtarefa alterada',
'Task updated' => 'Tarefa alterada',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceito, exemplo: "%s" e "%s"',
'New private project' => 'Novo projeto privado',
'This project is private' => 'Este projeto é privado',
- 'Type here to create a new sub-task' => 'Digite aqui para criar uma nova subtarefa',
'Add' => 'Adicionar',
'Start date' => 'Data de início',
'Time estimated' => 'Tempo estimado',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Exportação diária do resumo do projeto para "%s"',
'Exports' => 'Exportar',
'This export contains the number of tasks per column grouped per day.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.',
- 'Nothing to preview...' => 'Nada para pré-visualizar...',
- 'Preview' => 'Pré-visualizar',
- 'Write' => 'Escrever',
'Active swimlanes' => 'Ativar swimlanes',
'Add a new swimlane' => 'Adicionar swimlane',
'Change default swimlane' => 'Alterar swimlane padrão',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Idade da tarefa em dias',
'Days in this column' => 'Dias nesta coluna',
'%dd' => '%dd',
- 'Add a link' => 'Adicionar uma associação',
'Add a new link' => 'Adicionar uma nova associação',
'Do you really want to remove this link: "%s"?' => 'Você realmente deseja remover esta associação: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Você realmente deseja remover esta associação com a tarefa #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Meu feed de atividades',
'My calendar' => 'Minha agenda',
'Search tasks' => 'Pesquisar tarefas',
- 'Back to the calendar' => 'Voltar ao calendário',
- 'Filters' => 'Filtros',
'Reset filters' => 'Redefinir os filtros',
'My tasks due tomorrow' => 'Minhas tarefas que expiram amanhã',
'Tasks due today' => 'Tarefas que expiram hoje',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data de término:',
'There is no start date or end date for this project.' => 'Não há data de início ou data de término para este projeto.',
'Projects Gantt chart' => 'Gráfico de Gantt dos projetos',
- 'Link type' => 'Tipo de link',
'Change task color when using a specific task link' => 'Mudar a cor da tarefa quando um link específico é utilizado',
'Task link creation or modification' => 'Criação ou modificação de um link em uma tarefa',
'Milestone' => 'Milestone',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Compartilhado',
'Owner' => 'Líder',
'Unread notifications' => 'Notificações não lidas',
- 'My filters' => 'Meus filtros',
'Notification methods:' => 'Métodos de notificação:',
'Import tasks from CSV file' => 'Importar tarefas a partir de arquivo CSV',
'Unable to read your file' => 'Não foi possível ler seu arquivo',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Nomes de usuário devem ser únicos e em letras minúsculas',
'Passwords will be encrypted if present' => 'Senhas serão encriptadas, se presentes',
'%s attached a new file to the task %s' => '%s anexou um novo arquivo a tarefa %s',
+ 'Link type' => 'Tipo de link',
'Assign automatically a category based on a link' => 'Atribuir automaticamente uma categoria baseada num link',
'BAM - Konvertible Mark' => 'BAM - Mark conversível',
'Assignee Username' => 'Usuário designado',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Fechar uma tarefa sem atividade',
'Duration in days' => 'Duração em dias',
'Send email when there is no activity on a task' => 'Enviar um e-mail quando não há nenhuma atividade em uma tarefa',
- 'List of external links' => 'Lista dos links externos',
'Unable to fetch link information.' => 'Não foi possível obter informações sobre o link.',
'Daily background job for tasks' => 'Tarefa agendada diariamente para as tarefas',
'Auto' => 'Auto',
@@ -1061,94 +1050,121 @@ return array(
'Add external link' => 'Adicionar um link externo',
'Type' => 'Tipo',
'Dependency' => 'Dependência',
- // 'Add internal link' => '',
- // 'Add a new external link' => '',
- // 'Edit external link' => '',
- // 'External link' => '',
- // 'Copy and paste your link here...' => '',
- // 'URL' => '',
- // 'There is no external link for the moment.' => '',
- // 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
- // 'Assign to me' => '',
- // 'Me' => '',
- // 'Do not duplicate anything' => '',
- // 'Projects management' => '',
- // 'Users management' => '',
- // 'Groups management' => '',
- // 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
- // 'open' => '',
- // 'closed' => '',
- // 'Priority:' => '',
- // 'Reference:' => '',
- // 'Complexity:' => '',
- // 'Swimlane:' => '',
- // 'Column:' => '',
- // 'Position:' => '',
- // 'Creator:' => '',
- // 'Time estimated:' => '',
- // '%s hours' => '',
- // 'Time spent:' => '',
- // 'Created:' => '',
- // 'Modified:' => '',
- // 'Completed:' => '',
- // 'Started:' => '',
- // 'Moved:' => '',
- // 'Task #%d' => '',
- // 'Sub-tasks' => '',
- // 'Date and time format' => '',
- // 'Time format' => '',
- // 'Start date: ' => '',
- // 'End date: ' => '',
- // 'New due date: ' => '',
- // 'Start date changed: ' => '',
- // 'Disable private projects' => '',
- // 'Do you really want to remove this custom filter: "%s"?' => '',
- // 'Remove a custom filter' => '',
- // 'User activated successfully.' => '',
- // 'Unable to enable this user.' => '',
- // 'User disabled successfully.' => '',
- // 'Unable to disable this user.' => '',
- // 'All files have been uploaded successfully.' => '',
- // 'View uploaded files' => '',
- // 'The maximum allowed file size is %sB.' => '',
- // 'Choose files again' => '',
- // 'Drag and drop your files here' => '',
- // 'choose files' => '',
- // 'View profile' => '',
- // 'Two Factor' => '',
- // 'Disable user' => '',
- // 'Do you really want to disable this user: "%s"?' => '',
- // 'Enable user' => '',
- // 'Do you really want to enable this user: "%s"?' => '',
- // 'Download' => '',
- // 'Uploaded: %s' => '',
- // 'Size: %s' => '',
- // 'Uploaded by %s' => '',
- // 'Filename' => '',
- // 'Size' => '',
- // 'Column created successfully.' => '',
- // 'Another column with the same name exists in the project' => '',
- // 'Default filters' => '',
- // 'Your board doesn\'t have any column!' => '',
- // 'Change column position' => '',
- // 'Switch to the project overview' => '',
- // 'User filters' => '',
- // 'Category filters' => '',
- // 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
- // 'View file' => '',
- // 'Last activity' => '',
- // 'Change subtask position' => '',
- // 'This value must be greater than %d' => '',
- // 'Another swimlane with the same name exists in the project' => '',
- // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '',
- // 'Actions duplicated successfully.' => '',
- // 'Unable to duplicate actions.' => '',
- // 'Add a new action' => '',
- // 'Import from another project' => '',
- // 'There is no action at the moment.' => '',
- // 'Import actions from another project' => '',
- // 'There is no available project.' => '',
+ 'Add internal link' => 'Adicionar um link interno',
+ 'Add a new external link' => 'Adicionar um novo link externo',
+ 'Edit external link' => 'Editar um link externo',
+ 'External link' => 'Link externo',
+ 'Copy and paste your link here...' => 'Copie e cole o link aqui...',
+ 'URL' => 'URL',
+ 'Internal links' => 'Link interno',
+ 'Assign to me' => 'Atribuir-me',
+ 'Me' => 'Eu',
+ 'Do not duplicate anything' => 'Não duplique nada',
+ 'Projects management' => 'Gestão de projetos',
+ 'Users management' => 'Gestão dos usuários',
+ 'Groups management' => 'Gestão dos grupos',
+ 'Create from another project' => 'Criar a partir de outro projeto',
+ 'open' => 'aberto',
+ 'closed' => 'fechado',
+ 'Priority:' => 'Prioridade:',
+ 'Reference:' => 'Referência:',
+ 'Complexity:' => 'Complexidade:',
+ 'Swimlane:' => 'Swimlane:',
+ 'Column:' => 'Coluna:',
+ 'Position:' => 'Posição:',
+ 'Creator:' => 'Criador:',
+ 'Time estimated:' => 'Tempo estimado:',
+ '%s hours' => '%s horas',
+ 'Time spent:' => 'Tempo gasto:',
+ 'Created:' => 'Criado:',
+ 'Modified:' => 'Modificado:',
+ 'Completed:' => 'Completado:',
+ 'Started:' => 'Começado:',
+ 'Moved:' => 'Movido:',
+ 'Task #%d' => 'Tarefa #%d',
+ 'Date and time format' => 'Formato da hora e da data',
+ 'Time format' => 'Formato da hora',
+ 'Start date: ' => 'Data de início: ',
+ 'End date: ' => 'Data final: ',
+ 'New due date: ' => 'Nova data limite: ',
+ 'Start date changed: ' => 'Data de início alterada: ',
+ 'Disable private projects' => 'Desativar os projetos privados',
+ 'Do you really want to remove this custom filter: "%s"?' => 'Você realmente quer remover este filtro personalizado: "%s"?',
+ 'Remove a custom filter' => 'Remover um filtro personalizado',
+ 'User activated successfully.' => 'Usuário ativado com sucesso.',
+ 'Unable to enable this user.' => 'Impossível de ativar esse usuário.',
+ 'User disabled successfully.' => 'Usuário desactivado com sucesso.',
+ 'Unable to disable this user.' => 'Impossível de desativar esse usuário.',
+ 'All files have been uploaded successfully.' => 'Todos os arquivos foram enviados com sucesso.',
+ 'View uploaded files' => 'Ver os arquivos enviados',
+ 'The maximum allowed file size is %sB.' => 'O tamanho máximo dos arquivos é %sB.',
+ 'Choose files again' => 'Selecionar novamente arquivos',
+ 'Drag and drop your files here' => 'Arraste e solte os arquivos aqui',
+ 'choose files' => 'selecione os arquivos',
+ 'View profile' => 'Ver o perfil',
+ 'Two Factor' => 'Dois fatores',
+ 'Disable user' => 'Desativar o usuário',
+ 'Do you really want to disable this user: "%s"?' => 'Você realmente quer desativar este usuário: "%s"?',
+ 'Enable user' => 'Ativar um usuário',
+ 'Do you really want to enable this user: "%s"?' => 'Você realmente quer ativar este usuário: "%s"?',
+ 'Download' => 'Baixar',
+ 'Uploaded: %s' => 'Enviado: %s',
+ 'Size: %s' => 'Tamanho: %s',
+ 'Uploaded by %s' => 'Enviado por %s',
+ 'Filename' => 'Nome do arquivo',
+ 'Size' => 'Tamanho',
+ 'Column created successfully.' => 'A coluna criada com sucesso.',
+ 'Another column with the same name exists in the project' => 'Uma outra coluna com o mesmo nome já existe no projeto',
+ 'Default filters' => 'Filtros padrão',
+ 'Your board doesn\'t have any column!' => 'O seu painel não tem nenhuma coluna',
+ 'Change column position' => 'Alterar a posição da coluna',
+ 'Switch to the project overview' => 'Mudar para a vista geral do projeto',
+ 'User filters' => 'Filtros dos usuários',
+ 'Category filters' => 'Filtros das categorias',
+ 'Upload a file' => 'Enviar um arquivo',
+ 'View file' => 'Ver um arquivo',
+ 'Last activity' => 'Últimas atividades',
+ 'Change subtask position' => 'Alterar a posição da sub-tarefa',
+ 'This value must be greater than %d' => 'Este valor deve ser maior que %d',
+ 'Another swimlane with the same name exists in the project' => 'Outra Swimlane existe com o mesmo nome no projeto',
+ 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Exemplo: http://exemple.kanboard.net/ (usado para gerar URLs absolutos)',
+ 'Actions duplicated successfully.' => 'Ações duplicadas com sucesso.',
+ 'Unable to duplicate actions.' => 'Não foi possível duplicar as ações.',
+ 'Add a new action' => 'Adicionar uma nova ação',
+ 'Import from another project' => 'Importar a partir de outro projeto',
+ 'There is no action at the moment.' => 'Não há nenhuma ação actualmente.',
+ 'Import actions from another project' => 'Importar ações a partir de outro projeto',
+ 'There is no available project.' => 'Não há projetos disponíveis.',
+ 'Local File' => 'Arquivo local',
+ 'Configuration' => 'Configuração',
+ 'PHP version:' => 'Versão do PHP:',
+ 'PHP SAPI:' => 'PHP SAPI:',
+ 'OS version:' => 'Versão do sistema operacional:',
+ 'Database version:' => 'Versão do banco de dados:',
+ 'Browser:' => 'Browser:',
+ 'Task view' => 'Vista detalhada de uma tarefa',
+ 'Edit task' => 'Editar a tarefa',
+ 'Edit description' => 'Editar a descrição',
+ 'New internal link' => 'Novo link interno',
+ 'Display list of keyboard shortcuts' => 'Ver a lista dos atalhos de teclado',
+ 'Menu' => 'Menu',
+ 'Set start date' => 'Definir a data de início',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Enviar a minha imagem de avatar',
+ 'Remove my image' => 'Remover a minha imagem',
+ 'The OAuth2 state parameter is invalid' => 'O parâmetro "state" de OAuth2 não é válido',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 31a56882..aa51534b 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Todos os projectos',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
- 'Nobody assigned' => 'Ninguém assignado',
'Assigned to %s' => 'Designado para %s',
'Remove a column' => 'Remover uma coluna',
'Remove a column from a board' => 'Remover uma coluna do quadro',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Número de tarefas',
'User' => 'Utilizador',
'Comments' => 'Comentários',
- 'Write your text in Markdown' => 'Escreva o seu texto em Markdown',
'Leave a comment' => 'Deixe um comentário',
'Comment is required' => 'Comentário é obrigatório',
'Leave a description' => 'Deixe uma descrição',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Controle de tempo:',
'New sub-task' => 'Nova subtarefa',
'New attachment added "%s"' => 'Novo anexo adicionado "%s"',
- 'Comment updated' => 'Comentário actualizado',
'New comment posted by %s' => 'Novo comentário por %s',
'New attachment' => 'Novo anexo',
'New comment' => 'Novo comentário',
+ 'Comment updated' => 'Comentário actualizado',
'New subtask' => 'Nova subtarefa',
'Subtask updated' => 'Subtarefa alterada',
'Task updated' => 'Tarefa alterada',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceite, exemplo: "%s" e "%s"',
'New private project' => 'Novo projecto privado',
'This project is private' => 'Este projecto é privado',
- 'Type here to create a new sub-task' => 'Escreva aqui para criar uma nova subtarefa',
'Add' => 'Adicionar',
'Start date' => 'Data de início',
'Time estimated' => 'Tempo estimado',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Exportação diária do resumo do projecto para "%s"',
'Exports' => 'Exportar',
'This export contains the number of tasks per column grouped per day.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.',
- 'Nothing to preview...' => 'Nada para pré-visualizar...',
- 'Preview' => 'Pré-visualizar',
- 'Write' => 'Escrever',
'Active swimlanes' => 'Activar swimlanes',
'Add a new swimlane' => 'Adicionar novo swimlane',
'Change default swimlane' => 'Alterar swimlane padrão',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Idade da tarefa em dias',
'Days in this column' => 'Dias nesta coluna',
// '%dd' => '',
- 'Add a link' => 'Adicionar uma associação',
'Add a new link' => 'Adicionar uma nova associação',
'Do you really want to remove this link: "%s"?' => 'Tem a certeza que quer remover esta associação: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Tem a certeza que quer remover esta associação com a tarefa n°%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'O meu feed de actividade',
'My calendar' => 'A minha agenda',
'Search tasks' => 'Pesquisar tarefas',
- 'Back to the calendar' => 'Voltar ao calendário',
- 'Filters' => 'Filtros',
'Reset filters' => 'Redefinir os filtros',
'My tasks due tomorrow' => 'A minhas tarefas que expiram amanhã',
'Tasks due today' => 'Tarefas que expiram hoje',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data de fim:',
'There is no start date or end date for this project.' => 'Não existe data de inicio ou fim para este projecto.',
'Projects Gantt chart' => 'Gráfico de Gantt dos projectos',
- 'Link type' => 'Tipo de ligação',
'Change task color when using a specific task link' => 'Alterar cor da tarefa quando se usar um tipo especifico de ligação de tarefa',
'Task link creation or modification' => 'Criação ou modificação de ligação de tarefa',
'Milestone' => 'Objectivo',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Partilhado',
'Owner' => 'Dono',
'Unread notifications' => 'Notificações por ler',
- 'My filters' => 'Os meus filtros',
'Notification methods:' => 'Métodos de notificação:',
'Import tasks from CSV file' => 'Importar tarefas de um ficheiro CSV',
'Unable to read your file' => 'Não foi possivel ler o ficheiro',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Utilizadores tem de estar em letra pequena e ser unicos',
'Passwords will be encrypted if present' => 'Senhas serão encriptadas se presentes',
'%s attached a new file to the task %s' => '%s anexou um novo ficheiro à tarefa %s',
+ 'Link type' => 'Tipo de ligação',
'Assign automatically a category based on a link' => 'Assignar automáticamente a categoria baseada num link',
'BAM - Konvertible Mark' => 'BAM - Marca Conversível',
'Assignee Username' => 'Utilizador do Assignado',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Fechar tarefa quando não há actividade',
'Duration in days' => 'Duração em dias',
'Send email when there is no activity on a task' => 'Enviar email quando não há actividade numa tarefa',
- 'List of external links' => 'Lista de ligações externas',
'Unable to fetch link information.' => 'Impossivel obter informação da ligação.',
'Daily background job for tasks' => 'Trabalho diário em segundo plano para tarefas',
'Auto' => 'Auto',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Ligação externa',
'Copy and paste your link here...' => 'Copie e cole a sua ligação aqui...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'De momento não existe nenhuma ligação externa.',
'Internal links' => 'Ligações internas',
- 'There is no internal link for the moment.' => 'De momento não existe nenhuma ligação interna.',
'Assign to me' => 'Assignar a mim',
'Me' => 'Eu',
'Do not duplicate anything' => 'Não duplicar nada',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Gestão de utilizadores',
'Groups management' => 'Gestão de grupos',
'Create from another project' => 'Criar apartir de outro projecto',
- 'There is no subtask at the moment.' => 'De momento não existe sub-tarefa.',
'open' => 'aberto',
'closed' => 'fechado',
'Priority:' => 'Prioridade:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Iniciado:',
'Moved:' => 'Movido:',
'Task #%d' => 'Tarefa #%d',
- 'Sub-tasks' => 'Sub-tarefa',
'Date and time format' => 'Formato tempo e data',
'Time format' => 'Formato tempo',
'Start date: ' => 'Data inicio: ',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Filtros de utilizador',
'Category filters' => 'Filtros de categoria',
'Upload a file' => 'Enviar um ficheiro',
- 'There is no attachment at the moment.' => 'De momento não existe anexo.',
'View file' => 'Ver ficheiro',
'Last activity' => 'Ultima actividade',
'Change subtask position' => 'Mudar posição da sub-tarefa',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'De momento não existe acção.',
'Import actions from another project' => 'Importar acções de outro projecto',
'There is no available project.' => 'Não existe projecto disponivel.',
+ 'Local File' => 'Ficheiro Local',
+ 'Configuration' => 'Configuração',
+ 'PHP version:' => 'Versão PHP:',
+ 'PHP SAPI:' => 'SAPI PHP:',
+ 'OS version:' => 'Versão SO:',
+ 'Database version:' => 'Versão base de dados:',
+ 'Browser:' => 'Navegador:',
+ 'Task view' => 'Vista de Tarefas',
+ 'Edit task' => 'Editar tarefa',
+ 'Edit description' => 'Editar descrição',
+ 'New internal link' => 'Nova ligação interna',
+ 'Display list of keyboard shortcuts' => 'Mostrar lista de atalhos do teclado',
+ 'Menu' => 'Menu',
+ 'Set start date' => 'Definir data de inicio',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Enviar a minha imagem de avatar',
+ 'Remove my image' => 'Remover a minha imagem',
+ 'The OAuth2 state parameter is invalid' => 'O parametro de estado do OAuth2 é inválido',
+ 'User not found.' => 'Utilizador não encontrado.',
+ 'Search in activity stream' => 'Procurar no fluxo de atividade',
+ 'My activities' => 'Minhas actividades',
+ 'Activity until yesterday' => 'Actividade até ontem',
+ 'Activity until today' => 'Actividade até hoje',
+ 'Search by creator: ' => 'Procurar por criador: ',
+ 'Search by creation date: ' => 'Procurar por data de criação: ',
+ 'Search by task status: ' => 'Procurar por estado da tarefa: ',
+ 'Search by task title: ' => 'Procurar por titulo da tarefa: ',
+ 'Activity stream search' => 'Procurar fluxo de actividade',
+ 'Projects where "%s" is manager' => 'Projectos onde "%s" é gestor',
+ 'Projects where "%s" is member' => 'Projectos onde "%s" é membro',
+ 'Open tasks assigned to "%s"' => 'Tarefas abertas assignadas a "%s"',
+ 'Closed tasks assigned to "%s"' => 'Tarefas fechadas assignadas a "%s"',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 74f1deb8..bf2bc559 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Все проекты',
'Add a new column' => 'Добавить новую колонку',
'Title' => 'Название',
- 'Nobody assigned' => 'Никто не назначен',
'Assigned to %s' => 'Назначено %s',
'Remove a column' => 'Удалить колонку',
'Remove a column from a board' => 'Удалить колонку с доски',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Количество задач',
'User' => 'Пользователь',
'Comments' => 'Комментарии',
- 'Write your text in Markdown' => 'Справка по синтаксису Markdown',
'Leave a comment' => 'Оставить комментарий',
'Comment is required' => 'Нужен комментарий',
'Leave a description' => 'Напишите описание',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Отслеживание времени:',
'New sub-task' => 'Новая подзадача',
'New attachment added "%s"' => 'Добавлено вложение « %s »',
- 'Comment updated' => 'Комментарий обновлен',
'New comment posted by %s' => 'Новый комментарий написан « %s »',
'New attachment' => 'Новое вложение',
'New comment' => 'Новый комментарий',
+ 'Comment updated' => 'Комментарий обновлен',
'New subtask' => 'Новая подзадача',
'Subtask updated' => 'Подзадача обновлена',
'Task updated' => 'Задача обновлена',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Время должно быть в ISO-формате, например: "%s" или "%s"',
'New private project' => 'Новый проект с ограниченным доступом',
'This project is private' => 'Это проект с ограниченным доступом',
- 'Type here to create a new sub-task' => 'Печатайте сюда чтобы создать подзадачу',
'Add' => 'Добавить',
'Start date' => 'Дата начала',
'Time estimated' => 'Запланировано',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Экспорт ежедневного резюме проекта "%s"',
'Exports' => 'Экспорт',
'This export contains the number of tasks per column grouped per day.' => 'Этот экспорт содержит ряд задач в колонках, сгруппированные по дням.',
- 'Nothing to preview...' => 'Нет данных для предпросмотра...',
- 'Preview' => 'Предпросмотр',
- 'Write' => 'Написание',
'Active swimlanes' => 'Активные дорожки',
'Add a new swimlane' => 'Добавить новую дорожку',
'Change default swimlane' => 'Сменить стандартную дорожку',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Возраст задачи в днях',
'Days in this column' => 'Дней в этой колонке',
'%dd' => '%dd',
- 'Add a link' => 'Добавить ссылку на другие задачи',
'Add a new link' => 'Добавление новой ссылки',
'Do you really want to remove this link: "%s"?' => 'Вы уверены что хотите удалить ссылку: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Вы уверены что хотите удалить ссылку вместе с задачей #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Лента моей активности',
'My calendar' => 'Мой календарь',
'Search tasks' => 'Поиск задачи',
- 'Back to the calendar' => 'Вернуться в календарь',
- 'Filters' => 'Фильтры',
'Reset filters' => 'Сбросить фильтры',
'My tasks due tomorrow' => 'Мои задачи на завтра',
'Tasks due today' => 'Задачи, завершающиеся сегодня',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Дата завершения:',
'There is no start date or end date for this project.' => 'В проекте не указаны дата начала или завершения.',
'Projects Gantt chart' => 'Диаграмма Ганта проектов',
- 'Link type' => 'Тип ссылки',
'Change task color when using a specific task link' => 'Изменение цвета задач при использовании ссылки на определенные задачи',
'Task link creation or modification' => 'Ссылка на создание или модификацию задачи',
'Milestone' => 'Веха',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Общие',
'Owner' => 'Владелец',
'Unread notifications' => 'Непрочитанные уведомления',
- 'My filters' => 'Мои фильтры',
'Notification methods:' => 'Способы уведомления:',
'Import tasks from CSV file' => 'Импорт задач из CSV-файла',
'Unable to read your file' => 'Невозможно прочитать файл',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Логины пользователей должны быть строчными и уникальными',
'Passwords will be encrypted if present' => 'Пароли будут зашифрованы (если указаны)',
'%s attached a new file to the task %s' => '%s добавил новый файл к задаче %s',
+ 'Link type' => 'Тип ссылки',
'Assign automatically a category based on a link' => 'Автоматически назначать категории на основе ссылки',
'BAM - Konvertible Mark' => 'BAM - Конвертируемая марка',
'Assignee Username' => 'Логин назначенного',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Закрывать задачу, когда нет активности',
'Duration in days' => 'Длительность в днях',
'Send email when there is no activity on a task' => 'Отправлять email, когда активность по задаче отсутствует',
- 'List of external links' => 'Список внешних ссылок',
'Unable to fetch link information.' => 'Не удалось получить информацию о ссылке',
'Daily background job for tasks' => 'Ежедневные фоновые работы для задач',
'Auto' => 'Авто',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Внешняя ссылка',
'Copy and paste your link here...' => 'Скопируйте и вставьте вашу ссылку здесь',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'На данный момент внешние ссылки отсутствуют',
'Internal links' => 'Внутренние ссылки',
- 'There is no internal link for the moment.' => 'На данные момент внутреннии ссылки отсутствуют',
'Assign to me' => 'Связать со мной',
'Me' => 'Мне',
'Do not duplicate anything' => 'Не дублировать ничего',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Управление пользователями',
'Groups management' => 'Управление группами',
'Create from another project' => 'Создать из другого проекта',
- 'There is no subtask at the moment.' => 'На данный момент подзадачи отсутствуют',
'open' => 'открыто',
'closed' => 'закрыто',
'Priority:' => 'Приоритет:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Начата:',
'Moved:' => 'Перемещена:',
'Task #%d' => 'Задача #%d',
- 'Sub-tasks' => 'Подзадачи',
'Date and time format' => 'Формат даты и времени',
'Time format' => 'Формат времени',
'Start date: ' => 'Дата начала:',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Фильтры по пользователям',
'Category filters' => 'Фильтры по категориям',
'Upload a file' => 'Загрузить файл',
- 'There is no attachment at the moment.' => 'Вложения на данный момент отсутствуют',
'View file' => 'Просмотр файла',
'Last activity' => 'Последняя активность',
'Change subtask position' => 'Смена позиции подзадачи',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Действия на данный момент отсутствуют',
'Import actions from another project' => 'Импорт действий из другого проекта',
'There is no available project.' => 'Нет доступного проекта',
+ 'Local File' => 'Локальный файл',
+ 'Configuration' => 'Конфигурация',
+ 'PHP version:' => 'Версия PHP:',
+ 'PHP SAPI:' => 'PHP SAPI:',
+ 'OS version:' => 'Версия ОС:',
+ 'Database version:' => 'Версия БД:',
+ 'Browser:' => 'Браузер:',
+ 'Task view' => 'Просмотр задачи',
+ 'Edit task' => 'Изменение задачи',
+ 'Edit description' => 'Изменение описания',
+ 'New internal link' => 'Новая внутренняя ссылка',
+ 'Display list of keyboard shortcuts' => 'Показать список клавиатурных сокращений',
+ 'Menu' => 'Меню',
+ 'Set start date' => 'Установить дату начала',
+ 'Avatar' => 'Аватар',
+ 'Upload my avatar image' => 'Загрузить моё изображение для аватара',
+ 'Remove my image' => 'Удалить моё изображение',
+ 'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index e63a106c..0399530e 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Svi projekti',
'Add a new column' => 'Dodaj novu kolonu',
'Title' => 'Naslov',
- 'Nobody assigned' => 'Niko nije dodeljen',
'Assigned to %s' => 'Dodeljen korisniku %s',
'Remove a column' => 'Ukloni kolonu',
'Remove a column from a board' => 'Ukloni kolonu sa table',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Broj zadataka',
'User' => 'Korisnik',
'Comments' => 'Komentari',
- 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown',
'Leave a comment' => 'Ostavi komentar',
'Comment is required' => 'Komentar je obavezan',
'Leave a description' => 'Dodaj opis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Praćenje vremena: ',
'New sub-task' => 'Novi Pod-zadatak',
'New attachment added "%s"' => 'Novi prilog ubačen "%s"',
- 'Comment updated' => 'Komentar izmenjen',
'New comment posted by %s' => 'Novi komentar ostavio %s',
// 'New attachment' => '',
// 'New comment' => '',
+ 'Comment updated' => 'Komentar izmenjen',
// 'New subtask' => '',
// 'Subtask updated' => '',
// 'Task updated' => '',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek prihvatljiv, primer: "%s", "%s"',
'New private project' => 'Novi privatni projekat',
'This project is private' => 'Ovaj projekat je privatan',
- 'Type here to create a new sub-task' => 'Kucaj ovde za kreiranje novog pod-zadatka',
'Add' => 'Dodaj',
'Start date' => 'Datum početka',
'Time estimated' => 'Procenjeno vreme',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Izvoz zbirnig pregleda po danima za "%s"',
'Exports' => 'Izvoz',
// 'This export contains the number of tasks per column grouped per day.' => '',
- 'Nothing to preview...' => 'Ništa za prikazivanje...',
- 'Preview' => 'Pregled',
- 'Write' => 'Piši',
'Active swimlanes' => 'Aktivni razdelnik',
'Add a new swimlane' => 'Dodaj razdelnik',
'Change default swimlane' => 'Zameni osnovni razdelnik',
@@ -539,7 +533,6 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
- 'Add a link' => 'Dodaj link',
// 'Add a new link' => '',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 26952355..7e738e70 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alla projekt',
'Add a new column' => 'Lägg till ny kolumn',
'Title' => 'Titel',
- 'Nobody assigned' => 'Ingen tilldelad',
'Assigned to %s' => 'Tilldelad %s',
'Remove a column' => 'Ta bort en kolumn',
'Remove a column from a board' => 'Ta bort en kolumn från tavlan',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Antal uppgifter',
'User' => 'Användare',
'Comments' => 'Kommentarer',
- 'Write your text in Markdown' => 'Exempelsyntax för text',
'Leave a comment' => 'Lämna en kommentar',
'Comment is required' => 'En kommentar måste lämnas',
'Leave a description' => 'Lämna en beskrivning',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tidsspårning',
'New sub-task' => 'Ny deluppgift',
'New attachment added "%s"' => 'Ny bifogning tillagd "%s"',
- 'Comment updated' => 'Kommentaren har uppdaterats',
'New comment posted by %s' => 'Ny kommentar postad av %s',
'New attachment' => 'Ny bifogning',
'New comment' => 'Ny kommentar',
+ 'Comment updated' => 'Kommentaren har uppdaterats',
'New subtask' => 'Ny deluppgift',
'Subtask updated' => 'Deluppgiften har uppdaterats',
'Task updated' => 'Uppgiften har uppdaterats',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-format är alltid tillåtet, exempel: "%s" och "%s"',
'New private project' => 'Nytt privat projekt',
'This project is private' => 'Det här projektet är privat',
- 'Type here to create a new sub-task' => 'Skriv här för att skapa en ny deluppgift',
'Add' => 'Lägg till',
'Start date' => 'Startdatum',
'Time estimated' => 'Uppskattad tid',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export av daglig projektsummering för "%s"',
'Exports' => 'Exporter',
'This export contains the number of tasks per column grouped per day.' => 'Denna export innehåller antalet uppgifter per kolumn grupperade per dag.',
- 'Nothing to preview...' => 'Inget att förhandsgrandska...',
- 'Preview' => 'Förhandsgranska',
- 'Write' => 'Skriva',
'Active swimlanes' => 'Aktiva swimlanes',
'Add a new swimlane' => 'Lägg till en nytt swimlane',
'Change default swimlane' => 'Ändra standard swimlane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Uppgiftsålder i dagar',
'Days in this column' => 'Dagar i denna kolumn',
'%dd' => '%dd',
- 'Add a link' => 'Lägg till länk',
'Add a new link' => 'Lägg till ny länk',
'Do you really want to remove this link: "%s"?' => 'Vill du verkligen ta bort länken: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Vill du verkligen ta bort länken till uppgiften #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Min aktivitetsström',
'My calendar' => 'Min kalender',
'Search tasks' => 'Sök uppgifter',
- 'Back to the calendar' => 'Tillbaka till kalendern',
- 'Filters' => 'Filter',
'Reset filters' => 'Återställ filter',
'My tasks due tomorrow' => 'Mina uppgifter förfaller imorgon',
'Tasks due today' => 'Uppgifter förfaller idag',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 12f374b6..6765e8ea 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'โปรเจคทั้งหมด',
'Add a new column' => 'เพิ่มคอลัมน์ใหม่',
'Title' => 'หัวเรื่อง',
- 'Nobody assigned' => 'ไม่กำหนดใคร',
'Assigned to %s' => 'กำหนดให้ %s',
'Remove a column' => 'ลบคอลัมน์',
'Remove a column from a board' => 'ลบคอลัมน์ออกจากบอร์ด',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'นับงาน',
'User' => 'ผู้ใช้',
'Comments' => 'ความคิดเห็น',
- 'Write your text in Markdown' => 'เขียนข้อความในรูปแบบ Markdown',
'Leave a comment' => 'ออกความคิดเห็น',
'Comment is required' => 'ต้องการความคิดเห็น',
'Leave a description' => 'แสดงคำอธิบาย',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'การติดตามเวลา:',
'New sub-task' => 'งานย่อยใหม่',
'New attachment added "%s"' => 'เพิ่มการแนบใหม่ "%s"',
- 'Comment updated' => 'ปรับปรุงความคิดเห็น',
'New comment posted by %s' => 'ความคิดเห็นใหม่จาก %s',
'New attachment' => 'การแนบใหม่',
'New comment' => 'ความคิดเห็นใหม่',
+ 'Comment updated' => 'ปรับปรุงความคิดเห็น',
'New subtask' => 'งานย่อยใหม่',
'Subtask updated' => 'ปรับปรุงงานย่อยแล้ว',
'Task updated' => 'ปรับปรุงงานแล้ว',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ยอมรับรูปแบบ ISO ตัวอย่าง: "%s" และ "%s"',
'New private project' => 'เพิ่มโปรเจคส่วนตัวใหม่',
'This project is private' => 'โปรเจคนี้เป็นโปรเจคส่วนตัว',
- 'Type here to create a new sub-task' => 'พิมพ์ที่นี้เพื่อสร้างงานย่อยใหม่',
'Add' => 'เพิ่ม',
'Start date' => 'เริ่มวันที่',
'Time estimated' => 'เวลาโดยประมาณ',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'ส่งออกสรุปโปรเจครายวันสำหรับ "%s"',
'Exports' => 'ส่งออก',
'This export contains the number of tasks per column grouped per day.' => 'การส่งออกนี้เป็นการนับจำนวนงานในแต่ละคอลัมน์ในแต่ละวัน',
- 'Nothing to preview...' => 'ไม่มีพรีวิว...',
- 'Preview' => 'พรีวิว',
- 'Write' => 'เขียน',
'Active swimlanes' => 'สวิมเลนพร้อมใช้งาน',
'Add a new swimlane' => 'เพิ่มสวิมเลนใหม่',
'Change default swimlane' => 'เปลี่ยนสวิมเลนเริ่มต้น',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'อายุงาน',
'Days in this column' => 'วันในคอลัมน์นี้',
'%dd' => '%d วัน',
- 'Add a link' => 'เพิ่มลิงค์',
'Add a new link' => 'เพิ่มลิงค์ใหม่',
'Do you really want to remove this link: "%s"?' => 'คุณต้องการลบลิงค์นี้: "%s"?',
'Do you really want to remove this link with task #%d?' => 'คุณต้องการลบลิงค์นี้ของงาน #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'กิจกรรมที่เกิดขึ้นของฉัน',
'My calendar' => 'ปฎิทินของฉัน',
'Search tasks' => 'ค้นหางาน',
- 'Back to the calendar' => 'กลับไปที่ปฎิทิน',
- 'Filters' => 'ตัวกรอง',
'Reset filters' => 'ล้างตัวกรอง',
'My tasks due tomorrow' => 'งานถึงกำหนดของฉันวันพรุ่งนี้',
'Tasks due today' => 'งานถึงกำหนดวันนี้',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'วันที่จบ:',
'There is no start date or end date for this project.' => 'ไม่มีวันที่เริ่มหรือวันที่จบของโปรเจคนี้',
'Projects Gantt chart' => 'แผนภูมิแกรน์ของโปรเจค',
- 'Link type' => 'ประเภทลิงค์',
'Change task color when using a specific task link' => 'เปลี่ยนสีงานเมื่อมีการใช้การเชื่อมโยงงาน',
'Task link creation or modification' => 'การสร้างการเชื่อมโยงงานหรือการปรับเปลี่ยน',
'Milestone' => 'ขั้น',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'แชร์',
'Owner' => 'เจ้าของ',
'Unread notifications' => 'การแจ้งเตือนยังไม่ได้อ่าน',
- 'My filters' => 'ตัวกรองของฉัน',
'Notification methods:' => 'ลักษณะการแจ้งเตือน:',
'Import tasks from CSV file' => 'นำเข้างานจากไฟล์ CSV',
'Unable to read your file' => 'ไม่สามารถอ่านไฟล์ของคุณ',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'ชื่อผู้ใช้ต้องเป็นตัวพิมพ์เล็กและไม่ซ้ำ',
// 'Passwords will be encrypted if present' => '',
'%s attached a new file to the task %s' => '%s แนบไฟล์ใหม่ในงาน %s',
+ 'Link type' => 'ประเภทลิงค์',
'Assign automatically a category based on a link' => 'กำหนดหมวดอัตโนมัติตามลิงค์',
// 'BAM - Konvertible Mark' => '',
'Assignee Username' => 'กำหนดชื่อผู้ใช้',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'ปิดงานเมื่อไม่มีกิจกกรมเกิดขึ้น',
'Duration in days' => 'ระยะเวลาวันที่',
'Send email when there is no activity on a task' => 'ส่งอีเมลเมื่อไม่มีกิจกรรมเกิดขึ้นในงาน',
- 'List of external links' => 'รายการเชื่อมโยงภายนอก',
'Unable to fetch link information.' => 'ไม่สามารถดึงข้อมูลการเชื่อมโยง',
// 'Daily background job for tasks' => '',
'Auto' => 'อัตโนมัติ',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'เชื่อมโยงภายนอก',
'Copy and paste your link here...' => 'คัดลอกและวางลิงค์ของคุณที่นี้...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายนอก',
'Internal links' => 'เชื่อมโยงภายใน',
- 'There is no internal link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายใน',
'Assign to me' => 'ฉันรับผิดชอบ',
'Me' => 'ฉัน',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'การจัดการผู้ใช้',
'Groups management' => 'การจัดการกลุ่ม',
'Create from another project' => 'สร้างโปรเจคอื่น',
- 'There is no subtask at the moment.' => 'ขณะนี้ไม่มีงานย่อย',
'open' => 'เปิด',
'closed' => 'ปิด',
'Priority:' => 'ความสำคัญ:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'เริ่ม:',
'Moved:' => 'ย้าย:',
'Task #%d' => 'งานที่ #%d',
- 'Sub-tasks' => 'งานย่อย',
'Date and time format' => 'รูปแบบของวันเวลา',
'Time format' => 'รูปแบบของเวลา',
'Start date: ' => 'เริ่มวันที่:',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 2dc611e4..f771b106 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Tüm projeler',
'Add a new column' => 'Yeni sütun ekle',
'Title' => 'Başlık',
- 'Nobody assigned' => 'Kullanıcı atanmamış',
'Assigned to %s' => '%s kullanıcısına atanmış',
'Remove a column' => 'Bir sütunu sil',
'Remove a column from a board' => 'Tablodan bir sütunu sil',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Görev sayısı',
'User' => 'Kullanıcı',
'Comments' => 'Yorumlar',
- 'Write your text in Markdown' => 'Yazınızı Markdown ile yazın',
'Leave a comment' => 'Bir yorum ekle',
'Comment is required' => 'Yorum gerekli',
'Leave a description' => 'Açıklama ekleyin',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Zaman takibi',
'New sub-task' => 'Yeni alt görev',
'New attachment added "%s"' => 'Yeni dosya "%s" eklendi.',
- 'Comment updated' => 'Yorum güncellendi',
'New comment posted by %s' => '%s tarafından yeni yorum eklendi',
'New attachment' => 'Yeni dosya eki',
'New comment' => 'Yeni yorum',
+ 'Comment updated' => 'Yorum güncellendi',
'New subtask' => 'Yeni alt görev',
'Subtask updated' => 'Alt görev güncellendi',
'Task updated' => 'Görev güncellendi',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"',
'New private project' => 'Yeni özel proje',
'This project is private' => 'Bu proje özel',
- 'Type here to create a new sub-task' => 'Yeni bir alt görev oluşturmak için buraya yazın',
'Add' => 'Ekle',
'Start date' => 'Başlangıç tarihi',
'Time estimated' => 'Tahmini süre',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => '"%s" için günlük proje özetinin dışa',
'Exports' => 'Dışa aktarımlar',
'This export contains the number of tasks per column grouped per day.' => 'Bu dışa aktarım günlük gruplanmış olarak her sütundaki görev sayısını içerir.',
- 'Nothing to preview...' => 'Önizleme yapılacak bir şey yok ...',
- 'Preview' => 'Önizleme',
- 'Write' => 'Değiştir',
'Active swimlanes' => 'Aktif Kulvar',
'Add a new swimlane' => 'Yeni bir Kulvar ekle',
'Change default swimlane' => 'Varsayılan Kulvarı değiştir',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Görev yaşı gün olarak',
'Days in this column' => 'Bu sütunda geçirilen gün',
'%dd' => '%dG',
- 'Add a link' => 'Link ekle',
'Add a new link' => 'Yeni link ekle',
'Do you really want to remove this link: "%s"?' => '"%s" linkini gerçekten silmek istiyor musunuz?',
'Do you really want to remove this link with task #%d?' => '#%d numaralı görev ile linki gerçekten silmek istiyor musunuz?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Olay akışım',
'My calendar' => 'Takvimim',
'Search tasks' => 'Görevleri ara',
- 'Back to the calendar' => 'Takvime geri dön',
- 'Filters' => 'Filtreler',
'Reset filters' => 'Filtreleri sıfırla',
'My tasks due tomorrow' => 'Yarına tamamlanması gereken görevlerim',
'Tasks due today' => 'Bugün tamamlanması gereken görevler',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Bitiş tarihi:',
'There is no start date or end date for this project.' => 'Bu proje için başlangıç veya bitiş tarihi yok.',
'Projects Gantt chart' => 'Projeler Gantt diyagramı',
- 'Link type' => 'Bağlantı türü',
'Change task color when using a specific task link' => 'Belirli bir görev bağlantısı kullanıldığında görevin rengini değiştir',
'Task link creation or modification' => 'Görev bağlantısı oluşturulması veya değiştirilmesi',
'Milestone' => 'Kilometre taşı',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Paylaşılan',
'Owner' => 'Sahibi',
'Unread notifications' => 'Okunmamış bildirimler',
- 'My filters' => 'Filtrelerim',
'Notification methods:' => 'Bildirim yöntemleri:',
'Import tasks from CSV file' => 'CSV dosyasından görevleri içeri aktar',
'Unable to read your file' => 'Dosya okunamıyor',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Kullanıcı adları küçük harf ve tekil olmalı',
'Passwords will be encrypted if present' => 'Şifreler (eğer varsa) kriptolanır',
'%s attached a new file to the task %s' => '%s, %s görevine yeni dosya ekledi',
+ 'Link type' => 'Bağlantı türü',
'Assign automatically a category based on a link' => 'Bir bağlantıya göre otomatik olarak kategori ata',
'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
'Assignee Username' => 'Atanan kullanıcı adı',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index dadedc5b..baa7693a 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => '所有项目',
'Add a new column' => '添加新栏目',
'Title' => '标题',
- 'Nobody assigned' => '无人被指派',
'Assigned to %s' => '指派给 %s',
'Remove a column' => '移除一个栏目',
'Remove a column from a board' => '从看板移除一个栏目',
@@ -166,7 +165,6 @@ return array(
'Task count' => '任务数',
'User' => '用户',
'Comments' => '评论',
- 'Write your text in Markdown' => '用Markdown格式编写',
'Leave a comment' => '留言',
'Comment is required' => '必须得有评论',
'Leave a description' => '给一个描述',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => '时间记录',
'New sub-task' => '新建子任务',
'New attachment added "%s"' => '新附件已添加"%s"',
- 'Comment updated' => '更新了评论',
'New comment posted by %s' => '%s 的新评论',
'New attachment' => '新建附件',
'New comment' => '新建评论',
+ 'Comment updated' => '更新了评论',
'New subtask' => '新建子任务',
'Subtask updated' => '子任务更新',
'Task updated' => '任务更新',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO 格式总是允许的,例如:"%s" 和 "%s"',
'New private project' => '新建私有项目',
'This project is private' => '此项目为私有项目',
- 'Type here to create a new sub-task' => '要创建新的子任务,请在此输入',
'Add' => '添加',
'Start date' => '启动日期',
'Time estimated' => '预计时间',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => '导出项目"%s"的每日汇总',
'Exports' => '导出',
'This export contains the number of tasks per column grouped per day.' => '此导出包含每列的任务数,按天分组',
- 'Nothing to preview...' => '没有需要预览的内容',
- 'Preview' => '预览',
- 'Write' => '书写',
'Active swimlanes' => '活动里程碑',
'Add a new swimlane' => '添加新里程碑',
'Change default swimlane' => '修改默认里程碑',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => '任务存在天数',
'Days in this column' => '在此栏目的天数',
'%dd' => '%d天',
- 'Add a link' => '添加一个关联',
'Add a new link' => '添加一个新关联',
'Do you really want to remove this link: "%s"?' => '确认要删除此关联吗:"%s"?',
'Do you really want to remove this link with task #%d?' => '确认要删除到任务 #%d 的关联吗?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => '我的活动流',
'My calendar' => '我的日程表',
'Search tasks' => '搜索任务',
- 'Back to the calendar' => '返回日程',
- 'Filters' => '过滤器',
'Reset filters' => '重置过滤器',
'My tasks due tomorrow' => '我的明天到期的任务',
'Tasks due today' => '今天到期的任务',
@@ -850,7 +841,6 @@ return array(
'End date:' => '结束日期',
'There is no start date or end date for this project.' => '当前项目没有开始或结束日期',
'Projects Gantt chart' => '项目甘特图',
- 'Link type' => '关联类型',
'Change task color when using a specific task link' => '当任务关联到指定任务时改变颜色',
'Task link creation or modification' => '任务链接创建或更新时间',
'Milestone' => '里程碑',
@@ -902,7 +892,6 @@ return array(
'Shared' => '共享',
'Owner' => '所有人',
'Unread notifications' => '未读通知',
- 'My filters' => '我的过滤器',
'Notification methods:' => '通知提醒方式:',
'Import tasks from CSV file' => '从CSV文件导入任务',
'Unable to read your file' => '无法读取文件',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => '用户名必须小写且唯一',
'Passwords will be encrypted if present' => '密码将被加密',
'%s attached a new file to the task %s' => '"%s"添加了附件到任务"%s"',
+ 'Link type' => '关联类型',
'Assign automatically a category based on a link' => '基于链接自动关联分类',
'BAM - Konvertible Mark' => '波斯尼亚马克',
'Assignee Username' => '指派用户名',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => '当任务没有活动记录时关闭任务',
'Duration in days' => '持续天数',
'Send email when there is no activity on a task' => '当任务没有活动记录时发送邮件',
- 'List of external links' => '列出外部关联',
'Unable to fetch link information.' => '无法获取关联信息',
'Daily background job for tasks' => '每日后台任务',
'Auto' => '自动',
@@ -1067,9 +1056,7 @@ return array(
'External link' => '外部关联',
'Copy and paste your link here...' => '复制并粘贴链接到当前位置...',
'URL' => 'URL',
- 'There is no external link for the moment.' => '当前没有外部关联。',
'Internal links' => '内部关联',
- 'There is no internal link for the moment.' => '当前没有内部关联。',
'Assign to me' => '指派给我',
'Me' => '我',
'Do not duplicate anything' => '不再重复',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => '用户管理',
'Groups management' => '用户组管理',
'Create from another project' => '从另一个项目中创建',
- 'There is no subtask at the moment.' => '当前没有子任务。',
'open' => '打开',
'closed' => '已关闭',
'Priority:' => '优先级:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => '已开始:',
'Moved:' => '已移走',
'Task #%d' => '任务#%d',
- 'Sub-tasks' => '子任务',
'Date and time format' => '时间和日期格式',
'Time format' => '时间格式',
'Start date: ' => '开始时间:',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => '用户过滤器',
'Category filters' => '分类过滤器',
'Upload a file' => '上传文件',
- 'There is no attachment at the moment.' => '当前没有附件。',
'View file' => '查看文件',
'Last activity' => '最后活动',
'Change subtask position' => '更改子任务位置',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => '当前没有动作。',
'Import actions from another project' => '从另一个项目中导入动作',
'There is no available project.' => '当前没有可用项目',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Model/AvatarFile.php b/app/Model/AvatarFile.php
new file mode 100644
index 00000000..c49f9fd5
--- /dev/null
+++ b/app/Model/AvatarFile.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Exception;
+
+/**
+ * Avatar File
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class AvatarFile extends Base
+{
+ /**
+ * Path prefix
+ *
+ * @var string
+ */
+ const PATH_PREFIX = 'avatars';
+
+ /**
+ * Get image filename
+ *
+ * @access public
+ * @param integer $user_id
+ * @return string
+ */
+ public function getFilename($user_id)
+ {
+ return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path');
+ }
+
+ /**
+ * Add avatar in the user profile
+ *
+ * @access public
+ * @param integer $user_id Foreign key
+ * @param string $path Path on the disk
+ * @return bool
+ */
+ public function create($user_id, $path)
+ {
+ $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
+ 'avatar_path' => $path,
+ ));
+
+ $this->userSession->refresh($user_id);
+
+ return $result;
+ }
+
+ /**
+ * Remove avatar from the user profile
+ *
+ * @access public
+ * @param integer $user_id Foreign key
+ * @return bool
+ */
+ public function remove($user_id)
+ {
+ try {
+ $this->objectStorage->remove($this->getFilename($user_id));
+ $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('avatar_path' => ''));
+ $this->userSession->refresh($user_id);
+ return $result;
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Upload avatar image
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $file
+ * @return boolean
+ */
+ public function uploadFile($user_id, array $file)
+ {
+ try {
+ if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) {
+ $destination_filename = $this->generatePath($user_id, $file['name']);
+ $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename);
+ $this->create($user_id, $destination_filename);
+ } else {
+ throw new Exception('File not uploaded: '.var_export($file['error'], true));
+ }
+
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Generate the path for a new filename
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $filename
+ * @return string
+ */
+ public function generatePath($user_id, $filename)
+ {
+ return implode(DIRECTORY_SEPARATOR, array(self::PATH_PREFIX, $user_id, hash('sha1', $filename.time())));
+ }
+}
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 714b4308..a27560c8 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -31,28 +31,4 @@ abstract class Base extends \Kanboard\Core\Base
return (int) $db->getLastId();
});
}
-
- /**
- * Build SQL condition for a given time range
- *
- * @access protected
- * @param string $start_time Start timestamp
- * @param string $end_time End timestamp
- * @param string $start_column Start column name
- * @param string $end_column End column name
- * @return string
- */
- protected function getCalendarCondition($start_time, $end_time, $start_column, $end_column)
- {
- $start_column = $this->db->escapeIdentifier($start_column);
- $end_column = $this->db->escapeIdentifier($end_column);
-
- $conditions = array(
- "($start_column >= '$start_time' AND $start_column <= '$end_time')",
- "($start_column <= '$start_time' AND $end_column >= '$start_time')",
- "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
- );
-
- return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
- }
}
diff --git a/app/Model/Comment.php b/app/Model/Comment.php
index 6eb4a1e5..c5091d89 100644
--- a/app/Model/Comment.php
+++ b/app/Model/Comment.php
@@ -48,7 +48,8 @@ class Comment extends Base
self::TABLE.'.comment',
User::TABLE.'.username',
User::TABLE.'.name',
- User::TABLE.'.email'
+ User::TABLE.'.email',
+ User::TABLE.'.avatar_path'
)
->join(User::TABLE, 'id', 'user_id')
->orderBy(self::TABLE.'.date_creation', $sorting)
@@ -75,7 +76,9 @@ class Comment extends Base
self::TABLE.'.comment',
self::TABLE.'.reference',
User::TABLE.'.username',
- User::TABLE.'.name'
+ User::TABLE.'.name',
+ User::TABLE.'.email',
+ User::TABLE.'.avatar_path'
)
->join(User::TABLE, 'id', 'user_id')
->eq(self::TABLE.'.id', $comment_id)
diff --git a/app/Model/Config.php b/app/Model/Config.php
index 7b254c8d..0c363fb0 100644
--- a/app/Model/Config.php
+++ b/app/Model/Config.php
@@ -90,6 +90,7 @@ class Config extends Setting
'fi_FI' => 'Suomi',
'sv_SE' => 'Svenska',
'tr_TR' => 'Türkçe',
+ 'ko_KR' => '한국어',
'zh_CN' => '中文(简体)',
'ja_JP' => '日本語',
'th_TH' => 'ไทย',
@@ -129,6 +130,7 @@ class Config extends Setting
'fi_FI' => 'fi',
'sv_SE' => 'sv',
'tr_TR' => 'tr',
+ 'ko_KR' => 'ko',
'zh_CN' => 'zh-cn',
'ja_JP' => 'ja',
'th_TH' => 'th',
diff --git a/app/Model/File.php b/app/Model/File.php
index 03ea691d..e383235c 100644
--- a/app/Model/File.php
+++ b/app/Model/File.php
@@ -3,8 +3,8 @@
namespace Kanboard\Model;
use Exception;
+use Kanboard\Core\Thumbnail;
use Kanboard\Event\FileEvent;
-use Kanboard\Core\Tool;
use Kanboard\Core\ObjectStorage\ObjectStorageException;
/**
@@ -315,15 +315,15 @@ abstract class File extends Base
*/
public function generateThumbnailFromData($destination_filename, &$data)
{
- $temp_filename = tempnam(sys_get_temp_dir(), 'datafile');
+ $blob = Thumbnail::createFromString($data)
+ ->resize()
+ ->toString();
- file_put_contents($temp_filename, $data);
- $this->generateThumbnailFromFile($temp_filename, $destination_filename);
- unlink($temp_filename);
+ $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
}
/**
- * Generate thumbnail from a blob
+ * Generate thumbnail from a local file
*
* @access public
* @param string $uploaded_filename
@@ -331,8 +331,10 @@ abstract class File extends Base
*/
public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
{
- $thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail');
- Tool::generateThumbnail($uploaded_filename, $thumbnail_filename);
- $this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename));
+ $blob = Thumbnail::createFromFile($uploaded_filename)
+ ->resize()
+ ->toString();
+
+ $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
}
}
diff --git a/app/Model/Project.php b/app/Model/Project.php
index d2e5b7ce..6e3c2326 100644
--- a/app/Model/Project.php
+++ b/app/Model/Project.php
@@ -35,6 +35,20 @@ class Project extends Base
const INACTIVE = 0;
/**
+ * Value for private project
+ *
+ * @var integer
+ */
+ const TYPE_PRIVATE = 1;
+
+ /**
+ * Value for team project
+ *
+ * @var integer
+ */
+ const TYPE_TEAM = 0;
+
+ /**
* Get a project by the id
*
* @access public
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 74df26a1..d993015b 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use PicoDb\Table;
+
/**
* Project activity model
*
@@ -51,113 +53,26 @@ class ProjectActivity extends Base
}
/**
- * Get all events for the given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $limit Maximum events number
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
- */
- public function getProject($project_id, $limit = 50, $start = null, $end = null)
- {
- return $this->getProjects(array($project_id), $limit, $start, $end);
- }
-
- /**
- * Get all events for the given projects list
+ * Get query
*
* @access public
- * @param integer[] $project_ids Projects id
- * @param integer $limit Maximum events number
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
+ * @return Table
*/
- public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null)
+ public function getQuery()
{
- if (empty($project_ids)) {
- return array();
- }
-
- $query = $this
- ->db
- ->table(self::TABLE)
- ->columns(
- self::TABLE.'.*',
- User::TABLE.'.username AS author_username',
- User::TABLE.'.name AS author_name',
- User::TABLE.'.email'
- )
- ->in('project_id', $project_ids)
- ->join(User::TABLE, 'id', 'creator_id')
- ->desc(self::TABLE.'.id')
- ->limit($limit);
-
- return $this->getEvents($query, $start, $end);
- }
-
- /**
- * Get all events for the given task
- *
- * @access public
- * @param integer $task_id Task id
- * @param integer $limit Maximum events number
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
- */
- public function getTask($task_id, $limit = 50, $start = null, $end = null)
- {
- $query = $this
- ->db
- ->table(self::TABLE)
- ->columns(
- self::TABLE.'.*',
- User::TABLE.'.username AS author_username',
- User::TABLE.'.name AS author_name',
- User::TABLE.'.email'
- )
- ->eq('task_id', $task_id)
- ->join(User::TABLE, 'id', 'creator_id')
- ->desc(self::TABLE.'.id')
- ->limit($limit);
-
- return $this->getEvents($query, $start, $end);
- }
-
- /**
- * Common function to return events
- *
- * @access public
- * @param \PicoDb\Table $query PicoDb Query
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
- */
- private function getEvents(\PicoDb\Table $query, $start, $end)
- {
- if (! is_null($start)) {
- $query->gte('date_creation', $start);
- }
-
- if (! is_null($end)) {
- $query->lte('date_creation', $end);
- }
-
- $events = $query->findAll();
-
- foreach ($events as &$event) {
- $event += $this->decode($event['data']);
- unset($event['data']);
-
- $event['author'] = $event['author_name'] ?: $event['author_username'];
- $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event);
- $event['event_content'] = $this->getContent($event);
- }
-
- return $events;
+ return $this
+ ->db
+ ->table(ProjectActivity::TABLE)
+ ->columns(
+ ProjectActivity::TABLE.'.*',
+ 'uc.username AS author_username',
+ 'uc.name AS author_name',
+ 'uc.email',
+ 'uc.avatar_path'
+ )
+ ->join(Task::TABLE, 'id', 'task_id')
+ ->join(Project::TABLE, 'id', 'project_id')
+ ->left(User::TABLE, 'uc', 'id', ProjectActivity::TABLE, 'creator_id');
}
/**
@@ -175,35 +90,4 @@ class ProjectActivity extends Base
$this->db->table(self::TABLE)->in('id', $ids)->remove();
}
}
-
- /**
- * Get the event html content
- *
- * @access public
- * @param array $params Event properties
- * @return string
- */
- public function getContent(array $params)
- {
- return $this->template->render(
- 'event/'.str_replace('.', '_', $params['event_name']),
- $params
- );
- }
-
- /**
- * Decode event data, supports unserialize() and json_decode()
- *
- * @access public
- * @param string $data Serialized data
- * @return array
- */
- public function decode($data)
- {
- if ($data{0} === 'a') {
- return unserialize($data);
- }
-
- return json_decode($data, true) ?: array();
- }
}
diff --git a/app/Model/ProjectGroupRoleFilter.php b/app/Model/ProjectGroupRoleFilter.php
deleted file mode 100644
index 989d3073..00000000
--- a/app/Model/ProjectGroupRoleFilter.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project Group Role Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectGroupRoleFilter extends Base
-{
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @return UserFilter
- */
- public function create()
- {
- $this->query = $this->db->table(ProjectGroupRole::TABLE);
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @param string $column
- * @return array
- */
- public function findAll($column = '')
- {
- if ($column !== '') {
- return $this->query->asc($column)->findAllByColumn($column);
- }
-
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return ProjectUserRoleFilter
- */
- public function filterByProjectId($project_id)
- {
- $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id);
- return $this;
- }
-
- /**
- * Filter by username
- *
- * @access public
- * @param string $input
- * @return ProjectUserRoleFilter
- */
- public function startWithUsername($input)
- {
- $this->query
- ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
- ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
- ->ilike(User::TABLE.'.username', $input.'%');
-
- return $this;
- }
-}
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index db1573ae..59af2b58 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -3,6 +3,10 @@
namespace Kanboard\Model;
use Kanboard\Core\Security\Role;
+use Kanboard\Filter\ProjectGroupRoleProjectFilter;
+use Kanboard\Filter\ProjectGroupRoleUsernameFilter;
+use Kanboard\Filter\ProjectUserRoleProjectFilter;
+use Kanboard\Filter\ProjectUserRoleUsernameFilter;
/**
* Project Permission
@@ -53,8 +57,18 @@ class ProjectPermission extends Base
*/
public function findUsernames($project_id, $input)
{
- $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
- $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
+ $userMembers = $this->projectUserRoleQuery
+ ->withFilter(new ProjectUserRoleProjectFilter($project_id))
+ ->withFilter(new ProjectUserRoleUsernameFilter($input))
+ ->getQuery()
+ ->findAllByColumn('username');
+
+ $groupMembers = $this->projectGroupRoleQuery
+ ->withFilter(new ProjectGroupRoleProjectFilter($project_id))
+ ->withFilter(new ProjectGroupRoleUsernameFilter($input))
+ ->getQuery()
+ ->findAllByColumn('username');
+
$members = array_unique(array_merge($userMembers, $groupMembers));
sort($members);
diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php
index 56da679c..2956c524 100644
--- a/app/Model/ProjectUserRole.php
+++ b/app/Model/ProjectUserRole.php
@@ -251,8 +251,8 @@ class ProjectUserRole extends Base
/**
* Copy user access from a project to another one
*
- * @param integer $project_src_id Project Template
- * @return integer $project_dst_id Project that receives the copy
+ * @param integer $project_src_id
+ * @param integer $project_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id)
diff --git a/app/Model/ProjectUserRoleFilter.php b/app/Model/ProjectUserRoleFilter.php
deleted file mode 100644
index 64403643..00000000
--- a/app/Model/ProjectUserRoleFilter.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project User Role Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectUserRoleFilter extends Base
-{
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @return UserFilter
- */
- public function create()
- {
- $this->query = $this->db->table(ProjectUserRole::TABLE);
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @param string $column
- * @return array
- */
- public function findAll($column = '')
- {
- if ($column !== '') {
- return $this->query->asc($column)->findAllByColumn($column);
- }
-
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return ProjectUserRoleFilter
- */
- public function filterByProjectId($project_id)
- {
- $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id);
- return $this;
- }
-
- /**
- * Filter by username
- *
- * @access public
- * @param string $input
- * @return ProjectUserRoleFilter
- */
- public function startWithUsername($input)
- {
- $this->query
- ->join(User::TABLE, 'id', 'user_id')
- ->ilike(User::TABLE.'.username', $input.'%');
-
- return $this;
- }
-}
diff --git a/app/Model/Setting.php b/app/Model/Setting.php
index f98d7ce1..c5a4765c 100644
--- a/app/Model/Setting.php
+++ b/app/Model/Setting.php
@@ -22,6 +22,7 @@ abstract class Setting extends Base
*
* @abstract
* @access public
+ * @param array $values
* @return array
*/
abstract public function prepare(array $values);
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index b766b542..be04ee1b 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -146,94 +146,6 @@ class SubtaskTimeTracking extends Base
}
/**
- * Get user calendar events
- *
- * @access public
- * @param integer $user_id
- * @param string $start ISO-8601 format
- * @param string $end
- * @return array
- */
- public function getUserCalendarEvents($user_id, $start, $end)
- {
- $hook = 'model:subtask-time-tracking:calendar:events';
- $events = $this->getUserQuery($user_id)
- ->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'start',
- 'end'
- ))
- ->findAll();
-
- if ($this->hook->exists($hook)) {
- $events = $this->hook->first($hook, array(
- 'user_id' => $user_id,
- 'events' => $events,
- 'start' => $start,
- 'end' => $end,
- ));
- }
-
- return $this->toCalendarEvents($events);
- }
-
- /**
- * Get project calendar events
- *
- * @access public
- * @param integer $project_id
- * @param integer $start
- * @param integer $end
- * @return array
- */
- public function getProjectCalendarEvents($project_id, $start, $end)
- {
- $result = $this
- ->getProjectQuery($project_id)
- ->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'start',
- 'end'
- ))
- ->findAll();
-
- return $this->toCalendarEvents($result);
- }
-
- /**
- * Convert a record set to calendar events
- *
- * @access private
- * @param array $rows
- * @return array
- */
- private function toCalendarEvents(array $rows)
- {
- $events = array();
-
- foreach ($rows as $row) {
- $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
-
- $events[] = array(
- 'id' => $row['id'],
- 'subtask_id' => $row['subtask_id'],
- 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
- 'start' => date('Y-m-d\TH:i:s', $row['start']),
- 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
- 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
- 'borderColor' => $this->color->getBorderColor($row['color_id']),
- 'textColor' => 'black',
- 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
- 'editable' => false,
- );
- }
-
- return $events;
- }
-
- /**
* Return true if a timer is started for this use and subtask
*
* @access public
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
deleted file mode 100644
index 1883298d..00000000
--- a/app/Model/TaskFilter.php
+++ /dev/null
@@ -1,745 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Task Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class TaskFilter extends Base
-{
- /**
- * Filters mapping
- *
- * @access private
- * @var array
- */
- private $filters = array(
- 'T_ASSIGNEE' => 'filterByAssignee',
- 'T_COLOR' => 'filterByColors',
- 'T_DUE' => 'filterByDueDate',
- 'T_UPDATED' => 'filterByModificationDate',
- 'T_CREATED' => 'filterByCreationDate',
- 'T_TITLE' => 'filterByTitle',
- 'T_STATUS' => 'filterByStatusName',
- 'T_DESCRIPTION' => 'filterByDescription',
- 'T_CATEGORY' => 'filterByCategoryName',
- 'T_PROJECT' => 'filterByProjectName',
- 'T_COLUMN' => 'filterByColumnName',
- 'T_REFERENCE' => 'filterByReference',
- 'T_SWIMLANE' => 'filterBySwimlaneName',
- 'T_LINK' => 'filterByLinkName',
- );
-
- /**
- * Query
- *
- * @access public
- * @var \PicoDb\Table
- */
- public $query;
-
- /**
- * Apply filters according to the search input
- *
- * @access public
- * @param string $input
- * @return TaskFilter
- */
- public function search($input)
- {
- $tree = $this->lexer->map($this->lexer->tokenize($input));
- $this->query = $this->taskFinder->getExtendedQuery();
-
- if (empty($tree)) {
- $this->filterByTitle($input);
- }
-
- foreach ($tree as $filter => $value) {
- $method = $this->filters[$filter];
- $this->$method($value);
- }
-
- return $this;
- }
-
- /**
- * Create a new query
- *
- * @access public
- * @return TaskFilter
- */
- public function create()
- {
- $this->query = $this->db->table(Task::TABLE);
- $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id');
- $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id');
-
- $this->query->columns(
- Task::TABLE.'.*',
- 'ua.email AS assignee_email',
- 'ua.name AS assignee_name',
- 'ua.username AS assignee_username',
- 'uc.email AS creator_email',
- 'uc.username AS creator_username'
- );
-
- return $this;
- }
-
- /**
- * Create a new subtask query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function createSubtaskQuery()
- {
- return $this->db->table(Subtask::TABLE)
- ->columns(
- Subtask::TABLE.'.user_id',
- Subtask::TABLE.'.task_id',
- User::TABLE.'.name',
- User::TABLE.'.username'
- )
- ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
- ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
- }
-
- /**
- * Create a new link query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function createLinkQuery()
- {
- return $this->db->table(TaskLink::TABLE)
- ->columns(
- TaskLink::TABLE.'.task_id',
- Link::TABLE.'.label'
- )
- ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE);
- }
-
- /**
- * Clone the filter
- *
- * @access public
- * @return TaskFilter
- */
- public function copy()
- {
- $filter = new static($this->container);
- $filter->query = clone($this->query);
- $filter->query->condition = clone($this->query->condition);
- return $filter;
- }
-
- /**
- * Exclude a list of task_id
- *
- * @access public
- * @param integer[] $task_ids
- * @return TaskFilter
- */
- public function excludeTasks(array $task_ids)
- {
- $this->query->notin(Task::TABLE.'.id', $task_ids);
- return $this;
- }
-
- /**
- * Filter by id
- *
- * @access public
- * @param integer $task_id
- * @return TaskFilter
- */
- public function filterById($task_id)
- {
- if ($task_id > 0) {
- $this->query->eq(Task::TABLE.'.id', $task_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by reference
- *
- * @access public
- * @param string $reference
- * @return TaskFilter
- */
- public function filterByReference($reference)
- {
- if (! empty($reference)) {
- $this->query->eq(Task::TABLE.'.reference', $reference);
- }
-
- return $this;
- }
-
- /**
- * Filter by title
- *
- * @access public
- * @param string $title
- * @return TaskFilter
- */
- public function filterByDescription($title)
- {
- $this->query->ilike(Task::TABLE.'.description', '%'.$title.'%');
- return $this;
- }
-
- /**
- * Filter by title or id if the string is like #123 or an integer
- *
- * @access public
- * @param string $title
- * @return TaskFilter
- */
- public function filterByTitle($title)
- {
- if (ctype_digit($title) || (strlen($title) > 1 && $title{0} === '#' && ctype_digit(substr($title, 1)))) {
- $this->query->beginOr();
- $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $title));
- $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
- $this->query->closeOr();
- } else {
- $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
- }
-
- return $this;
- }
-
- /**
- * Filter by a list of project id
- *
- * @access public
- * @param array $project_ids
- * @return TaskFilter
- */
- public function filterByProjects(array $project_ids)
- {
- $this->query->in(Task::TABLE.'.project_id', $project_ids);
- return $this;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return TaskFilter
- */
- public function filterByProject($project_id)
- {
- if ($project_id > 0) {
- $this->query->eq(Task::TABLE.'.project_id', $project_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by project name
- *
- * @access public
- * @param array $values List of project name
- * @return TaskFilter
- */
- public function filterByProjectName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $project) {
- if (ctype_digit($project)) {
- $this->query->eq(Task::TABLE.'.project_id', $project);
- } else {
- $this->query->ilike(Project::TABLE.'.name', $project);
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by swimlane name
- *
- * @access public
- * @param array $values List of swimlane name
- * @return TaskFilter
- */
- public function filterBySwimlaneName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $swimlane) {
- if ($swimlane === 'default') {
- $this->query->eq(Task::TABLE.'.swimlane_id', 0);
- } else {
- $this->query->ilike(Swimlane::TABLE.'.name', $swimlane);
- $this->query->addCondition(Task::TABLE.'.swimlane_id=0 AND '.Project::TABLE.'.default_swimlane '.$this->db->getDriver()->getOperator('ILIKE')." '$swimlane'");
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by category id
- *
- * @access public
- * @param integer $category_id
- * @return TaskFilter
- */
- public function filterByCategory($category_id)
- {
- if ($category_id >= 0) {
- $this->query->eq(Task::TABLE.'.category_id', $category_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by category
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterByCategoryName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $category) {
- if ($category === 'none') {
- $this->query->eq(Task::TABLE.'.category_id', 0);
- } else {
- $this->query->eq(Category::TABLE.'.name', $category);
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by assignee
- *
- * @access public
- * @param integer $owner_id
- * @return TaskFilter
- */
- public function filterByOwner($owner_id)
- {
- if ($owner_id >= 0) {
- $this->query->eq(Task::TABLE.'.owner_id', $owner_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by assignee names
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterByAssignee(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $assignee) {
- switch ($assignee) {
- case 'me':
- $this->query->eq(Task::TABLE.'.owner_id', $this->userSession->getId());
- break;
- case 'nobody':
- $this->query->eq(Task::TABLE.'.owner_id', 0);
- break;
- default:
- $this->query->ilike(User::TABLE.'.username', '%'.$assignee.'%');
- $this->query->ilike(User::TABLE.'.name', '%'.$assignee.'%');
- }
- }
-
- $this->filterBySubtaskAssignee($values);
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by subtask assignee names
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterBySubtaskAssignee(array $values)
- {
- $subtaskQuery = $this->createSubtaskQuery();
- $subtaskQuery->beginOr();
-
- foreach ($values as $assignee) {
- if ($assignee === 'me') {
- $subtaskQuery->eq(Subtask::TABLE.'.user_id', $this->userSession->getId());
- } else {
- $subtaskQuery->ilike(User::TABLE.'.username', '%'.$assignee.'%');
- $subtaskQuery->ilike(User::TABLE.'.name', '%'.$assignee.'%');
- }
- }
-
- $subtaskQuery->closeOr();
-
- $this->query->in(Task::TABLE.'.id', $subtaskQuery->findAllByColumn('task_id'));
-
- return $this;
- }
-
- /**
- * Filter by color
- *
- * @access public
- * @param string $color_id
- * @return TaskFilter
- */
- public function filterByColor($color_id)
- {
- if ($color_id !== '') {
- $this->query->eq(Task::TABLE.'.color_id', $color_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by colors
- *
- * @access public
- * @param array $colors
- * @return TaskFilter
- */
- public function filterByColors(array $colors)
- {
- $this->query->beginOr();
-
- foreach ($colors as $color) {
- $this->filterByColor($this->color->find($color));
- }
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by column
- *
- * @access public
- * @param integer $column_id
- * @return TaskFilter
- */
- public function filterByColumn($column_id)
- {
- if ($column_id >= 0) {
- $this->query->eq(Task::TABLE.'.column_id', $column_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by column name
- *
- * @access public
- * @param array $values List of column name
- * @return TaskFilter
- */
- public function filterByColumnName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $project) {
- $this->query->ilike(Column::TABLE.'.title', $project);
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by swimlane
- *
- * @access public
- * @param integer $swimlane_id
- * @return TaskFilter
- */
- public function filterBySwimlane($swimlane_id)
- {
- if ($swimlane_id >= 0) {
- $this->query->eq(Task::TABLE.'.swimlane_id', $swimlane_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by status name
- *
- * @access public
- * @param string $status
- * @return TaskFilter
- */
- public function filterByStatusName($status)
- {
- if ($status === 'open' || $status === 'closed') {
- $this->filterByStatus($status === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED);
- }
-
- return $this;
- }
-
- /**
- * Filter by status
- *
- * @access public
- * @param integer $is_active
- * @return TaskFilter
- */
- public function filterByStatus($is_active)
- {
- if ($is_active >= 0) {
- $this->query->eq(Task::TABLE.'.is_active', $is_active);
- }
-
- return $this;
- }
-
- /**
- * Filter by link
- *
- * @access public
- * @param array $values List of links
- * @return TaskFilter
- */
- public function filterByLinkName(array $values)
- {
- $this->query->beginOr();
-
- $link_query = $this->createLinkQuery()->in(Link::TABLE.'.label', $values);
- $matching_task_ids = $link_query->findAllByColumn('task_id');
- if (empty($matching_task_ids)) {
- $this->query->eq(Task::TABLE.'.id', 0);
- } else {
- $this->query->in(Task::TABLE.'.id', $matching_task_ids);
- }
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by due date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByDueDate($date)
- {
- $this->query->neq(Task::TABLE.'.date_due', 0);
- $this->query->notNull(Task::TABLE.'.date_due');
- return $this->filterWithOperator(Task::TABLE.'.date_due', $date, true);
- }
-
- /**
- * Filter by due date (range)
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByDueDateRange($start, $end)
- {
- $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start));
- $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end));
-
- return $this;
- }
-
- /**
- * Filter by start date (range)
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByStartDateRange($start, $end)
- {
- $this->query->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'date_started',
- 'date_completed'
- ));
-
- return $this;
- }
-
- /**
- * Filter by creation date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByCreationDate($date)
- {
- if ($date === 'recently') {
- return $this->filterRecentlyDate(Task::TABLE.'.date_creation');
- }
-
- return $this->filterWithOperator(Task::TABLE.'.date_creation', $date, true);
- }
-
- /**
- * Filter by creation date
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByCreationDateRange($start, $end)
- {
- $this->query->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'date_creation',
- 'date_completed'
- ));
-
- return $this;
- }
-
- /**
- * Filter by modification date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByModificationDate($date)
- {
- if ($date === 'recently') {
- return $this->filterRecentlyDate(Task::TABLE.'.date_modification');
- }
-
- return $this->filterWithOperator(Task::TABLE.'.date_modification', $date, true);
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @return array
- */
- public function findAll()
- {
- return $this->query->asc(Task::TABLE.'.id')->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Get swimlanes and tasks to display the board
- *
- * @access public
- * @return array
- */
- public function getBoard($project_id)
- {
- $tasks = $this->filterByProject($project_id)->query->asc(Task::TABLE.'.position')->findAll();
-
- return $this->board->getBoard($project_id, function ($project_id, $column_id, $swimlane_id) use ($tasks) {
- return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) {
- return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id;
- });
- });
- }
-
- /**
- * Filter with an operator
- *
- * @access public
- * @param string $field
- * @param string $value
- * @param boolean $is_date
- * @return TaskFilter
- */
- private function filterWithOperator($field, $value, $is_date)
- {
- $operators = array(
- '<=' => 'lte',
- '>=' => 'gte',
- '<' => 'lt',
- '>' => 'gt',
- );
-
- foreach ($operators as $operator => $method) {
- if (strpos($value, $operator) === 0) {
- $value = substr($value, strlen($operator));
- $this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
- return $this;
- }
- }
-
- if ($is_date) {
- $timestamp = $this->dateParser->getTimestampFromIsoFormat($value);
- $this->query->gte($field, $timestamp);
- $this->query->lte($field, $timestamp + 86399);
- } else {
- $this->query->eq($field, $value);
- }
-
- return $this;
- }
-
- /**
- * Use the board_highlight_period for the "recently" keyword
- *
- * @access private
- * @param string $field
- * @return TaskFilter
- */
- private function filterRecentlyDate($field)
- {
- $duration = $this->config->get('board_highlight_period', 0);
-
- if ($duration > 0) {
- $this->query->gte($field, time() - $duration);
- }
-
- return $this;
- }
-}
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index d67372cc..d406b794 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -128,6 +128,7 @@ class TaskFinder extends Base
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name',
User::TABLE.'.email AS assignee_email',
+ User::TABLE.'.avatar_path AS assignee_avatar_path',
Category::TABLE.'.name AS category_name',
Category::TABLE.'.description AS category_description',
Column::TABLE.'.title AS column_name',
@@ -137,6 +138,7 @@ class TaskFinder extends Base
Project::TABLE.'.name AS project_name'
)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
+ ->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id')
->join(Category::TABLE, 'id', 'category_id', Task::TABLE)
->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE)
@@ -362,6 +364,27 @@ class TaskFinder extends Base
}
/**
+ * Get iCal query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getICalQuery()
+ {
+ return $this->db->table(Task::TABLE)
+ ->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id')
+ ->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id')
+ ->columns(
+ Task::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.name AS assignee_name',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.username AS creator_username'
+ );
+ }
+
+ /**
* Count all tasks for a given project and status
*
* @access public
diff --git a/app/Model/User.php b/app/Model/User.php
index 0e11422b..57993002 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -283,12 +283,7 @@ class User extends Base
{
$this->prepare($values);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
-
- // If the user is connected refresh his session
- if ($this->userSession->getId() == $values['id']) {
- $this->userSession->initialize($this->getById($this->userSession->getId()));
- }
-
+ $this->userSession->refresh($values['id']);
return $result;
}
@@ -325,6 +320,8 @@ class User extends Base
*/
public function remove($user_id)
{
+ $this->avatarFile->remove($user_id);
+
return $this->db->transaction(function (Database $db) use ($user_id) {
// All assigned tasks are now unassigned (no foreign key)
diff --git a/app/Model/UserFilter.php b/app/Model/UserFilter.php
deleted file mode 100644
index ff546e96..00000000
--- a/app/Model/UserFilter.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * User Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class UserFilter extends Base
-{
- /**
- * Search query
- *
- * @access private
- * @var string
- */
- private $input;
-
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @param string $input
- * @return UserFilter
- */
- public function create($input)
- {
- $this->query = $this->db->table(User::TABLE);
- $this->input = $input;
- return $this;
- }
-
- /**
- * Filter users by name or username
- *
- * @access public
- * @return UserFilter
- */
- public function filterByUsernameOrByName()
- {
- $this->query->beginOr()
- ->ilike('username', '%'.$this->input.'%')
- ->ilike('name', '%'.$this->input.'%')
- ->closeOr();
-
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @return array
- */
- public function findAll()
- {
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-}
diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php
index e8a967ac..7795da2e 100644
--- a/app/Model/UserNotification.php
+++ b/app/Model/UserNotification.php
@@ -117,23 +117,20 @@ class UserNotification extends Base
*/
public function saveSettings($user_id, array $values)
{
- $this->db->startTransaction();
+ $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
- if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) {
+ if (! empty($types)) {
$this->enableNotification($user_id);
-
- $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter'];
- $projects = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']);
- $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
-
- $this->userNotificationFilter->saveFilter($user_id, $filter);
- $this->userNotificationFilter->saveSelectedProjects($user_id, $projects);
- $this->userNotificationType->saveSelectedTypes($user_id, $types);
} else {
$this->disableNotification($user_id);
}
- $this->db->closeTransaction();
+ $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter'];
+ $project_ids = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']);
+
+ $this->userNotificationFilter->saveFilter($user_id, $filter);
+ $this->userNotificationFilter->saveSelectedProjects($user_id, $project_ids);
+ $this->userNotificationType->saveSelectedTypes($user_id, $types);
}
/**
diff --git a/app/Model/UserNotificationFilter.php b/app/Model/UserNotificationFilter.php
index d4afd278..780ddfc7 100644
--- a/app/Model/UserNotificationFilter.php
+++ b/app/Model/UserNotificationFilter.php
@@ -61,10 +61,11 @@ class UserNotificationFilter extends Base
* @access public
* @param integer $user_id
* @param string $filter
+ * @return boolean
*/
public function saveFilter($user_id, $filter)
{
- $this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
+ return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
'notifications_filter' => $filter,
));
}
@@ -87,17 +88,21 @@ class UserNotificationFilter extends Base
* @access public
* @param integer $user_id
* @param integer[] $project_ids
+ * @return boolean
*/
public function saveSelectedProjects($user_id, array $project_ids)
{
+ $results = array();
$this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove();
foreach ($project_ids as $project_id) {
- $this->db->table(self::PROJECT_TABLE)->insert(array(
+ $results[] = $this->db->table(self::PROJECT_TABLE)->insert(array(
'user_id' => $user_id,
'project_id' => $project_id,
));
}
+
+ return !in_array(false, $results, true);
}
/**
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 9bfe6649..934b063f 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,23 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 108;
+const VERSION = 110;
+
+function version_110(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE user_has_notifications DROP FOREIGN KEY `user_has_notifications_ibfk_1`");
+ $pdo->exec("ALTER TABLE user_has_notifications DROP FOREIGN KEY `user_has_notifications_ibfk_2`");
+ $pdo->exec("DROP INDEX `project_id` ON user_has_notifications");
+ $pdo->exec("ALTER TABLE user_has_notifications DROP KEY `user_id`");
+ $pdo->exec("CREATE UNIQUE INDEX `user_has_notifications_unique_idx` ON `user_has_notifications` (`user_id`, `project_id`)");
+ $pdo->exec("ALTER TABLE user_has_notifications ADD CONSTRAINT user_has_notifications_ibfk_1 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE");
+ $pdo->exec("ALTER TABLE user_has_notifications ADD CONSTRAINT user_has_notifications_ibfk_2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE");
+}
+
+function version_109(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)");
+}
function version_108(PDO $pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 28c563de..3ef49498 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,12 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 88;
+const VERSION = 89;
+
+function version_89(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)");
+}
function version_88(PDO $pdo)
{
diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql
index b5620400..ce2374f0 100644
--- a/app/Schema/Sql/mysql.sql
+++ b/app/Schema/Sql/mysql.sql
@@ -273,6 +273,8 @@ CREATE TABLE `project_has_metadata` (
`project_id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
UNIQUE KEY `project_id` (`project_id`,`name`),
CONSTRAINT `project_has_metadata_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -357,6 +359,8 @@ DROP TABLE IF EXISTS `settings`;
CREATE TABLE `settings` (
`option` varchar(100) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`option`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -469,6 +473,8 @@ CREATE TABLE `task_has_metadata` (
`task_id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
UNIQUE KEY `task_id` (`task_id`,`name`),
CONSTRAINT `task_has_metadata_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -548,6 +554,8 @@ CREATE TABLE `user_has_metadata` (
`user_id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
UNIQUE KEY `user_id` (`user_id`,`name`),
CONSTRAINT `user_has_metadata_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -570,8 +578,8 @@ DROP TABLE IF EXISTS `user_has_notifications`;
CREATE TABLE `user_has_notifications` (
`user_id` int(11) NOT NULL,
`project_id` int(11) NOT NULL,
- UNIQUE KEY `project_id` (`project_id`,`user_id`),
- KEY `user_id` (`user_id`),
+ UNIQUE KEY `user_has_notifications_unique_idx` (`user_id`,`project_id`),
+ KEY `user_has_notifications_ibfk_2` (`project_id`),
CONSTRAINT `user_has_notifications_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `user_has_notifications_ibfk_2` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -615,6 +623,7 @@ CREATE TABLE `users` (
`gitlab_id` int(11) DEFAULT NULL,
`role` varchar(25) NOT NULL DEFAULT 'app-user',
`is_active` tinyint(1) DEFAULT '1',
+ `avatar_path` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_username_idx` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -638,7 +647,7 @@ CREATE TABLE `users` (
LOCK TABLES `settings` WRITE;
/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
-INSERT INTO `settings` VALUES ('api_token','cd9c46c6bdaa6afc49b3385dabe0b78c059bc124b1f72c2f47c9ca604cf1'),('application_currency','USD'),('application_date_format','m/d/Y'),('application_language','en_US'),('application_stylesheet',''),('application_timezone','UTC'),('application_url',''),('board_columns',''),('board_highlight_period','172800'),('board_private_refresh_interval','10'),('board_public_refresh_interval','60'),('calendar_project_tasks','date_started'),('calendar_user_subtasks_time_tracking','0'),('calendar_user_tasks','date_started'),('cfd_include_closed_tasks','1'),('default_color','yellow'),('integration_gravatar','0'),('password_reset','1'),('project_categories',''),('subtask_restriction','0'),('subtask_time_tracking','1'),('webhook_token','32387b121de8fe6031a6b71b7b1b9cae411a909539aa9d494cf69ac5f2ee'),('webhook_url','');
+INSERT INTO `settings` VALUES ('api_token','9c55053ae1d523893efc820e2e8338c4cf47f5c6c2c26861fec637eba62b',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','aaed762f4f6b0860902af0e2a87e5ad3427d24ff9e3ce8a2e0b005b58dfc',0,0),('webhook_url','',0,0);
/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -667,4 +676,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$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('107');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$e.SftITKuBvXeNbxtmTKS.KAbIy4Mx09t254BAiEAuWOxkuS4xfLG', 'app-admin');INSERT INTO schema_version VALUES ('110');
diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql
index c613ddb4..48a269d3 100644
--- a/app/Schema/Sql/postgres.sql
+++ b/app/Schema/Sql/postgres.sql
@@ -512,7 +512,9 @@ CREATE TABLE project_has_groups (
CREATE TABLE project_has_metadata (
project_id integer NOT NULL,
name character varying(50) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -652,7 +654,9 @@ CREATE TABLE schema_version (
CREATE TABLE settings (
option character varying(100) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -847,7 +851,9 @@ ALTER SEQUENCE task_has_links_id_seq OWNED BY task_has_links.id;
CREATE TABLE task_has_metadata (
task_id integer NOT NULL,
name character varying(50) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -969,7 +975,9 @@ ALTER SEQUENCE transitions_id_seq OWNED BY transitions.id;
CREATE TABLE user_has_metadata (
user_id integer NOT NULL,
name character varying(50) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -1070,7 +1078,8 @@ CREATE TABLE users (
lock_expiration_date bigint DEFAULT 0,
gitlab_id integer,
role character varying(25) DEFAULT 'app-user'::character varying NOT NULL,
- is_active boolean DEFAULT true
+ is_active boolean DEFAULT true,
+ avatar_path character varying(255)
);
@@ -2141,29 +2150,29 @@ SET search_path = public, pg_catalog;
-- Data for Name: settings; Type: TABLE DATA; Schema: public; Owner: postgres
--
-INSERT INTO settings (option, value) VALUES ('board_highlight_period', '172800');
-INSERT INTO settings (option, value) VALUES ('board_public_refresh_interval', '60');
-INSERT INTO settings (option, value) VALUES ('board_private_refresh_interval', '10');
-INSERT INTO settings (option, value) VALUES ('board_columns', '');
-INSERT INTO settings (option, value) VALUES ('webhook_token', 'c7caaf8f87ad391800e3989d7abfd98a6066a6f801fc151012bb5c4ee3cb');
-INSERT INTO settings (option, value) VALUES ('api_token', 'b0a6f56fe236fc9639fc6914e92365aa627d95cd790aa7e0c5a3ebebf844');
-INSERT INTO settings (option, value) VALUES ('application_language', 'en_US');
-INSERT INTO settings (option, value) VALUES ('application_timezone', 'UTC');
-INSERT INTO settings (option, value) VALUES ('application_url', '');
-INSERT INTO settings (option, value) VALUES ('application_date_format', 'm/d/Y');
-INSERT INTO settings (option, value) VALUES ('project_categories', '');
-INSERT INTO settings (option, value) VALUES ('subtask_restriction', '0');
-INSERT INTO settings (option, value) VALUES ('application_stylesheet', '');
-INSERT INTO settings (option, value) VALUES ('application_currency', 'USD');
-INSERT INTO settings (option, value) VALUES ('integration_gravatar', '0');
-INSERT INTO settings (option, value) VALUES ('calendar_user_subtasks_time_tracking', '0');
-INSERT INTO settings (option, value) VALUES ('calendar_user_tasks', 'date_started');
-INSERT INTO settings (option, value) VALUES ('calendar_project_tasks', 'date_started');
-INSERT INTO settings (option, value) VALUES ('webhook_url', '');
-INSERT INTO settings (option, value) VALUES ('default_color', 'yellow');
-INSERT INTO settings (option, value) VALUES ('subtask_time_tracking', '1');
-INSERT INTO settings (option, value) VALUES ('cfd_include_closed_tasks', '1');
-INSERT INTO settings (option, value) VALUES ('password_reset', '1');
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_highlight_period', '172800', 0, 0);
+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', '67545fef6a0a3f43d60c7d57632d6e4af9930f064c12e72266b1c9b42381', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', 'c16b1c5896b258409a5eb344152b5b33c8ef4c58902bc543fc1348c37975', 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);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_date_format', 'm/d/Y', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('project_categories', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('subtask_restriction', '0', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_stylesheet', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_currency', 'USD', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('integration_gravatar', '0', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_user_subtasks_time_tracking', '0', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_user_tasks', 'date_started', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_project_tasks', 'date_started', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_url', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('default_color', 'yellow', 0, 0);
+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);
--
@@ -2211,4 +2220,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true);
-- PostgreSQL database dump complete
--
-INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('87');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$e.SftITKuBvXeNbxtmTKS.KAbIy4Mx09t254BAiEAuWOxkuS4xfLG', 'app-admin');INSERT INTO schema_version VALUES ('89');
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index c6f60332..9ded7ed9 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 100;
+const VERSION = 101;
+
+function version_101(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path TEXT");
+}
function version_100(PDO $pdo)
{
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
index d59ffd9e..776e65d5 100644
--- a/app/ServiceProvider/AuthenticationProvider.php
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -125,6 +125,7 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('Board', 'readonly', Role::APP_PUBLIC);
$acl->add('Ical', '*', Role::APP_PUBLIC);
$acl->add('Feed', '*', Role::APP_PUBLIC);
+ $acl->add('AvatarFile', 'show', Role::APP_PUBLIC);
$acl->add('Config', '*', Role::APP_ADMIN);
$acl->add('Currency', '*', Role::APP_ADMIN);
diff --git a/app/ServiceProvider/AvatarProvider.php b/app/ServiceProvider/AvatarProvider.php
index 73d37d5c..aac4fcab 100644
--- a/app/ServiceProvider/AvatarProvider.php
+++ b/app/ServiceProvider/AvatarProvider.php
@@ -6,6 +6,7 @@ use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Kanboard\Core\User\Avatar\AvatarManager;
use Kanboard\User\Avatar\GravatarProvider;
+use Kanboard\User\Avatar\AvatarFileProvider;
use Kanboard\User\Avatar\LetterAvatarProvider;
/**
@@ -28,6 +29,7 @@ class AvatarProvider implements ServiceProviderInterface
$container['avatarManager'] = new AvatarManager;
$container['avatarManager']->register(new LetterAvatarProvider($container));
$container['avatarManager']->register(new GravatarProvider($container));
+ $container['avatarManager']->register(new AvatarFileProvider($container));
return $container;
}
}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index b883c905..18c1d578 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface
'Model' => array(
'Action',
'ActionParameter',
+ 'AvatarFile',
'Board',
'Category',
'Color',
@@ -48,9 +49,7 @@ class ClassProvider implements ServiceProviderInterface
'ProjectNotification',
'ProjectMetadata',
'ProjectGroupRole',
- 'ProjectGroupRoleFilter',
'ProjectUserRole',
- 'ProjectUserRoleFilter',
'RememberMeSession',
'Subtask',
'SubtaskTimeTracking',
@@ -62,7 +61,6 @@ class ClassProvider implements ServiceProviderInterface
'TaskExternalLink',
'TaskFinder',
'TaskFile',
- 'TaskFilter',
'TaskLink',
'TaskModification',
'TaskPermission',
@@ -78,15 +76,6 @@ class ClassProvider implements ServiceProviderInterface
'UserUnreadNotification',
'UserMetadata',
),
- 'Formatter' => array(
- 'TaskFilterGanttFormatter',
- 'TaskFilterAutoCompleteFormatter',
- 'TaskFilterCalendarFormatter',
- 'TaskFilterICalendarFormatter',
- 'ProjectGanttFormatter',
- 'UserFilterAutoCompleteFormatter',
- 'GroupAutoCompleteFormatter',
- ),
'Validator' => array(
'ActionValidator',
'AuthValidator',
diff --git a/app/ServiceProvider/DatabaseProvider.php b/app/ServiceProvider/DatabaseProvider.php
index 8cede8af..d323807d 100644
--- a/app/ServiceProvider/DatabaseProvider.php
+++ b/app/ServiceProvider/DatabaseProvider.php
@@ -44,8 +44,8 @@ class DatabaseProvider implements ServiceProviderInterface
if ($db->schema()->check(\Schema\VERSION)) {
return $db;
} else {
- $errors = $db->getLogMessages();
- throw new RuntimeException('Unable to migrate database schema: '.(isset($errors[0]) ? $errors[0] : 'Unknown error'));
+ $messages = $db->getLogMessages();
+ throw new RuntimeException('Unable to run SQL migrations: '.implode(', ', $messages).' (You may have to fix it manually)');
}
}
diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php
new file mode 100644
index 00000000..f3918d77
--- /dev/null
+++ b/app/ServiceProvider/FilterProvider.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Kanboard\Core\Filter\LexerBuilder;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\ProjectActivityCreationDateFilter;
+use Kanboard\Filter\ProjectActivityCreatorFilter;
+use Kanboard\Filter\ProjectActivityProjectNameFilter;
+use Kanboard\Filter\ProjectActivityTaskStatusFilter;
+use Kanboard\Filter\ProjectActivityTaskTitleFilter;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskCategoryFilter;
+use Kanboard\Filter\TaskColorFilter;
+use Kanboard\Filter\TaskColumnFilter;
+use Kanboard\Filter\TaskCommentFilter;
+use Kanboard\Filter\TaskCreationDateFilter;
+use Kanboard\Filter\TaskCreatorFilter;
+use Kanboard\Filter\TaskDescriptionFilter;
+use Kanboard\Filter\TaskDueDateFilter;
+use Kanboard\Filter\TaskIdFilter;
+use Kanboard\Filter\TaskLinkFilter;
+use Kanboard\Filter\TaskModificationDateFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskReferenceFilter;
+use Kanboard\Filter\TaskStatusFilter;
+use Kanboard\Filter\TaskSubtaskAssigneeFilter;
+use Kanboard\Filter\TaskSwimlaneFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\ProjectUserRole;
+use Kanboard\Model\User;
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Filter Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class FilterProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $this->createUserFilter($container);
+ $this->createProjectFilter($container);
+ $this->createTaskFilter($container);
+ return $container;
+ }
+
+ public function createUserFilter(Container $container)
+ {
+ $container['userQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(User::TABLE));
+ return $builder;
+ });
+
+ return $container;
+ }
+
+ public function createProjectFilter(Container $container)
+ {
+ $container['projectGroupRoleQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(ProjectGroupRole::TABLE));
+ return $builder;
+ });
+
+ $container['projectUserRoleQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(ProjectUserRole::TABLE));
+ return $builder;
+ });
+
+ $container['projectQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(Project::TABLE));
+ return $builder;
+ });
+
+ $container['projectActivityLexer'] = $container->factory(function ($c) {
+ $builder = new LexerBuilder();
+ $builder
+ ->withQuery($c['projectActivity']->getQuery())
+ ->withFilter(new ProjectActivityTaskTitleFilter(), true)
+ ->withFilter(new ProjectActivityTaskStatusFilter())
+ ->withFilter(new ProjectActivityProjectNameFilter())
+ ->withFilter(ProjectActivityCreationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(ProjectActivityCreatorFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ;
+
+ return $builder;
+ });
+
+ $container['projectActivityQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['projectActivity']->getQuery());
+
+ return $builder;
+ });
+
+ return $container;
+ }
+
+ public function createTaskFilter(Container $container)
+ {
+ $container['taskQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['taskFinder']->getExtendedQuery());
+ return $builder;
+ });
+
+ $container['taskLexer'] = $container->factory(function ($c) {
+ $builder = new LexerBuilder();
+
+ $builder
+ ->withQuery($c['taskFinder']->getExtendedQuery())
+ ->withFilter(TaskAssigneeFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ->withFilter(new TaskCategoryFilter())
+ ->withFilter(TaskColorFilter::getInstance()
+ ->setColorModel($c['color'])
+ )
+ ->withFilter(new TaskColumnFilter())
+ ->withFilter(new TaskCommentFilter())
+ ->withFilter(TaskCreationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(TaskCreatorFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ->withFilter(new TaskDescriptionFilter())
+ ->withFilter(TaskDueDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(new TaskIdFilter())
+ ->withFilter(TaskLinkFilter::getInstance()
+ ->setDatabase($c['db'])
+ )
+ ->withFilter(TaskModificationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(new TaskProjectFilter())
+ ->withFilter(new TaskReferenceFilter())
+ ->withFilter(new TaskStatusFilter())
+ ->withFilter(TaskSubtaskAssigneeFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ ->setDatabase($c['db'])
+ )
+ ->withFilter(new TaskSwimlaneFilter())
+ ->withFilter(new TaskTitleFilter(), true)
+ ;
+
+ return $builder;
+ });
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/HelperProvider.php b/app/ServiceProvider/HelperProvider.php
index 37be5a05..bf3956a2 100644
--- a/app/ServiceProvider/HelperProvider.php
+++ b/app/ServiceProvider/HelperProvider.php
@@ -13,12 +13,14 @@ class HelperProvider implements ServiceProviderInterface
{
$container['helper'] = new Helper($container);
$container['helper']->register('app', '\Kanboard\Helper\AppHelper');
+ $container['helper']->register('calendar', '\Kanboard\Helper\CalendarHelper');
$container['helper']->register('asset', '\Kanboard\Helper\AssetHelper');
$container['helper']->register('board', '\Kanboard\Helper\BoardHelper');
$container['helper']->register('dt', '\Kanboard\Helper\DateHelper');
$container['helper']->register('file', '\Kanboard\Helper\FileHelper');
$container['helper']->register('form', '\Kanboard\Helper\FormHelper');
$container['helper']->register('hook', '\Kanboard\Helper\HookHelper');
+ $container['helper']->register('ical', '\Kanboard\Helper\ICalHelper');
$container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper');
$container['helper']->register('model', '\Kanboard\Helper\ModelHelper');
$container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper');
@@ -27,6 +29,8 @@ class HelperProvider implements ServiceProviderInterface
$container['helper']->register('url', '\Kanboard\Helper\UrlHelper');
$container['helper']->register('user', '\Kanboard\Helper\UserHelper');
$container['helper']->register('avatar', '\Kanboard\Helper\AvatarHelper');
+ $container['helper']->register('projectHeader', '\Kanboard\Helper\ProjectHeaderHelper');
+ $container['helper']->register('projectActivity', '\Kanboard\Helper\ProjectActivityHelper');
$container['template'] = new Template($container['helper']);
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
index 0e7548d4..30d23a51 100644
--- a/app/ServiceProvider/RouteProvider.php
+++ b/app/ServiceProvider/RouteProvider.php
@@ -42,7 +42,7 @@ class RouteProvider implements ServiceProviderInterface
// Search routes
$container['route']->addRoute('search', 'search', 'index');
- $container['route']->addRoute('search/:search', 'search', 'index');
+ $container['route']->addRoute('search/activity', 'search', 'activity');
// ProjectCreation routes
$container['route']->addRoute('project/create', 'ProjectCreation', 'create');
@@ -62,6 +62,7 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('project/:project_id/enable', 'project', 'enable');
$container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index');
$container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1');
+ $container['route']->addRoute('project/:project_id/activity', 'activity', 'project');
// Project Overview
$container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show');
diff --git a/app/Template/activity/filter_dropdown.php b/app/Template/activity/filter_dropdown.php
new file mode 100644
index 00000000..8d7a7de3
--- /dev/null
+++ b/app/Template/activity/filter_dropdown.php
@@ -0,0 +1,14 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Default filters') ?>"><i class="fa fa-filter fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><a href="#" class="filter-helper filter-reset" data-filter="" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="creator:me"><?= t('My activities') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d', strtotime('yesterday')) ?>"><?= t('Activity until yesterday') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d')?>"><?= t('Activity until today') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="status:closed"><?= t('Closed tasks') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="status:open"><?= t('Open tasks') ?></a></li>
+ <li>
+ <?= $this->url->doc(t('View advanced search syntax'), 'search') ?>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/activity/project.php b/app/Template/activity/project.php
index ba6d6629..176d9b99 100644
--- a/app/Template/activity/project.php
+++ b/app/Template/activity/project.php
@@ -1,40 +1,14 @@
<section id="main">
- <div class="page-header">
+ <?= $this->projectHeader->render($project, 'Analytic', $this->app->getRouterAction()) ?>
+
+ <?php if ($project['is_public']): ?>
+ <div class="menu-inline pull-right">
<ul>
- <li>
- <span class="dropdown">
- <span>
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?= $this->render('project/dropdown', array('project' => $project)) ?>
- </ul>
- </span>
- </span>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
- <li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php endif ?>
- <li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
- </li>
- <?php if ($project['is_public']): ?>
- <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li>
- <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
- <?php endif ?>
+ <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li>
+ <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
</ul>
</div>
+ <?php endif ?>
<?= $this->render('event/events', array('events' => $events)) ?>
</section> \ No newline at end of file
diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php
index f1dba552..35793cbb 100644
--- a/app/Template/analytic/layout.php
+++ b/app/Template/analytic/layout.php
@@ -1,36 +1,5 @@
<section id="main">
- <div class="page-header">
- <ul>
- <li>
- <span class="dropdown">
- <span>
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?= $this->render('project/dropdown', array('project' => $project)) ?>
- </ul>
- </span>
- </span>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
- <li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php endif ?>
- <li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
- </li>
- </ul>
- </div>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<section class="sidebar-container">
<?= $this->render($sidebar_template, array('project' => $project)) ?>
diff --git a/app/Template/app/layout.php b/app/Template/app/layout.php
index 200cb0d7..2a32ac02 100644
--- a/app/Template/app/layout.php
+++ b/app/Template/app/layout.php
@@ -1,5 +1,5 @@
<section id="main">
- <div class="page-header page-header-mobile">
+ <div class="page-header">
<ul>
<?php if ($this->user->hasAccess('ProjectCreation', 'create')): ?>
<li>
diff --git a/app/Template/avatar_file/show.php b/app/Template/avatar_file/show.php
new file mode 100644
index 00000000..266a2ccb
--- /dev/null
+++ b/app/Template/avatar_file/show.php
@@ -0,0 +1,20 @@
+<div class="page-header">
+ <h2><?= t('Avatar') ?></h2>
+</div>
+
+<?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], '') ?>
+
+<form method="post" enctype="multipart/form-data" action="<?= $this->url->href('AvatarFile', 'upload', array('user_id' => $user['id'])) ?>">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->label(t('Upload my avatar image'), 'avatar') ?>
+ <?= $this->form->file('avatar') ?>
+
+ <div class="form-actions">
+ <?php if (! empty($user['avatar_path'])): ?>
+ <?= $this->url->link(t('Remove my image'), 'AvatarFile', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?>
+ <?php endif ?>
+ <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </div>
+</form>
diff --git a/app/Template/board/task_avatar.php b/app/Template/board/task_avatar.php
index 5630c190..39f6b54d 100644
--- a/app/Template/board/task_avatar.php
+++ b/app/Template/board/task_avatar.php
@@ -12,6 +12,7 @@
$task['assignee_username'],
$task['assignee_name'],
$task['assignee_email'],
+ $task['assignee_avatar_path'],
'avatar-inline'
) ?>
</span>
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
index a8203739..a9d381a3 100644
--- a/app/Template/board/task_footer.php
+++ b/app/Template/board/task_footer.php
@@ -76,6 +76,8 @@
<i class="fa fa-flag flag-milestone"></i>
</span>
<?php endif ?>
+
+ <?= $this->hook->render('template:board:task:icons', array('task' => $task)) ?>
<?= $this->task->formatPriority($project, $task) ?>
diff --git a/app/Template/board/view_private.php b/app/Template/board/view_private.php
index b5e38c66..13702273 100644
--- a/app/Template/board/view_private.php
+++ b/app/Template/board/view_private.php
@@ -1,19 +1,12 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- 'categories_list' => $categories_list,
- 'users_list' => $users_list,
- 'custom_filters_list' => $custom_filters_list,
- 'is_board' => true,
- )) ?>
+ <?= $this->projectHeader->render($project, 'Board', 'show', true) ?>
<?= $this->render('board/table_container', array(
- 'project' => $project,
- 'swimlanes' => $swimlanes,
- 'board_private_refresh_interval' => $board_private_refresh_interval,
- 'board_highlight_period' => $board_highlight_period,
+ 'project' => $project,
+ 'swimlanes' => $swimlanes,
+ 'board_private_refresh_interval' => $board_private_refresh_interval,
+ 'board_highlight_period' => $board_highlight_period,
)) ?>
</section>
diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php
index 7085b51e..f00e810b 100644
--- a/app/Template/calendar/show.php
+++ b/app/Template/calendar/show.php
@@ -1,9 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- )) ?>
-
+ <?= $this->projectHeader->render($project, 'Calendar', 'show') ?>
<div id="calendar"
data-save-url="<?= $this->url->href('calendar', 'save', array('project_id' => $project['id'])) ?>"
data-check-url="<?= $this->url->href('calendar', 'project', array('project_id' => $project['id'])) ?>"
diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php
index ce456c5d..3f45e2e7 100644
--- a/app/Template/comment/show.php
+++ b/app/Template/comment/show.php
@@ -1,6 +1,6 @@
<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>">
- <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email']) ?>
+ <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email'], $comment['avatar_path']) ?>
<div class="comment-title">
<?php if (! empty($comment['username'])): ?>
diff --git a/app/Template/event/events.php b/app/Template/event/events.php
index ef651321..c58376c4 100644
--- a/app/Template/event/events.php
+++ b/app/Template/event/events.php
@@ -7,7 +7,8 @@
$event['creator_id'],
$event['author_username'],
$event['author_name'],
- $event['email']
+ $event['email'],
+ $event['avatar_path']
) ?>
<div class="activity-content">
diff --git a/app/Template/gantt/project.php b/app/Template/gantt/project.php
index fe193c2b..e6c8592f 100644
--- a/app/Template/gantt/project.php
+++ b/app/Template/gantt/project.php
@@ -1,10 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- 'users_list' => $users_list,
- )) ?>
-
+ <?= $this->projectHeader->render($project, 'Gantt', 'project') ?>
<div class="menu-inline">
<ul>
<li <?= $sorting === 'board' ? 'class="active"' : '' ?>>
diff --git a/app/Template/listing/show.php b/app/Template/listing/show.php
index 10dcedcc..98b9528a 100644
--- a/app/Template/listing/show.php
+++ b/app/Template/listing/show.php
@@ -1,11 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- 'custom_filters_list' => $custom_filters_list,
- 'users_list' => $users_list,
- 'categories_list' => $categories_list,
- )) ?>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('No tasks found.') ?></p>
@@ -24,7 +18,11 @@
<?php foreach ($paginator->getCollection() as $task): ?>
<tr>
<td class="task-table color-<?= $task['color_id'] ?>">
- <?= $this->render('task/dropdown', array('task' => $task)) ?>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
+ <?= $this->render('task/dropdown', array('task' => $task)) ?>
+ <?php else: ?>
+ #<?= $task['id'] ?>
+ <?php endif ?>
</td>
<td>
<?= $this->text->e($task['swimlane_name'] ?: $task['default_swimlane']) ?>
diff --git a/app/Template/project/layout.php b/app/Template/project/layout.php
index eb391ae5..fcb3e5f3 100644
--- a/app/Template/project/layout.php
+++ b/app/Template/project/layout.php
@@ -1,30 +1,5 @@
<section id="main">
- <div class="page-header">
- <ul>
- <li>
- <span class="dropdown">
- <span>
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?= $this->render('project/dropdown', array('project' => $project)) ?>
- </ul>
- </span>
- </span>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
- </li>
- </ul>
- </div>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<section class="sidebar-container">
<?= $this->render($sidebar_template, array('project' => $project)) ?>
diff --git a/app/Template/project_header/dropdown.php b/app/Template/project_header/dropdown.php
index bbc033bf..759a5135 100644
--- a/app/Template/project_header/dropdown.php
+++ b/app/Template/project_header/dropdown.php
@@ -1,7 +1,7 @@
<div class="dropdown">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <a href="#" class="dropdown-menu action-menu"><?= t('Menu') ?> <i class="fa fa-caret-down"></i></a>
<ul>
- <?php if ($is_board): ?>
+ <?php if ($board_view): ?>
<li>
<span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>>
<i class="fa fa-expand fa-fw"></i>
diff --git a/app/Template/project_header/header.php b/app/Template/project_header/header.php
index f6e5af9e..aaa8137b 100644
--- a/app/Template/project_header/header.php
+++ b/app/Template/project_header/header.php
@@ -1,7 +1,7 @@
<div class="project-header">
<?= $this->hook->render('template:project:header:before', array('project' => $project)) ?>
- <?= $this->render('project_header/dropdown', array('project' => $project, 'is_board' => isset($is_board))) ?>
+ <?= $this->render('project_header/dropdown', array('project' => $project, 'board_view' => $board_view)) ?>
<?= $this->render('project_header/views', array('project' => $project, 'filters' => $filters)) ?>
<?= $this->render('project_header/search', array(
'project' => $project,
diff --git a/app/Template/project_header/views.php b/app/Template/project_header/views.php
index f8fdbb02..353e4b62 100644
--- a/app/Template/project_header/views.php
+++ b/app/Template/project_header/views.php
@@ -1,7 +1,7 @@
<ul class="views">
<li <?= $this->app->getRouterController() === 'ProjectOverview' ? 'class="active"' : '' ?>>
<i class="fa fa-eye fa-fw"></i>
- <?= $this->url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?>
+ <?= $this->url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?>
</li>
<li <?= $this->app->getRouterController() === 'Board' ? 'class="active"' : '' ?>>
<i class="fa fa-th fa-fw"></i>
diff --git a/app/Template/project_overview/description.php b/app/Template/project_overview/description.php
index 9895aede..72ccf06a 100644
--- a/app/Template/project_overview/description.php
+++ b/app/Template/project_overview/description.php
@@ -5,7 +5,7 @@
<div class="accordion-content">
<?php if ($this->user->hasProjectAccess('ProjectEdit', 'description', $project['id'])): ?>
<div class="buttons-header">
- <?= $this->url->button('fa-edit', t('Edit Description'), 'ProjectEdit', 'description', array('project_id' => $project['id']), 'popover') ?>
+ <?= $this->url->button('fa-edit', t('Edit description'), 'ProjectEdit', 'description', array('project_id' => $project['id']), 'popover') ?>
</div>
<?php endif ?>
<article class="markdown">
diff --git a/app/Template/project_overview/show.php b/app/Template/project_overview/show.php
index 9a9786e8..6fe815b3 100644
--- a/app/Template/project_overview/show.php
+++ b/app/Template/project_overview/show.php
@@ -1,9 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- )) ?>
-
+ <?= $this->projectHeader->render($project, 'ProjectOverview', 'show') ?>
<?= $this->render('project_overview/columns', array('project' => $project)) ?>
<?= $this->render('project_overview/description', array('project' => $project)) ?>
<?= $this->render('project_overview/attachments', array('project' => $project, 'images' => $images, 'files' => $files)) ?>
diff --git a/app/Template/search/activity.php b/app/Template/search/activity.php
new file mode 100644
index 00000000..60362215
--- /dev/null
+++ b/app/Template/search/activity.php
@@ -0,0 +1,39 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <i class="fa fa-search fa-fw"></i>
+ <?= $this->url->link(t('Search tasks'), 'search', 'index') ?>
+ </li>
+ </ul>
+ </div>
+
+ <div class="filter-box">
+ <form method="get" action="<?= $this->url->dir() ?>" class="search">
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $this->form->text('search', $values, array(), array(empty($values['search']) ? 'autofocus' : '', 'placeholder="'.t('Search').'"'), 'form-input-large') ?>
+ <?= $this->render('activity/filter_dropdown') ?>
+ </form>
+ </div>
+
+ <?php if (empty($values['search'])): ?>
+ <div class="listing">
+ <h3><?= t('Advanced search') ?></h3>
+ <p><?= t('Example of query: ') ?><strong>project:"My project" creator:me</strong></p>
+ <ul>
+ <li><?= t('Search by project: ') ?><strong>project:"My project"</strong></li>
+ <li><?= t('Search by creator: ') ?><strong>creator:admin</strong></li>
+ <li><?= t('Search by creation date: ') ?><strong>created:today</strong></li>
+ <li><?= t('Search by task status: ') ?><strong>status:open</strong></li>
+ <li><?= t('Search by task title: ') ?><strong>title:"My task"</strong></li>
+ </ul>
+ <p><i class="fa fa-external-link fa-fw"></i><?= $this->url->doc(t('View advanced search syntax'), 'search') ?></p>
+ </div>
+ <?php elseif (! empty($values['search']) && $nb_events === 0): ?>
+ <p class="alert"><?= t('Nothing found.') ?></p>
+ <?php else: ?>
+ <?= $this->render('event/events', array('events' => $events)) ?>
+ <?php endif ?>
+
+</section> \ No newline at end of file
diff --git a/app/Template/search/index.php b/app/Template/search/index.php
index 9231a6f3..d5d07ed6 100644
--- a/app/Template/search/index.php
+++ b/app/Template/search/index.php
@@ -2,8 +2,8 @@
<div class="page-header">
<ul>
<li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ <i class="fa fa-search fa-fw"></i>
+ <?= $this->url->link(t('Activity stream search'), 'search', 'activity') ?>
</li>
</ul>
</div>
diff --git a/app/Template/task/details.php b/app/Template/task/details.php
index a7c4ad01..6093c157 100644
--- a/app/Template/task/details.php
+++ b/app/Template/task/details.php
@@ -1,5 +1,6 @@
<section id="task-summary">
<h2><?= $this->text->e($task['title']) ?></h2>
+
<div class="task-summary-container color-<?= $task['color_id'] ?>">
<div class="task-summary-column">
<ul class="no-bullet">
@@ -134,4 +135,10 @@
</ul>
</div>
</div>
+
+ <?php if ($editable && empty($task['date_started'])): ?>
+ <div class="task-summary-buttons">
+ <?= $this->url->button('fa-play', t('Set start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+ <?php endif ?>
</section>
diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php
index ca27fd4f..52db5d1b 100644
--- a/app/Template/task/layout.php
+++ b/app/Template/task/layout.php
@@ -1,25 +1,5 @@
<section id="main">
- <div class="page-header">
- <ul>
- <li>
- <?= $this->render('task/menu', array('task' => $task)) ?>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, $task['swimlane_id'] != 0 ? 'swimlane-'.$task['swimlane_id'] : '') ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $task['project_id'])) ?>
- </li>
- <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $task['project_id'])): ?>
- <li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $task['project_id'])) ?>
- </li>
- <?php endif ?>
- </ul>
- </div>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<section
class="sidebar-container" id="task-view"
data-edit-url="<?= $this->url->href('taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"
diff --git a/app/Template/task/menu.php b/app/Template/task/menu.php
deleted file mode 100644
index fe30d06e..00000000
--- a/app/Template/task/menu.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
-<div class="dropdown">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?php if (empty($task['date_started'])): ?>
- <li>
- <i class="fa fa-play fa-fw"></i>
- <?= $this->url->link(t('Set automatically the start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <?php endif ?>
- <li>
- <i class="fa fa-pencil-square-o fa-fw"></i>
- <?= $this->url->link(t('Edit the task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-align-left fa-fw"></i>
- <?= $this->url->link(t('Edit the description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-refresh fa-rotate-90 fa-fw"></i>
- <?= $this->url->link(t('Edit recurrence'), 'TaskRecurrence', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-plus fa-fw"></i>
- <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-code-fork fa-fw"></i>
- <?= $this->url->link(t('Add internal link'), 'TaskInternalLink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-external-link fa-fw"></i>
- <?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-comment-o fa-fw"></i>
- <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-file fa-fw"></i>
- <?= $this->url->link(t('Attach a document'), 'TaskFile', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-camera fa-fw"></i>
- <?= $this->url->link(t('Add a screenshot'), 'TaskFile', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-files-o fa-fw"></i>
- <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-clipboard fa-fw"></i>
- <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-clone fa-fw"></i>
- <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <?php if ($task['is_active'] == 1): ?>
- <i class="fa fa-times fa-fw"></i>
- <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- <?php else: ?>
- <i class="fa fa-check-square-o fa-fw"></i>
- <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- <?php endif ?>
- </li>
- <?php if ($this->task->canRemove($task)): ?>
- <li>
- <i class="fa fa-trash-o fa-fw"></i>
- <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <?php endif ?>
-
- <?= $this->hook->render('template:task:menu') ?>
- </ul>
-</div>
-<?php endif ?>
diff --git a/app/Template/task/show.php b/app/Template/task/show.php
index d68f6c48..86422941 100644
--- a/app/Template/task/show.php
+++ b/app/Template/task/show.php
@@ -14,7 +14,6 @@
'task' => $task,
'subtasks' => $subtasks,
'project' => $project,
- 'users_list' => isset($users_list) ? $users_list : array(),
'editable' => true,
)) ?>
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index ee3b1594..773b28dc 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -24,6 +24,8 @@
</li>
<?php endif ?>
</ul>
+
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
<h2><?= t('Actions') ?></h2>
<ul>
<li>
@@ -90,6 +92,7 @@
</li>
<?php endif ?>
</ul>
+ <?php endif ?>
<?= $this->hook->render('template:task:sidebar', array('task' => $task)) ?>
</div>
diff --git a/app/Template/task_creation/form.php b/app/Template/task_creation/form.php
index cecba9ef..9bfd839f 100644
--- a/app/Template/task_creation/form.php
+++ b/app/Template/task_creation/form.php
@@ -28,6 +28,8 @@
<?php if (! isset($duplicate)): ?>
<?= $this->form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?>
<?php endif ?>
+
+ <?= $this->hook->render('template:task:form:left-column', array('values'=>$values, 'errors'=>$errors)) ?>
</div>
<div class="form-column">
@@ -48,4 +50,4 @@
<button type="submit" class="btn btn-blue" tabindex="15"><?= t('Save') ?></button>
<?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?>
</div>
-</form> \ No newline at end of file
+</form>
diff --git a/app/Template/user/index.php b/app/Template/user/index.php
index 364fd965..0b5da17c 100644
--- a/app/Template/user/index.php
+++ b/app/Template/user/index.php
@@ -14,6 +14,7 @@
<?php else: ?>
<table class="table-stripped">
<tr>
+ <th class="column-5"><?= $paginator->order(t('Id'), 'id') ?></th>
<th class="column-18"><?= $paginator->order(t('Username'), 'username') ?></th>
<th class="column-18"><?= $paginator->order(t('Name'), 'name') ?></th>
<th class="column-15"><?= $paginator->order(t('Email'), 'email') ?></th>
@@ -26,7 +27,9 @@
<?php foreach ($paginator->getCollection() as $user): ?>
<tr>
<td>
- <?= '#'.$user['id'] ?>&nbsp;
+ <?= '#'.$user['id'] ?>
+ </td>
+ <td>
<?= $this->url->link($this->text->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?>
</td>
<td>
diff --git a/app/Template/user/notifications.php b/app/Template/user/notifications.php
index 2a5c8152..6e1a0004 100644
--- a/app/Template/user/notifications.php
+++ b/app/Template/user/notifications.php
@@ -3,11 +3,8 @@
</div>
<form method="post" action="<?= $this->url->href('user', 'notifications', array('user_id' => $user['id'])) ?>" autocomplete="off">
-
<?= $this->form->csrf() ?>
- <?= $this->form->checkbox('notifications_enabled', t('Enable notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br>
- <hr>
<h4><?= t('Notification methods:') ?></h4>
<?= $this->form->checkboxes('notification_types', $types, $notifications) ?>
diff --git a/app/Template/user/profile.php b/app/Template/user/profile.php
index 80a633e3..9c9d3282 100644
--- a/app/Template/user/profile.php
+++ b/app/Template/user/profile.php
@@ -1,5 +1,6 @@
<section id="main">
<br>
+ <?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path']) ?>
<ul class="listing">
<li><?= t('Username:') ?> <strong><?= $this->text->e($user['username']) ?></strong></li>
<li><?= t('Name:') ?> <strong><?= $this->text->e($user['name']) ?: t('None') ?></strong></li>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index 20fd2ad2..5ea2e355 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -37,6 +37,9 @@
<li <?= $this->app->checkMenuSelection('user', 'edit') ?>>
<?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
</li>
+ <li <?= $this->app->checkMenuSelection('AvatarFile') ?>>
+ <?= $this->url->link(t('Avatar'), 'AvatarFile', 'show', array('user_id' => $user['id'])) ?>
+ </li>
<?php endif ?>
<?php if ($user['is_ldap_user'] == 0): ?>
diff --git a/app/User/Avatar/AvatarFileProvider.php b/app/User/Avatar/AvatarFileProvider.php
new file mode 100644
index 00000000..eea565f0
--- /dev/null
+++ b/app/User/Avatar/AvatarFileProvider.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Kanboard\User\Avatar;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\User\Avatar\AvatarProviderInterface;
+
+/**
+ * Avatar Local Image File Provider
+ *
+ * @package avatar
+ * @author Frederic Guillot
+ */
+class AvatarFileProvider extends Base implements AvatarProviderInterface
+{
+ /**
+ * Render avatar html
+ *
+ * @access public
+ * @param array $user
+ * @param int $size
+ * @return string
+ */
+ public function render(array $user, $size)
+ {
+ $url = $this->helper->url->href('AvatarFile', 'image', array('user_id' => $user['id'], 'size' => $size));
+ $title = $this->helper->text->e($user['name'] ?: $user['username']);
+ return '<img src="' . $url . '" alt="' . $title . '" title="' . $title . '">';
+ }
+
+ /**
+ * Determine if the provider is active
+ *
+ * @access public
+ * @param array $user
+ * @return boolean
+ */
+ public function isActive(array $user)
+ {
+ return !empty($user['avatar_path']);
+ }
+}
diff --git a/app/User/Avatar/GravatarProvider.php b/app/User/Avatar/GravatarProvider.php
index 7a719734..87ca51b1 100644
--- a/app/User/Avatar/GravatarProvider.php
+++ b/app/User/Avatar/GravatarProvider.php
@@ -17,8 +17,9 @@ class GravatarProvider extends Base implements AvatarProviderInterface
* Render avatar html
*
* @access public
- * @param array $user
- * @param int $size
+ * @param array $user
+ * @param int $size
+ * @return string
*/
public function render(array $user, $size)
{
diff --git a/app/User/Avatar/LetterAvatarProvider.php b/app/User/Avatar/LetterAvatarProvider.php
index 81c4586d..f9659e61 100644
--- a/app/User/Avatar/LetterAvatarProvider.php
+++ b/app/User/Avatar/LetterAvatarProvider.php
@@ -24,13 +24,14 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface
* Render avatar html
*
* @access public
- * @param array $user
- * @param int $size
+ * @param array $user
+ * @param int $size
+ * @return string
*/
public function render(array $user, $size)
{
$initials = $this->helper->user->getInitials($user['name'] ?: $user['username']);
- $rgb = $this->getBackgroundColor($initials);
+ $rgb = $this->getBackgroundColor($user['name'] ?: $user['username']);
return sprintf(
'<div class="avatar-letter" style="background-color: rgb(%d, %d, %d)" title="%s">%s</div>',
diff --git a/app/common.php b/app/common.php
index 7dbd7587..da624844 100644
--- a/app/common.php
+++ b/app/common.php
@@ -39,4 +39,5 @@ $container->register(new Kanboard\ServiceProvider\RouteProvider);
$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\PluginProvider);
diff --git a/assets/css/app.css b/assets/css/app.css
index fc82c9e2..0081e693 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -24,4 +24,4 @@
* @link https://github.com/NextStepWebs/simplemde-markdown-editor
* @license MIT
*/
-.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror,.CodeMirror-scroll{min-height:300px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)}li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}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{list-style-type:none;margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.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}.smaller{font-size:.85em}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h2{font-size:1.3em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.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{width: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,0.55)}tr.draggable-item-selected td{border-top:0;border-bottom:0}tr.draggable-item-selected td:first-child{border-left:0}tr.draggable-item-selected td:last-child{border-right:0}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#fefff2}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}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,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input.form-numeric,input[type="number"]{width:70px}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}::-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:bold}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:0}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0;margin-right:15px}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-datetime,input.form-date{width:150px}input.form-input-large{width:400px}.form-column{float:left;margin-right:3%;max-width:50%;min-width:40%}.form-column ul{margin-top:15px}.form-clear{clear:both;padding-top:20px;padding-bottom:10px}.form-login{width:350px;margin:0 auto;margin-top:8%}.form-column li,.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:bold}.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:normal;cursor:pointer;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}a.btn{text-decoration:none}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:hover,.btn-red:focus{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:hover,.btn-blue:focus{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 li{margin-left:25px}.alert-fade-out{position:fixed;bottom:0;left:0;width:100%;font-size:1.1em;padding-top:15px;padding-bottom:15px;margin-bottom:0;border-width:1px 0 0;border-radius:0;z-index:9999}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.ui-tooltip-content .markdown p{margin-bottom:0}.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;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#777;text-decoration:none}nav .active a{color:#333;font-weight:bold}.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:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.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:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}@media only screen and (max-width:640px){.page-header-mobile li{display:block;margin-bottom:5px}}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.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:bold;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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.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}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.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;border:1px solid #000;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:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.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 1px 2px;border-radius:4px}.task-board-category:hover{opacity:.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee:hover{opacity:.6}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-board .task-score{font-weight:bold}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 3px 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-container{border:2px solid #000;border-radius:8px;padding:15px;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}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000;cursor:pointer}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,0.9)}.assign-me{font-size:.8em;vertical-align:bottom}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-username{font-weight:bold;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.task-links-table{font-size:.85em}.task-links-table td{vertical-align:middle}.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}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.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-bottom: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:bold;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}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:15px;padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:normal;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:bold;color:#000;border-bottom:1px dotted #efefef}.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:bold;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination{text-align:center}.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,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 a{text-decoration:none;color:#999;font-weight:300}.sidebar a:hover{color:#333}.sidebar li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef;padding-left:13px}.sidebar li:hover{border-left:5px solid #555;padding-left:8px}.sidebar li.active{border-left:5px solid #333;padding-left:8px}.sidebar li.active a{color:#333;font-weight:bold}.sidebar-icons li{padding-left:0}.sidebar-icons li:hover,.sidebar-icons li.active{padding-left:0;border-left:0}.sidebar li.active a:focus,.sidebar li.active a:hover{color:#555}.sidebar li:last-child{margin-bottom:15px}@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;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.textarea-dropdown li,.dropdown-submenu-open li{display:block;margin:0;padding:0;padding-left:10px;padding-right:10px;padding-top:8px;padding-bottom:8px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.textarea-dropdown li:last-child,.dropdown-submenu-open li:last-child{border:0}.textarea-dropdown .active,.textarea-dropdown li:hover,.dropdown-submenu-open li:not(.no-hover):hover{background:#4078c0;color:#fff}.textarea-dropdown .active a,.textarea-dropdown li:hover a,.dropdown-submenu-open li:hover a{color:#fff}.textarea-dropdown a,.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-text,.dropdown-menu-link-icon{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}#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:bold;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.project-header .filter-box{margin:0}.filter-box form{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,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:0;margin:0;padding:0;padding-left:5px;padding-right:8px;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-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:0}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;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}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:-webkit-flex;display:flex;-webkit-flex-direction:row;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{text-align:center;margin-right:80px;padding:3px 15px 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,0.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail img:hover{opacity:.5}.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}.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;padding-left:8px;padding-right:8px;padding-top:5px;padding-bottom:5px;display:inline}.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:bold;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=) repeat-x scroll 0 10px}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px;margin-bottom:25px}.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{margin-bottom:25px}.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 img,.avatar-48 div{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 img,.avatar-20 div{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff;text-align:center} \ No newline at end of file
+.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror,.CodeMirror-scroll{min-height:300px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)}li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}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{list-style-type:none;margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.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}.smaller{font-size:.85em}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h2{font-size:1.3em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.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{width: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,0.55)}tr.draggable-item-selected td{border-top:0;border-bottom:0}tr.draggable-item-selected td:first-child{border-left:0}tr.draggable-item-selected td:last-child{border-right:0}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#fefff2}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}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,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input.form-numeric,input[type="number"]{width:70px}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}::-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:bold}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:0}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0;margin-right:15px}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-datetime,input.form-date{width:150px}input.form-input-large{width:400px}.form-column{float:left;margin-right:3%;max-width:50%;min-width:40%}.form-column ul{margin-top:15px}.form-clear{clear:both;padding-top:20px;padding-bottom:10px}.form-login{width:350px;margin:0 auto;margin-top:8%}.form-column li,.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:bold}.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:normal;cursor:pointer;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}a.btn{text-decoration:none}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:hover,.btn-red:focus{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:hover,.btn-blue:focus{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 li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;margin-bottom:0;border-width:1px 0 0;border-radius:0;border-top-left-radius:4px;border-top-right-radius:4px;z-index:9999}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.ui-tooltip-content .markdown p{margin-bottom:0}.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;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#777;text-decoration:none}nav .active a{color:#333;font-weight:bold}.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:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.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:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.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:bold;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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.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}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.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;border:1px solid #000;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:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.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 1px 2px;border-radius:4px}.task-board-category:hover{opacity:.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee:hover{opacity:.6}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-board .task-score{font-weight:bold}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 3px 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;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}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000;cursor:pointer}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,0.9)}.assign-me{font-size:.8em;vertical-align:bottom}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-username{font-weight:bold;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.task-links-table{font-size:.85em}.task-links-table td{vertical-align:middle}.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}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.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-bottom: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:bold;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}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:15px;padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:normal;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:bold;color:#000;border-bottom:1px dotted #efefef}.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:bold;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination{text-align:center}.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,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 a{text-decoration:none;color:#999;font-weight:300}.sidebar a:hover{color:#333}.sidebar li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef;padding-left:13px}.sidebar li:hover{border-left:5px solid #555;padding-left:8px}.sidebar li.active{border-left:5px solid #333;padding-left:8px}.sidebar li.active a{color:#333;font-weight:bold}.sidebar-icons li{padding-left:0}.sidebar-icons li:hover,.sidebar-icons li.active{padding-left:0;border-left:0}.sidebar li.active a:focus,.sidebar li.active a:hover{color:#555}.sidebar li:last-child{margin-bottom:15px}@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;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.textarea-dropdown li,.dropdown-submenu-open li{display:block;margin:0;padding:0;padding-left:10px;padding-right:10px;padding-top:8px;padding-bottom:8px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.textarea-dropdown li:last-child,.dropdown-submenu-open li:last-child{border:0}.textarea-dropdown .active,.textarea-dropdown li:hover,.dropdown-submenu-open li:not(.no-hover):hover{background:#4078c0;color:#fff}.textarea-dropdown .active a,.textarea-dropdown li:hover a,.dropdown-submenu-open li:hover a{color:#fff}.textarea-dropdown a,.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-text,.dropdown-menu-link-icon{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}#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:bold;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:hover,.action-menu:focus{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.project-header .filter-box{margin:0}.filter-box form{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,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:0;margin:0;padding:0;padding-left:5px;padding-right:8px;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-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:0}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;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}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:-webkit-flex;display:flex;-webkit-flex-direction:row;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{text-align:center;margin-right:80px;padding:3px 15px 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,0.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail img:hover{opacity:.5}.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}.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;padding-left:8px;padding-right:8px;padding-top:5px;padding-bottom:5px;display:inline}.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:bold;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=) repeat-x scroll 0 10px}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px;margin-bottom:25px}.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{margin-bottom:25px}.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 img,.avatar-48 div{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 img,.avatar-20 div{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff;text-align:center} \ No newline at end of file
diff --git a/assets/css/print.css b/assets/css/print.css
index 8cb0303a..b9d0acd1 100644
--- a/assets/css/print.css
+++ b/assets/css/print.css
@@ -24,4 +24,4 @@
* @link https://github.com/NextStepWebs/simplemde-markdown-editor
* @license MIT
*/
-.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror,.CodeMirror-scroll{min-height:300px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)}header,.sidebar,#comments form,.page-header{display:none}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.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{width: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,0.55)}tr.draggable-item-selected td{border-top:0;border-bottom:0}tr.draggable-item-selected td:first-child{border-left:0}tr.draggable-item-selected td:last-child{border-right:0}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#fefff2}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.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:bold;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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.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}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.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;border:1px solid #000;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:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.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 1px 2px;border-radius:4px}.task-board-category:hover{opacity:.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee:hover{opacity:.6}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-board .task-score{font-weight:bold}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 3px 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-container{border:2px solid #000;border-radius:8px;padding:15px;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}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000;cursor:pointer}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,0.9)}.assign-me{font-size:.8em;vertical-align:bottom}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-username{font-weight:bold;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.task-links-table{font-size:.85em}.task-links-table td{vertical-align:middle}.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}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.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-bottom: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:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} \ No newline at end of file
+.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror,.CodeMirror-scroll{min-height:300px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)}header,.sidebar,#comments form,.page-header{display:none}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.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{width: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,0.55)}tr.draggable-item-selected td{border-top:0;border-bottom:0}tr.draggable-item-selected td:first-child{border-left:0}tr.draggable-item-selected td:last-child{border-right:0}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#fefff2}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.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:bold;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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.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}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.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;border:1px solid #000;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:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.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 1px 2px;border-radius:4px}.task-board-category:hover{opacity:.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee:hover{opacity:.6}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-board .task-score{font-weight:bold}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 3px 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;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}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000;cursor:pointer}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,0.9)}.assign-me{font-size:.8em;vertical-align:bottom}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-username{font-weight:bold;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.task-links-table{font-size:.85em}.task-links-table td{vertical-align:middle}.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}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.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-bottom: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:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} \ No newline at end of file
diff --git a/assets/css/src/alert.css b/assets/css/src/alert.css
index 874f37dd..99a8d254 100644
--- a/assets/css/src/alert.css
+++ b/assets/css/src/alert.css
@@ -43,15 +43,17 @@
}
.alert-fade-out {
+ text-align: center;
position: fixed;
bottom: 0;
- left: 0;
- width: 100%;
- font-size: 1.1em;
- padding-top: 15px;
- padding-bottom: 15px;
+ left: 20%;
+ width: 60%;
+ padding-top: 5px;
+ padding-bottom: 5px;
margin-bottom: 0;
border-width: 1px 0 0;
border-radius: 0;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
z-index: 9999;
}
diff --git a/assets/css/src/filters.css b/assets/css/src/filters.css
index c736787e..3e362863 100644
--- a/assets/css/src/filters.css
+++ b/assets/css/src/filters.css
@@ -1,8 +1,20 @@
+/* project header */
.project-header {
margin-top: 8px;
margin-bottom: 20px;
}
+.action-menu {
+ color: #333;
+ text-decoration: none;
+}
+
+.action-menu:hover,
+.action-menu:focus {
+ text-decoration: underline;
+}
+
+/* search box */
.filter-box {
display: inline-block;
position: relative;
diff --git a/assets/css/src/header.css b/assets/css/src/header.css
index f4903128..530eba45 100644
--- a/assets/css/src/header.css
+++ b/assets/css/src/header.css
@@ -116,10 +116,3 @@ header h1 .tooltip {
.menu-inline {
margin-bottom: 5px;
}
-
-@media only screen and (max-width: 640px) {
- .page-header-mobile li {
- display: block;
- margin-bottom: 5px;
- }
-}
diff --git a/assets/css/src/task.css b/assets/css/src/task.css
index 25de32eb..271d2f13 100644
--- a/assets/css/src/task.css
+++ b/assets/css/src/task.css
@@ -167,6 +167,11 @@ span.task-board-age-column {
padding-top: 0;
}
+.task-summary-buttons {
+ margin-top: 10px;
+ font-size: 0.85em;
+}
+
.task-summary-container {
border: 2px solid #000;
border-radius: 8px;
diff --git a/assets/js/app.js b/assets/js/app.js
index fb3b09e8..3857f3fd 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -64,4 +64,4 @@ var i=e[r];t+=i.chunkSize(),n+=i.height,i.parent=this}this.size=t,this.height=n,
},zl)}),deleteH:Ht(function(e,t){var n=this.doc.sel,r=this.doc;n.somethingSelected()?r.replaceSelection("",null,"+delete"):_n(this,function(n){var i=qn(r,n.head,e,t,!1);return 0>e?{from:i,to:n.head}:{from:n.head,to:i}})}),findPosV:function(e,t,n,r){var i=1,o=r;0>t&&(i=-1,t=-t);for(var l=0,a=me(this.doc,e);t>l;++l){var s=ft(this,a,"div");if(null==o?o=s.left:s.left=o,a=Un(this,s,i,n),a.hitSide)break}return a},moveV:Ht(function(e,t){var n=this,r=this.doc,i=[],o=!n.display.shift&&!r.extend&&r.sel.somethingSelected();if(r.extendSelectionsBy(function(l){if(o)return 0>e?l.from():l.to();var a=ft(n,l.head,"div");null!=l.goalColumn&&(a.left=l.goalColumn),i.push(a.left);var s=Un(n,a,e,t);return"page"==t&&l==r.sel.primary()&&Fn(n,null,ht(n,s,"div").top-a.top),s},zl),i.length)for(var l=0;l<r.sel.ranges.length;l++)r.sel.ranges[l].goalColumn=i[l]}),findWordAt:function(e){var t=this.doc,n=Zr(t,e.line).text,r=e.ch,i=e.ch;if(n){var o=this.getHelper(e,"wordChars");(e.xRel<0||i==n.length)&&r?--r:++i;for(var l=n.charAt(r),a=Ri(l,o)?function(e){return Ri(e,o)}:/\s/.test(l)?function(e){return/\s/.test(e)}:function(e){return!/\s/.test(e)&&!Ri(e)};r>0&&a(n.charAt(r-1));)--r;for(;i<n.length&&a(n.charAt(i));)++i}return new de(Fo(e.line,r),Fo(e.line,i))},toggleOverwrite:function(e){null!=e&&e==this.state.overwrite||((this.state.overwrite=!this.state.overwrite)?Zl(this.display.cursorDiv,"CodeMirror-overwrite"):Yl(this.display.cursorDiv,"CodeMirror-overwrite"),Wl(this,"overwriteToggle",this,this.state.overwrite))},hasFocus:function(){return this.display.input.getField()==Gi()},isReadOnly:function(){return!(!this.options.readOnly&&!this.doc.cantEdit)},scrollTo:Ht(function(e,t){null==e&&null==t||Rn(this),null!=e&&(this.curOp.scrollLeft=e),null!=t&&(this.curOp.scrollTop=t)}),getScrollInfo:function(){var e=this.display.scroller;return{left:e.scrollLeft,top:e.scrollTop,height:e.scrollHeight-$e(this)-this.display.barHeight,width:e.scrollWidth-$e(this)-this.display.barWidth,clientHeight:Ke(this),clientWidth:Ve(this)}},scrollIntoView:Ht(function(e,t){if(null==e?(e={from:this.doc.sel.primary().head,to:null},null==t&&(t=this.options.cursorScrollMargin)):"number"==typeof e?e={from:Fo(e,0),to:null}:null==e.from&&(e={from:e,to:null}),e.to||(e.to=e.from),e.margin=t||0,null!=e.from.line)Rn(this),this.curOp.scrollToPos=e;else{var n=Pn(this,Math.min(e.from.left,e.to.left),Math.min(e.from.top,e.to.top)-e.margin,Math.max(e.from.right,e.to.right),Math.max(e.from.bottom,e.to.bottom)+e.margin);this.scrollTo(n.scrollLeft,n.scrollTop)}}),setSize:Ht(function(e,t){function n(e){return"number"==typeof e||/^\d+$/.test(String(e))?e+"px":e}var r=this;null!=e&&(r.display.wrapper.style.width=n(e)),null!=t&&(r.display.wrapper.style.height=n(t)),r.options.lineWrapping&&lt(this);var i=r.display.viewFrom;r.doc.iter(i,r.display.viewTo,function(e){if(e.widgets)for(var t=0;t<e.widgets.length;t++)if(e.widgets[t].noHScroll){Pt(r,i,"widget");break}++i}),r.curOp.forceUpdate=!0,Wl(r,"refresh",this)}),operation:function(e){return At(this,e)},refresh:Ht(function(){var e=this.display.cachedTextHeight;Dt(this),this.curOp.forceUpdate=!0,at(this),this.scrollTo(this.doc.scrollLeft,this.doc.scrollTop),u(this),(null==e||Math.abs(e-yt(this.display))>.5)&&l(this),Wl(this,"refresh",this)}),swapDoc:Ht(function(e){var t=this.doc;return t.cm=null,Yr(this,e),at(this),this.display.input.reset(),this.scrollTo(e.scrollLeft,e.scrollTop),this.curOp.forceScroll=!0,Si(this,"swapDoc",this,t),t}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Ai(e);var Jo=e.defaults={},el=e.optionHandlers={},tl=e.Init={toString:function(){return"CodeMirror.Init"}};Gn("value","",function(e,t){e.setValue(t)},!0),Gn("mode",null,function(e,t){e.doc.modeOption=t,n(e)},!0),Gn("indentUnit",2,n,!0),Gn("indentWithTabs",!1),Gn("smartIndent",!0),Gn("tabSize",4,function(e){r(e),at(e),Dt(e)},!0),Gn("lineSeparator",null,function(e,t){if(e.doc.lineSep=t,t){var n=[],r=e.doc.first;e.doc.iter(function(e){for(var i=0;;){var o=e.text.indexOf(t,i);if(-1==o)break;i=o+t.length,n.push(Fo(r,o))}r++});for(var i=n.length-1;i>=0;i--)Wn(e.doc,t,n[i],Fo(n[i].line,n[i].ch+t.length))}}),Gn("specialChars",/[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g,function(t,n,r){t.state.specialChars=new RegExp(n.source+(n.test(" ")?"":"| "),"g"),r!=e.Init&&t.refresh()}),Gn("specialCharPlaceholder",Rr,function(e){e.refresh()},!0),Gn("electricChars",!0),Gn("inputStyle",Ao?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),Gn("rtlMoveVisually",!Ho),Gn("wholeLineUpdateBefore",!0),Gn("theme","default",function(e){a(e),s(e)},!0),Gn("keyMap","default",function(t,n,r){var i=Vn(n),o=r!=e.Init&&Vn(r);o&&o.detach&&o.detach(t,i),i.attach&&i.attach(t,o||null)}),Gn("extraKeys",null),Gn("lineWrapping",!1,i,!0),Gn("gutters",[],function(e){f(e.options),s(e)},!0),Gn("fixedGutter",!0,function(e,t){e.display.gutters.style.left=t?S(e.display)+"px":"0",e.refresh()},!0),Gn("coverGutterNextToScrollbar",!1,function(e){y(e)},!0),Gn("scrollbarStyle","native",function(e){v(e),y(e),e.display.scrollbars.setScrollTop(e.doc.scrollTop),e.display.scrollbars.setScrollLeft(e.doc.scrollLeft)},!0),Gn("lineNumbers",!1,function(e){f(e.options),s(e)},!0),Gn("firstLineNumber",1,s,!0),Gn("lineNumberFormatter",function(e){return e},s,!0),Gn("showCursorWhenSelecting",!1,Ee,!0),Gn("resetSelectionOnContextMenu",!0),Gn("lineWiseCopyCut",!0),Gn("readOnly",!1,function(e,t){"nocursor"==t?(yn(e),e.display.input.blur(),e.display.disabled=!0):e.display.disabled=!1,e.display.input.readOnlyChanged(t)}),Gn("disableInput",!1,function(e,t){t||e.display.input.reset()},!0),Gn("dragDrop",!0,qt),Gn("allowDropFileTypes",null),Gn("cursorBlinkRate",530),Gn("cursorScrollMargin",0),Gn("cursorHeight",1,Ee,!0),Gn("singleCursorHeightPerLine",!0,Ee,!0),Gn("workTime",100),Gn("workDelay",100),Gn("flattenSpans",!0,r,!0),Gn("addModeClass",!1,r,!0),Gn("pollInterval",100),Gn("undoDepth",200,function(e,t){e.doc.history.undoDepth=t}),Gn("historyEventDelay",1250),Gn("viewportMargin",10,function(e){e.refresh()},!0),Gn("maxHighlightLength",1e4,r,!0),Gn("moveInputWithCursor",!0,function(e,t){t||e.display.input.resetPosition()}),Gn("tabindex",null,function(e,t){e.display.input.getField().tabIndex=t||""}),Gn("autofocus",null);var nl=e.modes={},rl=e.mimeModes={};e.defineMode=function(t,n){e.defaults.mode||"null"==t||(e.defaults.mode=t),arguments.length>2&&(n.dependencies=Array.prototype.slice.call(arguments,2)),nl[t]=n},e.defineMIME=function(e,t){rl[e]=t},e.resolveMode=function(t){if("string"==typeof t&&rl.hasOwnProperty(t))t=rl[t];else if(t&&"string"==typeof t.name&&rl.hasOwnProperty(t.name)){var n=rl[t.name];"string"==typeof n&&(n={name:n}),t=Pi(n,t),t.name=n.name}else if("string"==typeof t&&/^[\w\-]+\/[\w\-]+\+xml$/.test(t))return e.resolveMode("application/xml");return"string"==typeof t?{name:t}:t||{name:"null"}},e.getMode=function(t,n){var n=e.resolveMode(n),r=nl[n.name];if(!r)return e.getMode(t,"text/plain");var i=r(t,n);if(il.hasOwnProperty(n.name)){var o=il[n.name];for(var l in o)o.hasOwnProperty(l)&&(i.hasOwnProperty(l)&&(i["_"+l]=i[l]),i[l]=o[l])}if(i.name=n.name,n.helperType&&(i.helperType=n.helperType),n.modeProps)for(var l in n.modeProps)i[l]=n.modeProps[l];return i},e.defineMode("null",function(){return{token:function(e){e.skipToEnd()}}}),e.defineMIME("text/plain","null");var il=e.modeExtensions={};e.extendMode=function(e,t){var n=il.hasOwnProperty(e)?il[e]:il[e]={};Fi(t,n)},e.defineExtension=function(t,n){e.prototype[t]=n},e.defineDocExtension=function(e,t){Cl.prototype[e]=t},e.defineOption=Gn;var ol=[];e.defineInitHook=function(e){ol.push(e)};var ll=e.helpers={};e.registerHelper=function(t,n,r){ll.hasOwnProperty(t)||(ll[t]=e[t]={_global:[]}),ll[t][n]=r},e.registerGlobalHelper=function(t,n,r,i){e.registerHelper(t,n,i),ll[t]._global.push({pred:r,val:i})};var al=e.copyState=function(e,t){if(t===!0)return t;if(e.copyState)return e.copyState(t);var n={};for(var r in t){var i=t[r];i instanceof Array&&(i=i.concat([])),n[r]=i}return n},sl=e.startState=function(e,t,n){return e.startState?e.startState(t,n):!0};e.innerMode=function(e,t){for(;e.innerMode;){var n=e.innerMode(t);if(!n||n.mode==e)break;t=n.state,e=n.mode}return n||{mode:e,state:t}};var cl=e.commands={selectAll:function(e){e.setSelection(Fo(e.firstLine(),0),Fo(e.lastLine()),Pl)},singleSelection:function(e){e.setSelection(e.getCursor("anchor"),e.getCursor("head"),Pl)},killLine:function(e){_n(e,function(t){if(t.empty()){var n=Zr(e.doc,t.head.line).text.length;return t.head.ch==n&&t.head.line<e.lastLine()?{from:t.head,to:Fo(t.head.line+1,0)}:{from:t.head,to:Fo(t.head.line,n)}}return{from:t.from(),to:t.to()}})},deleteLine:function(e){_n(e,function(t){return{from:Fo(t.from().line,0),to:me(e.doc,Fo(t.to().line+1,0))}})},delLineLeft:function(e){_n(e,function(e){return{from:Fo(e.from().line,0),to:e.from()}})},delWrappedLineLeft:function(e){_n(e,function(t){var n=e.charCoords(t.head,"div").top+5,r=e.coordsChar({left:0,top:n},"div");return{from:r,to:t.from()}})},delWrappedLineRight:function(e){_n(e,function(t){var n=e.charCoords(t.head,"div").top+5,r=e.coordsChar({left:e.display.lineDiv.offsetWidth+100,top:n},"div");return{from:t.from(),to:r}})},undo:function(e){e.undo()},redo:function(e){e.redo()},undoSelection:function(e){e.undoSelection()},redoSelection:function(e){e.redoSelection()},goDocStart:function(e){e.extendSelection(Fo(e.firstLine(),0))},goDocEnd:function(e){e.extendSelection(Fo(e.lastLine()))},goLineStart:function(e){e.extendSelectionsBy(function(t){return oo(e,t.head.line)},{origin:"+move",bias:1})},goLineStartSmart:function(e){e.extendSelectionsBy(function(t){return ao(e,t.head)},{origin:"+move",bias:1})},goLineEnd:function(e){e.extendSelectionsBy(function(t){return lo(e,t.head.line)},{origin:"+move",bias:-1})},goLineRight:function(e){e.extendSelectionsBy(function(t){var n=e.charCoords(t.head,"div").top+5;return e.coordsChar({left:e.display.lineDiv.offsetWidth+100,top:n},"div")},zl)},goLineLeft:function(e){e.extendSelectionsBy(function(t){var n=e.charCoords(t.head,"div").top+5;return e.coordsChar({left:0,top:n},"div")},zl)},goLineLeftSmart:function(e){e.extendSelectionsBy(function(t){var n=e.charCoords(t.head,"div").top+5,r=e.coordsChar({left:0,top:n},"div");return r.ch<e.getLine(r.line).search(/\S/)?ao(e,t.head):r},zl)},goLineUp:function(e){e.moveV(-1,"line")},goLineDown:function(e){e.moveV(1,"line")},goPageUp:function(e){e.moveV(-1,"page")},goPageDown:function(e){e.moveV(1,"page")},goCharLeft:function(e){e.moveH(-1,"char")},goCharRight:function(e){e.moveH(1,"char")},goColumnLeft:function(e){e.moveH(-1,"column")},goColumnRight:function(e){e.moveH(1,"column")},goWordLeft:function(e){e.moveH(-1,"word")},goGroupRight:function(e){e.moveH(1,"group")},goGroupLeft:function(e){e.moveH(-1,"group")},goWordRight:function(e){e.moveH(1,"word")},delCharBefore:function(e){e.deleteH(-1,"char")},delCharAfter:function(e){e.deleteH(1,"char")},delWordBefore:function(e){e.deleteH(-1,"word")},delWordAfter:function(e){e.deleteH(1,"word")},delGroupBefore:function(e){e.deleteH(-1,"group")},delGroupAfter:function(e){e.deleteH(1,"group")},indentAuto:function(e){e.indentSelection("smart")},indentMore:function(e){e.indentSelection("add")},indentLess:function(e){e.indentSelection("subtract")},insertTab:function(e){e.replaceSelection(" ")},insertSoftTab:function(e){for(var t=[],n=e.listSelections(),r=e.options.tabSize,i=0;i<n.length;i++){var o=n[i].from(),l=Rl(e.getLine(o.line),o.ch,r);t.push(new Array(r-l%r+1).join(" "))}e.replaceSelections(t)},defaultTab:function(e){e.somethingSelected()?e.indentSelection("add"):e.execCommand("insertTab")},transposeChars:function(e){At(e,function(){for(var t=e.listSelections(),n=[],r=0;r<t.length;r++){var i=t[r].head,o=Zr(e.doc,i.line).text;if(o)if(i.ch==o.length&&(i=new Fo(i.line,i.ch-1)),i.ch>0)i=new Fo(i.line,i.ch+1),e.replaceRange(o.charAt(i.ch-1)+o.charAt(i.ch-2),Fo(i.line,i.ch-2),i,"+transpose");else if(i.line>e.doc.first){var l=Zr(e.doc,i.line-1).text;l&&e.replaceRange(o.charAt(0)+e.doc.lineSeparator()+l.charAt(l.length-1),Fo(i.line-1,l.length-1),Fo(i.line,1),"+transpose")}n.push(new de(i,i))}e.setSelections(n)})},newlineAndIndent:function(e){At(e,function(){for(var t=e.listSelections().length,n=0;t>n;n++){var r=e.listSelections()[n];e.replaceRange(e.doc.lineSeparator(),r.anchor,r.head,"+input"),e.indentLine(r.from().line+1,null,!0)}zn(e)})},toggleOverwrite:function(e){e.toggleOverwrite()}},ul=e.keyMap={};ul.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},ul.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},ul.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars"},ul.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},ul["default"]=Oo?ul.macDefault:ul.pcDefault,e.normalizeKeyMap=function(e){var t={};for(var n in e)if(e.hasOwnProperty(n)){var r=e[n];if(/^(name|fallthrough|(de|at)tach)$/.test(n))continue;if("..."==r){delete e[n];continue}for(var i=Ei(n.split(" "),$n),o=0;o<i.length;o++){var l,a;o==i.length-1?(a=i.join(" "),l=r):(a=i.slice(0,o+1).join(" "),l="...");var s=t[a];if(s){if(s!=l)throw new Error("Inconsistent bindings for "+a)}else t[a]=l}delete e[n]}for(var c in t)e[c]=t[c];return e};var dl=e.lookupKey=function(e,t,n,r){t=Vn(t);var i=t.call?t.call(e,r):t[e];if(i===!1)return"nothing";if("..."===i)return"multi";if(null!=i&&n(i))return"handled";if(t.fallthrough){if("[object Array]"!=Object.prototype.toString.call(t.fallthrough))return dl(e,t.fallthrough,n,r);for(var o=0;o<t.fallthrough.length;o++){var l=dl(e,t.fallthrough[o],n,r);if(l)return l}}},hl=e.isModifierKey=function(e){var t="string"==typeof e?e:ia[e.keyCode];return"Ctrl"==t||"Alt"==t||"Shift"==t||"Mod"==t},fl=e.keyName=function(e,t){if(So&&34==e.keyCode&&e["char"])return!1;var n=ia[e.keyCode],r=n;return null==r||e.altGraphKey?!1:(e.altKey&&"Alt"!=n&&(r="Alt-"+r),(Io?e.metaKey:e.ctrlKey)&&"Ctrl"!=n&&(r="Ctrl-"+r),(Io?e.ctrlKey:e.metaKey)&&"Cmd"!=n&&(r="Cmd-"+r),!t&&e.shiftKey&&"Shift"!=n&&(r="Shift-"+r),r)};e.fromTextArea=function(t,n){function r(){t.value=c.getValue()}if(n=n?Fi(n):{},n.value=t.value,!n.tabindex&&t.tabIndex&&(n.tabindex=t.tabIndex),!n.placeholder&&t.placeholder&&(n.placeholder=t.placeholder),null==n.autofocus){var i=Gi();n.autofocus=i==t||null!=t.getAttribute("autofocus")&&i==document.body}if(t.form&&(Al(t.form,"submit",r),!n.leaveSubmitMethodAlone)){var o=t.form,l=o.submit;try{var a=o.submit=function(){r(),o.submit=l,o.submit(),o.submit=a}}catch(s){}}n.finishInit=function(e){e.save=r,e.getTextArea=function(){return t},e.toTextArea=function(){e.toTextArea=isNaN,r(),t.parentNode.removeChild(e.getWrapperElement()),t.style.display="",t.form&&(Hl(t.form,"submit",r),"function"==typeof t.form.submit&&(t.form.submit=l))}},t.style.display="none";var c=e(function(e){t.parentNode.insertBefore(e,t.nextSibling)},n);return c};var pl=e.StringStream=function(e,t){this.pos=this.start=0,this.string=e,this.tabSize=t||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0};pl.prototype={eol:function(){return this.pos>=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.pos<this.string.length?this.string.charAt(this.pos++):void 0},eat:function(e){var t=this.string.charAt(this.pos);if("string"==typeof e)var n=t==e;else var n=t&&(e.test?e.test(t):e(t));return n?(++this.pos,t):void 0},eatWhile:function(e){for(var t=this.pos;this.eat(e););return this.pos>t},eatSpace:function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},skipToEnd:function(){this.pos=this.string.length},skipTo:function(e){var t=this.string.indexOf(e,this.pos);return t>-1?(this.pos=t,!0):void 0},backUp:function(e){this.pos-=e},column:function(){return this.lastColumnPos<this.start&&(this.lastColumnValue=Rl(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start),this.lastColumnValue-(this.lineStart?Rl(this.string,this.lineStart,this.tabSize):0)},indentation:function(){return Rl(this.string,null,this.tabSize)-(this.lineStart?Rl(this.string,this.lineStart,this.tabSize):0)},match:function(e,t,n){if("string"!=typeof e){var r=this.string.slice(this.pos).match(e);return r&&r.index>0?null:(r&&t!==!1&&(this.pos+=r[0].length),r)}var i=function(e){return n?e.toLowerCase():e},o=this.string.substr(this.pos,e.length);return i(o)==i(e)?(t!==!1&&(this.pos+=e.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}}};var ml=0,gl=e.TextMarker=function(e,t){this.lines=[],this.type=t,this.doc=e,this.id=++ml};Ai(gl),gl.prototype.clear=function(){if(!this.explicitlyCleared){var e=this.doc.cm,t=e&&!e.curOp;if(t&&bt(e),Ni(this,"clear")){var n=this.find();n&&Si(this,"clear",n.from,n.to)}for(var r=null,i=null,o=0;o<this.lines.length;++o){var l=this.lines[o],a=er(l.markedSpans,this);e&&!this.collapsed?Pt(e,ti(l),"text"):e&&(null!=a.to&&(i=ti(l)),null!=a.from&&(r=ti(l))),l.markedSpans=tr(l.markedSpans,a),null==a.from&&this.collapsed&&!kr(this.doc,l)&&e&&ei(l,yt(e.display))}if(e&&this.collapsed&&!e.options.lineWrapping)for(var o=0;o<this.lines.length;++o){var s=yr(this.lines[o]),c=d(s);c>e.display.maxLineLength&&(e.display.maxLine=s,e.display.maxLineLength=c,e.display.maxLineChanged=!0)}null!=r&&e&&this.collapsed&&Dt(e,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&Ae(e.doc)),e&&Si(e,"markerCleared",e,this),t&&kt(e),this.parent&&this.parent.clear()}},gl.prototype.find=function(e,t){null==e&&"bookmark"==this.type&&(e=1);for(var n,r,i=0;i<this.lines.length;++i){var o=this.lines[i],l=er(o.markedSpans,this);if(null!=l.from&&(n=Fo(t?o:ti(o),l.from),-1==e))return n;if(null!=l.to&&(r=Fo(t?o:ti(o),l.to),1==e))return r}return n&&{from:n,to:r}},gl.prototype.changed=function(){var e=this.find(-1,!0),t=this,n=this.doc.cm;e&&n&&At(n,function(){var r=e.line,i=ti(e.line),o=Je(n,i);if(o&&(ot(o),n.curOp.selectionChanged=n.curOp.forceUpdate=!0),n.curOp.updateMaxLine=!0,!kr(t.doc,r)&&null!=t.height){var l=t.height;t.height=null;var a=Lr(t)-l;a&&ei(r,r.height+a)}})},gl.prototype.attachLine=function(e){if(!this.lines.length&&this.doc.cm){var t=this.doc.cm.curOp;t.maybeHiddenMarkers&&-1!=Ii(t.maybeHiddenMarkers,this)||(t.maybeUnhiddenMarkers||(t.maybeUnhiddenMarkers=[])).push(this)}this.lines.push(e)},gl.prototype.detachLine=function(e){if(this.lines.splice(Ii(this.lines,e),1),!this.lines.length&&this.doc.cm){var t=this.doc.cm.curOp;(t.maybeHiddenMarkers||(t.maybeHiddenMarkers=[])).push(this)}};var ml=0,vl=e.SharedTextMarker=function(e,t){this.markers=e,this.primary=t;for(var n=0;n<e.length;++n)e[n].parent=this};Ai(vl),vl.prototype.clear=function(){if(!this.explicitlyCleared){this.explicitlyCleared=!0;for(var e=0;e<this.markers.length;++e)this.markers[e].clear();Si(this,"clear")}},vl.prototype.find=function(e,t){return this.primary.find(e,t)};var yl=e.LineWidget=function(e,t,n){if(n)for(var r in n)n.hasOwnProperty(r)&&(this[r]=n[r]);this.doc=e,this.node=t};Ai(yl),yl.prototype.clear=function(){var e=this.doc.cm,t=this.line.widgets,n=this.line,r=ti(n);if(null!=r&&t){for(var i=0;i<t.length;++i)t[i]==this&&t.splice(i--,1);t.length||(n.widgets=null);var o=Lr(this);ei(n,Math.max(0,n.height-o)),e&&At(e,function(){Sr(e,n,-o),Pt(e,r,"widget")})}},yl.prototype.changed=function(){var e=this.height,t=this.doc.cm,n=this.line;this.height=null;var r=Lr(this)-e;r&&(ei(n,n.height+r),t&&At(t,function(){t.curOp.forceUpdate=!0,Sr(t,n,r)}))};var xl=e.Line=function(e,t,n){this.text=e,ur(this,t),this.height=n?n(this):1};Ai(xl),xl.prototype.lineNo=function(){return ti(this)};var bl={},wl={};Vr.prototype={chunkSize:function(){return this.lines.length},removeInner:function(e,t){for(var n=e,r=e+t;r>n;++n){var i=this.lines[n];this.height-=i.height,Nr(i),Si(i,"delete")}this.lines.splice(e,t)},collapse:function(e){e.push.apply(e,this.lines)},insertInner:function(e,t,n){this.height+=n,this.lines=this.lines.slice(0,e).concat(t).concat(this.lines.slice(e));for(var r=0;r<t.length;++r)t[r].parent=this},iterN:function(e,t,n){for(var r=e+t;r>e;++e)if(n(this.lines[e]))return!0}},Kr.prototype={chunkSize:function(){return this.size},removeInner:function(e,t){this.size-=t;for(var n=0;n<this.children.length;++n){var r=this.children[n],i=r.chunkSize();if(i>e){var o=Math.min(t,i-e),l=r.height;if(r.removeInner(e,o),this.height-=l-r.height,i==o&&(this.children.splice(n--,1),r.parent=null),0==(t-=o))break;e=0}else e-=i}if(this.size-t<25&&(this.children.length>1||!(this.children[0]instanceof Vr))){var a=[];this.collapse(a),this.children=[new Vr(a)],this.children[0].parent=this}},collapse:function(e){for(var t=0;t<this.children.length;++t)this.children[t].collapse(e)},insertInner:function(e,t,n){this.size+=t.length,this.height+=n;for(var r=0;r<this.children.length;++r){var i=this.children[r],o=i.chunkSize();if(o>=e){if(i.insertInner(e,t,n),i.lines&&i.lines.length>50){for(;i.lines.length>50;){var l=i.lines.splice(i.lines.length-25,25),a=new Vr(l);i.height-=a.height,this.children.splice(r+1,0,a),a.parent=this}this.maybeSpill()}break}e-=o}},maybeSpill:function(){if(!(this.children.length<=10)){var e=this;do{var t=e.children.splice(e.children.length-5,5),n=new Kr(t);if(e.parent){e.size-=n.size,e.height-=n.height;var r=Ii(e.parent.children,e);e.parent.children.splice(r+1,0,n)}else{var i=new Kr(e.children);i.parent=e,e.children=[i,n],e=i}n.parent=e.parent}while(e.children.length>10);e.parent.maybeSpill()}},iterN:function(e,t,n){for(var r=0;r<this.children.length;++r){var i=this.children[r],o=i.chunkSize();if(o>e){var l=Math.min(t,o-e);if(i.iterN(e,l,n))return!0;if(0==(t-=l))break;e=0}else e-=o}}};var kl=0,Cl=e.Doc=function(e,t,n,r){if(!(this instanceof Cl))return new Cl(e,t,n,r);null==n&&(n=0),Kr.call(this,[new Vr([new xl("",null)])]),this.first=n,this.scrollTop=this.scrollLeft=0,this.cantEdit=!1,this.cleanGeneration=1,this.frontier=n;var i=Fo(n,0);this.sel=fe(i),this.history=new oi(null),this.id=++kl,this.modeOption=t,this.lineSep=r,this.extend=!1,"string"==typeof e&&(e=this.splitLines(e)),$r(this,{from:i,to:i,text:e}),Te(this,fe(i),Pl)};Cl.prototype=Pi(Kr.prototype,{constructor:Cl,iter:function(e,t,n){n?this.iterN(e-this.first,t-e,n):this.iterN(this.first,this.first+this.size,e)},insert:function(e,t){for(var n=0,r=0;r<t.length;++r)n+=t[r].height;this.insertInner(e-this.first,t,n)},remove:function(e,t){this.removeInner(e-this.first,t)},getValue:function(e){var t=Jr(this,this.first,this.first+this.size);return e===!1?t:t.join(e||this.lineSeparator())},setValue:Wt(function(e){var t=Fo(this.first,0),n=this.first+this.size-1;Tn(this,{from:t,to:Fo(n,Zr(this,n).text.length),text:this.splitLines(e),origin:"setValue",full:!0},!0),Te(this,fe(t))}),replaceRange:function(e,t,n,r){t=me(this,t),n=n?me(this,n):t,Wn(this,e,t,n,r)},getRange:function(e,t,n){var r=Qr(this,me(this,e),me(this,t));return n===!1?r:r.join(n||this.lineSeparator())},getLine:function(e){var t=this.getLineHandle(e);return t&&t.text},getLineHandle:function(e){return ve(this,e)?Zr(this,e):void 0},getLineNumber:function(e){return ti(e)},getLineHandleVisualStart:function(e){return"number"==typeof e&&(e=Zr(this,e)),yr(e)},lineCount:function(){return this.size},firstLine:function(){return this.first},lastLine:function(){return this.first+this.size-1},clipPos:function(e){return me(this,e)},getCursor:function(e){var t,n=this.sel.primary();return t=null==e||"head"==e?n.head:"anchor"==e?n.anchor:"end"==e||"to"==e||e===!1?n.to():n.from()},listSelections:function(){return this.sel.ranges},somethingSelected:function(){return this.sel.somethingSelected()},setCursor:Wt(function(e,t,n){Ce(this,me(this,"number"==typeof e?Fo(e,t||0):e),null,n)}),setSelection:Wt(function(e,t,n){Ce(this,me(this,e),me(this,t||e),n)}),extendSelection:Wt(function(e,t,n){be(this,me(this,e),t&&me(this,t),n)}),extendSelections:Wt(function(e,t){we(this,ye(this,e),t)}),extendSelectionsBy:Wt(function(e,t){var n=Ei(this.sel.ranges,e);we(this,ye(this,n),t)}),setSelections:Wt(function(e,t,n){if(e.length){for(var r=0,i=[];r<e.length;r++)i[r]=new de(me(this,e[r].anchor),me(this,e[r].head));null==t&&(t=Math.min(e.length-1,this.sel.primIndex)),Te(this,he(i,t),n)}}),addSelection:Wt(function(e,t,n){var r=this.sel.ranges.slice(0);r.push(new de(me(this,e),me(this,t||e))),Te(this,he(r,r.length-1),n)}),getSelection:function(e){for(var t,n=this.sel.ranges,r=0;r<n.length;r++){var i=Qr(this,n[r].from(),n[r].to());t=t?t.concat(i):i}return e===!1?t:t.join(e||this.lineSeparator())},getSelections:function(e){for(var t=[],n=this.sel.ranges,r=0;r<n.length;r++){var i=Qr(this,n[r].from(),n[r].to());e!==!1&&(i=i.join(e||this.lineSeparator())),t[r]=i}return t},replaceSelection:function(e,t,n){for(var r=[],i=0;i<this.sel.ranges.length;i++)r[i]=e;this.replaceSelections(r,t,n||"+input")},replaceSelections:Wt(function(e,t,n){for(var r=[],i=this.sel,o=0;o<i.ranges.length;o++){var l=i.ranges[o];r[o]={from:l.from(),to:l.to(),text:this.splitLines(e[o]),origin:n}}for(var a=t&&"end"!=t&&Sn(this,r,t),o=r.length-1;o>=0;o--)Tn(this,r[o]);a?Le(this,a):this.cm&&zn(this.cm)}),undo:Wt(function(){Nn(this,"undo")}),redo:Wt(function(){Nn(this,"redo")}),undoSelection:Wt(function(){Nn(this,"undo",!0)}),redoSelection:Wt(function(){Nn(this,"redo",!0)}),setExtending:function(e){this.extend=e},getExtending:function(){return this.extend},historySize:function(){for(var e=this.history,t=0,n=0,r=0;r<e.done.length;r++)e.done[r].ranges||++t;for(var r=0;r<e.undone.length;r++)e.undone[r].ranges||++n;return{undo:t,redo:n}},clearHistory:function(){this.history=new oi(this.history.maxGeneration)},markClean:function(){this.cleanGeneration=this.changeGeneration(!0)},changeGeneration:function(e){return e&&(this.history.lastOp=this.history.lastSelOp=this.history.lastOrigin=null),this.history.generation},isClean:function(e){return this.history.generation==(e||this.cleanGeneration)},getHistory:function(){return{done:gi(this.history.done),undone:gi(this.history.undone)}},setHistory:function(e){var t=this.history=new oi(this.history.maxGeneration);t.done=gi(e.done.slice(0),null,!0),t.undone=gi(e.undone.slice(0),null,!0)},addLineClass:Wt(function(e,t,n){return jn(this,e,"gutter"==t?"gutter":"class",function(e){var r="text"==t?"textClass":"background"==t?"bgClass":"gutter"==t?"gutterClass":"wrapClass";if(e[r]){if($i(n).test(e[r]))return!1;e[r]+=" "+n}else e[r]=n;return!0})}),removeLineClass:Wt(function(e,t,n){return jn(this,e,"gutter"==t?"gutter":"class",function(e){var r="text"==t?"textClass":"background"==t?"bgClass":"gutter"==t?"gutterClass":"wrapClass",i=e[r];if(!i)return!1;if(null==n)e[r]=null;else{var o=i.match($i(n));if(!o)return!1;var l=o.index+o[0].length;e[r]=i.slice(0,o.index)+(o.index&&l!=i.length?" ":"")+i.slice(l)||null}return!0})}),addLineWidget:Wt(function(e,t,n){return Tr(this,e,t,n)}),removeLineWidget:function(e){e.clear()},markText:function(e,t,n){return Kn(this,me(this,e),me(this,t),n,n&&n.type||"range")},setBookmark:function(e,t){var n={replacedWith:t&&(null==t.nodeType?t.widget:t),insertLeft:t&&t.insertLeft,clearWhenEmpty:!1,shared:t&&t.shared,handleMouseEvents:t&&t.handleMouseEvents};return e=me(this,e),Kn(this,e,e,n,"bookmark")},findMarksAt:function(e){e=me(this,e);var t=[],n=Zr(this,e.line).markedSpans;if(n)for(var r=0;r<n.length;++r){var i=n[r];(null==i.from||i.from<=e.ch)&&(null==i.to||i.to>=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(e,t,n){e=me(this,e),t=me(this,t);var r=[],i=e.line;return this.iter(e.line,t.line+1,function(o){var l=o.markedSpans;if(l)for(var a=0;a<l.length;a++){var s=l[a];null!=s.to&&i==e.line&&e.ch>s.to||null==s.from&&i!=e.line||null!=s.from&&i==t.line&&s.from>t.ch||n&&!n(s.marker)||r.push(s.marker.parent||s.marker)}++i}),r},getAllMarks:function(){var e=[];return this.iter(function(t){var n=t.markedSpans;if(n)for(var r=0;r<n.length;++r)null!=n[r].from&&e.push(n[r].marker)}),e},posFromIndex:function(e){var t,n=this.first;return this.iter(function(r){var i=r.text.length+1;return i>e?(t=e,!0):(e-=i,void++n)}),me(this,Fo(n,t))},indexFromPos:function(e){e=me(this,e);var t=e.ch;return e.line<this.first||e.ch<0?0:(this.iter(this.first,e.line,function(e){t+=e.text.length+1}),t)},copy:function(e){var t=new Cl(Jr(this,this.first,this.first+this.size),this.modeOption,this.first,this.lineSep);return t.scrollTop=this.scrollTop,t.scrollLeft=this.scrollLeft,t.sel=this.sel,t.extend=!1,e&&(t.history.undoDepth=this.history.undoDepth,t.setHistory(this.getHistory())),t},linkedDoc:function(e){e||(e={});var t=this.first,n=this.first+this.size;null!=e.from&&e.from>t&&(t=e.from),null!=e.to&&e.to<n&&(n=e.to);var r=new Cl(Jr(this,t,n),e.mode||this.modeOption,t,this.lineSep);return e.sharedHist&&(r.history=this.history),(this.linked||(this.linked=[])).push({doc:r,sharedHist:e.sharedHist}),r.linked=[{doc:this,isParent:!0,sharedHist:e.sharedHist}],Zn(r,Yn(this)),r},unlinkDoc:function(t){if(t instanceof e&&(t=t.doc),this.linked)for(var n=0;n<this.linked.length;++n){var r=this.linked[n];if(r.doc==t){this.linked.splice(n,1),t.unlinkDoc(this),Qn(Yn(this));break}}if(t.history==this.history){var i=[t.id];Xr(t,function(e){i.push(e.id)},!0),t.history=new oi(null),t.history.done=gi(this.history.done,i),t.history.undone=gi(this.history.undone,i)}},iterLinkedDocs:function(e){Xr(this,e)},getMode:function(){return this.mode},getEditor:function(){return this.cm},splitLines:function(e){return this.lineSep?e.split(this.lineSep):ea(e)},lineSeparator:function(){return this.lineSep||"\n"}}),Cl.prototype.eachLine=Cl.prototype.iter;var Sl="iter insert remove copy getEditor constructor".split(" ");for(var Ll in Cl.prototype)Cl.prototype.hasOwnProperty(Ll)&&Ii(Sl,Ll)<0&&(e.prototype[Ll]=function(e){return function(){return e.apply(this.doc,arguments)}}(Cl.prototype[Ll]));Ai(Cl);var Tl=e.e_preventDefault=function(e){
e.preventDefault?e.preventDefault():e.returnValue=!1},Ml=e.e_stopPropagation=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0},Nl=e.e_stop=function(e){Tl(e),Ml(e)},Al=e.on=function(e,t,n){if(e.addEventListener)e.addEventListener(t,n,!1);else if(e.attachEvent)e.attachEvent("on"+t,n);else{var r=e._handlers||(e._handlers={}),i=r[t]||(r[t]=[]);i.push(n)}},Ol=[],Hl=e.off=function(e,t,n){if(e.removeEventListener)e.removeEventListener(t,n,!1);else if(e.detachEvent)e.detachEvent("on"+t,n);else for(var r=Ci(e,t,!1),i=0;i<r.length;++i)if(r[i]==n){r.splice(i,1);break}},Wl=e.signal=function(e,t){var n=Ci(e,t,!0);if(n.length)for(var r=Array.prototype.slice.call(arguments,2),i=0;i<n.length;++i)n[i].apply(null,r)},Il=null,El=30,Dl=e.Pass={toString:function(){return"CodeMirror.Pass"}},Pl={scroll:!1},Fl={origin:"*mouse"},zl={origin:"+move"};Oi.prototype.set=function(e,t){clearTimeout(this.id),this.id=setTimeout(t,e)};var Rl=e.countColumn=function(e,t,n,r,i){null==t&&(t=e.search(/[^\s\u00a0]/),-1==t&&(t=e.length));for(var o=r||0,l=i||0;;){var a=e.indexOf(" ",o);if(0>a||a>=t)return l+(t-o);l+=a-o,l+=n-l%n,o=a+1}},Bl=e.findColumn=function(e,t,n){for(var r=0,i=0;;){var o=e.indexOf(" ",r);-1==o&&(o=e.length);var l=o-r;if(o==e.length||i+l>=t)return r+Math.min(l,t-i);if(i+=o-r,i+=n-i%n,r=o+1,i>=t)return r}},jl=[""],_l=function(e){e.select()};No?_l=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:xo&&(_l=function(e){try{e.select()}catch(t){}});var ql,Ul=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,Gl=e.isWordChar=function(e){return/\w/.test(e)||e>"€"&&(e.toUpperCase()!=e.toLowerCase()||Ul.test(e))},$l=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;ql=document.createRange?function(e,t,n,r){var i=document.createRange();return i.setEnd(r||e,n),i.setStart(e,t),i}:function(e,t,n){var r=document.body.createTextRange();try{r.moveToElementText(e.parentNode)}catch(i){return r}return r.collapse(!0),r.moveEnd("character",n),r.moveStart("character",t),r};var Vl=e.contains=function(e,t){if(3==t.nodeType&&(t=t.parentNode),e.contains)return e.contains(t);do if(11==t.nodeType&&(t=t.host),t==e)return!0;while(t=t.parentNode)};xo&&11>bo&&(Gi=function(){try{return document.activeElement}catch(e){return document.body}});var Kl,Xl,Yl=e.rmClass=function(e,t){var n=e.className,r=$i(t).exec(n);if(r){var i=n.slice(r.index+r[0].length);e.className=n.slice(0,r.index)+(i?r[1]+i:"")}},Zl=e.addClass=function(e,t){var n=e.className;$i(t).test(n)||(e.className+=(n?" ":"")+t)},Ql=!1,Jl=function(){if(xo&&9>bo)return!1;var e=_i("div");return"draggable"in e||"dragDrop"in e}(),ea=e.splitLines=3!="\n\nb".split(/\n/).length?function(e){for(var t=0,n=[],r=e.length;r>=t;){var i=e.indexOf("\n",t);-1==i&&(i=e.length);var o=e.slice(t,"\r"==e.charAt(i-1)?i-1:i),l=o.indexOf("\r");-1!=l?(n.push(o.slice(0,l)),t+=l+1):(n.push(o),t=i+1)}return n}:function(e){return e.split(/\r\n?|\n/)},ta=window.getSelection?function(e){try{return e.selectionStart!=e.selectionEnd}catch(t){return!1}}:function(e){try{var t=e.ownerDocument.selection.createRange()}catch(n){}return t&&t.parentElement()==e?0!=t.compareEndPoints("StartToEnd",t):!1},na=function(){var e=_i("div");return"oncopy"in e?!0:(e.setAttribute("oncopy","return;"),"function"==typeof e.oncopy)}(),ra=null,ia=e.keyNames={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",61:"=",91:"Mod",92:"Mod",93:"Mod",106:"*",107:"=",109:"-",110:".",111:"/",127:"Delete",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"};!function(){for(var e=0;10>e;e++)ia[e+48]=ia[e+96]=String(e);for(var e=65;90>=e;e++)ia[e]=String.fromCharCode(e);for(var e=1;12>=e;e++)ia[e+111]=ia[e+63235]="F"+e}();var oa,la=function(){function e(e){return 247>=e?n.charAt(e):e>=1424&&1524>=e?"R":e>=1536&&1773>=e?r.charAt(e-1536):e>=1774&&2220>=e?"r":e>=8192&&8203>=e?"w":8204==e?"b":"L"}function t(e,t,n){this.level=e,this.from=t,this.to=n}var n="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",r="rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm",i=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,o=/[stwN]/,l=/[LRr]/,a=/[Lb1n]/,s=/[1n]/,c="L";return function(n){if(!i.test(n))return!1;for(var r,u=n.length,d=[],h=0;u>h;++h)d.push(r=e(n.charCodeAt(h)));for(var h=0,f=c;u>h;++h){var r=d[h];"m"==r?d[h]=f:f=r}for(var h=0,p=c;u>h;++h){var r=d[h];"1"==r&&"r"==p?d[h]="n":l.test(r)&&(p=r,"r"==r&&(d[h]="R"))}for(var h=1,f=d[0];u-1>h;++h){var r=d[h];"+"==r&&"1"==f&&"1"==d[h+1]?d[h]="1":","!=r||f!=d[h+1]||"1"!=f&&"n"!=f||(d[h]=f),f=r}for(var h=0;u>h;++h){var r=d[h];if(","==r)d[h]="N";else if("%"==r){for(var m=h+1;u>m&&"%"==d[m];++m);for(var g=h&&"!"==d[h-1]||u>m&&"1"==d[m]?"1":"N",v=h;m>v;++v)d[v]=g;h=m-1}}for(var h=0,p=c;u>h;++h){var r=d[h];"L"==p&&"1"==r?d[h]="L":l.test(r)&&(p=r)}for(var h=0;u>h;++h)if(o.test(d[h])){for(var m=h+1;u>m&&o.test(d[m]);++m);for(var y="L"==(h?d[h-1]:c),x="L"==(u>m?d[m]:c),g=y||x?"L":"R",v=h;m>v;++v)d[v]=g;h=m-1}for(var b,w=[],h=0;u>h;)if(a.test(d[h])){var k=h;for(++h;u>h&&a.test(d[h]);++h);w.push(new t(0,k,h))}else{var C=h,S=w.length;for(++h;u>h&&"L"!=d[h];++h);for(var v=C;h>v;)if(s.test(d[v])){v>C&&w.splice(S,0,new t(1,C,v));var L=v;for(++v;h>v&&s.test(d[v]);++v);w.splice(S,0,new t(2,L,v)),C=v}else++v;h>C&&w.splice(S,0,new t(1,C,h))}return 1==w[0].level&&(b=n.match(/^\s+/))&&(w[0].from=b[0].length,w.unshift(new t(0,0,b[0].length))),1==Wi(w).level&&(b=n.match(/\s+$/))&&(Wi(w).to-=b[0].length,w.push(new t(0,u-b[0].length,u))),2==w[0].level&&w.unshift(new t(1,w[0].to,w[0].to)),w[0].level!=Wi(w).level&&w.push(new t(w[0].level,u,u)),w}}();return e.version="5.12.1",e})},{}],8:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../markdown/markdown"),t("../../addon/mode/overlay")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../markdown/markdown","../../addon/mode/overlay"],i):i(CodeMirror)}(function(e){"use strict";var t=/^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i;e.defineMode("gfm",function(n,r){function i(e){return e.code=!1,null}var o=0,l={startState:function(){return{code:!1,codeBlock:!1,ateSpace:!1}},copyState:function(e){return{code:e.code,codeBlock:e.codeBlock,ateSpace:e.ateSpace}},token:function(e,n){if(n.combineTokens=null,n.codeBlock)return e.match(/^```+/)?(n.codeBlock=!1,null):(e.skipToEnd(),null);if(e.sol()&&(n.code=!1),e.sol()&&e.match(/^```+/))return e.skipToEnd(),n.codeBlock=!0,null;if("`"===e.peek()){e.next();var i=e.pos;e.eatWhile("`");var l=1+e.pos-i;return n.code?l===o&&(n.code=!1):(o=l,n.code=!0),null}if(n.code)return e.next(),null;if(e.eatSpace())return n.ateSpace=!0,null;if((e.sol()||n.ateSpace)&&(n.ateSpace=!1,r.gitHubSpice!==!1)){if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/))return n.combineTokens=!0,"link";if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/))return n.combineTokens=!0,"link"}return e.match(t)&&"]("!=e.string.slice(e.start-2,e.start)&&(0==e.start||/\W/.test(e.string.charAt(e.start-1)))?(n.combineTokens=!0,"link"):(e.next(),null)},blankLine:i},a={underscoresBreakWords:!1,taskLists:!0,fencedCodeBlocks:"```",strikethrough:!0};for(var s in r)a[s]=r[s];return a.name="markdown",e.overlayMode(e.getMode(n,a),l)},"markdown"),e.defineMIME("text/x-gfm","gfm")})},{"../../addon/mode/overlay":6,"../../lib/codemirror":7,"../markdown/markdown":9}],9:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../xml/xml"),t("../meta")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../xml/xml","../meta"],i):i(CodeMirror)}(function(e){"use strict";e.defineMode("markdown",function(t,n){function r(n){if(e.findModeByName){var r=e.findModeByName(n);r&&(n=r.mime||r.mimes[0])}var i=e.getMode(t,n);return"null"==i.name?null:i}function i(e,t,n){return t.f=t.inline=n,n(e,t)}function o(e,t,n){return t.f=t.block=n,n(e,t)}function l(e){return!e||!/\S/.test(e.string)}function a(e){return e.linkTitle=!1,e.em=!1,e.strong=!1,e.strikethrough=!1,e.quote=0,e.indentedCode=!1,C&&e.f==c&&(e.f=p,e.block=s),e.trailingSpace=0,e.trailingSpaceNewLine=!1,e.prevLine=e.thisLine,e.thisLine=null,null}function s(e,t){var o=e.sol(),a=t.list!==!1,s=t.indentedCode;t.indentedCode=!1,a&&(t.indentationDiff>=0?(t.indentationDiff<4&&(t.indentation-=t.indentationDiff),t.list=null):t.indentation>0?t.list=null:t.list=!1);var c=null;if(t.indentationDiff>=4)return e.skipToEnd(),s||l(t.prevLine)?(t.indentation-=4,t.indentedCode=!0,S.code):null;if(e.eatSpace())return null;if((c=e.match(O))&&c[1].length<=6)return t.header=c[1].length,n.highlightFormatting&&(t.formatting="header"),t.f=t.inline,h(t);if(!(l(t.prevLine)||t.quote||a||s)&&(c=e.match(H)))return t.header="="==c[0].charAt(0)?1:2,n.highlightFormatting&&(t.formatting="header"),t.f=t.inline,h(t);if(e.eat(">"))return t.quote=o?1:t.quote+1,n.highlightFormatting&&(t.formatting="quote"),e.eatSpace(),h(t);if("["===e.peek())return i(e,t,y);if(e.match(T,!0))return t.hr=!0,S.hr;if((l(t.prevLine)||a)&&(e.match(M,!1)||e.match(N,!1))){var d=null;for(e.match(M,!0)?d="ul":(e.match(N,!0),d="ol"),t.indentation=e.column()+e.current().length,t.list=!0;t.listStack&&e.column()<t.listStack[t.listStack.length-1];)t.listStack.pop();return t.listStack.push(t.indentation),n.taskLists&&e.match(A,!1)&&(t.taskList=!0),t.f=t.inline,n.highlightFormatting&&(t.formatting=["list","list-"+d]),h(t)}return n.fencedCodeBlocks&&(c=e.match(I,!0))?(t.fencedChars=c[1],t.localMode=r(c[2]),t.localMode&&(t.localState=t.localMode.startState()),t.f=t.block=u,n.highlightFormatting&&(t.formatting="code-block"),t.code=-1,h(t)):i(e,t,t.inline)}function c(t,n){var r=k.token(t,n.htmlState);if(!C){var i=e.innerMode(k,n.htmlState);("xml"==i.mode.name&&null===i.state.tagStart&&!i.state.context&&i.state.tokenize.isInText||n.md_inside&&t.current().indexOf(">")>-1)&&(n.f=p,n.block=s,n.htmlState=null)}return r}function u(e,t){return t.fencedChars&&e.match(t.fencedChars,!1)?(t.localMode=t.localState=null,t.f=t.block=d,null):t.localMode?t.localMode.token(e,t.localState):(e.skipToEnd(),S.code)}function d(e,t){e.match(t.fencedChars),t.block=s,t.f=p,t.fencedChars=null,n.highlightFormatting&&(t.formatting="code-block"),t.code=1;var r=h(t);return t.code=0,r}function h(e){var t=[];if(e.formatting){t.push(S.formatting),"string"==typeof e.formatting&&(e.formatting=[e.formatting]);for(var r=0;r<e.formatting.length;r++)t.push(S.formatting+"-"+e.formatting[r]),"header"===e.formatting[r]&&t.push(S.formatting+"-"+e.formatting[r]+"-"+e.header),"quote"===e.formatting[r]&&(!n.maxBlockquoteDepth||n.maxBlockquoteDepth>=e.quote?t.push(S.formatting+"-"+e.formatting[r]+"-"+e.quote):t.push("error"))}if(e.taskOpen)return t.push("meta"),t.length?t.join(" "):null;if(e.taskClosed)return t.push("property"),t.length?t.join(" "):null;if(e.linkHref?t.push(S.linkHref,"url"):(e.strong&&t.push(S.strong),e.em&&t.push(S.em),e.strikethrough&&t.push(S.strikethrough),e.linkText&&t.push(S.linkText),e.code&&t.push(S.code)),e.header&&t.push(S.header,S.header+"-"+e.header),e.quote&&(t.push(S.quote),!n.maxBlockquoteDepth||n.maxBlockquoteDepth>=e.quote?t.push(S.quote+"-"+e.quote):t.push(S.quote+"-"+n.maxBlockquoteDepth)),e.list!==!1){var i=(e.listStack.length-1)%3;i?1===i?t.push(S.list2):t.push(S.list3):t.push(S.list1)}return e.trailingSpaceNewLine?t.push("trailing-space-new-line"):e.trailingSpace&&t.push("trailing-space-"+(e.trailingSpace%2?"a":"b")),t.length?t.join(" "):null}function f(e,t){return e.match(W,!0)?h(t):void 0}function p(t,r){var i=r.text(t,r);if("undefined"!=typeof i)return i;if(r.list)return r.list=null,h(r);if(r.taskList){var l="x"!==t.match(A,!0)[1];return l?r.taskOpen=!0:r.taskClosed=!0,n.highlightFormatting&&(r.formatting="task"),r.taskList=!1,h(r)}if(r.taskOpen=!1,r.taskClosed=!1,r.header&&t.match(/^#+$/,!0))return n.highlightFormatting&&(r.formatting="header"),h(r);var a=t.sol(),s=t.next();if(r.linkTitle){r.linkTitle=!1;var u=s;"("===s&&(u=")"),u=(u+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1");var d="^\\s*(?:[^"+u+"\\\\]+|\\\\\\\\|\\\\.)"+u;if(t.match(new RegExp(d),!0))return S.linkHref}if("`"===s){var f=r.formatting;n.highlightFormatting&&(r.formatting="code"),t.eatWhile("`");var p=t.current().length;if(0==r.code)return r.code=p,h(r);if(p==r.code){var v=h(r);return r.code=0,v}return r.formatting=f,h(r)}if(r.code)return h(r);if("\\"===s&&(t.next(),n.highlightFormatting)){var y=h(r),x=S.formatting+"-escape";return y?y+" "+x:x}if("!"===s&&t.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return t.match(/\[[^\]]*\]/),r.inline=r.f=g,S.image;if("["===s&&t.match(/.*\](\(.*\)| ?\[.*\])/,!1))return r.linkText=!0,n.highlightFormatting&&(r.formatting="link"),h(r);if("]"===s&&r.linkText&&t.match(/\(.*\)| ?\[.*\]/,!1)){n.highlightFormatting&&(r.formatting="link");var y=h(r);return r.linkText=!1,r.inline=r.f=g,y}if("<"===s&&t.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkInline}if("<"===s&&t.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkEmail}if("<"===s&&t.match(/^(!--|\w)/,!1)){var b=t.string.indexOf(">",t.pos);if(-1!=b){var w=t.string.substring(t.start,b);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(w)&&(r.md_inside=!0)}return t.backUp(1),r.htmlState=e.startState(k),o(t,r,c)}if("<"===s&&t.match(/^\/\w*?>/))return r.md_inside=!1,"tag";var C=!1;if(!n.underscoresBreakWords&&"_"===s&&"_"!==t.peek()&&t.match(/(\w)/,!1)){var L=t.pos-2;if(L>=0){var T=t.string.charAt(L);"_"!==T&&T.match(/(\w)/,!1)&&(C=!0)}}if("*"===s||"_"===s&&!C)if(a&&" "===t.peek());else{if(r.strong===s&&t.eat(s)){n.highlightFormatting&&(r.formatting="strong");var v=h(r);return r.strong=!1,v}if(!r.strong&&t.eat(s))return r.strong=s,n.highlightFormatting&&(r.formatting="strong"),h(r);if(r.em===s){n.highlightFormatting&&(r.formatting="em");var v=h(r);return r.em=!1,v}if(!r.em)return r.em=s,n.highlightFormatting&&(r.formatting="em"),h(r)}else if(" "===s&&(t.eat("*")||t.eat("_"))){if(" "===t.peek())return h(r);t.backUp(1)}if(n.strikethrough)if("~"===s&&t.eatWhile(s)){if(r.strikethrough){n.highlightFormatting&&(r.formatting="strikethrough");var v=h(r);return r.strikethrough=!1,v}if(t.match(/^[^\s]/,!1))return r.strikethrough=!0,n.highlightFormatting&&(r.formatting="strikethrough"),h(r)}else if(" "===s&&t.match(/^~~/,!0)){if(" "===t.peek())return h(r);t.backUp(2)}return" "===s&&(t.match(/ +$/,!1)?r.trailingSpace++:r.trailingSpace&&(r.trailingSpaceNewLine=!0)),h(r)}function m(e,t){var r=e.next();if(">"===r){t.f=t.inline=p,n.highlightFormatting&&(t.formatting="link");var i=h(t);return i?i+=" ":i="",i+S.linkInline}return e.match(/^[^>]+/,!0),S.linkInline}function g(e,t){if(e.eatSpace())return null;var r=e.next();return"("===r||"["===r?(t.f=t.inline=v("("===r?")":"]"),n.highlightFormatting&&(t.formatting="link-string"),t.linkHref=!0,h(t)):"error"}function v(e){return function(t,r){var i=t.next();if(i===e){r.f=r.inline=p,n.highlightFormatting&&(r.formatting="link-string");var o=h(r);return r.linkHref=!1,o}return t.match(w(e),!0)&&t.backUp(1),r.linkHref=!0,h(r)}}function y(e,t){return e.match(/^([^\]\\]|\\.)*\]:/,!1)?(t.f=x,e.next(),n.highlightFormatting&&(t.formatting="link"),t.linkText=!0,h(t)):i(e,t,p)}function x(e,t){if(e.match(/^\]:/,!0)){t.f=t.inline=b,n.highlightFormatting&&(t.formatting="link");var r=h(t);return t.linkText=!1,r}return e.match(/^([^\]\\]|\\.)+/,!0),S.linkText}function b(e,t){return e.eatSpace()?null:(e.match(/^[^\s]+/,!0),void 0===e.peek()?t.linkTitle=!0:e.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,!0),t.f=t.inline=p,S.linkHref+" url")}function w(e){return E[e]||(e=(e+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1"),E[e]=new RegExp("^(?:[^\\\\]|\\\\.)*?("+e+")")),E[e]}var k=e.getMode(t,"text/html"),C="null"==k.name;void 0===n.highlightFormatting&&(n.highlightFormatting=!1),void 0===n.maxBlockquoteDepth&&(n.maxBlockquoteDepth=0),void 0===n.underscoresBreakWords&&(n.underscoresBreakWords=!0),void 0===n.taskLists&&(n.taskLists=!1),void 0===n.strikethrough&&(n.strikethrough=!1),void 0===n.tokenTypeOverrides&&(n.tokenTypeOverrides={});var S={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"tag",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough"};for(var L in S)S.hasOwnProperty(L)&&n.tokenTypeOverrides[L]&&(S[L]=n.tokenTypeOverrides[L]);var T=/^([*\-_])(?:\s*\1){2,}\s*$/,M=/^[*\-+]\s+/,N=/^[0-9]+([.)])\s+/,A=/^\[(x| )\](?=\s)/,O=n.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,H=/^ *(?:\={1,}|-{1,})\s*$/,W=/^[^#!\[\]*_\\<>` "'(~]+/,I=new RegExp("^("+(n.fencedCodeBlocks===!0?"~~~+|```+":n.fencedCodeBlocks)+")[ \\t]*([\\w+#]*)"),E=[],D={startState:function(){return{f:s,prevLine:null,thisLine:null,block:s,htmlState:null,indentation:0,inline:p,text:f,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,fencedChars:null}},copyState:function(t){return{f:t.f,prevLine:t.prevLine,thisLine:t.thisLine,block:t.block,htmlState:t.htmlState&&e.copyState(k,t.htmlState),indentation:t.indentation,localMode:t.localMode,localState:t.localMode?e.copyState(t.localMode,t.localState):null,inline:t.inline,text:t.text,formatting:!1,linkTitle:t.linkTitle,code:t.code,em:t.em,strong:t.strong,strikethrough:t.strikethrough,header:t.header,hr:t.hr,taskList:t.taskList,list:t.list,listStack:t.listStack.slice(0),quote:t.quote,indentedCode:t.indentedCode,trailingSpace:t.trailingSpace,trailingSpaceNewLine:t.trailingSpaceNewLine,md_inside:t.md_inside,fencedChars:t.fencedChars}},token:function(e,t){if(t.formatting=!1,e!=t.thisLine){var n=t.header||t.hr;if(t.header=0,t.hr=!1,e.match(/^\s*$/,!0)||n){if(a(t),!n)return null;t.prevLine=null}t.prevLine=t.thisLine,t.thisLine=e,t.taskList=!1,t.trailingSpace=0,t.trailingSpaceNewLine=!1,t.f=t.block;var r=e.match(/^\s*/,!0)[0].replace(/\t/g," ").length;if(t.indentationDiff=Math.min(r-t.indentation,4),t.indentation=t.indentation+t.indentationDiff,r>0)return null}return t.f(e,t)},innerMode:function(e){return e.block==c?{state:e.htmlState,mode:k}:e.localState?{state:e.localState,mode:e.localMode}:{state:e,mode:D}},blankLine:a,getType:h,fold:"markdown"};return D},"xml"),e.defineMIME("text/x-markdown","markdown")})},{"../../lib/codemirror":7,"../meta":10,"../xml/xml":11}],10:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../lib/codemirror")):"function"==typeof e&&e.amd?e(["../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.modeInfo=[{name:"APL",mime:"text/apl",mode:"apl",ext:["dyalog","apl"]},{name:"PGP",mimes:["application/pgp","application/pgp-keys","application/pgp-signature"],mode:"asciiarmor",ext:["pgp"]},{name:"ASN.1",mime:"text/x-ttcn-asn",mode:"asn.1",ext:["asn","asn1"]},{name:"Asterisk",mime:"text/x-asterisk",mode:"asterisk",file:/^extensions\.conf$/i},{name:"Brainfuck",mime:"text/x-brainfuck",mode:"brainfuck",ext:["b","bf"]},{name:"C",mime:"text/x-csrc",mode:"clike",ext:["c","h"]},{name:"C++",mime:"text/x-c++src",mode:"clike",ext:["cpp","c++","cc","cxx","hpp","h++","hh","hxx"],alias:["cpp"]},{name:"Cobol",mime:"text/x-cobol",mode:"cobol",ext:["cob","cpy"]},{name:"C#",mime:"text/x-csharp",mode:"clike",ext:["cs"],alias:["csharp"]},{name:"Clojure",mime:"text/x-clojure",mode:"clojure",ext:["clj","cljc","cljx"]},{name:"ClojureScript",mime:"text/x-clojurescript",mode:"clojure",ext:["cljs"]},{name:"Closure Stylesheets (GSS)",mime:"text/x-gss",mode:"css",ext:["gss"]},{name:"CMake",mime:"text/x-cmake",mode:"cmake",ext:["cmake","cmake.in"],file:/^CMakeLists.txt$/},{name:"CoffeeScript",mime:"text/x-coffeescript",mode:"coffeescript",ext:["coffee"],alias:["coffee","coffee-script"]},{name:"Common Lisp",mime:"text/x-common-lisp",mode:"commonlisp",ext:["cl","lisp","el"],alias:["lisp"]},{name:"Cypher",mime:"application/x-cypher-query",mode:"cypher",ext:["cyp","cypher"]},{name:"Cython",mime:"text/x-cython",mode:"python",ext:["pyx","pxd","pxi"]},{name:"Crystal",mime:"text/x-crystal",mode:"crystal",ext:["cr"]},{name:"CSS",mime:"text/css",mode:"css",ext:["css"]},{name:"CQL",mime:"text/x-cassandra",mode:"sql",ext:["cql"]},{name:"D",mime:"text/x-d",mode:"d",ext:["d"]},{name:"Dart",mimes:["application/dart","text/x-dart"],mode:"dart",ext:["dart"]},{name:"diff",mime:"text/x-diff",mode:"diff",ext:["diff","patch"]},{name:"Django",mime:"text/x-django",mode:"django"},{name:"Dockerfile",mime:"text/x-dockerfile",mode:"dockerfile",file:/^Dockerfile$/},{name:"DTD",mime:"application/xml-dtd",mode:"dtd",ext:["dtd"]},{name:"Dylan",mime:"text/x-dylan",mode:"dylan",ext:["dylan","dyl","intr"]},{name:"EBNF",mime:"text/x-ebnf",mode:"ebnf"},{name:"ECL",mime:"text/x-ecl",mode:"ecl",ext:["ecl"]},{name:"edn",mime:"application/edn",mode:"clojure",ext:["edn"]},{name:"Eiffel",mime:"text/x-eiffel",mode:"eiffel",ext:["e"]},{name:"Elm",mime:"text/x-elm",mode:"elm",ext:["elm"]},{name:"Embedded Javascript",mime:"application/x-ejs",mode:"htmlembedded",ext:["ejs"]},{name:"Embedded Ruby",mime:"application/x-erb",mode:"htmlembedded",ext:["erb"]},{name:"Erlang",mime:"text/x-erlang",mode:"erlang",ext:["erl"]},{name:"Factor",mime:"text/x-factor",mode:"factor",ext:["factor"]},{name:"FCL",mime:"text/x-fcl",mode:"fcl"},{name:"Forth",mime:"text/x-forth",mode:"forth",ext:["forth","fth","4th"]},{name:"Fortran",mime:"text/x-fortran",mode:"fortran",ext:["f","for","f77","f90"]},{name:"F#",mime:"text/x-fsharp",mode:"mllike",ext:["fs"],alias:["fsharp"]},{name:"Gas",mime:"text/x-gas",mode:"gas",ext:["s"]},{name:"Gherkin",mime:"text/x-feature",mode:"gherkin",ext:["feature"]},{name:"GitHub Flavored Markdown",mime:"text/x-gfm",mode:"gfm",file:/^(readme|contributing|history).md$/i},{name:"Go",mime:"text/x-go",mode:"go",ext:["go"]},{name:"Groovy",mime:"text/x-groovy",mode:"groovy",ext:["groovy","gradle"]},{name:"HAML",mime:"text/x-haml",mode:"haml",ext:["haml"]},{name:"Haskell",mime:"text/x-haskell",mode:"haskell",ext:["hs"]},{name:"Haskell (Literate)",mime:"text/x-literate-haskell",mode:"haskell-literate",ext:["lhs"]},{name:"Haxe",mime:"text/x-haxe",mode:"haxe",ext:["hx"]},{name:"HXML",mime:"text/x-hxml",mode:"haxe",ext:["hxml"]},{name:"ASP.NET",mime:"application/x-aspx",mode:"htmlembedded",ext:["aspx"],alias:["asp","aspx"]},{name:"HTML",mime:"text/html",mode:"htmlmixed",ext:["html","htm"],alias:["xhtml"]},{name:"HTTP",mime:"message/http",mode:"http"},{name:"IDL",mime:"text/x-idl",mode:"idl",ext:["pro"]},{name:"Jade",mime:"text/x-jade",mode:"jade",ext:["jade"]},{name:"Java",mime:"text/x-java",mode:"clike",ext:["java"]},{name:"Java Server Pages",mime:"application/x-jsp",mode:"htmlembedded",ext:["jsp"],alias:["jsp"]},{name:"JavaScript",mimes:["text/javascript","text/ecmascript","application/javascript","application/x-javascript","application/ecmascript"],mode:"javascript",ext:["js"],alias:["ecmascript","js","node"]},{name:"JSON",mimes:["application/json","application/x-json"],mode:"javascript",ext:["json","map"],alias:["json5"]},{name:"JSON-LD",mime:"application/ld+json",mode:"javascript",ext:["jsonld"],alias:["jsonld"]},{name:"JSX",mime:"text/jsx",mode:"jsx",ext:["jsx"]},{name:"Jinja2",mime:"null",mode:"jinja2"},{name:"Julia",mime:"text/x-julia",mode:"julia",ext:["jl"]},{name:"Kotlin",mime:"text/x-kotlin",mode:"clike",ext:["kt"]},{name:"LESS",mime:"text/x-less",mode:"css",ext:["less"]},{name:"LiveScript",mime:"text/x-livescript",mode:"livescript",ext:["ls"],alias:["ls"]},{name:"Lua",mime:"text/x-lua",mode:"lua",ext:["lua"]},{name:"Markdown",mime:"text/x-markdown",mode:"markdown",ext:["markdown","md","mkd"]},{name:"mIRC",mime:"text/mirc",mode:"mirc"},{name:"MariaDB SQL",mime:"text/x-mariadb",mode:"sql"},{name:"Mathematica",mime:"text/x-mathematica",mode:"mathematica",ext:["m","nb"]},{name:"Modelica",mime:"text/x-modelica",mode:"modelica",ext:["mo"]},{name:"MUMPS",mime:"text/x-mumps",mode:"mumps",ext:["mps"]},{name:"MS SQL",mime:"text/x-mssql",mode:"sql"},{name:"MySQL",mime:"text/x-mysql",mode:"sql"},{name:"Nginx",mime:"text/x-nginx-conf",mode:"nginx",file:/nginx.*\.conf$/i},{name:"NSIS",mime:"text/x-nsis",mode:"nsis",ext:["nsh","nsi"]},{name:"NTriples",mime:"text/n-triples",mode:"ntriples",ext:["nt"]},{name:"Objective C",mime:"text/x-objectivec",mode:"clike",ext:["m","mm"]},{name:"OCaml",mime:"text/x-ocaml",mode:"mllike",ext:["ml","mli","mll","mly"]},{name:"Octave",mime:"text/x-octave",mode:"octave",ext:["m"]},{name:"Oz",mime:"text/x-oz",mode:"oz",ext:["oz"]},{name:"Pascal",mime:"text/x-pascal",mode:"pascal",ext:["p","pas"]},{name:"PEG.js",mime:"null",mode:"pegjs",ext:["jsonld"]},{name:"Perl",mime:"text/x-perl",mode:"perl",ext:["pl","pm"]},{name:"PHP",mime:"application/x-httpd-php",mode:"php",ext:["php","php3","php4","php5","phtml"]},{name:"Pig",mime:"text/x-pig",mode:"pig",ext:["pig"]},{name:"Plain Text",mime:"text/plain",mode:"null",ext:["txt","text","conf","def","list","log"]},{name:"PLSQL",mime:"text/x-plsql",mode:"sql",ext:["pls"]},{name:"Properties files",mime:"text/x-properties",mode:"properties",ext:["properties","ini","in"],alias:["ini","properties"]},{name:"ProtoBuf",mime:"text/x-protobuf",mode:"protobuf",ext:["proto"]},{name:"Python",mime:"text/x-python",mode:"python",ext:["py","pyw"]},{name:"Puppet",mime:"text/x-puppet",mode:"puppet",ext:["pp"]},{name:"Q",mime:"text/x-q",mode:"q",ext:["q"]},{name:"R",mime:"text/x-rsrc",mode:"r",ext:["r"],alias:["rscript"]},{name:"reStructuredText",mime:"text/x-rst",mode:"rst",ext:["rst"],alias:["rst"]},{name:"RPM Changes",mime:"text/x-rpm-changes",mode:"rpm"},{name:"RPM Spec",mime:"text/x-rpm-spec",mode:"rpm",ext:["spec"]},{name:"Ruby",mime:"text/x-ruby",mode:"ruby",ext:["rb"],alias:["jruby","macruby","rake","rb","rbx"]},{name:"Rust",mime:"text/x-rustsrc",mode:"rust",ext:["rs"]},{name:"Sass",mime:"text/x-sass",mode:"sass",ext:["sass"]},{name:"Scala",mime:"text/x-scala",mode:"clike",ext:["scala"]},{name:"Scheme",mime:"text/x-scheme",mode:"scheme",ext:["scm","ss"]},{name:"SCSS",mime:"text/x-scss",mode:"css",ext:["scss"]},{name:"Shell",mime:"text/x-sh",mode:"shell",ext:["sh","ksh","bash"],alias:["bash","sh","zsh"],file:/^PKGBUILD$/},{name:"Sieve",mime:"application/sieve",mode:"sieve",ext:["siv","sieve"]},{name:"Slim",mimes:["text/x-slim","application/x-slim"],mode:"slim",ext:["slim"]},{name:"Smalltalk",mime:"text/x-stsrc",mode:"smalltalk",ext:["st"]},{name:"Smarty",mime:"text/x-smarty",mode:"smarty",ext:["tpl"]},{name:"Solr",mime:"text/x-solr",mode:"solr"},{name:"Soy",mime:"text/x-soy",mode:"soy",ext:["soy"],alias:["closure template"]},{name:"SPARQL",mime:"application/sparql-query",mode:"sparql",ext:["rq","sparql"],alias:["sparul"]},{name:"Spreadsheet",mime:"text/x-spreadsheet",mode:"spreadsheet",alias:["excel","formula"]},{name:"SQL",mime:"text/x-sql",mode:"sql",ext:["sql"]},{name:"Squirrel",mime:"text/x-squirrel",mode:"clike",ext:["nut"]},{name:"Swift",mime:"text/x-swift",mode:"swift",ext:["swift"]},{name:"sTeX",mime:"text/x-stex",mode:"stex"},{name:"LaTeX",mime:"text/x-latex",mode:"stex",ext:["text","ltx"],alias:["tex"]},{name:"SystemVerilog",mime:"text/x-systemverilog",mode:"verilog",ext:["v"]},{name:"Tcl",mime:"text/x-tcl",mode:"tcl",ext:["tcl"]},{name:"Textile",mime:"text/x-textile",mode:"textile",ext:["textile"]},{name:"TiddlyWiki ",mime:"text/x-tiddlywiki",mode:"tiddlywiki"},{name:"Tiki wiki",mime:"text/tiki",mode:"tiki"},{name:"TOML",mime:"text/x-toml",mode:"toml",ext:["toml"]},{name:"Tornado",mime:"text/x-tornado",mode:"tornado"},{name:"troff",mime:"text/troff",mode:"troff",ext:["1","2","3","4","5","6","7","8","9"]},{name:"TTCN",mime:"text/x-ttcn",mode:"ttcn",ext:["ttcn","ttcn3","ttcnpp"]},{name:"TTCN_CFG",mime:"text/x-ttcn-cfg",mode:"ttcn-cfg",ext:["cfg"]},{name:"Turtle",mime:"text/turtle",mode:"turtle",ext:["ttl"]},{name:"TypeScript",mime:"application/typescript",mode:"javascript",ext:["ts"],alias:["ts"]},{name:"Twig",mime:"text/x-twig",mode:"twig"},{name:"VB.NET",mime:"text/x-vb",mode:"vb",ext:["vb"]},{name:"VBScript",mime:"text/vbscript",mode:"vbscript",ext:["vbs"]},{name:"Velocity",mime:"text/velocity",mode:"velocity",ext:["vtl"]},{name:"Verilog",mime:"text/x-verilog",mode:"verilog",
ext:["v"]},{name:"VHDL",mime:"text/x-vhdl",mode:"vhdl",ext:["vhd","vhdl"]},{name:"XML",mimes:["application/xml","text/xml"],mode:"xml",ext:["xml","xsl","xsd"],alias:["rss","wsdl","xsd"]},{name:"XQuery",mime:"application/xquery",mode:"xquery",ext:["xy","xquery"]},{name:"YAML",mime:"text/x-yaml",mode:"yaml",ext:["yaml","yml"],alias:["yml"]},{name:"Z80",mime:"text/x-z80",mode:"z80",ext:["z80"]},{name:"mscgen",mime:"text/x-mscgen",mode:"mscgen",ext:["mscgen","mscin","msc"]},{name:"xu",mime:"text/x-xu",mode:"mscgen",ext:["xu"]},{name:"msgenny",mime:"text/x-msgenny",mode:"mscgen",ext:["msgenny"]}];for(var t=0;t<e.modeInfo.length;t++){var n=e.modeInfo[t];n.mimes&&(n.mime=n.mimes[0])}e.findModeByMIME=function(t){t=t.toLowerCase();for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.mime==t)return r;if(r.mimes)for(var i=0;i<r.mimes.length;i++)if(r.mimes[i]==t)return r}},e.findModeByExtension=function(t){for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.ext)for(var i=0;i<r.ext.length;i++)if(r.ext[i]==t)return r}},e.findModeByFileName=function(t){for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.file&&r.file.test(t))return r}var i=t.lastIndexOf("."),o=i>-1&&t.substring(i+1,t.length);return o?e.findModeByExtension(o):void 0},e.findModeByName=function(t){t=t.toLowerCase();for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.name.toLowerCase()==t)return r;if(r.alias)for(var i=0;i<r.alias.length;i++)if(r.alias[i].toLowerCase()==t)return r}}})},{"../lib/codemirror":7}],11:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";var t={autoSelfClosers:{area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},implicitlyClosed:{dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},contextGrabbers:{dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!0,caseFold:!0},n={autoSelfClosers:{},implicitlyClosed:{},contextGrabbers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1,caseFold:!1};e.defineMode("xml",function(r,i){function o(e,t){function n(n){return t.tokenize=n,n(e,t)}var r=e.next();if("<"==r)return e.eat("!")?e.eat("[")?e.match("CDATA[")?n(s("atom","]]>")):null:e.match("--")?n(s("comment","-->")):e.match("DOCTYPE",!0,!0)?(e.eatWhile(/[\w\._\-]/),n(c(1))):null:e.eat("?")?(e.eatWhile(/[\w\._\-]/),t.tokenize=s("meta","?>"),"meta"):(T=e.eat("/")?"closeTag":"openTag",t.tokenize=l,"tag bracket");if("&"==r){var i;return i=e.eat("#")?e.eat("x")?e.eatWhile(/[a-fA-F\d]/)&&e.eat(";"):e.eatWhile(/[\d]/)&&e.eat(";"):e.eatWhile(/[\w\.\-:]/)&&e.eat(";"),i?"atom":"error"}return e.eatWhile(/[^&<]/),null}function l(e,t){var n=e.next();if(">"==n||"/"==n&&e.eat(">"))return t.tokenize=o,T=">"==n?"endTag":"selfcloseTag","tag bracket";if("="==n)return T="equals",null;if("<"==n){t.tokenize=o,t.state=f,t.tagName=t.tagStart=null;var r=t.tokenize(e,t);return r?r+" tag error":"tag error"}return/[\'\"]/.test(n)?(t.tokenize=a(n),t.stringStartCol=e.column(),t.tokenize(e,t)):(e.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function a(e){var t=function(t,n){for(;!t.eol();)if(t.next()==e){n.tokenize=l;break}return"string"};return t.isInAttribute=!0,t}function s(e,t){return function(n,r){for(;!n.eol();){if(n.match(t)){r.tokenize=o;break}n.next()}return e}}function c(e){return function(t,n){for(var r;null!=(r=t.next());){if("<"==r)return n.tokenize=c(e+1),n.tokenize(t,n);if(">"==r){if(1==e){n.tokenize=o;break}return n.tokenize=c(e-1),n.tokenize(t,n)}}return"meta"}}function u(e,t,n){this.prev=e.context,this.tagName=t,this.indent=e.indented,this.startOfLine=n,(C.doNotIndent.hasOwnProperty(t)||e.context&&e.context.noIndent)&&(this.noIndent=!0)}function d(e){e.context&&(e.context=e.context.prev)}function h(e,t){for(var n;;){if(!e.context)return;if(n=e.context.tagName,!C.contextGrabbers.hasOwnProperty(n)||!C.contextGrabbers[n].hasOwnProperty(t))return;d(e)}}function f(e,t,n){return"openTag"==e?(n.tagStart=t.column(),p):"closeTag"==e?m:f}function p(e,t,n){return"word"==e?(n.tagName=t.current(),M="tag",y):(M="error",p)}function m(e,t,n){if("word"==e){var r=t.current();return n.context&&n.context.tagName!=r&&C.implicitlyClosed.hasOwnProperty(n.context.tagName)&&d(n),n.context&&n.context.tagName==r||C.matchClosing===!1?(M="tag",g):(M="tag error",v)}return M="error",v}function g(e,t,n){return"endTag"!=e?(M="error",g):(d(n),f)}function v(e,t,n){return M="error",g(e,t,n)}function y(e,t,n){if("word"==e)return M="attribute",x;if("endTag"==e||"selfcloseTag"==e){var r=n.tagName,i=n.tagStart;return n.tagName=n.tagStart=null,"selfcloseTag"==e||C.autoSelfClosers.hasOwnProperty(r)?h(n,r):(h(n,r),n.context=new u(n,r,i==n.indented)),f}return M="error",y}function x(e,t,n){return"equals"==e?b:(C.allowMissing||(M="error"),y(e,t,n))}function b(e,t,n){return"string"==e?w:"word"==e&&C.allowUnquoted?(M="string",y):(M="error",y(e,t,n))}function w(e,t,n){return"string"==e?w:y(e,t,n)}var k=r.indentUnit,C={},S=i.htmlMode?t:n;for(var L in S)C[L]=S[L];for(var L in i)C[L]=i[L];var T,M;return o.isInText=!0,{startState:function(e){var t={tokenize:o,state:f,indented:e||0,tagName:null,tagStart:null,context:null};return null!=e&&(t.baseIndent=e),t},token:function(e,t){if(!t.tagName&&e.sol()&&(t.indented=e.indentation()),e.eatSpace())return null;T=null;var n=t.tokenize(e,t);return(n||T)&&"comment"!=n&&(M=null,t.state=t.state(T||n,e,t),M&&(n="error"==M?n+" error":M)),n},indent:function(t,n,r){var i=t.context;if(t.tokenize.isInAttribute)return t.tagStart==t.indented?t.stringStartCol+1:t.indented+k;if(i&&i.noIndent)return e.Pass;if(t.tokenize!=l&&t.tokenize!=o)return r?r.match(/^(\s*)/)[0].length:0;if(t.tagName)return C.multilineTagIndentPastTag!==!1?t.tagStart+t.tagName.length+2:t.tagStart+k*(C.multilineTagIndentFactor||1);if(C.alignCDATA&&/<!\[CDATA\[/.test(n))return 0;var a=n&&/^<(\/)?([\w_:\.-]*)/.exec(n);if(a&&a[1])for(;i;){if(i.tagName==a[2]){i=i.prev;break}if(!C.implicitlyClosed.hasOwnProperty(i.tagName))break;i=i.prev}else if(a)for(;i;){var s=C.contextGrabbers[i.tagName];if(!s||!s.hasOwnProperty(a[2]))break;i=i.prev}for(;i&&i.prev&&!i.startOfLine;)i=i.prev;return i?i.indent+k:t.baseIndent||0},electricInput:/<\/[\s\w:]+>$/,blockCommentStart:"<!--",blockCommentEnd:"-->",configuration:C.htmlMode?"html":"xml",helperType:C.htmlMode?"html":"xml",skipAttribute:function(e){e.state==b&&(e.state=y)}}}),e.defineMIME("text/xml","xml"),e.defineMIME("application/xml","xml"),e.mimeModes.hasOwnProperty("text/html")||e.defineMIME("text/html",{name:"xml",htmlMode:!0})})},{"../../lib/codemirror":7}],12:[function(t,n,r){(function(t){(function(){function t(e){this.tokens=[],this.tokens.links={},this.options=e||h.defaults,this.rules=f.normal,this.options.gfm&&(this.options.tables?this.rules=f.tables:this.rules=f.gfm)}function i(e,t){if(this.options=t||h.defaults,this.links=e,this.rules=p.normal,this.renderer=this.options.renderer||new o,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=p.breaks:this.rules=p.gfm:this.options.pedantic&&(this.rules=p.pedantic)}function o(e){this.options=e||{}}function l(e){this.tokens=[],this.token=null,this.options=e||h.defaults,this.options.renderer=this.options.renderer||new o,this.renderer=this.options.renderer,this.renderer.options=this.options}function a(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function s(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function c(e,t){return e=e.source,t=t||"",function n(r,i){return r?(i=i.source||i,i=i.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,i),n):new RegExp(e,t)}}function u(){}function d(e){for(var t,n,r=1;r<arguments.length;r++){t=arguments[r];for(n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])}return e}function h(e,n,r){if(r||"function"==typeof n){r||(r=n,n=null),n=d({},h.defaults,n||{});var i,o,s=n.highlight,c=0;try{i=t.lex(e,n)}catch(u){return r(u)}o=i.length;var f=function(e){if(e)return n.highlight=s,r(e);var t;try{t=l.parse(i,n)}catch(o){e=o}return n.highlight=s,e?r(e):r(null,t)};if(!s||s.length<3)return f();if(delete n.highlight,!o)return f();for(;c<i.length;c++)!function(e){return"code"!==e.type?--o||f():s(e.text,e.lang,function(t,n){return t?f(t):null==n||n===e.text?--o||f():(e.text=n,e.escaped=!0,void(--o||f()))})}(i[c])}else try{return n&&(n=d({},h.defaults,n)),l.parse(t.lex(e,n),n)}catch(u){if(u.message+="\nPlease report this to https://github.com/chjj/marked.",(n||h.defaults).silent)return"<p>An error occured:</p><pre>"+a(u.message+"",!0)+"</pre>";throw u}}var f={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:u,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:u,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:u,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};f.bullet=/(?:[*+-]|\d+\.)/,f.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,f.item=c(f.item,"gm")(/bull/g,f.bullet)(),f.list=c(f.list)(/bull/g,f.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+f.def.source+")")(),f.blockquote=c(f.blockquote)("def",f.def)(),f._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",f.html=c(f.html)("comment",/<!--[\s\S]*?-->/)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)(/tag/g,f._tag)(),f.paragraph=c(f.paragraph)("hr",f.hr)("heading",f.heading)("lheading",f.lheading)("blockquote",f.blockquote)("tag","<"+f._tag)("def",f.def)(),f.normal=d({},f),f.gfm=d({},f.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),f.gfm.paragraph=c(f.paragraph)("(?!","(?!"+f.gfm.fences.source.replace("\\1","\\2")+"|"+f.list.source.replace("\\1","\\3")+"|")(),f.tables=d({},f.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.rules=f,t.lex=function(e,n){var r=new t(n);return r.lex(e)},t.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},t.prototype.token=function(e,t,n){for(var r,i,o,l,a,s,c,u,d,e=e.replace(/^ +$/gm,"");e;)if((o=this.rules.newline.exec(e))&&(e=e.substring(o[0].length),o[0].length>1&&this.tokens.push({type:"space"})),o=this.rules.code.exec(e))e=e.substring(o[0].length),o=o[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?o:o.replace(/\n+$/,"")});else if(o=this.rules.fences.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"code",lang:o[2],text:o[3]||""});else if(o=this.rules.heading.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"heading",depth:o[1].length,text:o[2]});else if(t&&(o=this.rules.nptable.exec(e))){for(e=e.substring(o[0].length),s={type:"table",header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/\n$/,"").split("\n")},u=0;u<s.align.length;u++)/^ *-+: *$/.test(s.align[u])?s.align[u]="right":/^ *:-+: *$/.test(s.align[u])?s.align[u]="center":/^ *:-+ *$/.test(s.align[u])?s.align[u]="left":s.align[u]=null;for(u=0;u<s.cells.length;u++)s.cells[u]=s.cells[u].split(/ *\| */);this.tokens.push(s)}else if(o=this.rules.lheading.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"heading",depth:"="===o[2]?1:2,text:o[1]});else if(o=this.rules.hr.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"hr"});else if(o=this.rules.blockquote.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"blockquote_start"}),o=o[0].replace(/^ *> ?/gm,""),this.token(o,t,!0),this.tokens.push({type:"blockquote_end"});else if(o=this.rules.list.exec(e)){for(e=e.substring(o[0].length),l=o[2],this.tokens.push({type:"list_start",ordered:l.length>1}),o=o[0].match(this.rules.item),r=!1,d=o.length,u=0;d>u;u++)s=o[u],c=s.length,s=s.replace(/^ *([*+-]|\d+\.) +/,""),~s.indexOf("\n ")&&(c-=s.length,s=this.options.pedantic?s.replace(/^ {1,4}/gm,""):s.replace(new RegExp("^ {1,"+c+"}","gm"),"")),this.options.smartLists&&u!==d-1&&(a=f.bullet.exec(o[u+1])[0],l===a||l.length>1&&a.length>1||(e=o.slice(u+1).join("\n")+e,u=d-1)),i=r||/\n\n(?!\s*$)/.test(s),u!==d-1&&(r="\n"===s.charAt(s.length-1),i||(i=r)),this.tokens.push({type:i?"loose_item_start":"list_item_start"}),this.token(s,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(o=this.rules.html.exec(e))e=e.substring(o[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===o[1]||"script"===o[1]||"style"===o[1]),text:o[0]});else if(!n&&t&&(o=this.rules.def.exec(e)))e=e.substring(o[0].length),this.tokens.links[o[1].toLowerCase()]={href:o[2],title:o[3]};else if(t&&(o=this.rules.table.exec(e))){for(e=e.substring(o[0].length),s={type:"table",header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u<s.align.length;u++)/^ *-+: *$/.test(s.align[u])?s.align[u]="right":/^ *:-+: *$/.test(s.align[u])?s.align[u]="center":/^ *:-+ *$/.test(s.align[u])?s.align[u]="left":s.align[u]=null;for(u=0;u<s.cells.length;u++)s.cells[u]=s.cells[u].replace(/^ *\| *| *\| *$/g,"").split(/ *\| */);this.tokens.push(s)}else if(t&&(o=this.rules.paragraph.exec(e)))e=e.substring(o[0].length),this.tokens.push({type:"paragraph",text:"\n"===o[1].charAt(o[1].length-1)?o[1].slice(0,-1):o[1]});else if(o=this.rules.text.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"text",text:o[0]});else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0));return this.tokens};var p={escape:/^\\([\\`*{}\[\]()#+\-.!_>])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/};p._inside=/(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/,p._href=/\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/,p.link=c(p.link)("inside",p._inside)("href",p._href)(),p.reflink=c(p.reflink)("inside",p._inside)(),p.normal=d({},p),p.pedantic=d({},p.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),p.gfm=d({},p.normal,{escape:c(p.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:c(p.text)("]|","~]|")("|","|https?://|")()}),p.breaks=d({},p.gfm,{br:c(p.br)("{2,}","*")(),text:c(p.gfm.text)("{2,}","*")()}),i.rules=p,i.output=function(e,t,n){var r=new i(t,n);return r.output(e)},i.prototype.output=function(e){for(var t,n,r,i,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=a(i[1]),r=n),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^<a /i.test(i[0])?this.inLink=!0:this.inLink&&/^<\/a>/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):a(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,o+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(a(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),o+=this.renderer.text(a(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=a(i[1]),r=n,o+=this.renderer.link(r,null,n);return o},i.prototype.outputLink=function(e,t){var n=a(t.href),r=t.title?a(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,a(e[1]))},i.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},i.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;r>i;i++)t=e.charCodeAt(i),Math.random()>.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},o.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'<pre><code class="'+this.options.langPrefix+a(t,!0)+'">'+(n?e:a(e,!0))+"\n</code></pre>\n":"<pre><code>"+(n?e:a(e,!0))+"\n</code></pre>"},o.prototype.blockquote=function(e){return"<blockquote>\n"+e+"</blockquote>\n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"<h"+t+' id="'+this.options.headerPrefix+n.toLowerCase().replace(/[^\w]+/g,"-")+'">'+e+"</h"+t+">\n"},o.prototype.hr=function(){return this.options.xhtml?"<hr/>\n":"<hr>\n"},o.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"</"+n+">\n"},o.prototype.listitem=function(e){return"<li>"+e+"</li>\n"},o.prototype.paragraph=function(e){return"<p>"+e+"</p>\n"},o.prototype.table=function(e,t){return"<table>\n<thead>\n"+e+"</thead>\n<tbody>\n"+t+"</tbody>\n</table>\n"},o.prototype.tablerow=function(e){return"<tr>\n"+e+"</tr>\n"},o.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+"</"+n+">\n"},o.prototype.strong=function(e){return"<strong>"+e+"</strong>"},o.prototype.em=function(e){return"<em>"+e+"</em>"},o.prototype.codespan=function(e){return"<code>"+e+"</code>"},o.prototype.br=function(){return this.options.xhtml?"<br/>":"<br>"},o.prototype.del=function(e){return"<del>"+e+"</del>"},o.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(s(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(i){return""}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:"))return""}var o='<a href="'+e+'"';return t&&(o+=' title="'+t+'"'),o+=">"+n+"</a>"},o.prototype.image=function(e,t,n){var r='<img src="'+e+'" alt="'+n+'"';return t&&(r+=' title="'+t+'"'),r+=this.options.xhtml?"/>":">"},o.prototype.text=function(e){return e},l.parse=function(e,t,n){var r=new l(t,n);return r.parse(e)},l.prototype.parse=function(e){this.inline=new i(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},l.prototype.next=function(){return this.token=this.tokens.pop()},l.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},l.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},l.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i,o="",l="";for(n="",e=0;e<this.token.header.length;e++)r={header:!0,align:this.token.align[e]},n+=this.renderer.tablecell(this.inline.output(this.token.header[e]),{header:!0,align:this.token.align[e]});for(o+=this.renderer.tablerow(n),e=0;e<this.token.cells.length;e++){for(t=this.token.cells[e],n="",i=0;i<t.length;i++)n+=this.renderer.tablecell(this.inline.output(t[i]),{header:!1,align:this.token.align[i]});l+=this.renderer.tablerow(n)}return this.renderer.table(o,l);case"blockquote_start":for(var l="";"blockquote_end"!==this.next().type;)l+=this.tok();return this.renderer.blockquote(l);case"list_start":for(var l="",a=this.token.ordered;"list_end"!==this.next().type;)l+=this.tok();return this.renderer.list(l,a);case"list_item_start":for(var l="";"list_item_end"!==this.next().type;)l+="text"===this.token.type?this.parseText():this.tok();return this.renderer.listitem(l);case"loose_item_start":for(var l="";"list_item_end"!==this.next().type;)l+=this.tok();return this.renderer.listitem(l);case"html":var s=this.token.pre||this.options.pedantic?this.token.text:this.inline.output(this.token.text);return this.renderer.html(s);case"paragraph":return this.renderer.paragraph(this.inline.output(this.token.text));case"text":return this.renderer.paragraph(this.parseText())}},u.exec=u,h.options=h.setOptions=function(e){return d(h.defaults,e),h},h.defaults={gfm:!0,tables:!0,breaks:!1,pedantic:!1,sanitize:!1,sanitizer:null,mangle:!0,smartLists:!1,silent:!1,highlight:null,langPrefix:"lang-",smartypants:!1,headerPrefix:"",renderer:new o,xhtml:!1},h.Parser=l,h.parser=l.parse,h.Renderer=o,h.Lexer=t,h.lexer=t.lex,h.InlineLexer=i,h.inlineLexer=i.output,h.parse=h,"undefined"!=typeof n&&"object"==typeof r?n.exports=h:"function"==typeof e&&e.amd?e(function(){return h}):this.marked=h}).call(function(){return this||("undefined"!=typeof window?window:t)}())}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],13:[function(e,t,n){var r=e("codemirror");r.commands.tabAndIndentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentMore");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}},r.commands.shiftTabAndUnindentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentLess");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}}},{codemirror:7}],14:[function(e,t,n){"use strict";function r(e){return e=_?e.replace("Ctrl","Cmd"):e.replace("Cmd","Ctrl")}function i(e,t,n){e=e||{};var r=document.createElement("a");return t=void 0==t?!0:t,e.title&&t&&(r.title=l(e.title,e.action,n),_&&(r.title=r.title.replace("Ctrl","⌘"),r.title=r.title.replace("Alt","⌥"))),r.tabIndex=-1,r.className=e.className,r}function o(){var e=document.createElement("i");return e.className="separator",e.innerHTML="|",e}function l(e,t,n){var i,o=e;return t&&(i=G(t),n[i]&&(o+=" ("+r(n[i])+")")),o}function a(e,t){t=t||e.getCursor("start");var n=e.getTokenAt(t);if(!n.type)return{};for(var r,i,o=n.type.split(" "),l={},a=0;a<o.length;a++)r=o[a],"strong"===r?l.bold=!0:"variable-2"===r?(i=e.getLine(t.line),/^\s*\d+\.\s/.test(i)?l["ordered-list"]=!0:l["unordered-list"]=!0):"atom"===r?l.quote=!0:"em"===r?l.italic=!0:"quote"===r?l.quote=!0:"strikethrough"===r?l.strikethrough=!0:"comment"===r?l.code=!0:"link"===r?l.link=!0:"tag"===r?l.image=!0:r.match(/^header(\-[1-6])?$/)&&(l[r.replace("header","heading")]=!0);return l}function s(e){var t=e.codemirror;t.setOption("fullScreen",!t.getOption("fullScreen")),t.getOption("fullScreen")?(V=document.body.style.overflow,document.body.style.overflow="hidden"):document.body.style.overflow=V;var n=t.getWrapperElement();/fullscreen/.test(n.previousSibling.className)?n.previousSibling.className=n.previousSibling.className.replace(/\s*fullscreen\b/,""):n.previousSibling.className+=" fullscreen";var r=e.toolbarElements.fullscreen;/active/.test(r.className)?r.className=r.className.replace(/\s*active\s*/g,""):r.className+=" active";var i=t.getWrapperElement().nextSibling;/editor-preview-active-side/.test(i.className)&&N(e)}function c(e){I(e,"bold",e.options.blockStyles.bold)}function u(e){I(e,"italic",e.options.blockStyles.italic)}function d(e){I(e,"strikethrough","~~")}function h(e){function t(e){if("object"!=typeof e)throw"fencing_line() takes a 'line' object (not a line number, or line text). Got: "+typeof e+": "+e;return e.styles&&e.styles[2]&&-1!==e.styles[2].indexOf("formatting-code-block")}function n(e){return e.state.base.base||e.state.base}function r(e,r,i,o,l){i=i||e.getLineHandle(r),o=o||e.getTokenAt({line:r,ch:1}),l=l||!!i.text&&e.getTokenAt({line:r,ch:i.text.length-1});var a=o.type?o.type.split(" "):[];return l&&n(l).indentedCode?"indented":-1===a.indexOf("comment")?!1:n(o).fencedChars||n(l).fencedChars||t(i)?"fenced":"single"}function i(e,t,n,r){var i=t.line+1,o=n.line+1,l=t.line!==n.line,a=r+"\n",s="\n"+r;l&&o++,l&&0===n.ch&&(s=r+"\n",o--),O(e,!1,[a,s]),e.setSelection({line:i,ch:0},{line:o,ch:0})}var o,l,a,s=e.options.blockStyles.code,c=e.codemirror,u=c.getCursor("start"),d=c.getCursor("end"),h=c.getTokenAt({line:u.line,ch:u.ch||1}),f=c.getLineHandle(u.line),p=r(c,u.line,f,h);if("single"===p){var m=f.text.slice(0,u.ch).replace("`",""),g=f.text.slice(u.ch).replace("`","");c.replaceRange(m+g,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),u.ch--,u!==d&&d.ch--,c.setSelection(u,d),c.focus()}else if("fenced"===p)if(u.line!==d.line||u.ch!==d.ch){for(o=u.line;o>=0&&(f=c.getLineHandle(o),!t(f));o--);var v,y,x,b,w=c.getTokenAt({line:o,ch:1}),k=n(w).fencedChars;t(c.getLineHandle(u.line))?(v="",y=u.line):t(c.getLineHandle(u.line-1))?(v="",y=u.line-1):(v=k+"\n",y=u.line),t(c.getLineHandle(d.line))?(x="",b=d.line,0===d.ch&&(b+=1)):0!==d.ch&&t(c.getLineHandle(d.line+1))?(x="",b=d.line+1):(x=k+"\n",b=d.line+1),0===d.ch&&(b-=1),c.operation(function(){c.replaceRange(x,{line:b,ch:0},{line:b+(x?0:1),ch:0}),c.replaceRange(v,{line:y,ch:0},{line:y+(v?0:1),ch:0})}),c.setSelection({line:y+(v?1:0),ch:0},{line:b+(v?1:-1),ch:0}),c.focus()}else{var C=u.line;if(t(c.getLineHandle(u.line))&&("fenced"===r(c,u.line+1)?(o=u.line,C=u.line+1):(l=u.line,C=u.line-1)),void 0===o)for(o=C;o>=0&&(f=c.getLineHandle(o),!t(f));o--);if(void 0===l)for(a=c.lineCount(),l=C;a>l&&(f=c.getLineHandle(l),!t(f));l++);c.operation(function(){c.replaceRange("",{line:o,ch:0},{line:o+1,ch:0}),c.replaceRange("",{line:l-1,ch:0},{line:l,ch:0})}),c.focus()}else if("indented"===p){if(u.line!==d.line||u.ch!==d.ch)o=u.line,l=d.line,0===d.ch&&l--;else{for(o=u.line;o>=0;o--)if(f=c.getLineHandle(o),!f.text.match(/^\s*$/)&&"indented"!==r(c,o,f)){o+=1;break}for(a=c.lineCount(),l=u.line;a>l;l++)if(f=c.getLineHandle(l),!f.text.match(/^\s*$/)&&"indented"!==r(c,l,f)){l-=1;break}}var S=c.getLineHandle(l+1),L=S&&c.getTokenAt({line:l+1,ch:S.text.length-1}),T=L&&n(L).indentedCode;T&&c.replaceRange("\n",{line:l+1,ch:0});for(var M=o;l>=M;M++)c.indentLine(M,"subtract");c.focus()}else{var N=u.line===d.line&&u.ch===d.ch&&0===u.ch,A=u.line!==d.line;N||A?i(c,u,d,s):O(c,!1,["`","`"])}}function f(e){var t=e.codemirror;W(t,"quote")}function p(e){var t=e.codemirror;H(t,"smaller")}function m(e){var t=e.codemirror;H(t,"bigger")}function g(e){var t=e.codemirror;H(t,void 0,1)}function v(e){var t=e.codemirror;H(t,void 0,2)}function y(e){var t=e.codemirror;H(t,void 0,3)}function x(e){var t=e.codemirror;W(t,"unordered-list")}function b(e){var t=e.codemirror;W(t,"ordered-list")}function w(e){var t=e.codemirror;E(t)}function k(e){var t=e.codemirror,n=a(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.link),!i)?!1:void O(t,n.link,r.insertTexts.link,i)}function C(e){var t=e.codemirror,n=a(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.image),!i)?!1:void O(t,n.image,r.insertTexts.image,i)}function S(e){var t=e.codemirror,n=a(t),r=e.options;O(t,n.table,r.insertTexts.table)}function L(e){var t=e.codemirror,n=a(t),r=e.options;O(t,n.image,r.insertTexts.horizontalRule)}function T(e){var t=e.codemirror;t.undo(),t.focus()}function M(e){var t=e.codemirror;t.redo(),t.focus()}function N(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.nextSibling,i=e.toolbarElements["side-by-side"],o=!1;/editor-preview-active-side/.test(r.className)?(r.className=r.className.replace(/\s*editor-preview-active-side\s*/g,""),i.className=i.className.replace(/\s*active\s*/g,""),n.className=n.className.replace(/\s*CodeMirror-sided\s*/g," ")):(setTimeout(function(){t.getOption("fullScreen")||s(e),r.className+=" editor-preview-active-side"},1),i.className+=" active",n.className+=" CodeMirror-sided",o=!0);var l=n.lastChild;if(/editor-preview-active/.test(l.className)){l.className=l.className.replace(/\s*editor-preview-active\s*/g,"");var a=e.toolbarElements.preview,c=n.previousSibling;a.className=a.className.replace(/\s*active\s*/g,""),c.className=c.className.replace(/\s*disabled-for-preview*/g,"")}var u=function(){r.innerHTML=e.options.previewRender(e.value(),r)};t.sideBySideRenderingFunction||(t.sideBySideRenderingFunction=u),o?(r.innerHTML=e.options.previewRender(e.value(),r),t.on("update",t.sideBySideRenderingFunction)):t.off("update",t.sideBySideRenderingFunction)}function A(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.previousSibling,i=e.options.toolbar?e.toolbarElements.preview:!1,o=n.lastChild;o&&/editor-preview/.test(o.className)||(o=document.createElement("div"),o.className="editor-preview",n.appendChild(o)),/editor-preview-active/.test(o.className)?(o.className=o.className.replace(/\s*editor-preview-active\s*/g,""),i&&(i.className=i.className.replace(/\s*active\s*/g,""),r.className=r.className.replace(/\s*disabled-for-preview*/g,""))):(setTimeout(function(){o.className+=" editor-preview-active"},1),i&&(i.className+=" active",r.className+=" disabled-for-preview")),o.innerHTML=e.options.previewRender(e.value(),o);var l=t.getWrapperElement().nextSibling;/editor-preview-active-side/.test(l.className)&&N(e)}function O(e,t,n,r){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){var i,o=n[0],l=n[1],a=e.getCursor("start"),s=e.getCursor("end");r&&(l=l.replace("#url#",r)),t?(i=e.getLine(a.line),o=i.slice(0,a.ch),l=i.slice(a.ch),e.replaceRange(o+l,{line:a.line,ch:0})):(i=e.getSelection(),e.replaceSelection(o+i+l),a.ch+=o.length,a!==s&&(s.ch+=o.length)),e.setSelection(a,s),e.focus()}}function H(e,t,n){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){
-for(var r=e.getCursor("start"),i=e.getCursor("end"),o=r.line;o<=i.line;o++)!function(r){var i=e.getLine(r),o=i.search(/[^#]/);i=void 0!==t?0>=o?"bigger"==t?"###### "+i:"# "+i:6==o&&"smaller"==t?i.substr(7):1==o&&"bigger"==t?i.substr(2):"bigger"==t?i.substr(1):"#"+i:1==n?0>=o?"# "+i:o==n?i.substr(o+1):"# "+i.substr(o+1):2==n?0>=o?"## "+i:o==n?i.substr(o+1):"## "+i.substr(o+1):0>=o?"### "+i:o==n?i.substr(o+1):"### "+i.substr(o+1),e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(o);e.focus()}}function W(e,t){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var n=a(e),r=e.getCursor("start"),i=e.getCursor("end"),o={quote:/^(\s*)\>\s+/,"unordered-list":/^(\s*)(\*|\-|\+)\s+/,"ordered-list":/^(\s*)\d+\.\s+/},l={quote:"> ","unordered-list":"* ","ordered-list":"1. "},s=r.line;s<=i.line;s++)!function(r){var i=e.getLine(r);i=n[t]?i.replace(o[t],"$1"):l[t]+i,e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(s);e.focus()}}function I(e,t,n,r){if(!/editor-preview-active/.test(e.codemirror.getWrapperElement().lastChild.className)){r="undefined"==typeof r?n:r;var i,o=e.codemirror,l=a(o),s=n,c=r,u=o.getCursor("start"),d=o.getCursor("end");l[t]?(i=o.getLine(u.line),s=i.slice(0,u.ch),c=i.slice(u.ch),"bold"==t?(s=s.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/,""),c=c.replace(/(\*\*|__)/,"")):"italic"==t?(s=s.replace(/(\*|_)(?![\s\S]*(\*|_))/,""),c=c.replace(/(\*|_)/,"")):"strikethrough"==t&&(s=s.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/,""),c=c.replace(/(\*\*|~~)/,"")),o.replaceRange(s+c,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),"bold"==t||"strikethrough"==t?(u.ch-=2,u!==d&&(d.ch-=2)):"italic"==t&&(u.ch-=1,u!==d&&(d.ch-=1))):(i=o.getSelection(),"bold"==t?(i=i.split("**").join(""),i=i.split("__").join("")):"italic"==t?(i=i.split("*").join(""),i=i.split("_").join("")):"strikethrough"==t&&(i=i.split("~~").join("")),o.replaceSelection(s+i+c),u.ch+=n.length,d.ch=u.ch+i.length),o.setSelection(u,d),o.focus()}}function E(e){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className))for(var t,n=e.getCursor("start"),r=e.getCursor("end"),i=n.line;i<=r.line;i++)t=e.getLine(i),t=t.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/,""),e.replaceRange(t,{line:i,ch:0},{line:i,ch:99999999999999})}function D(e,t){for(var n in t)t.hasOwnProperty(n)&&(t[n]instanceof Array?e[n]=t[n].concat(e[n]instanceof Array?e[n]:[]):null!==t[n]&&"object"==typeof t[n]&&t[n].constructor===Object?e[n]=D(e[n]||{},t[n]):e[n]=t[n]);return e}function P(e){for(var t=1;t<arguments.length;t++)e=D(e,arguments[t]);return e}function F(e){var t=/[a-zA-Z0-9_\u0392-\u03c9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g,n=e.match(t),r=0;if(null===n)return r;for(var i=0;i<n.length;i++)r+=n[i].charCodeAt(0)>=19968?n[i].length:1;return r}function z(e){e=e||{},e.parent=this;var t=!0;if(e.autoDownloadFontAwesome===!1&&(t=!1),e.autoDownloadFontAwesome!==!0)for(var n=document.styleSheets,r=0;r<n.length;r++)n[r].href&&n[r].href.indexOf("//maxcdn.bootstrapcdn.com/font-awesome/")>-1&&(t=!1);if(t){var i=document.createElement("link");i.rel="stylesheet",i.href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css",document.getElementsByTagName("head")[0].appendChild(i)}if(e.element)this.element=e.element;else if(null===e.element)return void console.log("SimpleMDE: Error. No element was found.");if(void 0===e.toolbar){e.toolbar=[];for(var o in K)K.hasOwnProperty(o)&&(-1!=o.indexOf("separator-")&&e.toolbar.push("|"),(K[o]["default"]===!0||e.showIcons&&e.showIcons.constructor===Array&&-1!=e.showIcons.indexOf(o))&&e.toolbar.push(o))}e.hasOwnProperty("status")||(e.status=["autosave","lines","words","cursor"]),e.previewRender||(e.previewRender=function(e){return this.parent.markdown(e)}),e.parsingConfig=P({highlightFormatting:!0},e.parsingConfig||{}),e.insertTexts=P({},X,e.insertTexts||{}),e.promptTexts=Y,e.blockStyles=P({},Z,e.blockStyles||{}),e.shortcuts=P({},U,e.shortcuts||{}),void 0!=e.autosave&&void 0!=e.autosave.unique_id&&""!=e.autosave.unique_id&&(e.autosave.uniqueId=e.autosave.unique_id),this.options=e,this.render(),!e.initialValue||this.options.autosave&&this.options.autosave.foundSavedValue===!0||this.value(e.initialValue)}function R(){if("object"!=typeof localStorage)return!1;try{localStorage.setItem("smde_localStorage",1),localStorage.removeItem("smde_localStorage")}catch(e){return!1}return!0}var B=e("codemirror");e("codemirror/addon/edit/continuelist.js"),e("./codemirror/tablist"),e("codemirror/addon/display/fullscreen.js"),e("codemirror/mode/markdown/markdown.js"),e("codemirror/addon/mode/overlay.js"),e("codemirror/addon/display/placeholder.js"),e("codemirror/mode/gfm/gfm.js"),e("codemirror/mode/xml/xml.js"),e("spell-checker");var j=e("marked"),_=/Mac/.test(navigator.platform),q={toggleBold:c,toggleItalic:u,drawLink:k,toggleHeadingSmaller:p,toggleHeadingBigger:m,drawImage:C,toggleBlockquote:f,toggleOrderedList:b,toggleUnorderedList:x,toggleCodeBlock:h,togglePreview:A,toggleStrikethrough:d,toggleHeading1:g,toggleHeading2:v,toggleHeading3:y,cleanBlock:w,drawTable:S,drawHorizontalRule:L,undo:T,redo:M,toggleSideBySide:N,toggleFullScreen:s},U={toggleBold:"Cmd-B",toggleItalic:"Cmd-I",drawLink:"Cmd-K",toggleHeadingSmaller:"Cmd-H",toggleHeadingBigger:"Shift-Cmd-H",cleanBlock:"Cmd-E",drawImage:"Cmd-Alt-I",toggleBlockquote:"Cmd-'",toggleOrderedList:"Cmd-Alt-L",toggleUnorderedList:"Cmd-L",toggleCodeBlock:"Cmd-Alt-C",togglePreview:"Cmd-P",toggleSideBySide:"F9",toggleFullScreen:"F11"},G=function(e){for(var t in q)if(q[t]===e)return t;return null},$=function(){var e=!1;return function(t){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(t)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(t.substr(0,4)))&&(e=!0)}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:d,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:f,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:C,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:S,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:["![","](#url#)"],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Y={link:"URL for the link:",image:"URL of the image:"},Z={bold:"**",code:"```",italic:"*"};z.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},z.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var l,a;if(t.spellChecker!==!1?(l="spell-checker",a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1):(l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1),this.codemirror=B.fromTextArea(e,{mode:l,backdrop:a,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||""}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element}},z.prototype.autosave=function(){if(R()){var e=this;if(void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to use the autosave feature");null!=e.element.form&&void 0!=e.element.form&&e.element.form.addEventListener("submit",function(){localStorage.removeItem("smde_"+e.options.autosave.uniqueId)}),this.options.autosave.loaded!==!0&&("string"==typeof localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&""!=localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&(this.codemirror.setValue(localStorage.getItem("smde_"+this.options.autosave.uniqueId)),this.options.autosave.foundSavedValue=!0),this.options.autosave.loaded=!0),localStorage.setItem("smde_"+this.options.autosave.uniqueId,e.value());var t=document.getElementById("autosaved");if(null!=t&&void 0!=t&&""!=t){var n=new Date,r=n.getHours(),i=n.getMinutes(),o="am",l=r;l>=12&&(l=r-12,o="pm"),0==l&&(l=12),i=10>i?"0"+i:i,t.innerHTML="Autosaved: "+l+":"+i+" "+o}this.autosaveTimeoutId=setTimeout(function(){e.autosave()},this.options.autosave.delay||1e4)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},z.prototype.clearAutosavedValue=function(){if(R()){if(void 0==this.options.autosave||void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to clear the autosave value");localStorage.removeItem("smde_"+this.options.autosave.uniqueId)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},z.prototype.createSideBySide=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;n&&/editor-preview-side/.test(n.className)||(n=document.createElement("div"),n.className="editor-preview-side",t.parentNode.insertBefore(n,t.nextSibling));var r=!1,i=!1;return e.on("scroll",function(e){if(r)return void(r=!1);i=!0;var t=e.getScrollInfo().height-e.getScrollInfo().clientHeight,o=parseFloat(e.getScrollInfo().top)/t,l=(n.scrollHeight-n.clientHeight)*o;n.scrollTop=l}),n.onscroll=function(){if(i)return void(i=!1);r=!0;var t=n.scrollHeight-n.clientHeight,o=parseFloat(n.scrollTop)/t,l=(e.getScrollInfo().height-e.getScrollInfo().clientHeight)*o;e.scrollTo(0,l)},n},z.prototype.createToolbar=function(e){if(e=e||this.options.toolbar,e&&0!==e.length){var t;for(t=0;t<e.length;t++)void 0!=K[e[t]]&&(e[t]=K[e[t]]);var n=document.createElement("div");n.className="editor-toolbar";var r=this,l={};for(r.toolbar=e,t=0;t<e.length;t++)if(("guide"!=e[t].name||r.options.toolbarGuideIcon!==!1)&&!(r.options.hideIcons&&-1!=r.options.hideIcons.indexOf(e[t].name)||("fullscreen"==e[t].name||"side-by-side"==e[t].name)&&$())){if("|"===e[t]){for(var s=!1,c=t+1;c<e.length;c++)console.log(c),"|"===e[c]||r.options.hideIcons&&-1!=r.options.hideIcons.indexOf(e[c].name)||(console.log(e[c]),s=!0);if(!s)continue}!function(e){var t;t="|"===e?o():i(e,r.options.toolbarTips,r.options.shortcuts),e.action&&("function"==typeof e.action?t.onclick=function(){e.action(r)}:"string"==typeof e.action&&(t.href=e.action,t.target="_blank")),l[e.name||e]=t,n.appendChild(t)}(e[t])}r.toolbarElements=l;var u=this.codemirror;u.on("cursorActivity",function(){var e=a(u);for(var t in l)!function(t){var n=l[t];e[t]?n.className+=" active":"fullscreen"!=t&&"side-by-side"!=t&&(n.className=n.className.replace(/\s*active\s*/g,""))}(t)});var d=u.getWrapperElement();return d.parentNode.insertBefore(n,d),n}},z.prototype.createStatusbar=function(e){e=e||this.options.status;var t=this.options,n=this.codemirror;if(e&&0!==e.length){var r,i,o,l=[];for(r=0;r<e.length;r++)if(i=void 0,o=void 0,"object"==typeof e[r])l.push({className:e[r].className,defaultValue:e[r].defaultValue,onUpdate:e[r].onUpdate});else{var a=e[r];"words"===a?(o=function(e){e.innerHTML="0"},i=function(e){e.innerHTML=F(n.getValue())}):"lines"===a?(o=function(e){e.innerHTML="0"},i=function(e){e.innerHTML=n.lineCount()}):"cursor"===a?(o=function(e){e.innerHTML="0:0"},i=function(e){var t=n.getCursor();e.innerHTML=t.line+":"+t.ch}):"autosave"===a&&(o=function(e){void 0!=t.autosave&&t.autosave.enabled===!0&&e.setAttribute("id","autosaved")}),l.push({className:a,defaultValue:o,onUpdate:i})}var s=document.createElement("div");for(s.className="editor-statusbar",r=0;r<l.length;r++){var c=l[r],u=document.createElement("span");u.className=c.className,"function"==typeof c.defaultValue&&c.defaultValue(u),"function"==typeof c.onUpdate&&this.codemirror.on("update",function(e,t){return function(){t.onUpdate(e)}}(u,c)),s.appendChild(u)}var d=this.codemirror.getWrapperElement();return d.parentNode.insertBefore(s,d.nextSibling),s}},z.prototype.value=function(e){return void 0===e?this.codemirror.getValue():(this.codemirror.getDoc().setValue(e),this)},z.toggleBold=c,z.toggleItalic=u,z.toggleStrikethrough=d,z.toggleBlockquote=f,z.toggleHeadingSmaller=p,z.toggleHeadingBigger=m,z.toggleHeading1=g,z.toggleHeading2=v,z.toggleHeading3=y,z.toggleCodeBlock=h,z.toggleUnorderedList=x,z.toggleOrderedList=b,z.cleanBlock=w,z.drawLink=k,z.drawImage=C,z.drawTable=S,z.drawHorizontalRule=L,z.undo=T,z.redo=M,z.togglePreview=A,z.toggleSideBySide=N,z.toggleFullScreen=s,z.prototype.toggleBold=function(){c(this)},z.prototype.toggleItalic=function(){u(this)},z.prototype.toggleStrikethrough=function(){d(this)},z.prototype.toggleBlockquote=function(){f(this)},z.prototype.toggleHeadingSmaller=function(){p(this)},z.prototype.toggleHeadingBigger=function(){m(this)},z.prototype.toggleHeading1=function(){g(this)},z.prototype.toggleHeading2=function(){v(this)},z.prototype.toggleHeading3=function(){y(this)},z.prototype.toggleCodeBlock=function(){h(this)},z.prototype.toggleUnorderedList=function(){x(this)},z.prototype.toggleOrderedList=function(){b(this)},z.prototype.cleanBlock=function(){w(this)},z.prototype.drawLink=function(){k(this)},z.prototype.drawImage=function(){C(this)},z.prototype.drawTable=function(){S(this)},z.prototype.drawHorizontalRule=function(){L(this)},z.prototype.undo=function(){T(this)},z.prototype.redo=function(){M(this)},z.prototype.togglePreview=function(){A(this)},z.prototype.toggleSideBySide=function(){N(this)},z.prototype.toggleFullScreen=function(){s(this)},z.prototype.isPreviewActive=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.lastChild;return/editor-preview-active/.test(n.className)},z.prototype.isSideBySideActive=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;return/editor-preview-active-side/.test(n.className)},z.prototype.isFullscreenActive=function(){var e=this.codemirror;return e.getOption("fullScreen")},z.prototype.getState=function(){var e=this.codemirror;return a(e)},z.prototype.toTextArea=function(){var e=this.codemirror,t=e.getWrapperElement();t.parentNode.removeChild(this.gui.toolbar),t.parentNode.removeChild(this.gui.statusbar),t.parentNode.removeChild(this.gui.sideBySide),e.toTextArea(),this.autosaveTimeoutId&&(clearTimeout(this.autosaveTimeoutId),this.autosaveTimeoutId=void 0,this.clearAutosavedValue())},t.exports=z},{"./codemirror/tablist":13,codemirror:7,"codemirror/addon/display/fullscreen.js":3,"codemirror/addon/display/placeholder.js":4,"codemirror/addon/edit/continuelist.js":5,"codemirror/addon/mode/overlay.js":6,"codemirror/mode/gfm/gfm.js":8,"codemirror/mode/markdown/markdown.js":9,"codemirror/mode/xml/xml.js":11,marked:12,"spell-checker":1}]},{},[14])(14)});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return a>1&&5>a&&1!==~~(a/10)}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekund":"pár sekundami";case"m":return b?"minuta":e?"minutu":"minutou";case"mm":return b||e?f+(c(a)?"minuty":"minut"):f+"minutami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodin"):f+"hodinami";case"d":return b||e?"den":"dnem";case"dd":return b||e?f+(c(a)?"dny":"dní"):f+"dny";case"M":return b||e?"měsíc":"měsícem";case"MM":return b||e?f+(c(a)?"měsíce":"měsíců"):f+"měsíci";case"y":return b||e?"rok":"rokem";case"yy":return b||e?f+(c(a)?"roky":"let"):f+"lety"}}var e="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),f="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");(b.defineLocale||b.lang).call(b,"cs",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"&#x3C;Dříve",nextText:"Později&#x3E;",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"&#x3C;Forrige",nextText:"Næste&#x3E;",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"&#x3C;Zurück",nextText:"Vor&#x3E;",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),d="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");(b.defineLocale||b.lang).call(b,"es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"&#x3C;Ant",nextText:"Sig&#x3E;",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(a,b){return/D/.test(b.substring(0,b.indexOf("MMMM")))?this._monthsGenitiveEl[a.month()]:this._monthsNominativeEl[a.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(a,b,c){return a>11?c?"μμ":"ΜΜ":c?"πμ":"ΠΜ"},isPM:function(a){return"μ"===(a+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(a,b){var c=this._calendarEl[a],d=b&&b.hours();return"function"==typeof c&&(c=c.apply(b)),c.replace("{}",d%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("el","el",{closeText:"Κλείσιμο",prevText:"Προηγούμενος",nextText:"Επόμενος",currentText:"Σήμερα",monthNames:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthNamesShort:["Ιαν","Φεβ","Μαρ","Απρ","Μαι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],dayNames:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],dayNamesShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayNamesMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],weekHeader:"Εβδ",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("el",{buttonText:{month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},allDayText:"Ολοήμερο",eventLimitText:"περισσότερα"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,e){var f="";switch(c){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=d(a,e)+" "+f}function d(a,b){return 10>a?b?f[a]:e[a]:a}var e="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),f=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",e[7],e[8],e[9]];(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"&#xAB;Edellinen",nextText:"Seuraava&#xBB;",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function d(a){return(a?"":"[múlt] ")+"["+e[this.day()]+"] LT[-kor]"}var e="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return d.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return d.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"&#x3C;mundur",nextText:"maju&#x3E;",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari<br/>penuh",eventLimitText:"lebih"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"&#x3C;Prec",nextText:"Succ&#x3E;",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il<br/>giorno",eventLimitText:function(a){return"+altri "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}}),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"&#x3C;前",nextText:"次&#x3E;",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),d="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"&#xAB;Forrige",nextText:"Neste&#xBB;",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function d(a,b,d){var e=a+" ";switch(d){case"m":return b?"minuta":"minutę";case"mm":return e+(c(a)?"minuty":"minut");case"h":return b?"godzina":"godzinę";case"hh":return e+(c(a)?"godziny":"godzin");case"MM":return e+(c(a)?"miesiące":"miesięcy");case"yy":return e+(c(a)?"lata":"lat")}}var e="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),f="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return/D MMMM/.test(b)?f[a.month()]:e[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:d,mm:d,h:d,hh:d,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:d,y:"rok",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"&#x3C;Poprzedni",nextText:"Następny&#x3E;",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"}),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"&#x3C;Anterior",nextText:"Próximo&#x3E;",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:b?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?b?"минута":"минуту":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function g(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}(b.defineLocale||b.lang).call(b,"ru",{months:e,monthsShort:f,weekdays:g,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:d,mm:d,h:"час",hh:d,d:"день",dd:d,M:"месяц",MM:d,y:"год",yy:d},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"&#x3C;Пред",nextText:"След&#x3E;",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"&#xAB;Förra",nextText:"Nästa&#xBB;",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"dan",dd:c.translate,M:"mesec",MM:c.translate,y:"godinu",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}}),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"&#xAB;&#xA0;ย้อน",nextText:"ถัดไป&#xA0;&#xBB;",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var b=a%10,d=a%100-b,e=a>=100?100:null;return a+(c[b]||c[d]||c[e])},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"&#x3C;geri",nextText:"ileri&#x3e",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()-a.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()<a.unix()?"[上]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},sameElse:"LL"},ordinalParse:/\d{1,2}(日|月|周)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"周";default:return a}},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1分钟",mm:"%d分钟",h:"1小时",hh:"%d小时",d:"1天",dd:"%d天",M:"1个月",MM:"%d个月",y:"1年",yy:"%d年"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("zh-cn","zh-CN",{closeText:"关闭",prevText:"&#x3C;上月",nextText:"下月&#x3E;",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-cn",{buttonText:{month:"月",week:"周",day:"日",list:"日程"},allDayText:"全天",eventLimitText:function(a){return"另外 "+a+" 个"}})});"use strict";var Kanboard={};Kanboard.App=function(){this.controllers={}};Kanboard.App.prototype.get=function(a){return this.controllers[a]};Kanboard.App.prototype.execute=function(){for(var b in Kanboard){if(b!=="App"){var a=new Kanboard[b](this);this.controllers[b]=a;if(typeof a.execute==="function"){a.execute()}if(typeof a.listen==="function"){a.listen()}if(typeof a.focus==="function"){a.focus()}if(typeof a.keyboardShortcuts==="function"){a.keyboardShortcuts()}}}this.focus();this.chosen();this.keyboardShortcuts();this.datePicker();this.autoComplete()};Kanboard.App.prototype.keyboardShortcuts=function(){var a=this;Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(b){b.preventDefault();$("#board-selector").trigger("chosen:open")});Mousetrap.bindGlobal("esc",function(){a.get("Popover").close();a.get("Dropdown").close()});Mousetrap.bind("?",function(){a.get("Popover").open($("body").data("keyboard-shortcut-url"))})};Kanboard.App.prototype.focus=function(){$(document).on("focus",".auto-select",function(){$(this).select()});$(document).on("mouseup",".auto-select",function(a){a.preventDefault()})};Kanboard.App.prototype.chosen=function(){$(".chosen-select").each(function(){var a=$(this).data("search-threshold");if(a===undefined){a=10}$(this).chosen({width:"180px",no_results_text:$(this).data("notfound"),disable_search_threshold:a})});$(".select-auto-redirect").change(function(){var a=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(a,$(this).val())})};Kanboard.App.prototype.datePicker=function(){$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".form-date").datepicker({showOtherMonths:true,selectOtherMonths:true,dateFormat:"yy-mm-dd",constrainInput:false});$(".form-datetime").datetimepicker({controlType:"select",oneLine:true,dateFormat:"yy-mm-dd",constrainInput:false})};Kanboard.App.prototype.autoComplete=function(){$(".autocomplete").each(function(){var b=$(this);var c=b.data("dst-field");var a=b.data("dst-extra-field");if($("#form-"+c).val()==""){b.parent().find("button[type=submit]").attr("disabled","disabled")}b.autocomplete({source:b.data("search-url"),minLength:1,select:function(d,e){$("input[name="+c+"]").val(e.item.id);if(a){$("input[name="+a+"]").val(e.item[a])}b.parent().find("button[type=submit]").removeAttr("disabled")}})})};Kanboard.App.prototype.hasId=function(a){return !!document.getElementById(a)};Kanboard.App.prototype.showLoadingIcon=function(){$("body").append('<span id="app-loading-icon">&nbsp;<i class="fa fa-spinner fa-spin"></i></span>')};Kanboard.App.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()};Kanboard.App.prototype.formatDuration=function(a){if(a>=86400){return Math.round(a/86400)+"d"}else{if(a>=3600){return Math.round(a/3600)+"h"}else{if(a>=60){return Math.round(a/60)+"m"}}}return a+"s"};Kanboard.App.prototype.isVisible=function(){var a="";if(typeof document.hidden!=="undefined"){a="visibilityState"}else{if(typeof document.mozHidden!=="undefined"){a="mozVisibilityState"}else{if(typeof document.msHidden!=="undefined"){a="msVisibilityState"}else{if(typeof document.webkitHidden!=="undefined"){a="webkitVisibilityState"}}}}if(a!=""){return document[a]=="visible"}return true};Kanboard.Dropdown=function(a){this.app=a};Kanboard.Dropdown.prototype.listen=function(){var a=this;$(document).on("click",function(){a.close()});$(document).on("click",".dropdown-menu",function(f){f.preventDefault();f.stopImmediatePropagation();a.close();var c=$(this).next("ul");var g=$(this).offset();$("body").append(jQuery("<div>",{id:"dropdown"}));c.clone().appendTo("#dropdown");var h=$("#dropdown ul");h.addClass("dropdown-submenu-open");var d=h.outerHeight();var b=h.outerWidth();if(g.top+d-$(window).scrollTop()<$(window).height()||$(window).scrollTop()+g.top<d){h.css("top",g.top+$(this).height())}else{h.css("top",g.top-d-5)}if(g.left+b>$(window).width()){h.css("left",g.left-b+$(this).outerWidth())}else{h.css("left",g.left)}});$(document).on("click",".dropdown-submenu-open li",function(b){if($(b.target).is("li")){$(this).find("a:visible")[0].click()}})};Kanboard.Dropdown.prototype.close=function(){$("#dropdown").remove()};Kanboard.Dropdown.prototype.onPopoverOpened=function(){this.close()};Kanboard.Popover=function(a){this.app=a};Kanboard.Popover.prototype.listen=function(){var a=this;$(document).on("click",".popover",function(b){a.onClick(b)});$(document).on("click",".close-popover",function(b){a.close(b)});$(document).on("click","#popover-container",function(b){a.close(b)});$(document).on("click","#popover-content",function(b){b.stopPropagation()})};Kanboard.Popover.prototype.onClick=function(c){c.preventDefault();c.stopPropagation();var b=c.currentTarget||c.target;var a=b.getAttribute("href");if(!a){a=b.getAttribute("data-href")}if(a){this.open(a)}};Kanboard.Popover.prototype.isOpen=function(){return $("#popover-container").size()>0};Kanboard.Popover.prototype.open=function(b){var a=this;$.get(b,function(c){$("body").prepend('<div id="popover-container"><div id="popover-content">'+c+"</div></div>");a.executeOnOpenedListeners()})};Kanboard.Popover.prototype.close=function(a){if(this.isOpen()){if(a){a.preventDefault()}$("#popover-container").remove();this.executeOnClosedListeners()}};Kanboard.Popover.prototype.ajaxReload=function(c,b,a){var d=b.getResponseHeader("X-Ajax-Redirect");if(d){window.location=d==="self"?window.location.href.split("#")[0]:d}else{$("#popover-content").html(c);$("#popover-content input[autofocus]").focus();a.executeOnOpenedListeners()}};Kanboard.Popover.prototype.executeOnOpenedListeners=function(){for(var b in this.app.controllers){var a=this.app.get(b);if(typeof a.onPopoverOpened==="function"){a.onPopoverOpened()}}this.afterOpen()};Kanboard.Popover.prototype.executeOnClosedListeners=function(){for(var b in this.app.controllers){var a=this.app.get(b);if(typeof a.onPopoverClosed==="function"){a.onPopoverClosed()}}};Kanboard.Popover.prototype.afterOpen=function(){var b=this;var a=$("#popover-content .popover-form");if(a){a.on("submit",function(c){c.preventDefault();$.ajax({type:"POST",url:a.attr("action"),data:a.serialize(),success:function(e,f,d){b.ajaxReload(e,d,b)},beforeSend:function(){var d=$('.popover-form button[type="submit"]');d.html('<i class="fa fa-spinner fa-pulse"></i> '+d.html());d.attr("disabled",true)}})})}$(document).on("click",".popover-link",function(c){c.preventDefault();$.ajax({type:"GET",url:$(this).attr("href"),success:function(e,f,d){b.ajaxReload(e,d,b)}})});$("[autofocus]").each(function(){$(this).focus()});this.app.datePicker();this.app.autoComplete()};Kanboard.Notification=function(a){this.app=a};Kanboard.Notification.prototype.execute=function(){$(".alert-fade-out").delay(4000).fadeOut(800,function(){$(this).remove()})};Kanboard.Accordion=function(a){this.app=a};Kanboard.Accordion.prototype.listen=function(){$(document).on("click",".accordion-toggle",function(b){var a=$(this).parents(".accordion-section");b.preventDefault();if(a.hasClass("accordion-collapsed")){a.find(".accordion-content").show();a.removeClass("accordion-collapsed")}else{a.find(".accordion-content").hide();a.addClass("accordion-collapsed")}})};Kanboard.Session=function(a){this.app=a};Kanboard.Session.prototype.execute=function(){window.setInterval(this.checkSession,60000)};Kanboard.Session.prototype.checkSession=function(){if(!$(".form-login").length){$.ajax({cache:false,url:$("body").data("status-url"),statusCode:{401:function(){window.location=$("body").data("login-url")}}})}};Kanboard.Calendar=function(a){this.app=a};Kanboard.Calendar.prototype.execute=function(){var a=$("#calendar");if(a.length==1){this.show(a)}};Kanboard.Calendar.prototype.show=function(a){a.fullCalendar({lang:$("body").data("js-lang"),editable:true,eventLimit:true,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(b){$.ajax({cache:false,url:a.data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:b.id,date_due:b.start.format()})})},viewRender:function(){var b=a.data("check-url");var d={start:a.fullCalendar("getView").start.format(),end:a.fullCalendar("getView").end.format()};for(var c in d){b+="&"+c+"="+d[c]}$.getJSON(b,function(e){a.fullCalendar("removeEvents");a.fullCalendar("addEventSource",e);a.fullCalendar("rerenderEvents")})}})};Kanboard.AvgTimeColumnChart=function(a){this.app=a};Kanboard.AvgTimeColumnChart.prototype.execute=function(){if(this.app.hasId("analytic-avg-time-column")){this.show()}};Kanboard.AvgTimeColumnChart.prototype.show=function(){var d=$("#chart");var c=d.data("metrics");var e=[d.data("label")];var a=[];for(var b in c){e.push(c[b].average);a.push(c[b].title)}c3.generate({data:{columns:[e],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:a},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};Kanboard.BurndownChart=function(a){this.app=a};Kanboard.BurndownChart.prototype.execute=function(){if(this.app.hasId("analytic-burndown")){this.show()}};Kanboard.BurndownChart.prototype.show=function(){var g=$("#chart");var f=g.data("metrics");var e=[[g.data("label-total")]];var a=[];var c=d3.time.format("%Y-%m-%d");var h=d3.time.format(g.data("date-format"));for(var d=0;d<f.length;d++){for(var b=0;b<f[d].length;b++){if(d==0){e.push([f[d][b]])}else{e[b+1].push(f[d][b]);if(b>0){if(e[0][d]==undefined){e[0].push(0)}e[0][d]+=f[d][b]}if(b==0){a.push(h(c.parse(f[d][b])))}}}}c3.generate({data:{columns:e},axis:{x:{type:"category",categories:a}}})};Kanboard.CompareHoursColumnChart=function(a){this.app=a};Kanboard.CompareHoursColumnChart.prototype.execute=function(){if(this.app.hasId("analytic-compare-hours")){this.show()}};Kanboard.CompareHoursColumnChart.prototype.show=function(){var g=$("#chart");var f=g.data("metrics");var b=g.data("label-open");var a=g.data("label-closed");var h=[g.data("label-spent")];var e=[g.data("label-estimated")];var d=[];for(var c in f){h.push(parseFloat(f[c].time_spent));e.push(parseFloat(f[c].time_estimated));d.push(c=="open"?b:a)}c3.generate({data:{columns:[h,e],type:"bar"},bar:{width:{ratio:0.2}},axis:{x:{type:"category",categories:d}},legend:{show:true}})};Kanboard.CumulativeFlowDiagram=function(a){this.app=a};Kanboard.CumulativeFlowDiagram.prototype.execute=function(){if(this.app.hasId("analytic-cfd")){this.show()}};Kanboard.CumulativeFlowDiagram.prototype.show=function(){var g=$("#chart");var h=g.data("metrics");var b=[];var a=[];var e=[];var k=d3.time.format("%Y-%m-%d");var f=d3.time.format(g.data("date-format"));for(var d=0;d<h.length;d++){for(var c=0;c<h[d].length;c++){if(d==0){b.push([h[d][c]]);if(c>0){a.push(h[d][c])}}else{b[c].push(h[d][c]);if(c==0){e.push(f(k.parse(h[d][c])))}}}}c3.generate({data:{columns:b,type:"area-spline",groups:[a]},axis:{x:{type:"category",categories:e}}})};Kanboard.LeadCycleTimeChart=function(a){this.app=a};Kanboard.LeadCycleTimeChart.prototype.execute=function(){if(this.app.hasId("analytic-lead-cycle-time")){this.show()}};Kanboard.LeadCycleTimeChart.prototype.show=function(){var h=$("#chart");var g=h.data("metrics");var f=[h.data("label-cycle")];var c=[h.data("label-lead")];var b=[];var e={};e[h.data("label-cycle")]="area";e[h.data("label-lead")]="area-spline";var a={};a[h.data("label-lead")]="#afb42b";a[h.data("label-cycle")]="#4e342e";for(var d=0;d<g.length;d++){f.push(parseInt(g[d].avg_cycle_time));c.push(parseInt(g[d].avg_lead_time));b.push(g[d].day)}c3.generate({data:{columns:[c,f],types:e,colors:a},axis:{x:{type:"category",categories:b},y:{tick:{format:this.app.formatDuration}}}})};Kanboard.UserRepartitionChart=function(a){this.app=a};Kanboard.UserRepartitionChart.prototype.execute=function(){if(this.app.hasId("analytic-user-repartition")){this.show()}};Kanboard.UserRepartitionChart.prototype.show=function(){var c=$("#chart").data("metrics");var b=[];for(var a=0;a<c.length;a++){b.push([c[a].user,c[a].nb_tasks])}c3.generate({data:{columns:b,type:"donut"}})};Kanboard.TaskTimeColumnChart=function(a){this.app=a};Kanboard.TaskTimeColumnChart.prototype.execute=function(){if(this.app.hasId("analytic-task-time-column")){this.show()}};Kanboard.TaskTimeColumnChart.prototype.show=function(){var d=$("#chart");var c=d.data("metrics");var e=[d.data("label")];var a=[];for(var b=0;b<c.length;b++){e.push(c[b].time_spent);a.push(c[b].title)}c3.generate({data:{columns:[e],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:a},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};Kanboard.TaskRepartitionChart=function(a){this.app=a};Kanboard.TaskRepartitionChart.prototype.execute=function(){if(this.app.hasId("analytic-task-repartition")){this.show()}};Kanboard.TaskRepartitionChart.prototype.show=function(){var c=$("#chart").data("metrics");var b=[];for(var a=0;a<c.length;a++){b.push([c[a].column_title,c[a].nb_tasks])}c3.generate({data:{columns:b,type:"donut"}})};Kanboard.Gantt=function(a){this.app=a;this.data=[];this.options={container:"#gantt-chart",showWeekends:true,allowMoves:true,allowResizes:true,cellWidth:21,cellHeight:31,slideWidth:1000,vHeaderWidth:200}};Kanboard.Gantt.prototype.execute=function(){if(this.app.hasId("gantt-chart")){this.show()}};Kanboard.Gantt.prototype.saveRecord=function(a){this.app.showLoadingIcon();$.ajax({cache:false,url:$(this.options.container).data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify(a),complete:this.app.hideLoadingIcon.bind(this)})};Kanboard.Gantt.prototype.show=function(){this.data=this.prepareData($(this.options.container).data("records"));var d=Math.floor((this.options.slideWidth/this.options.cellWidth)+5);var c=this.getDateRange(d);var a=c[0];var f=c[1];var b=$(this.options.container);var e=jQuery("<div>",{"class":"ganttview"});e.append(this.renderVerticalHeader());e.append(this.renderSlider(a,f));b.append(e);jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child",b).addClass("last");jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child",b).addClass("last");jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child",b).addClass("last");if(!$(this.options.container).data("readonly")){this.listenForBlockResize(a);this.listenForBlockMove(a)}else{this.options.allowResizes=false;this.options.allowMoves=false}};Kanboard.Gantt.prototype.renderVerticalHeader=function(){var e=jQuery("<div>",{"class":"ganttview-vtheader"});var b=jQuery("<div>",{"class":"ganttview-vtheader-item"});var d=jQuery("<div>",{"class":"ganttview-vtheader-series"});for(var a=0;a<this.data.length;a++){var c=jQuery("<span>").append(jQuery("<i>",{"class":"fa fa-info-circle tooltip",title:this.getVerticalHeaderTooltip(this.data[a])})).append("&nbsp;");if(this.data[a].type=="task"){c.append(jQuery("<a>",{href:this.data[a].link,target:"_blank",title:this.data[a].title}).append(this.data[a].title))}else{c.append(jQuery("<a>",{href:this.data[a].board_link,target:"_blank",title:$(this.options.container).data("label-board-link")}).append('<i class="fa fa-th"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[a].gantt_link,target:"_blank",title:$(this.options.container).data("label-gantt-link")}).append('<i class="fa fa-sliders"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[a].link,target:"_blank"}).append(this.data[a].title))}d.append(jQuery("<div>",{"class":"ganttview-vtheader-series-name"}).append(c))}b.append(d);e.append(b);return e};Kanboard.Gantt.prototype.renderSlider=function(b,d){var a=jQuery("<div>",{"class":"ganttview-slide-container"});var c=this.getDates(b,d);a.append(this.renderHorizontalHeader(c));a.append(this.renderGrid(c));a.append(this.addBlockContainers());this.addBlocks(a,b);return a};Kanboard.Gantt.prototype.renderHorizontalHeader=function(a){var h=jQuery("<div>",{"class":"ganttview-hzheader"});var f=jQuery("<div>",{"class":"ganttview-hzheader-months"});var e=jQuery("<div>",{"class":"ganttview-hzheader-days"});var c=0;for(var i in a){for(var b in a[i]){var j=a[i][b].length*this.options.cellWidth;c=c+j;f.append(jQuery("<div>",{"class":"ganttview-hzheader-month",css:{width:(j-1)+"px"}}).append($.datepicker.regional[$("body").data("js-lang")].monthNames[b]+" "+i));for(var g in a[i][b]){e.append(jQuery("<div>",{"class":"ganttview-hzheader-day"}).append(a[i][b][g].getDate()))}}}f.css("width",c+"px");e.css("width",c+"px");h.append(f).append(e);return h};Kanboard.Gantt.prototype.renderGrid=function(a){var k=jQuery("<div>",{"class":"ganttview-grid"});var e=jQuery("<div>",{"class":"ganttview-grid-row"});for(var h in a){for(var b in a[h]){for(var g in a[h][b]){var c=jQuery("<div>",{"class":"ganttview-grid-row-cell"});if(this.options.showWeekends&&this.isWeekend(a[h][b][g])){c.addClass("ganttview-weekend")}e.append(c)}}}var j=jQuery("div.ganttview-grid-row-cell",e).length*this.options.cellWidth;e.css("width",j+"px");k.css("width",j+"px");for(var f=0;f<this.data.length;f++){k.append(e.clone())}return k};Kanboard.Gantt.prototype.addBlockContainers=function(){var b=jQuery("<div>",{"class":"ganttview-blocks"});for(var a=0;a<this.data.length;a++){b.append(jQuery("<div>",{"class":"ganttview-block-container"}))}return b};Kanboard.Gantt.prototype.addBlocks=function(b,a){var j=jQuery("div.ganttview-blocks div.ganttview-block-container",b);var c=0;for(var f=0;f<this.data.length;f++){var g=this.data[f];var k=this.daysBetween(g.start,g.end)+1;var e=this.daysBetween(a,g.start);var h=jQuery("<div>",{"class":"ganttview-block-text"});var d=jQuery("<div>",{"class":"ganttview-block tooltip"+(this.options.allowMoves?" ganttview-block-movable":""),title:this.getBarTooltip(g),css:{width:((k*this.options.cellWidth)-9)+"px","margin-left":(e*this.options.cellWidth)+"px"}}).append(h);if(k>=2){h.append(g.progress)}d.data("record",g);this.setBarColor(d,g);if(g.progress!="0%"){d.append(jQuery("<div>",{css:{"z-index":0,position:"absolute",top:0,bottom:0,"background-color":g.color.border,width:g.progress,opacity:0.4}}))}jQuery(j[c]).append(d);c=c+1}};Kanboard.Gantt.prototype.getVerticalHeaderTooltip=function(b){var g="";if(b.type=="task"){g="<strong>"+b.column_title+"</strong> ("+b.progress+")<br/>"+b.title}else{var d=["managers","members"];for(var c in d){var e=d[c];if(!jQuery.isEmptyObject(b.users[e])){var f=jQuery("<ul>");for(var a in b.users[e]){f.append(jQuery("<li>").append(b.users[e][a]))}g+="<p><strong>"+$(this.options.container).data("label-"+e)+"</strong></p>"+f[0].outerHTML}}}return g};Kanboard.Gantt.prototype.getBarTooltip=function(a){var b="";if(a.not_defined){b=$(this.options.container).data("label-not-defined")}else{if(a.type=="task"){b="<strong>"+a.progress+"</strong><br/>"+$(this.options.container).data("label-assignee")+" "+(a.assignee?a.assignee:"")+"<br/>"}b+=$(this.options.container).data("label-start-date")+" "+$.datepicker.formatDate("yy-mm-dd",a.start)+"<br/>";b+=$(this.options.container).data("label-end-date")+" "+$.datepicker.formatDate("yy-mm-dd",a.end)}return b};Kanboard.Gantt.prototype.setBarColor=function(b,a){if(a.not_defined){b.addClass("ganttview-block-not-defined")}else{b.css("background-color",a.color.background);b.css("border-color",a.color.border)}};Kanboard.Gantt.prototype.listenForBlockResize=function(a){var b=this;jQuery("div.ganttview-block",this.options.container).resizable({grid:this.options.cellWidth,handles:"e,w",delay:300,stop:function(){var c=jQuery(this);b.updateDataAndPosition(c,a);b.saveRecord(c.data("record"))}})};Kanboard.Gantt.prototype.listenForBlockMove=function(a){var b=this;jQuery("div.ganttview-block",this.options.container).draggable({axis:"x",delay:300,grid:[this.options.cellWidth,this.options.cellWidth],stop:function(){var c=jQuery(this);b.updateDataAndPosition(c,a);b.saveRecord(c.data("record"))}})};Kanboard.Gantt.prototype.updateDataAndPosition=function(f,d){var a=jQuery("div.ganttview-slide-container",this.options.container);var j=a.scrollLeft();var g=f.offset().left-a.offset().left-1+j;var i=f.data("record");i.not_defined=false;this.setBarColor(f,i);var c=Math.round(g/this.options.cellWidth);var h=this.addDays(this.cloneDate(d),c);i.start=h;var b=f.outerWidth();var e=Math.round(b/this.options.cellWidth)-1;i.end=this.addDays(this.cloneDate(h),e);if(i.type==="task"&&e>0){jQuery("div.ganttview-block-text",f).text(i.progress)}f.attr("title",this.getBarTooltip(i));f.data("record",i);f.css("top","").css("left","").css("position","relative").css("margin-left",g+"px")};Kanboard.Gantt.prototype.getDates=function(e,a){var d=[];d[e.getFullYear()]=[];d[e.getFullYear()][e.getMonth()]=[e];var c=e;while(this.compareDate(c,a)==-1){var b=this.addDays(this.cloneDate(c),1);if(!d[b.getFullYear()]){d[b.getFullYear()]=[]}if(!d[b.getFullYear()][b.getMonth()]){d[b.getFullYear()][b.getMonth()]=[]}d[b.getFullYear()][b.getMonth()].push(b);c=b}return d};Kanboard.Gantt.prototype.prepareData=function(c){for(var b=0;b<c.length;b++){var d=new Date(c[b].start[0],c[b].start[1]-1,c[b].start[2],0,0,0,0);c[b].start=d;var a=new Date(c[b].end[0],c[b].end[1]-1,c[b].end[2],0,0,0,0);c[b].end=a}return c};Kanboard.Gantt.prototype.getDateRange=function(c){var f=new Date();var b=new Date();for(var d=0;d<this.data.length;d++){var e=new Date();e.setTime(Date.parse(this.data[d].start));var a=new Date();a.setTime(Date.parse(this.data[d].end));if(d==0){f=e;b=a}if(this.compareDate(f,e)==1){f=e}if(this.compareDate(b,a)==-1){b=a}}if(this.daysBetween(f,b)<c){b=this.addDays(this.cloneDate(f),c)}f.setDate(f.getDate()-1);return[f,b]};Kanboard.Gantt.prototype.daysBetween=function(d,a){if(!d||!a){return 0}var c=0,b=this.cloneDate(d);while(this.compareDate(b,a)==-1){c=c+1;this.addDays(b,1)}return c};Kanboard.Gantt.prototype.isWeekend=function(a){return a.getDay()%6==0};Kanboard.Gantt.prototype.cloneDate=function(a){return new Date(a.getTime())};Kanboard.Gantt.prototype.addDays=function(a,b){a.setDate(a.getDate()+b*1);return a};Kanboard.Gantt.prototype.compareDate=function(b,a){if(isNaN(b)||isNaN(a)){throw new Error(b+" - "+a)}else{if(b instanceof Date&&a instanceof Date){return(b<a)?-1:(b>a)?1:0}else{throw new TypeError(b+" - "+a)}}};Kanboard.Column=function(a){this.app=a};Kanboard.Column.prototype.listen=function(){this.dragAndDrop()};Kanboard.Column.prototype.dragAndDrop=function(){var a=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".columns-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(c,b){b.children().each(function(){$(this).width($(this).width())});return b},stop:function(c,d){var b=d.item;b.removeClass("draggable-item-selected");a.savePosition(b.data("column-id"),b.index()+1)},start:function(b,c){c.item.addClass("draggable-item-selected")}}).disableSelection()};Kanboard.Column.prototype.savePosition=function(d,a){var c=$(".columns-table").data("save-position-url");var b=this;this.app.showLoadingIcon();$.ajax({cache:false,url:c,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({column_id:d,position:a}),complete:function(){b.app.hideLoadingIcon()}})};Kanboard.Markdown=function(a){this.app=a;this.editor=null};Kanboard.Markdown.prototype.onPopoverOpened=function(){this.listen()};Kanboard.Markdown.prototype.onPopoverClosed=function(){this.listen()};Kanboard.Markdown.prototype.listen=function(){var a=$(".markdown-editor");if(this.editor){this.destroy()}if(a.length>0){this.show(a[0])}};Kanboard.Markdown.prototype.destroy=function(){var a=this.editor.codemirror;var c=a.getWrapperElement();for(var b in ["toolbar","statusbar","sideBySide"]){if(this.editor.gui[b]){c.parentNode.removeChild(this.editor.gui[b])}}a.toTextArea();this.editor=null};Kanboard.Markdown.prototype.show=function(a){var b=["bold","italic","strikethrough","heading","|","unordered-list","ordered-list","link","|","code","table"];this.editor=new SimpleMDE({element:a,status:false,toolbarTips:false,autoDownloadFontAwesome:false,spellChecker:false,autosave:{enabled:false},forceSync:true,blockStyles:{italic:"_"},toolbar:a.hasAttribute("data-markdown-editor-disable-toolbar")?false:b,placeholder:a.getAttribute("placeholder")})};Kanboard.ProjectPermission=function(a){this.app=a};Kanboard.ProjectPermission.prototype.listen=function(){$(".project-change-role").on("change",function(){$.ajax({cache:false,url:$(this).data("url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({id:$(this).data("id"),role:$(this).val()})})})};Kanboard.ProjectCreation=function(a){this.app=a};Kanboard.ProjectCreation.prototype.onPopoverOpened=function(){$("#project-creation-form #form-src_project_id").on("change",function(){var a=$(this).val();if(a==0){$(".project-creation-options").hide()}else{$(".project-creation-options").show()}})};Kanboard.Screenshot=function(a){this.app=a;this.pasteCatcher=null};Kanboard.Screenshot.prototype.onPopoverOpened=function(){if(this.app.hasId("screenshot-zone")){this.initialize()}};Kanboard.Screenshot.prototype.initialize=function(){this.destroy();if(!window.Clipboard){this.pasteCatcher=document.createElement("div");this.pasteCatcher.id="screenshot-pastezone";this.pasteCatcher.contentEditable="true";this.pasteCatcher.style.opacity=0;this.pasteCatcher.style.position="fixed";this.pasteCatcher.style.top=0;this.pasteCatcher.style.right=0;this.pasteCatcher.style.width=0;document.body.insertBefore(this.pasteCatcher,document.body.firstChild);this.pasteCatcher.focus();document.addEventListener("click",this.setFocus.bind(this));document.getElementById("screenshot-zone").addEventListener("click",this.setFocus.bind(this))}window.addEventListener("paste",this.pasteHandler.bind(this))};Kanboard.Screenshot.prototype.destroy=function(){if(this.pasteCatcher!=null){document.body.removeChild(this.pasteCatcher)}else{if(document.getElementById("screenshot-pastezone")){document.body.removeChild(document.getElementById("screenshot-pastezone"))}}document.removeEventListener("click",this.setFocus.bind(this));this.pasteCatcher=null};Kanboard.Screenshot.prototype.setFocus=function(){if(this.pasteCatcher!==null){this.pasteCatcher.focus()}};Kanboard.Screenshot.prototype.pasteHandler=function(g){if(g.clipboardData&&g.clipboardData.items){var d=g.clipboardData.items;if(d){for(var f=0;f<d.length;f++){if(d[f].type.indexOf("image")!==-1){var c=d[f].getAsFile();var a=new FileReader();var b=this;a.onload=function(e){b.createImage(e.target.result)};a.readAsDataURL(c)}}}}else{setTimeout(this.checkInput.bind(this),100)}};Kanboard.Screenshot.prototype.checkInput=function(){var a=this.pasteCatcher.childNodes[0];if(a){if(a.tagName==="IMG"){this.createImage(a.src)}}this.pasteCatcher.innerHTML=""};Kanboard.Screenshot.prototype.createImage=function(c){var b=new Image();b.src=c;b.onload=function(){var d=c.split("base64,");var e=d[1];$("input[name=screenshot]").val(e)};var a=document.getElementById("screenshot-zone");a.innerHTML="";a.className="screenshot-pasted";a.appendChild(b);this.destroy();this.initialize()};Kanboard.FileUpload=function(a){this.app=a;this.files=[];this.currentFile=0};Kanboard.FileUpload.prototype.onPopoverOpened=function(){var a=document.getElementById("file-dropzone");var b=this;if(a){a.ondragover=a.ondragenter=function(c){c.stopPropagation();c.preventDefault()};a.ondrop=function(c){c.stopPropagation();c.preventDefault();b.files=c.dataTransfer.files;b.show();$("#file-error-max-size").hide()};$(document).on("click","#file-browser",function(c){c.preventDefault();$("#file-form-element").get(0).click()});$(document).on("click","#file-upload-button",function(c){c.preventDefault();b.currentFile=0;b.checkFiles()});$("#file-form-element").change(function(){b.files=document.getElementById("file-form-element").files;b.show();$("#file-error-max-size").hide()})}};Kanboard.FileUpload.prototype.show=function(){$("#file-list").remove();if(this.files.length>0){$("#file-upload-button").prop("disabled",false);$("#file-dropzone-inner").hide();var e=jQuery("<ul>",{id:"file-list"});for(var d=0;d<this.files.length;d++){var b=jQuery("<span>",{id:"file-percentage-"+d}).append("(0%)");var c=jQuery("<progress>",{id:"file-progress-"+d,value:0});var a=jQuery("<li>",{id:"file-label-"+d}).append(c).append("&nbsp;").append(this.files[d].name).append("&nbsp;").append(b);e.append(a)}$("#file-dropzone").append(e)}else{$("#file-dropzone-inner").show()}};Kanboard.FileUpload.prototype.checkFiles=function(){var a=parseInt($("#file-dropzone").data("max-size"));for(var b=0;b<this.files.length;b++){if(this.files[b].size>a){$("#file-error-max-size").show();$("#file-label-"+b).addClass("file-error");$("#file-upload-button").prop("disabled",true);return}}this.uploadFiles()};Kanboard.FileUpload.prototype.uploadFiles=function(){if(this.files.length>0){this.uploadFile(this.files[this.currentFile])}};Kanboard.FileUpload.prototype.uploadFile=function(d){var a=document.getElementById("file-dropzone");var b=a.dataset.url;var e=new XMLHttpRequest();var c=new FormData();e.upload.addEventListener("progress",this.updateProgress.bind(this));e.upload.addEventListener("load",this.transferComplete.bind(this));e.open("POST",b,true);c.append("files[]",d);e.send(c)};Kanboard.FileUpload.prototype.updateProgress=function(a){if(a.lengthComputable){$("#file-progress-"+this.currentFile).val(a.loaded/a.total);$("#file-percentage-"+this.currentFile).text("("+Math.floor((a.loaded/a.total)*100)+"%)")}};Kanboard.FileUpload.prototype.transferComplete=function(){this.currentFile++;if(this.currentFile<this.files.length){this.uploadFile(this.files[this.currentFile])}else{var a=$("#file-upload-button");a.prop("disabled",true);a.parent().hide();$("#file-done").show()}};Kanboard.Search=function(a){this.app=a};Kanboard.Search.prototype.focus=function(){$(document).on("focus","#form-search",function(){var b=$("#form-search");if(b[0].setSelectionRange){var a=b.val().length*2;b[0].setSelectionRange(a,a)}})};Kanboard.Search.prototype.listen=function(){$(document).on("click",".filter-helper",function(d){d.preventDefault();var c=$(this).data("filter");var b=$(this).data("append-filter");var a=$("#form-search");if(b){c=a.val()+" "+b}a.val(c);$("form.search").submit()})};Kanboard.Search.prototype.goToView=function(a){var b=$(a);if(b.length){window.location=b.attr("href")}};Kanboard.Search.prototype.keyboardShortcuts=function(){var a=this;Mousetrap.bind("v o",function(){a.goToView(".view-overview")});Mousetrap.bind("v b",function(){a.goToView(".view-board")});Mousetrap.bind("v c",function(){a.goToView(".view-calendar")});Mousetrap.bind("v l",function(){a.goToView(".view-listing")});Mousetrap.bind("v g",function(){a.goToView(".view-gantt")});Mousetrap.bind("f",function(c){c.preventDefault();var b=document.getElementById("form-search");if(b){b.focus()}});Mousetrap.bind("r",function(d){d.preventDefault();var c=$(".filter-reset").data("filter");var b=$("#form-search");b.val(c);$("form.search").submit()})};Kanboard.Task=function(a){this.app=a};Kanboard.Task.prototype.keyboardShortcuts=function(){var b=$("#task-view");var a=this;if(this.app.hasId("task-view")){Mousetrap.bind("e",function(){a.app.get("Popover").open(b.data("edit-url"))});Mousetrap.bind("d",function(){a.app.get("Popover").open(b.data("description-url"))});Mousetrap.bind("c",function(){a.app.get("Popover").open(b.data("comment-url"))});Mousetrap.bind("s",function(){a.app.get("Popover").open(b.data("subtask-url"))});Mousetrap.bind("l",function(){a.app.get("Popover").open(b.data("internal-link-url"))})}};Kanboard.Task.prototype.onPopoverOpened=function(){var a=this;var b=0;$(document).on("click",".color-square",function(){$(".color-square-selected").removeClass("color-square-selected");$(this).addClass("color-square-selected");$("#form-color_id").val($(this).data("color-id"))});$(document).on("click",".assign-me",function(f){var c=$(this).data("current-id");var d="#"+$(this).data("target-id");f.preventDefault();if($(d+" option[value="+c+"]").length){$(d).val(c)}});$(document).on("change","select.task-reload-project-destination",function(){if(b>0){$(this).val(b)}else{b=$(this).val();var c=$(this).data("redirect").replace(/PROJECT_ID/g,b);$(".loading-icon").show();$.ajax({type:"GET",url:c,success:function(e,f,d){b=0;$(".loading-icon").hide();a.app.get("Popover").ajaxReload(e,d,a.app.get("Popover"))}})}})};Kanboard.Subtask=function(a){this.app=a};Kanboard.Subtask.prototype.listen=function(){var a=this;this.dragAndDrop();$(document).on("click",".subtask-toggle-status",function(c){var b=$(this);c.preventDefault();$.ajax({cache:false,url:b.attr("href"),success:function(d){if(b.hasClass("subtask-refresh-table")){$(".subtasks-table").replaceWith(d)}else{b.replaceWith(d)}a.dragAndDrop()}})});$(document).on("click",".subtask-toggle-timer",function(c){var b=$(this);c.preventDefault();$.ajax({cache:false,url:b.attr("href"),success:function(d){$(".subtasks-table").replaceWith(d);a.dragAndDrop()}})})};Kanboard.Subtask.prototype.dragAndDrop=function(){var a=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".subtasks-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(c,b){b.children().each(function(){$(this).width($(this).width())});return b},stop:function(b,c){var d=c.item;d.removeClass("draggable-item-selected");a.savePosition(d.data("subtask-id"),d.index()+1)},start:function(b,c){c.item.addClass("draggable-item-selected")}}).disableSelection()};Kanboard.Subtask.prototype.savePosition=function(d,a){var c=$(".subtasks-table").data("save-position-url");var b=this;this.app.showLoadingIcon();$.ajax({cache:false,url:c,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({subtask_id:d,position:a}),complete:function(){b.app.hideLoadingIcon()}})};Kanboard.Swimlane=function(a){this.app=a};Kanboard.Swimlane.prototype.execute=function(){if($(".swimlanes-table").length){this.dragAndDrop()}};Kanboard.Swimlane.prototype.listen=function(){var a=this;$(document).on("click",".board-swimlane-toggle",function(c){c.preventDefault();var b=$(this).data("swimlane-id");if(a.isCollapsed(b)){a.expand(b)}else{a.collapse(b)}})};Kanboard.Swimlane.prototype.onBoardRendered=function(){var b=this.getAllCollapsed();for(var a=0;a<b.length;a++){this.collapse(b[a])}};Kanboard.Swimlane.prototype.getStorageKey=function(){return"hidden_swimlanes_"+$("#board").data("project-id")};Kanboard.Swimlane.prototype.expand=function(b){var c=this.getAllCollapsed();var a=c.indexOf(b);if(a>-1){c.splice(a,1)}localStorage.setItem(this.getStorageKey(),JSON.stringify(c));$(".board-swimlane-columns-"+b).css("display","table-row");$(".board-swimlane-tasks-"+b).css("display","table-row");$(".hide-icon-swimlane-"+b).css("display","inline");$(".show-icon-swimlane-"+b).css("display","none")};Kanboard.Swimlane.prototype.collapse=function(a){var b=this.getAllCollapsed();if(b.indexOf(a)<0){b.push(a);localStorage.setItem(this.getStorageKey(),JSON.stringify(b))}$(".board-swimlane-columns-"+a+":not(:first-child)").css("display","none");$(".board-swimlane-tasks-"+a).css("display","none");$(".hide-icon-swimlane-"+a).css("display","none");$(".show-icon-swimlane-"+a).css("display","inline")};Kanboard.Swimlane.prototype.isCollapsed=function(a){return this.getAllCollapsed().indexOf(a)>-1};Kanboard.Swimlane.prototype.getAllCollapsed=function(){return JSON.parse(localStorage.getItem(this.getStorageKey()))||[]};Kanboard.Swimlane.prototype.dragAndDrop=function(){var a=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".swimlanes-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(c,b){b.children().each(function(){$(this).width($(this).width())});return b},stop:function(b,d){var c=d.item;c.removeClass("draggable-item-selected");a.savePosition(c.data("swimlane-id"),c.index()+1)},start:function(b,c){c.item.addClass("draggable-item-selected")}}).disableSelection()};Kanboard.Swimlane.prototype.savePosition=function(d,a){var c=$(".swimlanes-table").data("save-position-url");var b=this;this.app.showLoadingIcon();$.ajax({cache:false,url:c,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({swimlane_id:d,position:a}),complete:function(){b.app.hideLoadingIcon()}})};Kanboard.BoardColumnView=function(a){this.app=a};Kanboard.BoardColumnView.prototype.execute=function(){if(this.app.hasId("board")){this.render()}};Kanboard.BoardColumnView.prototype.listen=function(){var a=this;$(document).on("click",".board-toggle-column-view",function(){a.toggle($(this).data("column-id"))})};Kanboard.BoardColumnView.prototype.onBoardRendered=function(){this.render()};Kanboard.BoardColumnView.prototype.render=function(){var a=this;$(".board-column-header").each(function(){var b=$(this).data("column-id");if(localStorage.getItem("hidden_column_"+b)){a.hideColumn(b)}})};Kanboard.BoardColumnView.prototype.toggle=function(a){if(localStorage.getItem("hidden_column_"+a)){this.showColumn(a)}else{this.hideColumn(a)}};Kanboard.BoardColumnView.prototype.hideColumn=function(a){$(".board-column-"+a+" .board-column-expanded").hide();$(".board-column-"+a+" .board-column-collapsed").show();$(".board-column-header-"+a+" .board-column-expanded").hide();$(".board-column-header-"+a+" .board-column-collapsed").show();$(".board-column-header-"+a).each(function(){$(this).removeClass("board-column-compact");$(this).addClass("board-column-header-collapsed")});$(".board-column-"+a).each(function(){$(this).addClass("board-column-task-collapsed")});$(".board-column-"+a+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+a+"").height())});localStorage.setItem("hidden_column_"+a,1)};Kanboard.BoardColumnView.prototype.showColumn=function(a){$(".board-column-"+a+" .board-column-expanded").show();$(".board-column-"+a+" .board-column-collapsed").hide();$(".board-column-header-"+a+" .board-column-expanded").show();$(".board-column-header-"+a+" .board-column-collapsed").hide();$(".board-column-header-"+a).removeClass("board-column-header-collapsed");$(".board-column-"+a).removeClass("board-column-task-collapsed");if(localStorage.getItem("horizontal_scroll")==0){$(".board-column-header-"+a).addClass("board-column-compact")}localStorage.removeItem("hidden_column_"+a)};Kanboard.BoardColumnScrolling=function(a){this.app=a};Kanboard.BoardColumnScrolling.prototype.execute=function(){if(this.app.hasId("board")){this.render();$(window).on("load",this.render);$(window).resize(this.render)}};Kanboard.BoardColumnScrolling.prototype.listen=function(){var a=this;$(document).on("click",".filter-toggle-height",function(b){b.preventDefault();a.toggle()})};Kanboard.BoardColumnScrolling.prototype.onBoardRendered=function(){this.render()};Kanboard.BoardColumnScrolling.prototype.toggle=function(){var a=localStorage.getItem("column_scroll");if(a==undefined){a=1}localStorage.setItem("column_scroll",a==0?1:0);this.render()};Kanboard.BoardColumnScrolling.prototype.render=function(){var d=$(".board-task-list");var c=$(".board-rotation-wrapper");var b=$(".filter-max-height");var e=$(".filter-min-height");if(localStorage.getItem("column_scroll")==0){var a=80;b.show();e.hide();c.css("min-height","");d.each(function(){var f=$(this).height();if(f>a){a=f}});d.css("min-height",a);d.css("height","")}else{b.hide();e.show();if($(".board-swimlane").length>1){d.each(function(){if($(this).height()>500){$(this).css("height",500)}else{$(this).css("min-height",320);c.css("min-height",320)}})}else{var a=$(window).height()-170;d.css("height",a);c.css("min-height",a)}}};Kanboard.BoardHorizontalScrolling=function(a){this.app=a};Kanboard.BoardHorizontalScrolling.prototype.execute=function(){if(this.app.hasId("board")){this.render()}};Kanboard.BoardHorizontalScrolling.prototype.listen=function(){var a=this;$(document).on("click",".filter-toggle-scrolling",function(b){b.preventDefault();a.toggle()})};Kanboard.BoardHorizontalScrolling.prototype.keyboardShortcuts=function(){var a=this;if(a.app.hasId("board")){Mousetrap.bind("c",function(){a.toggle()})}};Kanboard.BoardHorizontalScrolling.prototype.onBoardRendered=function(){this.render()};Kanboard.BoardHorizontalScrolling.prototype.toggle=function(){var a=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",a==0?1:0);this.render()};Kanboard.BoardHorizontalScrolling.prototype.render=function(){if(localStorage.getItem("horizontal_scroll")==0){$(".filter-wide").show();$(".filter-compact").hide();$("#board-container").addClass("board-container-compact");$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")}else{$(".filter-wide").hide();$(".filter-compact").show();$("#board-container").removeClass("board-container-compact");$("#board th").removeClass("board-column-compact")}};Kanboard.BoardCollapsedMode=function(a){this.app=a};Kanboard.BoardCollapsedMode.prototype.keyboardShortcuts=function(){var a=this;if(a.app.hasId("board")){Mousetrap.bind("s",function(){a.toggle()})}};Kanboard.BoardCollapsedMode.prototype.toggle=function(){var a=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(b){$(".filter-display-mode").toggle();a.app.get("BoardDragAndDrop").refresh(b)}})};Kanboard.BoardDragAndDrop=function(a){this.app=a;this.savingInProgress=false};Kanboard.BoardDragAndDrop.prototype.execute=function(){if(this.app.hasId("board")){this.dragAndDrop();this.executeListeners()}};Kanboard.BoardDragAndDrop.prototype.dragAndDrop=function(){var a=this;var b={forcePlaceholderSize:true,tolerance:"pointer",connectWith:".board-task-list",placeholder:"draggable-placeholder",items:".draggable-item",stop:function(d,k){var f=k.item;var j=f.attr("data-task-id");var l=f.attr("data-position");var i=f.attr("data-column-id");var h=f.attr("data-swimlane-id");var e=f.parent().attr("data-column-id");var c=f.parent().attr("data-swimlane-id");var g=f.index()+1;f.removeClass("draggable-item-selected");if(e!=i||c!=h||g!=l){a.changeTaskState(j);a.save(j,e,g,c)}},start:function(c,d){d.item.addClass("draggable-item-selected");d.placeholder.height(d.item.height())}};if($.support.touch){$(".task-board-sort-handle").css("display","inline");b.handle=".task-board-sort-handle"}$(".board-task-list").sortable(b)};Kanboard.BoardDragAndDrop.prototype.changeTaskState=function(b){var a=$("div[data-task-id="+b+"]");a.addClass("task-board-saving-state");a.find(".task-board-saving-icon").show()};Kanboard.BoardDragAndDrop.prototype.save=function(d,e,a,c){var b=this;b.app.showLoadingIcon();b.savingInProgress=true;$.ajax({cache:false,url:$("#board").data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:d,column_id:e,swimlane_id:c,position:a}),success:function(f){b.refresh(f);b.savingInProgress=false},error:function(){b.app.hideLoadingIcon();b.savingInProgress=false}})};Kanboard.BoardDragAndDrop.prototype.refresh=function(a){$("#board-container").replaceWith(a);this.app.hideLoadingIcon();this.dragAndDrop();this.executeListeners()};Kanboard.BoardDragAndDrop.prototype.executeListeners=function(){for(var b in this.app.controllers){var a=this.app.get(b);if(typeof a.onBoardRendered==="function"){a.onBoardRendered()}}};Kanboard.BoardTask=function(a){this.app=a};Kanboard.BoardTask.prototype.listen=function(){var a=this;$(document).on("click",".task-board-change-assignee",function(b){b.preventDefault();b.stopPropagation();a.app.get("Popover").open($(this).data("url"))});$(document).on("click",".task-board",function(b){if(b.target.tagName!="A"&&b.target.tagName!="IMG"){window.location=$(this).data("task-url")}})};Kanboard.BoardTask.prototype.keyboardShortcuts=function(){var a=this;if(a.app.hasId("board")){Mousetrap.bind("n",function(){a.app.get("Popover").open($("#board").data("task-creation-url"))})}};Kanboard.BoardPolling=function(a){this.app=a};Kanboard.BoardPolling.prototype.execute=function(){if(this.app.hasId("board")){var a=parseInt($("#board").attr("data-check-interval"));if(a>0){window.setInterval(this.check.bind(this),a*1000)}}};Kanboard.BoardPolling.prototype.check=function(){if(this.app.isVisible()&&!this.app.get("BoardDragAndDrop").savingInProgress){var a=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("check-url"),statusCode:{200:function(b){a.app.get("BoardDragAndDrop").refresh(b)},304:function(){a.app.hideLoadingIcon()}}})}};Kanboard.Tooltip=function(a){this.app=a};Kanboard.Tooltip.prototype.onBoardRendered=function(){this.execute()};Kanboard.Tooltip.prototype.execute=function(){$(".tooltip").tooltip({track:false,show:false,hide:false,position:{my:"left-20 top",at:"center bottom+9",using:function(a,b){$(this).css(a);var c=b.target.left+b.target.width/2-b.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(b.vertical).addClass(c<1?"align-left":"align-right").appendTo(this)}},content:function(){var c=this;var a=$(this).attr("data-href");if(!a){return'<div class="markdown">'+$(this).attr("title")+"</div>"}$.get(a,function b(f){var e=$(".ui-tooltip:visible");$(".ui-tooltip-content:visible").html(f);e.css({top:"",left:""});e.children(".tooltip-arrow").remove();var d=$(c).tooltip("option","position");d.of=$(c);e.position(d)});return'<i class="fa fa-spinner fa-spin"></i>'}}).on("mouseenter",function(){var a=this;$(this).tooltip("open");$(".ui-tooltip").on("mouseleave",function(){$(a).tooltip("close")})}).on("mouseleave focusout",function(a){a.stopImmediatePropagation();var b=this;setTimeout(function(){if(!$(".ui-tooltip:hover").length){$(b).tooltip("close")}},100)})};jQuery(document).ready(function(){var a=new Kanboard.App();a.execute()}); \ No newline at end of file
+for(var r=e.getCursor("start"),i=e.getCursor("end"),o=r.line;o<=i.line;o++)!function(r){var i=e.getLine(r),o=i.search(/[^#]/);i=void 0!==t?0>=o?"bigger"==t?"###### "+i:"# "+i:6==o&&"smaller"==t?i.substr(7):1==o&&"bigger"==t?i.substr(2):"bigger"==t?i.substr(1):"#"+i:1==n?0>=o?"# "+i:o==n?i.substr(o+1):"# "+i.substr(o+1):2==n?0>=o?"## "+i:o==n?i.substr(o+1):"## "+i.substr(o+1):0>=o?"### "+i:o==n?i.substr(o+1):"### "+i.substr(o+1),e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(o);e.focus()}}function W(e,t){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var n=a(e),r=e.getCursor("start"),i=e.getCursor("end"),o={quote:/^(\s*)\>\s+/,"unordered-list":/^(\s*)(\*|\-|\+)\s+/,"ordered-list":/^(\s*)\d+\.\s+/},l={quote:"> ","unordered-list":"* ","ordered-list":"1. "},s=r.line;s<=i.line;s++)!function(r){var i=e.getLine(r);i=n[t]?i.replace(o[t],"$1"):l[t]+i,e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(s);e.focus()}}function I(e,t,n,r){if(!/editor-preview-active/.test(e.codemirror.getWrapperElement().lastChild.className)){r="undefined"==typeof r?n:r;var i,o=e.codemirror,l=a(o),s=n,c=r,u=o.getCursor("start"),d=o.getCursor("end");l[t]?(i=o.getLine(u.line),s=i.slice(0,u.ch),c=i.slice(u.ch),"bold"==t?(s=s.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/,""),c=c.replace(/(\*\*|__)/,"")):"italic"==t?(s=s.replace(/(\*|_)(?![\s\S]*(\*|_))/,""),c=c.replace(/(\*|_)/,"")):"strikethrough"==t&&(s=s.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/,""),c=c.replace(/(\*\*|~~)/,"")),o.replaceRange(s+c,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),"bold"==t||"strikethrough"==t?(u.ch-=2,u!==d&&(d.ch-=2)):"italic"==t&&(u.ch-=1,u!==d&&(d.ch-=1))):(i=o.getSelection(),"bold"==t?(i=i.split("**").join(""),i=i.split("__").join("")):"italic"==t?(i=i.split("*").join(""),i=i.split("_").join("")):"strikethrough"==t&&(i=i.split("~~").join("")),o.replaceSelection(s+i+c),u.ch+=n.length,d.ch=u.ch+i.length),o.setSelection(u,d),o.focus()}}function E(e){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className))for(var t,n=e.getCursor("start"),r=e.getCursor("end"),i=n.line;i<=r.line;i++)t=e.getLine(i),t=t.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/,""),e.replaceRange(t,{line:i,ch:0},{line:i,ch:99999999999999})}function D(e,t){for(var n in t)t.hasOwnProperty(n)&&(t[n]instanceof Array?e[n]=t[n].concat(e[n]instanceof Array?e[n]:[]):null!==t[n]&&"object"==typeof t[n]&&t[n].constructor===Object?e[n]=D(e[n]||{},t[n]):e[n]=t[n]);return e}function P(e){for(var t=1;t<arguments.length;t++)e=D(e,arguments[t]);return e}function F(e){var t=/[a-zA-Z0-9_\u0392-\u03c9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g,n=e.match(t),r=0;if(null===n)return r;for(var i=0;i<n.length;i++)r+=n[i].charCodeAt(0)>=19968?n[i].length:1;return r}function z(e){e=e||{},e.parent=this;var t=!0;if(e.autoDownloadFontAwesome===!1&&(t=!1),e.autoDownloadFontAwesome!==!0)for(var n=document.styleSheets,r=0;r<n.length;r++)n[r].href&&n[r].href.indexOf("//maxcdn.bootstrapcdn.com/font-awesome/")>-1&&(t=!1);if(t){var i=document.createElement("link");i.rel="stylesheet",i.href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css",document.getElementsByTagName("head")[0].appendChild(i)}if(e.element)this.element=e.element;else if(null===e.element)return void console.log("SimpleMDE: Error. No element was found.");if(void 0===e.toolbar){e.toolbar=[];for(var o in K)K.hasOwnProperty(o)&&(-1!=o.indexOf("separator-")&&e.toolbar.push("|"),(K[o]["default"]===!0||e.showIcons&&e.showIcons.constructor===Array&&-1!=e.showIcons.indexOf(o))&&e.toolbar.push(o))}e.hasOwnProperty("status")||(e.status=["autosave","lines","words","cursor"]),e.previewRender||(e.previewRender=function(e){return this.parent.markdown(e)}),e.parsingConfig=P({highlightFormatting:!0},e.parsingConfig||{}),e.insertTexts=P({},X,e.insertTexts||{}),e.promptTexts=Y,e.blockStyles=P({},Z,e.blockStyles||{}),e.shortcuts=P({},U,e.shortcuts||{}),void 0!=e.autosave&&void 0!=e.autosave.unique_id&&""!=e.autosave.unique_id&&(e.autosave.uniqueId=e.autosave.unique_id),this.options=e,this.render(),!e.initialValue||this.options.autosave&&this.options.autosave.foundSavedValue===!0||this.value(e.initialValue)}function R(){if("object"!=typeof localStorage)return!1;try{localStorage.setItem("smde_localStorage",1),localStorage.removeItem("smde_localStorage")}catch(e){return!1}return!0}var B=e("codemirror");e("codemirror/addon/edit/continuelist.js"),e("./codemirror/tablist"),e("codemirror/addon/display/fullscreen.js"),e("codemirror/mode/markdown/markdown.js"),e("codemirror/addon/mode/overlay.js"),e("codemirror/addon/display/placeholder.js"),e("codemirror/mode/gfm/gfm.js"),e("codemirror/mode/xml/xml.js"),e("spell-checker");var j=e("marked"),_=/Mac/.test(navigator.platform),q={toggleBold:c,toggleItalic:u,drawLink:k,toggleHeadingSmaller:p,toggleHeadingBigger:m,drawImage:C,toggleBlockquote:f,toggleOrderedList:b,toggleUnorderedList:x,toggleCodeBlock:h,togglePreview:A,toggleStrikethrough:d,toggleHeading1:g,toggleHeading2:v,toggleHeading3:y,cleanBlock:w,drawTable:S,drawHorizontalRule:L,undo:T,redo:M,toggleSideBySide:N,toggleFullScreen:s},U={toggleBold:"Cmd-B",toggleItalic:"Cmd-I",drawLink:"Cmd-K",toggleHeadingSmaller:"Cmd-H",toggleHeadingBigger:"Shift-Cmd-H",cleanBlock:"Cmd-E",drawImage:"Cmd-Alt-I",toggleBlockquote:"Cmd-'",toggleOrderedList:"Cmd-Alt-L",toggleUnorderedList:"Cmd-L",toggleCodeBlock:"Cmd-Alt-C",togglePreview:"Cmd-P",toggleSideBySide:"F9",toggleFullScreen:"F11"},G=function(e){for(var t in q)if(q[t]===e)return t;return null},$=function(){var e=!1;return function(t){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(t)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(t.substr(0,4)))&&(e=!0)}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:d,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:f,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:C,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:S,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:["![","](#url#)"],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Y={link:"URL for the link:",image:"URL of the image:"},Z={bold:"**",code:"```",italic:"*"};z.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},z.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var l,a;if(t.spellChecker!==!1?(l="spell-checker",a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1):(l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1),this.codemirror=B.fromTextArea(e,{mode:l,backdrop:a,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||""}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element}},z.prototype.autosave=function(){if(R()){var e=this;if(void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to use the autosave feature");null!=e.element.form&&void 0!=e.element.form&&e.element.form.addEventListener("submit",function(){localStorage.removeItem("smde_"+e.options.autosave.uniqueId)}),this.options.autosave.loaded!==!0&&("string"==typeof localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&""!=localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&(this.codemirror.setValue(localStorage.getItem("smde_"+this.options.autosave.uniqueId)),this.options.autosave.foundSavedValue=!0),this.options.autosave.loaded=!0),localStorage.setItem("smde_"+this.options.autosave.uniqueId,e.value());var t=document.getElementById("autosaved");if(null!=t&&void 0!=t&&""!=t){var n=new Date,r=n.getHours(),i=n.getMinutes(),o="am",l=r;l>=12&&(l=r-12,o="pm"),0==l&&(l=12),i=10>i?"0"+i:i,t.innerHTML="Autosaved: "+l+":"+i+" "+o}this.autosaveTimeoutId=setTimeout(function(){e.autosave()},this.options.autosave.delay||1e4)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},z.prototype.clearAutosavedValue=function(){if(R()){if(void 0==this.options.autosave||void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to clear the autosave value");localStorage.removeItem("smde_"+this.options.autosave.uniqueId)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},z.prototype.createSideBySide=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;n&&/editor-preview-side/.test(n.className)||(n=document.createElement("div"),n.className="editor-preview-side",t.parentNode.insertBefore(n,t.nextSibling));var r=!1,i=!1;return e.on("scroll",function(e){if(r)return void(r=!1);i=!0;var t=e.getScrollInfo().height-e.getScrollInfo().clientHeight,o=parseFloat(e.getScrollInfo().top)/t,l=(n.scrollHeight-n.clientHeight)*o;n.scrollTop=l}),n.onscroll=function(){if(i)return void(i=!1);r=!0;var t=n.scrollHeight-n.clientHeight,o=parseFloat(n.scrollTop)/t,l=(e.getScrollInfo().height-e.getScrollInfo().clientHeight)*o;e.scrollTo(0,l)},n},z.prototype.createToolbar=function(e){if(e=e||this.options.toolbar,e&&0!==e.length){var t;for(t=0;t<e.length;t++)void 0!=K[e[t]]&&(e[t]=K[e[t]]);var n=document.createElement("div");n.className="editor-toolbar";var r=this,l={};for(r.toolbar=e,t=0;t<e.length;t++)if(("guide"!=e[t].name||r.options.toolbarGuideIcon!==!1)&&!(r.options.hideIcons&&-1!=r.options.hideIcons.indexOf(e[t].name)||("fullscreen"==e[t].name||"side-by-side"==e[t].name)&&$())){if("|"===e[t]){for(var s=!1,c=t+1;c<e.length;c++)console.log(c),"|"===e[c]||r.options.hideIcons&&-1!=r.options.hideIcons.indexOf(e[c].name)||(console.log(e[c]),s=!0);if(!s)continue}!function(e){var t;t="|"===e?o():i(e,r.options.toolbarTips,r.options.shortcuts),e.action&&("function"==typeof e.action?t.onclick=function(){e.action(r)}:"string"==typeof e.action&&(t.href=e.action,t.target="_blank")),l[e.name||e]=t,n.appendChild(t)}(e[t])}r.toolbarElements=l;var u=this.codemirror;u.on("cursorActivity",function(){var e=a(u);for(var t in l)!function(t){var n=l[t];e[t]?n.className+=" active":"fullscreen"!=t&&"side-by-side"!=t&&(n.className=n.className.replace(/\s*active\s*/g,""))}(t)});var d=u.getWrapperElement();return d.parentNode.insertBefore(n,d),n}},z.prototype.createStatusbar=function(e){e=e||this.options.status;var t=this.options,n=this.codemirror;if(e&&0!==e.length){var r,i,o,l=[];for(r=0;r<e.length;r++)if(i=void 0,o=void 0,"object"==typeof e[r])l.push({className:e[r].className,defaultValue:e[r].defaultValue,onUpdate:e[r].onUpdate});else{var a=e[r];"words"===a?(o=function(e){e.innerHTML="0"},i=function(e){e.innerHTML=F(n.getValue())}):"lines"===a?(o=function(e){e.innerHTML="0"},i=function(e){e.innerHTML=n.lineCount()}):"cursor"===a?(o=function(e){e.innerHTML="0:0"},i=function(e){var t=n.getCursor();e.innerHTML=t.line+":"+t.ch}):"autosave"===a&&(o=function(e){void 0!=t.autosave&&t.autosave.enabled===!0&&e.setAttribute("id","autosaved")}),l.push({className:a,defaultValue:o,onUpdate:i})}var s=document.createElement("div");for(s.className="editor-statusbar",r=0;r<l.length;r++){var c=l[r],u=document.createElement("span");u.className=c.className,"function"==typeof c.defaultValue&&c.defaultValue(u),"function"==typeof c.onUpdate&&this.codemirror.on("update",function(e,t){return function(){t.onUpdate(e)}}(u,c)),s.appendChild(u)}var d=this.codemirror.getWrapperElement();return d.parentNode.insertBefore(s,d.nextSibling),s}},z.prototype.value=function(e){return void 0===e?this.codemirror.getValue():(this.codemirror.getDoc().setValue(e),this)},z.toggleBold=c,z.toggleItalic=u,z.toggleStrikethrough=d,z.toggleBlockquote=f,z.toggleHeadingSmaller=p,z.toggleHeadingBigger=m,z.toggleHeading1=g,z.toggleHeading2=v,z.toggleHeading3=y,z.toggleCodeBlock=h,z.toggleUnorderedList=x,z.toggleOrderedList=b,z.cleanBlock=w,z.drawLink=k,z.drawImage=C,z.drawTable=S,z.drawHorizontalRule=L,z.undo=T,z.redo=M,z.togglePreview=A,z.toggleSideBySide=N,z.toggleFullScreen=s,z.prototype.toggleBold=function(){c(this)},z.prototype.toggleItalic=function(){u(this)},z.prototype.toggleStrikethrough=function(){d(this)},z.prototype.toggleBlockquote=function(){f(this)},z.prototype.toggleHeadingSmaller=function(){p(this)},z.prototype.toggleHeadingBigger=function(){m(this)},z.prototype.toggleHeading1=function(){g(this)},z.prototype.toggleHeading2=function(){v(this)},z.prototype.toggleHeading3=function(){y(this)},z.prototype.toggleCodeBlock=function(){h(this)},z.prototype.toggleUnorderedList=function(){x(this)},z.prototype.toggleOrderedList=function(){b(this)},z.prototype.cleanBlock=function(){w(this)},z.prototype.drawLink=function(){k(this)},z.prototype.drawImage=function(){C(this)},z.prototype.drawTable=function(){S(this)},z.prototype.drawHorizontalRule=function(){L(this)},z.prototype.undo=function(){T(this)},z.prototype.redo=function(){M(this)},z.prototype.togglePreview=function(){A(this)},z.prototype.toggleSideBySide=function(){N(this)},z.prototype.toggleFullScreen=function(){s(this)},z.prototype.isPreviewActive=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.lastChild;return/editor-preview-active/.test(n.className)},z.prototype.isSideBySideActive=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;return/editor-preview-active-side/.test(n.className)},z.prototype.isFullscreenActive=function(){var e=this.codemirror;return e.getOption("fullScreen")},z.prototype.getState=function(){var e=this.codemirror;return a(e)},z.prototype.toTextArea=function(){var e=this.codemirror,t=e.getWrapperElement();t.parentNode.removeChild(this.gui.toolbar),t.parentNode.removeChild(this.gui.statusbar),t.parentNode.removeChild(this.gui.sideBySide),e.toTextArea(),this.autosaveTimeoutId&&(clearTimeout(this.autosaveTimeoutId),this.autosaveTimeoutId=void 0,this.clearAutosavedValue())},t.exports=z},{"./codemirror/tablist":13,codemirror:7,"codemirror/addon/display/fullscreen.js":3,"codemirror/addon/display/placeholder.js":4,"codemirror/addon/edit/continuelist.js":5,"codemirror/addon/mode/overlay.js":6,"codemirror/mode/gfm/gfm.js":8,"codemirror/mode/markdown/markdown.js":9,"codemirror/mode/xml/xml.js":11,marked:12,"spell-checker":1}]},{},[14])(14)});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return a>1&&5>a&&1!==~~(a/10)}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekund":"pár sekundami";case"m":return b?"minuta":e?"minutu":"minutou";case"mm":return b||e?f+(c(a)?"minuty":"minut"):f+"minutami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodin"):f+"hodinami";case"d":return b||e?"den":"dnem";case"dd":return b||e?f+(c(a)?"dny":"dní"):f+"dny";case"M":return b||e?"měsíc":"měsícem";case"MM":return b||e?f+(c(a)?"měsíce":"měsíců"):f+"měsíci";case"y":return b||e?"rok":"rokem";case"yy":return b||e?f+(c(a)?"roky":"let"):f+"lety"}}var e="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),f="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");(b.defineLocale||b.lang).call(b,"cs",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"&#x3C;Dříve",nextText:"Později&#x3E;",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"&#x3C;Forrige",nextText:"Næste&#x3E;",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"&#x3C;Zurück",nextText:"Vor&#x3E;",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),d="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");(b.defineLocale||b.lang).call(b,"es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"&#x3C;Ant",nextText:"Sig&#x3E;",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(a,b){return/D/.test(b.substring(0,b.indexOf("MMMM")))?this._monthsGenitiveEl[a.month()]:this._monthsNominativeEl[a.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(a,b,c){return a>11?c?"μμ":"ΜΜ":c?"πμ":"ΠΜ"},isPM:function(a){return"μ"===(a+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(a,b){var c=this._calendarEl[a],d=b&&b.hours();return"function"==typeof c&&(c=c.apply(b)),c.replace("{}",d%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("el","el",{closeText:"Κλείσιμο",prevText:"Προηγούμενος",nextText:"Επόμενος",currentText:"Σήμερα",monthNames:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthNamesShort:["Ιαν","Φεβ","Μαρ","Απρ","Μαι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],dayNames:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],dayNamesShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayNamesMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],weekHeader:"Εβδ",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("el",{buttonText:{month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},allDayText:"Ολοήμερο",eventLimitText:"περισσότερα"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,e){var f="";switch(c){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=d(a,e)+" "+f}function d(a,b){return 10>a?b?f[a]:e[a]:a}var e="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),f=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",e[7],e[8],e[9]];(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"&#xAB;Edellinen",nextText:"Seuraava&#xBB;",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function d(a){return(a?"":"[múlt] ")+"["+e[this.day()]+"] LT[-kor]"}var e="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return d.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return d.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"&#x3C;mundur",nextText:"maju&#x3E;",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari<br/>penuh",eventLimitText:"lebih"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"&#x3C;Prec",nextText:"Succ&#x3E;",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il<br/>giorno",eventLimitText:function(a){return"+altri "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}}),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"&#x3C;前",nextText:"次&#x3E;",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"ko",{months:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),monthsShort:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),weekdays:"일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"),weekdaysShort:"일_월_화_수_목_금_토".split("_"),weekdaysMin:"일_월_화_수_목_금_토".split("_"),longDateFormat:{LT:"A h시 m분",LTS:"A h시 m분 s초",L:"YYYY.MM.DD",LL:"YYYY년 MMMM D일",LLL:"YYYY년 MMMM D일 LT",LLLL:"YYYY년 MMMM D일 dddd LT"},calendar:{sameDay:"오늘 LT",nextDay:"내일 LT",nextWeek:"dddd LT",lastDay:"어제 LT",lastWeek:"지난주 dddd LT",sameElse:"L"},relativeTime:{future:"%s 후",past:"%s 전",s:"몇초",ss:"%d초",m:"일분",mm:"%d분",h:"한시간",hh:"%d시간",d:"하루",dd:"%d일",M:"한달",MM:"%d달",y:"일년",yy:"%d년"},ordinalParse:/\d{1,2}일/,ordinal:"%d일",meridiemParse:/오전|오후/,isPM:function(a){return"오후"===a},meridiem:function(a,b,c){return 12>a?"오전":"오후"}}),a.fullCalendar.datepickerLang("ko","ko",{closeText:"닫기",prevText:"이전달",nextText:"다음달",currentText:"오늘",monthNames:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthNamesShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],dayNames:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],dayNamesShort:["일","월","화","수","목","금","토"],dayNamesMin:["일","월","화","수","목","금","토"],weekHeader:"Wk",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"년"}),a.fullCalendar.lang("ko",{buttonText:{month:"월",week:"주",day:"일",list:"일정목록"},allDayText:"종일",eventLimitText:"개"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),d="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"&#xAB;Forrige",nextText:"Neste&#xBB;",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function d(a,b,d){var e=a+" ";switch(d){case"m":return b?"minuta":"minutę";case"mm":return e+(c(a)?"minuty":"minut");case"h":return b?"godzina":"godzinę";case"hh":return e+(c(a)?"godziny":"godzin");case"MM":return e+(c(a)?"miesiące":"miesięcy");case"yy":return e+(c(a)?"lata":"lat")}}var e="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),f="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return/D MMMM/.test(b)?f[a.month()]:e[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:d,mm:d,h:d,hh:d,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:d,y:"rok",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"&#x3C;Poprzedni",nextText:"Następny&#x3E;",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"}),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"&#x3C;Anterior",nextText:"Próximo&#x3E;",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:b?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?b?"минута":"минуту":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function g(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}(b.defineLocale||b.lang).call(b,"ru",{months:e,monthsShort:f,weekdays:g,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:d,mm:d,h:"час",hh:d,d:"день",dd:d,M:"месяц",MM:d,y:"год",yy:d},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"&#x3C;Пред",nextText:"След&#x3E;",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"&#xAB;Förra",nextText:"Nästa&#xBB;",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"dan",dd:c.translate,M:"mesec",MM:c.translate,y:"godinu",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}}),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"&#xAB;&#xA0;ย้อน",nextText:"ถัดไป&#xA0;&#xBB;",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var b=a%10,d=a%100-b,e=a>=100?100:null;return a+(c[b]||c[d]||c[e])},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"&#x3C;geri",nextText:"ileri&#x3e",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()-a.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()<a.unix()?"[上]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},sameElse:"LL"},ordinalParse:/\d{1,2}(日|月|周)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"周";default:return a}},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1分钟",mm:"%d分钟",h:"1小时",hh:"%d小时",d:"1天",dd:"%d天",M:"1个月",MM:"%d个月",y:"1年",yy:"%d年"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("zh-cn","zh-CN",{closeText:"关闭",prevText:"&#x3C;上月",nextText:"下月&#x3E;",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-cn",{buttonText:{month:"月",week:"周",day:"日",list:"日程"},allDayText:"全天",eventLimitText:function(a){return"另外 "+a+" 个"}})});"use strict";var Kanboard={};Kanboard.App=function(){this.controllers={}};Kanboard.App.prototype.get=function(a){return this.controllers[a]};Kanboard.App.prototype.execute=function(){for(var b in Kanboard){if(b!=="App"){var a=new Kanboard[b](this);this.controllers[b]=a;if(typeof a.execute==="function"){a.execute()}if(typeof a.listen==="function"){a.listen()}if(typeof a.focus==="function"){a.focus()}if(typeof a.keyboardShortcuts==="function"){a.keyboardShortcuts()}}}this.focus();this.chosen();this.keyboardShortcuts();this.datePicker();this.autoComplete()};Kanboard.App.prototype.keyboardShortcuts=function(){var a=this;Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(b){b.preventDefault();$("#board-selector").trigger("chosen:open")});Mousetrap.bindGlobal("esc",function(){a.get("Popover").close();a.get("Dropdown").close()});Mousetrap.bind("?",function(){a.get("Popover").open($("body").data("keyboard-shortcut-url"))})};Kanboard.App.prototype.focus=function(){$(document).on("focus",".auto-select",function(){$(this).select()});$(document).on("mouseup",".auto-select",function(a){a.preventDefault()})};Kanboard.App.prototype.chosen=function(){$(".chosen-select").each(function(){var a=$(this).data("search-threshold");if(a===undefined){a=10}$(this).chosen({width:"180px",no_results_text:$(this).data("notfound"),disable_search_threshold:a})});$(".select-auto-redirect").change(function(){var a=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(a,$(this).val())})};Kanboard.App.prototype.datePicker=function(){$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".form-date").datepicker({showOtherMonths:true,selectOtherMonths:true,dateFormat:"yy-mm-dd",constrainInput:false});$(".form-datetime").datetimepicker({controlType:"select",oneLine:true,dateFormat:"yy-mm-dd",constrainInput:false})};Kanboard.App.prototype.autoComplete=function(){$(".autocomplete").each(function(){var b=$(this);var c=b.data("dst-field");var a=b.data("dst-extra-field");if($("#form-"+c).val()==""){b.parent().find("button[type=submit]").attr("disabled","disabled")}b.autocomplete({source:b.data("search-url"),minLength:1,select:function(d,e){$("input[name="+c+"]").val(e.item.id);if(a){$("input[name="+a+"]").val(e.item[a])}b.parent().find("button[type=submit]").removeAttr("disabled")}})})};Kanboard.App.prototype.hasId=function(a){return !!document.getElementById(a)};Kanboard.App.prototype.showLoadingIcon=function(){$("body").append('<span id="app-loading-icon">&nbsp;<i class="fa fa-spinner fa-spin"></i></span>')};Kanboard.App.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()};Kanboard.App.prototype.formatDuration=function(a){if(a>=86400){return Math.round(a/86400)+"d"}else{if(a>=3600){return Math.round(a/3600)+"h"}else{if(a>=60){return Math.round(a/60)+"m"}}}return a+"s"};Kanboard.App.prototype.isVisible=function(){var a="";if(typeof document.hidden!=="undefined"){a="visibilityState"}else{if(typeof document.mozHidden!=="undefined"){a="mozVisibilityState"}else{if(typeof document.msHidden!=="undefined"){a="msVisibilityState"}else{if(typeof document.webkitHidden!=="undefined"){a="webkitVisibilityState"}}}}if(a!=""){return document[a]=="visible"}return true};Kanboard.Dropdown=function(a){this.app=a};Kanboard.Dropdown.prototype.listen=function(){var a=this;$(document).on("click",function(){a.close()});$(document).on("click",".dropdown-menu",function(f){f.preventDefault();f.stopImmediatePropagation();a.close();var c=$(this).next("ul");var g=$(this).offset();$("body").append(jQuery("<div>",{id:"dropdown"}));c.clone().appendTo("#dropdown");var h=$("#dropdown ul");h.addClass("dropdown-submenu-open");var d=h.outerHeight();var b=h.outerWidth();if(g.top+d-$(window).scrollTop()<$(window).height()||$(window).scrollTop()+g.top<d){h.css("top",g.top+$(this).height())}else{h.css("top",g.top-d-5)}if(g.left+b>$(window).width()){h.css("left",g.left-b+$(this).outerWidth())}else{h.css("left",g.left)}});$(document).on("click",".dropdown-submenu-open li",function(b){if($(b.target).is("li")){$(this).find("a:visible")[0].click()}})};Kanboard.Dropdown.prototype.close=function(){$("#dropdown").remove()};Kanboard.Dropdown.prototype.onPopoverOpened=function(){this.close()};Kanboard.Popover=function(a){this.app=a};Kanboard.Popover.prototype.listen=function(){var a=this;$(document).on("click",".popover",function(b){a.onClick(b)});$(document).on("click",".close-popover",function(b){a.close(b)});$(document).on("click","#popover-container",function(b){a.close(b)});$(document).on("click","#popover-content",function(b){b.stopPropagation()})};Kanboard.Popover.prototype.onClick=function(c){c.preventDefault();c.stopPropagation();var b=c.currentTarget||c.target;var a=b.getAttribute("href");if(!a){a=b.getAttribute("data-href")}if(a){this.open(a)}};Kanboard.Popover.prototype.isOpen=function(){return $("#popover-container").size()>0};Kanboard.Popover.prototype.open=function(b){var a=this;$.get(b,function(c){$("body").prepend('<div id="popover-container"><div id="popover-content">'+c+"</div></div>");a.executeOnOpenedListeners()})};Kanboard.Popover.prototype.close=function(a){if(this.isOpen()){if(a){a.preventDefault()}$("#popover-container").remove();this.executeOnClosedListeners()}};Kanboard.Popover.prototype.ajaxReload=function(c,b,a){var d=b.getResponseHeader("X-Ajax-Redirect");if(d){window.location=d==="self"?window.location.href.split("#")[0]:d}else{$("#popover-content").html(c);$("#popover-content input[autofocus]").focus();a.executeOnOpenedListeners()}};Kanboard.Popover.prototype.executeOnOpenedListeners=function(){for(var b in this.app.controllers){var a=this.app.get(b);if(typeof a.onPopoverOpened==="function"){a.onPopoverOpened()}}this.afterOpen()};Kanboard.Popover.prototype.executeOnClosedListeners=function(){for(var b in this.app.controllers){var a=this.app.get(b);if(typeof a.onPopoverClosed==="function"){a.onPopoverClosed()}}};Kanboard.Popover.prototype.afterOpen=function(){var b=this;var a=$("#popover-content .popover-form");if(a){a.on("submit",function(c){c.preventDefault();$.ajax({type:"POST",url:a.attr("action"),data:a.serialize(),success:function(e,f,d){b.ajaxReload(e,d,b)},beforeSend:function(){var d=$('.popover-form button[type="submit"]');d.html('<i class="fa fa-spinner fa-pulse"></i> '+d.html());d.attr("disabled",true)}})})}$(document).on("click",".popover-link",function(c){c.preventDefault();$.ajax({type:"GET",url:$(this).attr("href"),success:function(e,f,d){b.ajaxReload(e,d,b)}})});$("[autofocus]").each(function(){$(this).focus()});this.app.datePicker();this.app.autoComplete()};Kanboard.Notification=function(a){this.app=a};Kanboard.Notification.prototype.execute=function(){$(".alert-fade-out").delay(4000).fadeOut(800,function(){$(this).remove()})};Kanboard.Accordion=function(a){this.app=a};Kanboard.Accordion.prototype.listen=function(){$(document).on("click",".accordion-toggle",function(b){var a=$(this).parents(".accordion-section");b.preventDefault();if(a.hasClass("accordion-collapsed")){a.find(".accordion-content").show();a.removeClass("accordion-collapsed")}else{a.find(".accordion-content").hide();a.addClass("accordion-collapsed")}})};Kanboard.Session=function(a){this.app=a};Kanboard.Session.prototype.execute=function(){window.setInterval(this.checkSession,60000)};Kanboard.Session.prototype.checkSession=function(){if(!$(".form-login").length){$.ajax({cache:false,url:$("body").data("status-url"),statusCode:{401:function(){window.location=$("body").data("login-url")}}})}};Kanboard.Calendar=function(a){this.app=a};Kanboard.Calendar.prototype.execute=function(){var a=$("#calendar");if(a.length==1){this.show(a)}};Kanboard.Calendar.prototype.show=function(a){a.fullCalendar({lang:$("body").data("js-lang"),editable:true,eventLimit:true,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(b){$.ajax({cache:false,url:a.data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:b.id,date_due:b.start.format()})})},viewRender:function(){var b=a.data("check-url");var d={start:a.fullCalendar("getView").start.format(),end:a.fullCalendar("getView").end.format()};for(var c in d){b+="&"+c+"="+d[c]}$.getJSON(b,function(e){a.fullCalendar("removeEvents");a.fullCalendar("addEventSource",e);a.fullCalendar("rerenderEvents")})}})};Kanboard.AvgTimeColumnChart=function(a){this.app=a};Kanboard.AvgTimeColumnChart.prototype.execute=function(){if(this.app.hasId("analytic-avg-time-column")){this.show()}};Kanboard.AvgTimeColumnChart.prototype.show=function(){var d=$("#chart");var c=d.data("metrics");var e=[d.data("label")];var a=[];for(var b in c){e.push(c[b].average);a.push(c[b].title)}c3.generate({data:{columns:[e],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:a},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};Kanboard.BurndownChart=function(a){this.app=a};Kanboard.BurndownChart.prototype.execute=function(){if(this.app.hasId("analytic-burndown")){this.show()}};Kanboard.BurndownChart.prototype.show=function(){var g=$("#chart");var f=g.data("metrics");var e=[[g.data("label-total")]];var a=[];var c=d3.time.format("%Y-%m-%d");var h=d3.time.format(g.data("date-format"));for(var d=0;d<f.length;d++){for(var b=0;b<f[d].length;b++){if(d==0){e.push([f[d][b]])}else{e[b+1].push(f[d][b]);if(b>0){if(e[0][d]==undefined){e[0].push(0)}e[0][d]+=f[d][b]}if(b==0){a.push(h(c.parse(f[d][b])))}}}}c3.generate({data:{columns:e},axis:{x:{type:"category",categories:a}}})};Kanboard.CompareHoursColumnChart=function(a){this.app=a};Kanboard.CompareHoursColumnChart.prototype.execute=function(){if(this.app.hasId("analytic-compare-hours")){this.show()}};Kanboard.CompareHoursColumnChart.prototype.show=function(){var g=$("#chart");var f=g.data("metrics");var b=g.data("label-open");var a=g.data("label-closed");var h=[g.data("label-spent")];var e=[g.data("label-estimated")];var d=[];for(var c in f){h.push(parseFloat(f[c].time_spent));e.push(parseFloat(f[c].time_estimated));d.push(c=="open"?b:a)}c3.generate({data:{columns:[h,e],type:"bar"},bar:{width:{ratio:0.2}},axis:{x:{type:"category",categories:d}},legend:{show:true}})};Kanboard.CumulativeFlowDiagram=function(a){this.app=a};Kanboard.CumulativeFlowDiagram.prototype.execute=function(){if(this.app.hasId("analytic-cfd")){this.show()}};Kanboard.CumulativeFlowDiagram.prototype.show=function(){var g=$("#chart");var h=g.data("metrics");var b=[];var a=[];var e=[];var k=d3.time.format("%Y-%m-%d");var f=d3.time.format(g.data("date-format"));for(var d=0;d<h.length;d++){for(var c=0;c<h[d].length;c++){if(d==0){b.push([h[d][c]]);if(c>0){a.push(h[d][c])}}else{b[c].push(h[d][c]);if(c==0){e.push(f(k.parse(h[d][c])))}}}}c3.generate({data:{columns:b,type:"area-spline",groups:[a]},axis:{x:{type:"category",categories:e}}})};Kanboard.LeadCycleTimeChart=function(a){this.app=a};Kanboard.LeadCycleTimeChart.prototype.execute=function(){if(this.app.hasId("analytic-lead-cycle-time")){this.show()}};Kanboard.LeadCycleTimeChart.prototype.show=function(){var h=$("#chart");var g=h.data("metrics");var f=[h.data("label-cycle")];var c=[h.data("label-lead")];var b=[];var e={};e[h.data("label-cycle")]="area";e[h.data("label-lead")]="area-spline";var a={};a[h.data("label-lead")]="#afb42b";a[h.data("label-cycle")]="#4e342e";for(var d=0;d<g.length;d++){f.push(parseInt(g[d].avg_cycle_time));c.push(parseInt(g[d].avg_lead_time));b.push(g[d].day)}c3.generate({data:{columns:[c,f],types:e,colors:a},axis:{x:{type:"category",categories:b},y:{tick:{format:this.app.formatDuration}}}})};Kanboard.UserRepartitionChart=function(a){this.app=a};Kanboard.UserRepartitionChart.prototype.execute=function(){if(this.app.hasId("analytic-user-repartition")){this.show()}};Kanboard.UserRepartitionChart.prototype.show=function(){var c=$("#chart").data("metrics");var b=[];for(var a=0;a<c.length;a++){b.push([c[a].user,c[a].nb_tasks])}c3.generate({data:{columns:b,type:"donut"}})};Kanboard.TaskTimeColumnChart=function(a){this.app=a};Kanboard.TaskTimeColumnChart.prototype.execute=function(){if(this.app.hasId("analytic-task-time-column")){this.show()}};Kanboard.TaskTimeColumnChart.prototype.show=function(){var d=$("#chart");var c=d.data("metrics");var e=[d.data("label")];var a=[];for(var b=0;b<c.length;b++){e.push(c[b].time_spent);a.push(c[b].title)}c3.generate({data:{columns:[e],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:a},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};Kanboard.TaskRepartitionChart=function(a){this.app=a};Kanboard.TaskRepartitionChart.prototype.execute=function(){if(this.app.hasId("analytic-task-repartition")){this.show()}};Kanboard.TaskRepartitionChart.prototype.show=function(){var c=$("#chart").data("metrics");var b=[];for(var a=0;a<c.length;a++){b.push([c[a].column_title,c[a].nb_tasks])}c3.generate({data:{columns:b,type:"donut"}})};Kanboard.Gantt=function(a){this.app=a;this.data=[];this.options={container:"#gantt-chart",showWeekends:true,allowMoves:true,allowResizes:true,cellWidth:21,cellHeight:31,slideWidth:1000,vHeaderWidth:200}};Kanboard.Gantt.prototype.execute=function(){if(this.app.hasId("gantt-chart")){this.show()}};Kanboard.Gantt.prototype.saveRecord=function(a){this.app.showLoadingIcon();$.ajax({cache:false,url:$(this.options.container).data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify(a),complete:this.app.hideLoadingIcon.bind(this)})};Kanboard.Gantt.prototype.show=function(){this.data=this.prepareData($(this.options.container).data("records"));var d=Math.floor((this.options.slideWidth/this.options.cellWidth)+5);var c=this.getDateRange(d);var a=c[0];var f=c[1];var b=$(this.options.container);var e=jQuery("<div>",{"class":"ganttview"});e.append(this.renderVerticalHeader());e.append(this.renderSlider(a,f));b.append(e);jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child",b).addClass("last");jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child",b).addClass("last");jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child",b).addClass("last");if(!$(this.options.container).data("readonly")){this.listenForBlockResize(a);this.listenForBlockMove(a)}else{this.options.allowResizes=false;this.options.allowMoves=false}};Kanboard.Gantt.prototype.renderVerticalHeader=function(){var e=jQuery("<div>",{"class":"ganttview-vtheader"});var b=jQuery("<div>",{"class":"ganttview-vtheader-item"});var d=jQuery("<div>",{"class":"ganttview-vtheader-series"});for(var a=0;a<this.data.length;a++){var c=jQuery("<span>").append(jQuery("<i>",{"class":"fa fa-info-circle tooltip",title:this.getVerticalHeaderTooltip(this.data[a])})).append("&nbsp;");if(this.data[a].type=="task"){c.append(jQuery("<a>",{href:this.data[a].link,title:this.data[a].title}).append(this.data[a].title))}else{c.append(jQuery("<a>",{href:this.data[a].board_link,target:"_blank",title:$(this.options.container).data("label-board-link")}).append('<i class="fa fa-th"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[a].gantt_link,target:"_blank",title:$(this.options.container).data("label-gantt-link")}).append('<i class="fa fa-sliders"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[a].link,target:"_blank"}).append(this.data[a].title))}d.append(jQuery("<div>",{"class":"ganttview-vtheader-series-name"}).append(c))}b.append(d);e.append(b);return e};Kanboard.Gantt.prototype.renderSlider=function(b,d){var a=jQuery("<div>",{"class":"ganttview-slide-container"});var c=this.getDates(b,d);a.append(this.renderHorizontalHeader(c));a.append(this.renderGrid(c));a.append(this.addBlockContainers());this.addBlocks(a,b);return a};Kanboard.Gantt.prototype.renderHorizontalHeader=function(a){var h=jQuery("<div>",{"class":"ganttview-hzheader"});var f=jQuery("<div>",{"class":"ganttview-hzheader-months"});var e=jQuery("<div>",{"class":"ganttview-hzheader-days"});var c=0;for(var i in a){for(var b in a[i]){var j=a[i][b].length*this.options.cellWidth;c=c+j;f.append(jQuery("<div>",{"class":"ganttview-hzheader-month",css:{width:(j-1)+"px"}}).append($.datepicker.regional[$("body").data("js-lang")].monthNames[b]+" "+i));for(var g in a[i][b]){e.append(jQuery("<div>",{"class":"ganttview-hzheader-day"}).append(a[i][b][g].getDate()))}}}f.css("width",c+"px");e.css("width",c+"px");h.append(f).append(e);return h};Kanboard.Gantt.prototype.renderGrid=function(a){var k=jQuery("<div>",{"class":"ganttview-grid"});var e=jQuery("<div>",{"class":"ganttview-grid-row"});for(var h in a){for(var b in a[h]){for(var g in a[h][b]){var c=jQuery("<div>",{"class":"ganttview-grid-row-cell"});if(this.options.showWeekends&&this.isWeekend(a[h][b][g])){c.addClass("ganttview-weekend")}e.append(c)}}}var j=jQuery("div.ganttview-grid-row-cell",e).length*this.options.cellWidth;e.css("width",j+"px");k.css("width",j+"px");for(var f=0;f<this.data.length;f++){k.append(e.clone())}return k};Kanboard.Gantt.prototype.addBlockContainers=function(){var b=jQuery("<div>",{"class":"ganttview-blocks"});for(var a=0;a<this.data.length;a++){b.append(jQuery("<div>",{"class":"ganttview-block-container"}))}return b};Kanboard.Gantt.prototype.addBlocks=function(b,a){var j=jQuery("div.ganttview-blocks div.ganttview-block-container",b);var c=0;for(var f=0;f<this.data.length;f++){var g=this.data[f];var k=this.daysBetween(g.start,g.end)+1;var e=this.daysBetween(a,g.start);var h=jQuery("<div>",{"class":"ganttview-block-text"});var d=jQuery("<div>",{"class":"ganttview-block tooltip"+(this.options.allowMoves?" ganttview-block-movable":""),title:this.getBarTooltip(g),css:{width:((k*this.options.cellWidth)-9)+"px","margin-left":(e*this.options.cellWidth)+"px"}}).append(h);if(k>=2){h.append(g.progress)}d.data("record",g);this.setBarColor(d,g);if(g.progress!="0%"){d.append(jQuery("<div>",{css:{"z-index":0,position:"absolute",top:0,bottom:0,"background-color":g.color.border,width:g.progress,opacity:0.4}}))}jQuery(j[c]).append(d);c=c+1}};Kanboard.Gantt.prototype.getVerticalHeaderTooltip=function(b){var g="";if(b.type=="task"){g="<strong>"+b.column_title+"</strong> ("+b.progress+")<br/>"+b.title}else{var d=["managers","members"];for(var c in d){var e=d[c];if(!jQuery.isEmptyObject(b.users[e])){var f=jQuery("<ul>");for(var a in b.users[e]){f.append(jQuery("<li>").append(b.users[e][a]))}g+="<p><strong>"+$(this.options.container).data("label-"+e)+"</strong></p>"+f[0].outerHTML}}}return g};Kanboard.Gantt.prototype.getBarTooltip=function(a){var b="";if(a.not_defined){b=$(this.options.container).data("label-not-defined")}else{if(a.type=="task"){b="<strong>"+a.progress+"</strong><br/>"+$(this.options.container).data("label-assignee")+" "+(a.assignee?a.assignee:"")+"<br/>"}b+=$(this.options.container).data("label-start-date")+" "+$.datepicker.formatDate("yy-mm-dd",a.start)+"<br/>";b+=$(this.options.container).data("label-end-date")+" "+$.datepicker.formatDate("yy-mm-dd",a.end)}return b};Kanboard.Gantt.prototype.setBarColor=function(b,a){if(a.not_defined){b.addClass("ganttview-block-not-defined")}else{b.css("background-color",a.color.background);b.css("border-color",a.color.border)}};Kanboard.Gantt.prototype.listenForBlockResize=function(a){var b=this;jQuery("div.ganttview-block",this.options.container).resizable({grid:this.options.cellWidth,handles:"e,w",delay:300,stop:function(){var c=jQuery(this);b.updateDataAndPosition(c,a);b.saveRecord(c.data("record"))}})};Kanboard.Gantt.prototype.listenForBlockMove=function(a){var b=this;jQuery("div.ganttview-block",this.options.container).draggable({axis:"x",delay:300,grid:[this.options.cellWidth,this.options.cellWidth],stop:function(){var c=jQuery(this);b.updateDataAndPosition(c,a);b.saveRecord(c.data("record"))}})};Kanboard.Gantt.prototype.updateDataAndPosition=function(f,d){var a=jQuery("div.ganttview-slide-container",this.options.container);var j=a.scrollLeft();var g=f.offset().left-a.offset().left-1+j;var i=f.data("record");i.not_defined=false;this.setBarColor(f,i);var c=Math.round(g/this.options.cellWidth);var h=this.addDays(this.cloneDate(d),c);i.start=h;var b=f.outerWidth();var e=Math.round(b/this.options.cellWidth)-1;i.end=this.addDays(this.cloneDate(h),e);if(i.type==="task"&&e>0){jQuery("div.ganttview-block-text",f).text(i.progress)}f.attr("title",this.getBarTooltip(i));f.data("record",i);f.css("top","").css("left","").css("position","relative").css("margin-left",g+"px")};Kanboard.Gantt.prototype.getDates=function(e,a){var d=[];d[e.getFullYear()]=[];d[e.getFullYear()][e.getMonth()]=[e];var c=e;while(this.compareDate(c,a)==-1){var b=this.addDays(this.cloneDate(c),1);if(!d[b.getFullYear()]){d[b.getFullYear()]=[]}if(!d[b.getFullYear()][b.getMonth()]){d[b.getFullYear()][b.getMonth()]=[]}d[b.getFullYear()][b.getMonth()].push(b);c=b}return d};Kanboard.Gantt.prototype.prepareData=function(c){for(var b=0;b<c.length;b++){var d=new Date(c[b].start[0],c[b].start[1]-1,c[b].start[2],0,0,0,0);c[b].start=d;var a=new Date(c[b].end[0],c[b].end[1]-1,c[b].end[2],0,0,0,0);c[b].end=a}return c};Kanboard.Gantt.prototype.getDateRange=function(c){var f=new Date();var b=new Date();for(var d=0;d<this.data.length;d++){var e=new Date();e.setTime(Date.parse(this.data[d].start));var a=new Date();a.setTime(Date.parse(this.data[d].end));if(d==0){f=e;b=a}if(this.compareDate(f,e)==1){f=e}if(this.compareDate(b,a)==-1){b=a}}if(this.daysBetween(f,b)<c){b=this.addDays(this.cloneDate(f),c)}f.setDate(f.getDate()-1);return[f,b]};Kanboard.Gantt.prototype.daysBetween=function(d,a){if(!d||!a){return 0}var c=0,b=this.cloneDate(d);while(this.compareDate(b,a)==-1){c=c+1;this.addDays(b,1)}return c};Kanboard.Gantt.prototype.isWeekend=function(a){return a.getDay()%6==0};Kanboard.Gantt.prototype.cloneDate=function(a){return new Date(a.getTime())};Kanboard.Gantt.prototype.addDays=function(a,b){a.setDate(a.getDate()+b*1);return a};Kanboard.Gantt.prototype.compareDate=function(b,a){if(isNaN(b)||isNaN(a)){throw new Error(b+" - "+a)}else{if(b instanceof Date&&a instanceof Date){return(b<a)?-1:(b>a)?1:0}else{throw new TypeError(b+" - "+a)}}};Kanboard.Column=function(a){this.app=a};Kanboard.Column.prototype.listen=function(){this.dragAndDrop()};Kanboard.Column.prototype.dragAndDrop=function(){var a=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".columns-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(c,b){b.children().each(function(){$(this).width($(this).width())});return b},stop:function(c,d){var b=d.item;b.removeClass("draggable-item-selected");a.savePosition(b.data("column-id"),b.index()+1)},start:function(b,c){c.item.addClass("draggable-item-selected")}}).disableSelection()};Kanboard.Column.prototype.savePosition=function(d,a){var c=$(".columns-table").data("save-position-url");var b=this;this.app.showLoadingIcon();$.ajax({cache:false,url:c,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({column_id:d,position:a}),complete:function(){b.app.hideLoadingIcon()}})};Kanboard.Markdown=function(a){this.app=a;this.editor=null};Kanboard.Markdown.prototype.onPopoverOpened=function(){this.listen()};Kanboard.Markdown.prototype.onPopoverClosed=function(){this.listen()};Kanboard.Markdown.prototype.listen=function(){var a=$(".markdown-editor");if(this.editor){this.destroy()}if(a.length>0){this.show(a[0])}};Kanboard.Markdown.prototype.destroy=function(){var a=this.editor.codemirror;var c=a.getWrapperElement();for(var b in ["toolbar","statusbar","sideBySide"]){if(this.editor.gui[b]){c.parentNode.removeChild(this.editor.gui[b])}}a.toTextArea();this.editor=null};Kanboard.Markdown.prototype.show=function(a){var b=["bold","italic","strikethrough","heading","|","unordered-list","ordered-list","link","|","code","table"];this.editor=new SimpleMDE({element:a,status:false,toolbarTips:false,autoDownloadFontAwesome:false,spellChecker:false,autosave:{enabled:false},forceSync:true,blockStyles:{italic:"_"},toolbar:a.hasAttribute("data-markdown-editor-disable-toolbar")?false:b,placeholder:a.getAttribute("placeholder")})};Kanboard.ProjectPermission=function(a){this.app=a};Kanboard.ProjectPermission.prototype.listen=function(){$(".project-change-role").on("change",function(){$.ajax({cache:false,url:$(this).data("url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({id:$(this).data("id"),role:$(this).val()})})})};Kanboard.ProjectCreation=function(a){this.app=a};Kanboard.ProjectCreation.prototype.onPopoverOpened=function(){$("#project-creation-form #form-src_project_id").on("change",function(){var a=$(this).val();if(a==0){$(".project-creation-options").hide()}else{$(".project-creation-options").show()}})};Kanboard.Screenshot=function(a){this.app=a;this.pasteCatcher=null};Kanboard.Screenshot.prototype.onPopoverOpened=function(){if(this.app.hasId("screenshot-zone")){this.initialize()}};Kanboard.Screenshot.prototype.initialize=function(){this.destroy();if(!window.Clipboard){this.pasteCatcher=document.createElement("div");this.pasteCatcher.id="screenshot-pastezone";this.pasteCatcher.contentEditable="true";this.pasteCatcher.style.opacity=0;this.pasteCatcher.style.position="fixed";this.pasteCatcher.style.top=0;this.pasteCatcher.style.right=0;this.pasteCatcher.style.width=0;document.body.insertBefore(this.pasteCatcher,document.body.firstChild);this.pasteCatcher.focus();document.addEventListener("click",this.setFocus.bind(this));document.getElementById("screenshot-zone").addEventListener("click",this.setFocus.bind(this))}window.addEventListener("paste",this.pasteHandler.bind(this))};Kanboard.Screenshot.prototype.destroy=function(){if(this.pasteCatcher!=null){document.body.removeChild(this.pasteCatcher)}else{if(document.getElementById("screenshot-pastezone")){document.body.removeChild(document.getElementById("screenshot-pastezone"))}}document.removeEventListener("click",this.setFocus.bind(this));this.pasteCatcher=null};Kanboard.Screenshot.prototype.setFocus=function(){if(this.pasteCatcher!==null){this.pasteCatcher.focus()}};Kanboard.Screenshot.prototype.pasteHandler=function(g){if(g.clipboardData&&g.clipboardData.items){var d=g.clipboardData.items;if(d){for(var f=0;f<d.length;f++){if(d[f].type.indexOf("image")!==-1){var c=d[f].getAsFile();var a=new FileReader();var b=this;a.onload=function(e){b.createImage(e.target.result)};a.readAsDataURL(c)}}}}else{setTimeout(this.checkInput.bind(this),100)}};Kanboard.Screenshot.prototype.checkInput=function(){var a=this.pasteCatcher.childNodes[0];if(a){if(a.tagName==="IMG"){this.createImage(a.src)}}this.pasteCatcher.innerHTML=""};Kanboard.Screenshot.prototype.createImage=function(c){var b=new Image();b.src=c;b.onload=function(){var d=c.split("base64,");var e=d[1];$("input[name=screenshot]").val(e)};var a=document.getElementById("screenshot-zone");a.innerHTML="";a.className="screenshot-pasted";a.appendChild(b);this.destroy();this.initialize()};Kanboard.FileUpload=function(a){this.app=a;this.files=[];this.currentFile=0};Kanboard.FileUpload.prototype.onPopoverOpened=function(){var a=document.getElementById("file-dropzone");var b=this;if(a){a.ondragover=a.ondragenter=function(c){c.stopPropagation();c.preventDefault()};a.ondrop=function(c){c.stopPropagation();c.preventDefault();b.files=c.dataTransfer.files;b.show();$("#file-error-max-size").hide()};$(document).on("click","#file-browser",function(c){c.preventDefault();$("#file-form-element").get(0).click()});$(document).on("click","#file-upload-button",function(c){c.preventDefault();b.currentFile=0;b.checkFiles()});$("#file-form-element").change(function(){b.files=document.getElementById("file-form-element").files;b.show();$("#file-error-max-size").hide()})}};Kanboard.FileUpload.prototype.show=function(){$("#file-list").remove();if(this.files.length>0){$("#file-upload-button").prop("disabled",false);$("#file-dropzone-inner").hide();var e=jQuery("<ul>",{id:"file-list"});for(var d=0;d<this.files.length;d++){var b=jQuery("<span>",{id:"file-percentage-"+d}).append("(0%)");var c=jQuery("<progress>",{id:"file-progress-"+d,value:0});var a=jQuery("<li>",{id:"file-label-"+d}).append(c).append("&nbsp;").append(this.files[d].name).append("&nbsp;").append(b);e.append(a)}$("#file-dropzone").append(e)}else{$("#file-dropzone-inner").show()}};Kanboard.FileUpload.prototype.checkFiles=function(){var a=parseInt($("#file-dropzone").data("max-size"));for(var b=0;b<this.files.length;b++){if(this.files[b].size>a){$("#file-error-max-size").show();$("#file-label-"+b).addClass("file-error");$("#file-upload-button").prop("disabled",true);return}}this.uploadFiles()};Kanboard.FileUpload.prototype.uploadFiles=function(){if(this.files.length>0){this.uploadFile(this.files[this.currentFile])}};Kanboard.FileUpload.prototype.uploadFile=function(d){var a=document.getElementById("file-dropzone");var b=a.dataset.url;var e=new XMLHttpRequest();var c=new FormData();e.upload.addEventListener("progress",this.updateProgress.bind(this));e.upload.addEventListener("load",this.transferComplete.bind(this));e.open("POST",b,true);c.append("files[]",d);e.send(c)};Kanboard.FileUpload.prototype.updateProgress=function(a){if(a.lengthComputable){$("#file-progress-"+this.currentFile).val(a.loaded/a.total);$("#file-percentage-"+this.currentFile).text("("+Math.floor((a.loaded/a.total)*100)+"%)")}};Kanboard.FileUpload.prototype.transferComplete=function(){this.currentFile++;if(this.currentFile<this.files.length){this.uploadFile(this.files[this.currentFile])}else{var a=$("#file-upload-button");a.prop("disabled",true);a.parent().hide();$("#file-done").show()}};Kanboard.Search=function(a){this.app=a};Kanboard.Search.prototype.focus=function(){$(document).on("focus","#form-search",function(){var b=$("#form-search");if(b[0].setSelectionRange){var a=b.val().length*2;b[0].setSelectionRange(a,a)}})};Kanboard.Search.prototype.listen=function(){$(document).on("click",".filter-helper",function(d){d.preventDefault();var c=$(this).data("filter");var b=$(this).data("append-filter");var a=$("#form-search");if(b){c=a.val()+" "+b}a.val(c);$("form.search").submit()})};Kanboard.Search.prototype.goToView=function(a){var b=$(a);if(b.length){window.location=b.attr("href")}};Kanboard.Search.prototype.keyboardShortcuts=function(){var a=this;Mousetrap.bind("v o",function(){a.goToView(".view-overview")});Mousetrap.bind("v b",function(){a.goToView(".view-board")});Mousetrap.bind("v c",function(){a.goToView(".view-calendar")});Mousetrap.bind("v l",function(){a.goToView(".view-listing")});Mousetrap.bind("v g",function(){a.goToView(".view-gantt")});Mousetrap.bind("f",function(c){c.preventDefault();var b=document.getElementById("form-search");if(b){b.focus()}});Mousetrap.bind("r",function(d){d.preventDefault();var c=$(".filter-reset").data("filter");var b=$("#form-search");b.val(c);$("form.search").submit()})};Kanboard.Task=function(a){this.app=a};Kanboard.Task.prototype.keyboardShortcuts=function(){var b=$("#task-view");var a=this;if(this.app.hasId("task-view")){Mousetrap.bind("e",function(){a.app.get("Popover").open(b.data("edit-url"))});Mousetrap.bind("d",function(){a.app.get("Popover").open(b.data("description-url"))});Mousetrap.bind("c",function(){a.app.get("Popover").open(b.data("comment-url"))});Mousetrap.bind("s",function(){a.app.get("Popover").open(b.data("subtask-url"))});Mousetrap.bind("l",function(){a.app.get("Popover").open(b.data("internal-link-url"))})}};Kanboard.Task.prototype.onPopoverOpened=function(){var a=this;var b=0;$(document).on("click",".color-square",function(){$(".color-square-selected").removeClass("color-square-selected");$(this).addClass("color-square-selected");$("#form-color_id").val($(this).data("color-id"))});$(document).on("click",".assign-me",function(f){var c=$(this).data("current-id");var d="#"+$(this).data("target-id");f.preventDefault();if($(d+" option[value="+c+"]").length){$(d).val(c)}});$(document).on("change","select.task-reload-project-destination",function(){if(b>0){$(this).val(b)}else{b=$(this).val();var c=$(this).data("redirect").replace(/PROJECT_ID/g,b);$(".loading-icon").show();$.ajax({type:"GET",url:c,success:function(e,f,d){b=0;$(".loading-icon").hide();a.app.get("Popover").ajaxReload(e,d,a.app.get("Popover"))}})}})};Kanboard.Subtask=function(a){this.app=a};Kanboard.Subtask.prototype.listen=function(){var a=this;this.dragAndDrop();$(document).on("click",".subtask-toggle-status",function(c){var b=$(this);c.preventDefault();$.ajax({cache:false,url:b.attr("href"),success:function(d){if(b.hasClass("subtask-refresh-table")){$(".subtasks-table").replaceWith(d)}else{b.replaceWith(d)}a.dragAndDrop()}})});$(document).on("click",".subtask-toggle-timer",function(c){var b=$(this);c.preventDefault();$.ajax({cache:false,url:b.attr("href"),success:function(d){$(".subtasks-table").replaceWith(d);a.dragAndDrop()}})})};Kanboard.Subtask.prototype.dragAndDrop=function(){var a=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".subtasks-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(c,b){b.children().each(function(){$(this).width($(this).width())});return b},stop:function(b,c){var d=c.item;d.removeClass("draggable-item-selected");a.savePosition(d.data("subtask-id"),d.index()+1)},start:function(b,c){c.item.addClass("draggable-item-selected")}}).disableSelection()};Kanboard.Subtask.prototype.savePosition=function(d,a){var c=$(".subtasks-table").data("save-position-url");var b=this;this.app.showLoadingIcon();$.ajax({cache:false,url:c,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({subtask_id:d,position:a}),complete:function(){b.app.hideLoadingIcon()}})};Kanboard.Swimlane=function(a){this.app=a};Kanboard.Swimlane.prototype.execute=function(){if($(".swimlanes-table").length){this.dragAndDrop()}};Kanboard.Swimlane.prototype.listen=function(){var a=this;$(document).on("click",".board-swimlane-toggle",function(c){c.preventDefault();var b=$(this).data("swimlane-id");if(a.isCollapsed(b)){a.expand(b)}else{a.collapse(b)}})};Kanboard.Swimlane.prototype.onBoardRendered=function(){var b=this.getAllCollapsed();for(var a=0;a<b.length;a++){this.collapse(b[a])}};Kanboard.Swimlane.prototype.getStorageKey=function(){return"hidden_swimlanes_"+$("#board").data("project-id")};Kanboard.Swimlane.prototype.expand=function(b){var c=this.getAllCollapsed();var a=c.indexOf(b);if(a>-1){c.splice(a,1)}localStorage.setItem(this.getStorageKey(),JSON.stringify(c));$(".board-swimlane-columns-"+b).css("display","table-row");$(".board-swimlane-tasks-"+b).css("display","table-row");$(".hide-icon-swimlane-"+b).css("display","inline");$(".show-icon-swimlane-"+b).css("display","none")};Kanboard.Swimlane.prototype.collapse=function(a){var b=this.getAllCollapsed();if(b.indexOf(a)<0){b.push(a);localStorage.setItem(this.getStorageKey(),JSON.stringify(b))}$(".board-swimlane-columns-"+a+":not(:first-child)").css("display","none");$(".board-swimlane-tasks-"+a).css("display","none");$(".hide-icon-swimlane-"+a).css("display","none");$(".show-icon-swimlane-"+a).css("display","inline")};Kanboard.Swimlane.prototype.isCollapsed=function(a){return this.getAllCollapsed().indexOf(a)>-1};Kanboard.Swimlane.prototype.getAllCollapsed=function(){return JSON.parse(localStorage.getItem(this.getStorageKey()))||[]};Kanboard.Swimlane.prototype.dragAndDrop=function(){var a=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".swimlanes-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(c,b){b.children().each(function(){$(this).width($(this).width())});return b},stop:function(b,d){var c=d.item;c.removeClass("draggable-item-selected");a.savePosition(c.data("swimlane-id"),c.index()+1)},start:function(b,c){c.item.addClass("draggable-item-selected")}}).disableSelection()};Kanboard.Swimlane.prototype.savePosition=function(d,a){var c=$(".swimlanes-table").data("save-position-url");var b=this;this.app.showLoadingIcon();$.ajax({cache:false,url:c,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({swimlane_id:d,position:a}),complete:function(){b.app.hideLoadingIcon()}})};Kanboard.BoardColumnView=function(a){this.app=a};Kanboard.BoardColumnView.prototype.execute=function(){if(this.app.hasId("board")){this.render()}};Kanboard.BoardColumnView.prototype.listen=function(){var a=this;$(document).on("click",".board-toggle-column-view",function(){a.toggle($(this).data("column-id"))})};Kanboard.BoardColumnView.prototype.onBoardRendered=function(){this.render()};Kanboard.BoardColumnView.prototype.render=function(){var a=this;$(".board-column-header").each(function(){var b=$(this).data("column-id");if(localStorage.getItem("hidden_column_"+b)){a.hideColumn(b)}})};Kanboard.BoardColumnView.prototype.toggle=function(a){if(localStorage.getItem("hidden_column_"+a)){this.showColumn(a)}else{this.hideColumn(a)}};Kanboard.BoardColumnView.prototype.hideColumn=function(a){$(".board-column-"+a+" .board-column-expanded").hide();$(".board-column-"+a+" .board-column-collapsed").show();$(".board-column-header-"+a+" .board-column-expanded").hide();$(".board-column-header-"+a+" .board-column-collapsed").show();$(".board-column-header-"+a).each(function(){$(this).removeClass("board-column-compact");$(this).addClass("board-column-header-collapsed")});$(".board-column-"+a).each(function(){$(this).addClass("board-column-task-collapsed")});$(".board-column-"+a+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+a+"").height())});localStorage.setItem("hidden_column_"+a,1)};Kanboard.BoardColumnView.prototype.showColumn=function(a){$(".board-column-"+a+" .board-column-expanded").show();$(".board-column-"+a+" .board-column-collapsed").hide();$(".board-column-header-"+a+" .board-column-expanded").show();$(".board-column-header-"+a+" .board-column-collapsed").hide();$(".board-column-header-"+a).removeClass("board-column-header-collapsed");$(".board-column-"+a).removeClass("board-column-task-collapsed");if(localStorage.getItem("horizontal_scroll")==0){$(".board-column-header-"+a).addClass("board-column-compact")}localStorage.removeItem("hidden_column_"+a)};Kanboard.BoardColumnScrolling=function(a){this.app=a};Kanboard.BoardColumnScrolling.prototype.execute=function(){if(this.app.hasId("board")){this.render();$(window).on("load",this.render);$(window).resize(this.render)}};Kanboard.BoardColumnScrolling.prototype.listen=function(){var a=this;$(document).on("click",".filter-toggle-height",function(b){b.preventDefault();a.toggle()})};Kanboard.BoardColumnScrolling.prototype.onBoardRendered=function(){this.render()};Kanboard.BoardColumnScrolling.prototype.toggle=function(){var a=localStorage.getItem("column_scroll");if(a==undefined){a=1}localStorage.setItem("column_scroll",a==0?1:0);this.render()};Kanboard.BoardColumnScrolling.prototype.render=function(){var d=$(".board-task-list");var c=$(".board-rotation-wrapper");var b=$(".filter-max-height");var e=$(".filter-min-height");if(localStorage.getItem("column_scroll")==0){var a=80;b.show();e.hide();c.css("min-height","");d.each(function(){var f=$(this).height();if(f>a){a=f}});d.css("min-height",a);d.css("height","")}else{b.hide();e.show();if($(".board-swimlane").length>1){d.each(function(){if($(this).height()>500){$(this).css("height",500)}else{$(this).css("min-height",320);c.css("min-height",320)}})}else{var a=$(window).height()-170;d.css("height",a);c.css("min-height",a)}}};Kanboard.BoardHorizontalScrolling=function(a){this.app=a};Kanboard.BoardHorizontalScrolling.prototype.execute=function(){if(this.app.hasId("board")){this.render()}};Kanboard.BoardHorizontalScrolling.prototype.listen=function(){var a=this;$(document).on("click",".filter-toggle-scrolling",function(b){b.preventDefault();a.toggle()})};Kanboard.BoardHorizontalScrolling.prototype.keyboardShortcuts=function(){var a=this;if(a.app.hasId("board")){Mousetrap.bind("c",function(){a.toggle()})}};Kanboard.BoardHorizontalScrolling.prototype.onBoardRendered=function(){this.render()};Kanboard.BoardHorizontalScrolling.prototype.toggle=function(){var a=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",a==0?1:0);this.render()};Kanboard.BoardHorizontalScrolling.prototype.render=function(){if(localStorage.getItem("horizontal_scroll")==0){$(".filter-wide").show();$(".filter-compact").hide();$("#board-container").addClass("board-container-compact");$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")}else{$(".filter-wide").hide();$(".filter-compact").show();$("#board-container").removeClass("board-container-compact");$("#board th").removeClass("board-column-compact")}};Kanboard.BoardCollapsedMode=function(a){this.app=a};Kanboard.BoardCollapsedMode.prototype.keyboardShortcuts=function(){var a=this;if(a.app.hasId("board")){Mousetrap.bind("s",function(){a.toggle()})}};Kanboard.BoardCollapsedMode.prototype.toggle=function(){var a=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(b){$(".filter-display-mode").toggle();a.app.get("BoardDragAndDrop").refresh(b)}})};Kanboard.BoardDragAndDrop=function(a){this.app=a;this.savingInProgress=false};Kanboard.BoardDragAndDrop.prototype.execute=function(){if(this.app.hasId("board")){this.dragAndDrop();this.executeListeners()}};Kanboard.BoardDragAndDrop.prototype.dragAndDrop=function(){var a=this;var b={forcePlaceholderSize:true,tolerance:"pointer",connectWith:".board-task-list",placeholder:"draggable-placeholder",items:".draggable-item",stop:function(d,k){var f=k.item;var j=f.attr("data-task-id");var l=f.attr("data-position");var i=f.attr("data-column-id");var h=f.attr("data-swimlane-id");var e=f.parent().attr("data-column-id");var c=f.parent().attr("data-swimlane-id");var g=f.index()+1;f.removeClass("draggable-item-selected");if(e!=i||c!=h||g!=l){a.changeTaskState(j);a.save(j,e,g,c)}},start:function(c,d){d.item.addClass("draggable-item-selected");d.placeholder.height(d.item.height())}};if($.support.touch){$(".task-board-sort-handle").css("display","inline");b.handle=".task-board-sort-handle"}$(".board-task-list").sortable(b)};Kanboard.BoardDragAndDrop.prototype.changeTaskState=function(b){var a=$("div[data-task-id="+b+"]");a.addClass("task-board-saving-state");a.find(".task-board-saving-icon").show()};Kanboard.BoardDragAndDrop.prototype.save=function(d,e,a,c){var b=this;b.app.showLoadingIcon();b.savingInProgress=true;$.ajax({cache:false,url:$("#board").data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:d,column_id:e,swimlane_id:c,position:a}),success:function(f){b.refresh(f);b.savingInProgress=false},error:function(){b.app.hideLoadingIcon();b.savingInProgress=false}})};Kanboard.BoardDragAndDrop.prototype.refresh=function(a){$("#board-container").replaceWith(a);this.app.hideLoadingIcon();this.dragAndDrop();this.executeListeners()};Kanboard.BoardDragAndDrop.prototype.executeListeners=function(){for(var b in this.app.controllers){var a=this.app.get(b);if(typeof a.onBoardRendered==="function"){a.onBoardRendered()}}};Kanboard.BoardTask=function(a){this.app=a};Kanboard.BoardTask.prototype.listen=function(){var a=this;$(document).on("click",".task-board-change-assignee",function(b){b.preventDefault();b.stopPropagation();a.app.get("Popover").open($(this).data("url"))});$(document).on("click",".task-board",function(b){if(b.target.tagName!="A"&&b.target.tagName!="IMG"){window.location=$(this).data("task-url")}})};Kanboard.BoardTask.prototype.keyboardShortcuts=function(){var a=this;if(a.app.hasId("board")){Mousetrap.bind("n",function(){a.app.get("Popover").open($("#board").data("task-creation-url"))})}};Kanboard.BoardPolling=function(a){this.app=a};Kanboard.BoardPolling.prototype.execute=function(){if(this.app.hasId("board")){var a=parseInt($("#board").attr("data-check-interval"));if(a>0){window.setInterval(this.check.bind(this),a*1000)}}};Kanboard.BoardPolling.prototype.check=function(){if(this.app.isVisible()&&!this.app.get("BoardDragAndDrop").savingInProgress){var a=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("check-url"),statusCode:{200:function(b){a.app.get("BoardDragAndDrop").refresh(b)},304:function(){a.app.hideLoadingIcon()}}})}};Kanboard.Tooltip=function(a){this.app=a};Kanboard.Tooltip.prototype.onBoardRendered=function(){this.execute()};Kanboard.Tooltip.prototype.execute=function(){$(".tooltip").tooltip({track:false,show:false,hide:false,position:{my:"left-20 top",at:"center bottom+9",using:function(a,b){$(this).css(a);var c=b.target.left+b.target.width/2-b.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(b.vertical).addClass(c<1?"align-left":"align-right").appendTo(this)}},content:function(){var c=this;var a=$(this).attr("data-href");if(!a){return'<div class="markdown">'+$(this).attr("title")+"</div>"}$.get(a,function b(f){var e=$(".ui-tooltip:visible");$(".ui-tooltip-content:visible").html(f);e.css({top:"",left:""});e.children(".tooltip-arrow").remove();var d=$(c).tooltip("option","position");d.of=$(c);e.position(d)});return'<i class="fa fa-spinner fa-spin"></i>'}}).on("mouseenter",function(){var a=this;$(this).tooltip("open");$(".ui-tooltip").on("mouseleave",function(){$(a).tooltip("close")})}).on("mouseleave focusout",function(a){a.stopImmediatePropagation();var b=this;setTimeout(function(){if(!$(".ui-tooltip:hover").length){$(b).tooltip("close")}},100)})};jQuery(document).ready(function(){var a=new Kanboard.App();a.execute()}); \ No newline at end of file
diff --git a/assets/js/src/Gantt.js b/assets/js/src/Gantt.js
index 254d8f3c..584df087 100644
--- a/assets/js/src/Gantt.js
+++ b/assets/js/src/Gantt.js
@@ -77,7 +77,7 @@ Kanboard.Gantt.prototype.renderVerticalHeader = function() {
.append("&nbsp;");
if (this.data[i].type == "task") {
- content.append(jQuery("<a>", {"href": this.data[i].link, "target": "_blank", "title": this.data[i].title}).append(this.data[i].title));
+ content.append(jQuery("<a>", {"href": this.data[i].link, "title": this.data[i].title}).append(this.data[i].title));
}
else {
content
diff --git a/composer.json b/composer.json
index 969b0606..5dd9a4cf 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,7 @@
"eluceo/ical": "0.8.0",
"erusev/parsedown" : "1.6.0",
"fguillot/json-rpc" : "1.0.3",
- "fguillot/picodb" : "1.0.7",
+ "fguillot/picodb" : "1.0.8",
"fguillot/simpleLogger" : "1.0.0",
"fguillot/simple-validator" : "1.0.0",
"paragonie/random_compat": "@stable",
diff --git a/composer.lock b/composer.lock
index caeb4906..70881a39 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,21 +4,21 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "hash": "d60e4b6b7ceb60202c48112ffbc33fba",
- "content-hash": "fbb704fa621ed6dd3c60241913ea1686",
+ "hash": "ecdd93c089273876816339ff22d67cc7",
+ "content-hash": "a5edc6f9c9ae2cd356e3f8ac96ef5532",
"packages": [
{
"name": "christian-riesen/base32",
- "version": "1.2.2",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/ChristianRiesen/base32.git",
- "reference": "fbe67d49d45dc789f942ef828c787550ebb894bc"
+ "reference": "fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/fbe67d49d45dc789f942ef828c787550ebb894bc",
- "reference": "fbe67d49d45dc789f942ef828c787550ebb894bc",
+ "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4",
+ "reference": "fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4",
"shasum": ""
},
"require": {
@@ -59,7 +59,7 @@
"encode",
"rfc4648"
],
- "time": "2015-09-27 23:45:02"
+ "time": "2016-04-07 07:45:31"
},
{
"name": "christian-riesen/otp",
@@ -239,16 +239,16 @@
},
{
"name": "fguillot/picodb",
- "version": "v1.0.7",
+ "version": "v1.0.8",
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoDb.git",
- "reference": "7f36dc3a7814ca0fc63439cd948e8acfeda672de"
+ "reference": "672a819ba2757a9e22a3572a230e735e84bcf625"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fguillot/picoDb/zipball/7f36dc3a7814ca0fc63439cd948e8acfeda672de",
- "reference": "7f36dc3a7814ca0fc63439cd948e8acfeda672de",
+ "url": "https://api.github.com/repos/fguillot/picoDb/zipball/672a819ba2757a9e22a3572a230e735e84bcf625",
+ "reference": "672a819ba2757a9e22a3572a230e735e84bcf625",
"shasum": ""
},
"require": {
@@ -272,7 +272,7 @@
],
"description": "Minimalist database query builder",
"homepage": "https://github.com/fguillot/picoDb",
- "time": "2016-03-12 14:31:33"
+ "time": "2016-03-27 20:15:25"
},
{
"name": "fguillot/simple-validator",
@@ -397,16 +397,16 @@
},
{
"name": "paragonie/random_compat",
- "version": "v1.2.2",
+ "version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
- "reference": "b3313b618f4edd76523572531d5d7e22fe747430"
+ "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/random_compat/zipball/b3313b618f4edd76523572531d5d7e22fe747430",
- "reference": "b3313b618f4edd76523572531d5d7e22fe747430",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/088c04e2f261c33bed6ca5245491cfca69195ccf",
+ "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf",
"shasum": ""
},
"require": {
@@ -441,7 +441,7 @@
"pseudorandom",
"random"
],
- "time": "2016-03-11 19:54:08"
+ "time": "2016-04-03 06:00:07"
},
{
"name": "pimple/pimple",
@@ -627,16 +627,16 @@
},
{
"name": "symfony/console",
- "version": "v2.8.3",
+ "version": "v2.8.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "56cc5caf051189720b8de974e4746090aaa10d44"
+ "reference": "9a5aef5fc0d4eff86853d44202b02be8d5a20154"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/56cc5caf051189720b8de974e4746090aaa10d44",
- "reference": "56cc5caf051189720b8de974e4746090aaa10d44",
+ "url": "https://api.github.com/repos/symfony/console/zipball/9a5aef5fc0d4eff86853d44202b02be8d5a20154",
+ "reference": "9a5aef5fc0d4eff86853d44202b02be8d5a20154",
"shasum": ""
},
"require": {
@@ -683,20 +683,20 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
- "time": "2016-02-28 16:20:50"
+ "time": "2016-03-17 09:19:04"
},
{
"name": "symfony/event-dispatcher",
- "version": "v2.8.3",
+ "version": "v2.8.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3"
+ "reference": "47d2d8cade9b1c3987573d2943bb9352536cdb87"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/78c468665c9568c3faaa9c416a7134308f2d85c3",
- "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/47d2d8cade9b1c3987573d2943bb9352536cdb87",
+ "reference": "47d2d8cade9b1c3987573d2943bb9352536cdb87",
"shasum": ""
},
"require": {
@@ -743,7 +743,7 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
- "time": "2016-01-27 05:14:19"
+ "time": "2016-03-07 14:04:32"
},
{
"name": "symfony/polyfill-mbstring",
@@ -808,16 +808,16 @@
"packages-dev": [
{
"name": "symfony/stopwatch",
- "version": "v2.8.3",
+ "version": "v2.8.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
- "reference": "e3bc8e2a984f4382690a438c8bb650f3ffd71e73"
+ "reference": "9e24824b2a9a16e17ab997f61d70bc03948e434e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e3bc8e2a984f4382690a438c8bb650f3ffd71e73",
- "reference": "e3bc8e2a984f4382690a438c8bb650f3ffd71e73",
+ "url": "https://api.github.com/repos/symfony/stopwatch/zipball/9e24824b2a9a16e17ab997f61d70bc03948e434e",
+ "reference": "9e24824b2a9a16e17ab997f61d70bc03948e434e",
"shasum": ""
},
"require": {
@@ -853,7 +853,7 @@
],
"description": "Symfony Stopwatch Component",
"homepage": "https://symfony.com",
- "time": "2016-01-03 15:33:41"
+ "time": "2016-03-04 07:54:35"
}
],
"aliases": [],
diff --git a/doc/.htaccess b/doc/.htaccess
deleted file mode 100644
index c47998cb..00000000
--- a/doc/.htaccess
+++ /dev/null
@@ -1,7 +0,0 @@
-<IfVersion >= 2.3>
- Require all denied
-</IfVersion>
-<IfVersion < 2.3>
- Order allow,deny
- Deny from all
-</IfVersion>
diff --git a/doc/automatic-actions.markdown b/doc/automatic-actions.markdown
index db56ccc0..d109a95b 100644
--- a/doc/automatic-actions.markdown
+++ b/doc/automatic-actions.markdown
@@ -14,17 +14,13 @@ Each project has a different set of automatic actions, the configuration panel i
Add a new action
----------------
-### Choose an action
+Click on the link **Add a new automatic action**.
-![Choose action](http://kanboard.net/screenshots/documentation/project-automatic-action-step1.png)
+![Automatique action](screenshots/automatic-action-creation.png)
-### Choose an event
-
-![Choose an event](http://kanboard.net/screenshots/documentation/project-automatic-action-step2.png)
-
-### Define action parameters
-
-![Define parameters](http://kanboard.net/screenshots/documentation/project-automatic-action-step3.png)
+- Choose an action
+- Then, select an event
+- And finally, define the parameters
List of available actions
-------------------------
diff --git a/doc/board-collapsed-expanded.markdown b/doc/board-collapsed-expanded.markdown
index 594676bc..0164da95 100644
--- a/doc/board-collapsed-expanded.markdown
+++ b/doc/board-collapsed-expanded.markdown
@@ -7,7 +7,7 @@ Switching from one view to another can be done with the keyboard shortcut **"s"*
Collapsed mode
--------------
-![Tasks collapsed](http://kanboard.net/screenshots/documentation/board-collapsed-mode.png)
+![Tasks collapsed](screenshots/board-collapsed-mode.png)
- If the task is assigned to someone, the initials of the person are shown next to the task number
- If the task title is too long, you can put your mouse over the task to show a tooltip with the full title.
@@ -15,5 +15,4 @@ Collapsed mode
Expanded mode
-------------
-![Tasks expanded](http://kanboard.net/screenshots/documentation/board-expanded-mode.png)
-
+![Tasks expanded](screenshots/board-expanded-mode.png)
diff --git a/doc/board-horizontal-scrolling-and-compact-view.markdown b/doc/board-horizontal-scrolling-and-compact-view.markdown
index 323d0de5..51885529 100644
--- a/doc/board-horizontal-scrolling-and-compact-view.markdown
+++ b/doc/board-horizontal-scrolling-and-compact-view.markdown
@@ -5,8 +5,6 @@ When the board cannot fit on your screen, a horizontal scroll bar will appear at
However, it's possible to switch to the compact the view to display all columns in your screen.
-![Board in compact mode](http://kanboard.net/screenshots/documentation/board-compact-mode.png)
+![Switch to compact mode](screenshots/board-compact-mode.png)
Switching between horizontal scrolling and compact view can be done with the keyboard shortcut **"c"** or by using the drop-down menu on the top left.
-
-Note: It's possible that text overlaps in compact mode, that will be improved over the next releases. \ No newline at end of file
diff --git a/doc/board-show-hide-columns.markdown b/doc/board-show-hide-columns.markdown
index 40edc5d8..c3f22424 100644
--- a/doc/board-show-hide-columns.markdown
+++ b/doc/board-show-hide-columns.markdown
@@ -3,9 +3,10 @@ Show and hide columns on the board
You can hide or display columns very easily on the board:
-![Board with hidden columns](http://kanboard.net/screenshots/documentation/board-hide-show-column.png)
+![Hide a column](screenshots/hide-column.png)
-- To hide a column, just click on the column title
-- To show a hidden column, click on the vertical title
+Pour cacher une colonne, ouvrez le menu déroulant de la colonne.
-When a column is hidden, the number of tasks is displayed at the top.
+![Show a column](screenshots/show-column.png)
+
+To show again the column, click on the "plus icon".
diff --git a/doc/cli.markdown b/doc/cli.markdown
index 9334d84b..d38d8b53 100644
--- a/doc/cli.markdown
+++ b/doc/cli.markdown
@@ -45,6 +45,9 @@ Available commands:
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
@@ -147,3 +150,17 @@ This command send a "daily cronjob event" to all open tasks of each project.
./kanboard trigger:tasks
Trigger task event: project_id=2, nb_tasks=1
```
+
+### Reset user password
+
+```bash
+./kanboard user:reset-password my_user
+```
+
+You will be prompted for a password and confirmation. Characters are not printed to the screen.
+
+### Remove two-factor authentication for a user
+
+```bash
+./kanboard user:reset-2fa my_user
+```
diff --git a/doc/creating-projects.markdown b/doc/creating-projects.markdown
index a87dbca0..373d93eb 100644
--- a/doc/creating-projects.markdown
+++ b/doc/creating-projects.markdown
@@ -14,7 +14,7 @@ Creating projects for multiple users
From the dashboard, click on the link **New project**:
-![Project creation form](http://kanboard.net/screenshots/documentation/project-creation-form.png)
+![Project creation form](screenshots/new-project.png)
It's very easy, you just have to find a name for your project!
@@ -27,6 +27,13 @@ Creating a private project
From the dashboard, click on the link **New private project**.
-![New private project](http://kanboard.net/screenshots/documentation/new-private-project.png)
+Creating projects from another project
+--------------------------------------
-Note: project names must be unique across the application.
+When you create a new project, your can choose to duplicate the properties of another project :
+
+- Permissions
+- Actions
+- Swimlanes
+- Catégories
+- Tasks
diff --git a/doc/editing-projects.markdown b/doc/editing-projects.markdown
index 781a7cc6..e7fea212 100644
--- a/doc/editing-projects.markdown
+++ b/doc/editing-projects.markdown
@@ -5,7 +5,7 @@ Projects can be renamed and disabled at any time.
To rename a project, just click on the link "Edit project" on the left.
-![Project edition](http://kanboard.net/screenshots/documentation/project-edition.png)
+![Project edition](screenshots/project-edition.png)
- The start date and end date are used to generate the project Gantt chart
- The description is visible as tooltip on the board and on the projects listing page
diff --git a/doc/faq.markdown b/doc/faq.markdown
index 953f98e1..0a9129f2 100644
--- a/doc/faq.markdown
+++ b/doc/faq.markdown
@@ -20,6 +20,12 @@ I get a blank page after installing or upgrading Kanboard
- If you use an aggressive OPcode caching, reload your web-server or php-fpm
+I have the error "There is no suitable CSPRNG installed on your system"
+-----------------------------------------------------------------------
+
+If you use PHP < 7.0, you need to have the openssl extension enabled or `/dev/urandom` accessible from the application if restricted by an `open_basedir` restriction.
+
+
Page not found and the URL seems wrong (&amp;amp;)
--------------------------------------------------
diff --git a/doc/fr/automatic-actions.markdown b/doc/fr/automatic-actions.markdown
index 844354a5..f136b98c 100644
--- a/doc/fr/automatic-actions.markdown
+++ b/doc/fr/automatic-actions.markdown
@@ -1,5 +1,5 @@
Actions automatiques
-=================
+====================
Pour réduire au minimum l'interaction avec les utilisateurs, Kanboard dispose d'actions automatiques.
@@ -12,22 +12,18 @@ Chaque action automatique est définie ainsi :
Chaque projet a une série d'actions automatisées qui lui sont propres, le panneau de configuration est situé sur la page qui liste les projets, il vous suffit de cliquer sur le lien **Actions automatiques**.
Ajouter une nouvelle action
-----------------
+---------------------------
-### Choisir une action
+Cliquez sur le lien **Ajouter une nouvelle action**.
-![Choisir une action](http://kanboard.net/screenshots/documentation/project-automatic-action-step1.png)
+![Action automatique](screenshots/automatic-action-creation.png)
-### Choisir un évènement
-
-![Choisir un évènement](http://kanboard.net/screenshots/documentation/project-automatic-action-step2.png)
-
-### Définir les paramètres d'une action
-
-![Definir les paramètres](http://kanboard.net/screenshots/documentation/project-automatic-action-step3.png)
+- Commencez par choisir une action
+- Ensuite, sélectionnez un évènement
+- Et pour finir, les paramètres de l'action
Liste des évènements disponibles
-------------------------
+--------------------------------
- Déplacement d'une tâche vers une autre colonne
- Déplacement d'une tâche à un autre emplacement de la même colonne
@@ -56,7 +52,7 @@ Liste des évènements disponibles
- Création d'un commentaire d'une *issue* de Bitbucket
Liste des actions disponibles
--------------------------
+-----------------------------
- Fermer une tâche
- Ouvrir une tâche
@@ -110,7 +106,7 @@ Supposons que nous ayons deux projets : "Commande du client" et "Production". Un
- Choisir l'évènement : **Fermer une tâche**
- Définir les paramètres de l'action : **Colonne = Validé** et **Projet = Production**
-### Quand une tâche est déplacée vers la toute dernière colonne, déplacer la même tâche exactement vers un autre projet
+### Quand une tâche est déplacée vers la toute dernière colonne, déplacer la même tâche exactement vers un autre projet
Supposons que nous ayons deux projets : "Idées" et "Développement". Une fois validée l'idée, la basculer vers le projet "Développement".
diff --git a/doc/fr/board-collapsed-expanded.markdown b/doc/fr/board-collapsed-expanded.markdown
index f72d990d..29396772 100644
--- a/doc/fr/board-collapsed-expanded.markdown
+++ b/doc/fr/board-collapsed-expanded.markdown
@@ -1,19 +1,18 @@
-Mode Replié et Déplié
-===========================
+Mode replié et déplié
+=====================
+Les tâches peuvent être affichées sur le tableau en mode replié ou déplié.
+Basculer d'un mode à l'autre peut être fait à l'aide du raccourci clavier **« s »** ou en utilisant le menu déroulant sur la gauche.
-Les tâches peuvent être affichées sur le Tableau en mode Replié ou Déplié.
-Basculer d'un mode à l'autre peut être fait à l'aide du raccourci clavier **"s"** ou en utilisant le menu déroulant sur la gauche.
+Mode replié
+-----------
-Mode Replié
---------------
-![Tâches repliées](http://kanboard.net/screenshots/documentation/board-collapsed-mode.png)
+![Tâches repliées](screenshots/board-collapsed-mode.png)
- Si la tâche est affectée à quelqu'un, les initiales de la personne sont affichées à côté du numéro de la tâche.
- Si le titre de la tâche est trop long, mettez le curseur de la souris au-dessus de la tâche pour voir une boite flottante avec le titre entier.
-Mode Déplié
--------------
-
-![Tâches dépliées](http://kanboard.net/screenshots/documentation/board-expanded-mode.png)
+Mode déplié
+-----------
+![Tâches dépliées](screenshots/board-expanded-mode.png)
diff --git a/doc/fr/board-horizontal-scrolling-and-compact-view.markdown b/doc/fr/board-horizontal-scrolling-and-compact-view.markdown
index 51d28bbb..7ad9c23c 100644
--- a/doc/fr/board-horizontal-scrolling-and-compact-view.markdown
+++ b/doc/fr/board-horizontal-scrolling-and-compact-view.markdown
@@ -1,13 +1,11 @@
Défilement horizontal et mode compact
=====================================
-Lorsque le tableau ne tient pas dans votre écran, une barre de défilement horizontal appaîtra au bas de l'écran.
+Lorsque le tableau ne loge pas dans votre écran, une barre de défilement horizontal appaîtra en bas de l'écran.
Cependant, il est possible de basculer vers la vue compacte pour afficher toutes les colonnes dans votre écran.
-![Tableau en mode compact](http://kanboard.net/screenshots/documentation/board-compact-mode.png)
+![Tableau en mode compact](screenshots/board-compact-mode.png)
-Basculer entre le défilement horizontal et la vue compacte s'effectue avec le raccourci clavier **"c"** ou en utilisant le menu déroulant sur la gauche.
-
-Note : Il est possible que du texte se superpose dans le mode compact, ceci sera amélioré dans les prochaines versions.
+Basculer entre le défilement horizontal et la vue compacte s'effectue avec le raccourci clavier **« c »** ou en utilisant le menu déroulant sur la gauche.
diff --git a/doc/fr/board-show-hide-columns.markdown b/doc/fr/board-show-hide-columns.markdown
index e1e02c6b..8eac0b2c 100644
--- a/doc/fr/board-show-hide-columns.markdown
+++ b/doc/fr/board-show-hide-columns.markdown
@@ -1,12 +1,12 @@
Afficher ou cacher des colonnes dans le tableau
-==================================
+===============================================
Vous pouvez très facilement cacher ou afficher des colonnes dans le tableau :
-![Tableau avec des colonnes cachées](http://kanboard.net/screenshots/documentation/board-hide-show-column.png)
+![Cacher une colonne](screenshots/hide-column.png)
-- Pour cacher une colonne, cliquez juste sur le titre de la colonne
-- Pour afficher une colonne cachée, cliquez sur le titre vertical
+Pour cacher une colonne, ouvrez le menu déroulant de la colonne.
-Lorsqu'une colonne est cachée, le nombre de tâches est affiché en haut de la colonne.
+![Afficher une colonne](screenshots/show-column.png)
+Pour afficher de nouveau la colonne, cliquez sur l'icône avec le « plus ».
diff --git a/doc/fr/captures/kanboard-acces-prive.png b/doc/fr/captures/kanboard-acces-prive.png
deleted file mode 100644
index 8528ef4c..00000000
--- a/doc/fr/captures/kanboard-acces-prive.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-acces-public.png b/doc/fr/captures/kanboard-acces-public.png
deleted file mode 100644
index e7ff5531..00000000
--- a/doc/fr/captures/kanboard-acces-public.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-board.png b/doc/fr/captures/kanboard-board.png
deleted file mode 100644
index 9e81e48e..00000000
--- a/doc/fr/captures/kanboard-board.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-creer-utilisateur.png b/doc/fr/captures/kanboard-creer-utilisateur.png
deleted file mode 100644
index 37a72311..00000000
--- a/doc/fr/captures/kanboard-creer-utilisateur.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-nouveauprojet-prive.png b/doc/fr/captures/kanboard-nouveauprojet-prive.png
deleted file mode 100644
index 1523a9d8..00000000
--- a/doc/fr/captures/kanboard-nouveauprojet-prive.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-nouveauprojet.png b/doc/fr/captures/kanboard-nouveauprojet.png
deleted file mode 100644
index 6ee30681..00000000
--- a/doc/fr/captures/kanboard-nouveauprojet.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-swimlanes.png b/doc/fr/captures/kanboard-swimlanes.png
deleted file mode 100644
index 06602ad9..00000000
--- a/doc/fr/captures/kanboard-swimlanes.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-vue-calendrier.png b/doc/fr/captures/kanboard-vue-calendrier.png
deleted file mode 100644
index 799add41..00000000
--- a/doc/fr/captures/kanboard-vue-calendrier.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-vue-gantt.png b/doc/fr/captures/kanboard-vue-gantt.png
deleted file mode 100644
index 445e8a87..00000000
--- a/doc/fr/captures/kanboard-vue-gantt.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/captures/kanboard-vue-liste.png b/doc/fr/captures/kanboard-vue-liste.png
deleted file mode 100644
index 41311245..00000000
--- a/doc/fr/captures/kanboard-vue-liste.png
+++ /dev/null
Binary files differ
diff --git a/doc/fr/creating-projects.markdown b/doc/fr/creating-projects.markdown
index e869e89f..e5da7cc6 100644
--- a/doc/fr/creating-projects.markdown
+++ b/doc/fr/creating-projects.markdown
@@ -7,26 +7,33 @@ Kanboard peut gérer de multiples projets. Voici deux sortes de projets :
- Les projets privés, réservés à un seul utilisateur
Créer des projets multi-utilisateurs
--------------------------------------
+------------------------------------
-- Seuls les administrateurs et administrateurs de projets peuvent créer ce type de projets
+- Seuls les administrateurs et les gestionnaires de projets peuvent créer ce type de projets
- La gestion des utilisateurs est disponible
-Depuis le tableau principal, cliquez sur le lien **Nouveau projet** :
+Depuis le tableau de bord, cliquez sur le lien **Nouveau projet** :
-![Formulaire de création de projet](captures/kanboard-nouveauprojet.png)
+![Formulaire de création de projet](screenshots/new-project.png)
C'est vraiment très simple, il vous suffit de trouver un nom pour votre projet !
Créer un projet privé
---------------------------
+---------------------
-- Tout le monde peut créer un projet privé
+- Tout le monde peut créer un projet privé (sauf si désactivé par l'administrateur)
- Il n'y a **pas** de gestion des utilisateurs
- Seuls le propriétaire et les administrateurs peuvent accéder au projet
Depuis le tableau principal, cliquez sur le lien **Nouveau projet privé**.
-![Nouveau projet privé](captures/kanboard-nouveauprojet-prive.png)
+Créer un projet depuis un autre projet
+--------------------------------------
-Remarque : les noms de projets doivent être uniques dans toute l'application.
+Lorsque vous créez un nouveau projet, vous pouvez choisir de dupliquer les propriétés d'un projet existant :
+
+- Permissions
+- Catégories
+- Actions
+- Swimlanes
+- Tâches
diff --git a/doc/fr/editing-projects.markdown b/doc/fr/editing-projects.markdown
index 67f8a0db..2186a1b9 100644
--- a/doc/fr/editing-projects.markdown
+++ b/doc/fr/editing-projects.markdown
@@ -1,11 +1,11 @@
Modifier des projets
-================
+====================
Les projets peuvent être renommés et désactivés à tout moment.
Pour renommer un projet, il suffit de cliquer sur le lien « Modifier un projet » sur la gauche.
-![Modification de projet](http://kanboard.net/screenshots/documentation/project-edition.png)
+![Modification de projet](screenshots/project-edition.png)
- Les dates de début et de fin sont utilisées pour créer le diagramme de Gantt du projet
- La description est visible en infobulle sur le tableau et sur la page qui liste les projets
@@ -13,4 +13,3 @@ Pour renommer un projet, il suffit de cliquer sur le lien « Modifier un projet
- Vous pouvez également convertir un projet multi-utilisateur en projet privé.
Remarque : quand vous rendez un projet privé, tous les utilisateurs existants auront accès au projet. Ajustez la liste des utilisateurs selon vos besoins.
-
diff --git a/doc/fr/index.markdown b/doc/fr/index.markdown
index 3f7930f3..f74c3fce 100644
--- a/doc/fr/index.markdown
+++ b/doc/fr/index.markdown
@@ -19,6 +19,7 @@ Utiliser Kanboard
### Travailler avec les projets
+- [Types de projets](project-types.markdown)
- [Créer des projets](creating-projects.markdown)
- [Modifier des projets](editing-projects.markdown)
- [Partager des tableaux et des tâches](sharing-projects.markdown)
@@ -46,6 +47,7 @@ Utiliser Kanboard
### Travailler avec les utilisateurs
+- [Rôles](roles.markdown)
- [Gestion des utilisateurs](user-management.markdown)
- [Notifications](notifications.markdown)
- [Authentification à deux facteurs](2fa.markdown)
diff --git a/doc/fr/project-permissions.markdown b/doc/fr/project-permissions.markdown
index 9e21e1ad..c4ef4df4 100644
--- a/doc/fr/project-permissions.markdown
+++ b/doc/fr/project-permissions.markdown
@@ -1,49 +1,22 @@
Permissions des projets
-===================
+=======================
-Deux sortes d'utilisateurs sont en charge d'un projet : les **gestionnaires de projet** et les **membres du projet**.
+Chaque projet est isolé des autres.
+Les accès au projet doivent être autorisés par le chef de projet.
-- Les gestionnaires de projet peuvent gérer la configuration du projet et accéder aux rapports.
-- Les membres du projet ne peuvent effectuer que des opérations de base (créer ou déplacer des tâches).
+Chaque utilisateur et chaque groupe peut avoir un rôle différent.
+Il y a 3 types de [rôles pour les projets](roles.markdown) :
-Quand vous créez un nouveau projet, le statut de gestionnaire de projet vous est automatiquement attribué.
+- Chef de projet
+- Membre du projet
+- Visualiseur
-Les administrateurs de Kanboard peuvent accéder à tout mais ils ne sont pas nécessairement gestionnaires de projet ni membres du projet. **Ces permissions sont définies au niveau du projet**.
+L'assignation des rôles est disponible depuis **Paramètres du projet > Permissions**:
-Permissions selon chaque rôle
--------------------------
+![Permissions du projet](screenshots/project-permissions.png)
-### Membres du projet
+Si vous choisissez d'autoriser tout le monde, tous les utilisateurs de Kanboard seront considérés comme **Membre du projet**.
+Ce qui signifie qu'il n'y a plus des gestion de rôles.
+Les permissions par utilisateur ou par groupe ne peuvent plus être appliquées.
-- Utiliser le tableau (créer, déplacer et modifier les tâches)
-- Supprimer seulement les tâches créées par eux-mêmes
-
-### Gestionnaires du projet
-
-- Utiliser le tableau
-- Configurer le projet
-- Partager, renommer, dupliquer et désactiver le projet
-- Gérer les swimlanes, les catégories, colonnes et utilisateurs
-- Modifier les actions automatisées
-- Exporter en CSV
-- Supprimer les tâches de n'importe quel membre du projet
-- Accéder à la section analytique
-
-Ils ne **peuvent pas supprimer un projet**.
-
-Gérer les utilisateurs et les permissions
-----------------------------
-
-Pour définir les rôles dans un projet, allez sur la page de **configuration de projet** puis cliquez sur **Gestion des utilisateurs**.
-
-### Gestion des utilisateurs
-
-![Permissions du projet](http://kanboard.net/screenshots/documentation/project-permissions.png)
-
-C'est l'endroit où vous pouvez choisir de nouveaux membres, modifier leur rôle ou interrompre l'accès d'un utilisateur.
-
-### Permission générale
-
-Si vous choisissez d'autoriser tout le monde (tous les utilisateurs de Kanboard), le projet est considéré comme public.
-
-Cela signifie qu'il n'y a plus de rôle de gestionnaire de projet. Les permissions par utilisateur ne peuvent pas s'appliquer.
+Les projets privés ne peuvent pas définir de permissions.
diff --git a/doc/fr/project-types.markdown b/doc/fr/project-types.markdown
new file mode 100644
index 00000000..70434ec8
--- /dev/null
+++ b/doc/fr/project-types.markdown
@@ -0,0 +1,14 @@
+Types de projets
+================
+
+Il y a deux types de projets :
+
+| Type | Description |
+|-------------------|-------------------------------------------------------------------------------------|
+| Projet d'équipe | La gestion des utilisateurs est activée |
+| Projet privé | Projet qui appartient à une seule personne, il n'y a pas de gestion d'utilisateurs |
+
+- Seulement les administrateurs et les gestionnaires peuvent créer des projets d'équipe.
+- Les projets privés peuvent être créé par tout le monde.
+
+[Lire la documentation à propos des rôles dans Kanboard](roles.markdown)
diff --git a/doc/fr/project-views.markdown b/doc/fr/project-views.markdown
index 0f893d44..603108f6 100644
--- a/doc/fr/project-views.markdown
+++ b/doc/fr/project-views.markdown
@@ -1,44 +1,61 @@
Vues Tableau, Agenda et Liste
-==============================
-Pour chaque projet, les tâches peuvent être visualisées dans différentes vues : **Tableau, Agenda, Liste ou Gantt**. Chaque vue affiche le résultat filtré par le champ de recherche en haut de page. Le moteur de recherche utilise la [syntaxe avancée](search.markdown).
+=============================
+
+Pour chaque projet, les tâches peuvent être visualisées dans différentes vues : **Tableau, Agenda, Liste ou Gantt**.
+Chaque vue affiche le résultat filtré par le champ de recherche en haut de page.
+Le moteur de recherche utilise la [syntaxe avancée](search.markdown).
Vue Tableau
-----------
-![Vue Tableau](captures/kanboard-board.png)
+-----------
+
+![Vue Tableau](screenshots/board-view.png)
- Dans cette vue, il est possible de glisser-déposer facilement des tâches d'une colonne à l'autre.
-- Il est également possible d'utiliser le raccourci clavier **"v b"** pour afficher la vue Tableau.
+- Il est également possible d'utiliser le raccourci clavier **« v b »** pour afficher la vue Tableau.
- Les tâches avec une ombre ont été modifiées récemment.
-![Tableau Limite de tâches](http://kanboard.net/screenshots/documentation/board-task-limit.png)
+![Tableau Limite de tâches](screenshots/board-task-limit.png)
-Lorsque la limite de tâches est atteinte pour une colonne, l'arrière-plan devient rouge. Ce qui signifie qu'il y a trop de tâches en cours en même temps.
+Lorsque la limite de tâches est atteinte pour une colonne, l'arrière-plan devient rouge.
+Ce qui signifie qu'il y a trop de tâches en cours en même temps.
[En apprendre plus sur la configuration du Tableau](board-configuration.markdown)
-
+
Vue Agenda
---------------
+----------
-![Vue Agenda](captures/kanboard-vue-calendrier.png)
+![Vue Agenda](screenshots/calendar-view.png)
- Dans cette vue, il est possible de voir les tâches avec des dates d'échéance.
- Selon les paramètres, il est également possible de voir les tâches en cours.
-- Il est également possible d'utiliser le raccourci clavier **"v c"** pour afficher la vue Agenda.
+- Il est également possible d'utiliser le raccourci clavier **« v c »** pour afficher la vue Agenda.
- [En apprendre plus sur la configuration de l'Agenda](calendar-configuration.markdown)
Vue Liste
-----------
+---------
-![Vue liste](captures/kanboard-vue-liste.png)
+![Vue liste](screenshots/list-view.png)
- Dans cette vue, tous les résultats de votre recherche sont affichés dans un tableau.
-- Il est également possible d'utiliser le raccourci clavier **"v l"** pour afficher la vue Liste.
+- Il est également possible d'utiliser le raccourci clavier **« v l »** pour afficher la vue Liste.
Vue Gantt
-----------
+---------
-![Vue Gantt](captures/kanboard-vue-gantt.png)
+![Vue Gantt](screenshots/gantt-view.png)
- La vue Gantt affiche les tâches dans une fresque horizontale
- Le diagramme utilise la date de début et la date d'échéance pour afficher les tâches
-- Il est également possible d'utiliser le raccourci clavier **"v g"** pour afficher la vue Gantt.
+- Il est également possible d'utiliser le raccourci clavier **« v g »** pour afficher la vue Gantt.
+
+Aperçu du projet
+----------------
+
+![Aperçu du projet](screenshots/project-view.png)
+
+Ce mode permet d'afficher une vue d'ensemble du projet :
+
+- Vous pouvez voir la description du projet
+- Attacher et visualiser des pièces-jointes au projet
+- Visualiser la liste des membres
+- Voir les dernières activités du projet
diff --git a/doc/fr/roles.markdown b/doc/fr/roles.markdown
new file mode 100644
index 00000000..e55a3969
--- /dev/null
+++ b/doc/fr/roles.markdown
@@ -0,0 +1,24 @@
+Rôles des utilisateurs
+======================
+
+Rôles au niveau de l'application
+--------------------------------
+
+Chaque utilisateur possède un de ces rôles :
+
+| Rôle | Description |
+|----------------|----------------------------------------------------------------------------------------|
+| Administrateur | Accès à tout |
+| Gestionnaire | Peut créer des projets d'équipe mais ne peut pas changer les réglages de l'application |
+| Utilisateur | Peut créer des projets privés |
+
+Rôles au niveau des projets
+---------------------------
+
+Chaque membre d'un projet peut avoir un rôle différent :
+
+| Rôle | Description |
+|------------------------|----------------------------------------------------------------------|
+| Chef de projet | Peut changer les paramètres du projet, accéder aux rapports |
+| Membre du projet | Peut créer des tâches et utiliser le tableau Kanban |
+| Visualiseur de projet | Accès en lecture seule au projet |
diff --git a/doc/fr/screenshots/automatic-action-creation.png b/doc/fr/screenshots/automatic-action-creation.png
new file mode 100644
index 00000000..ad90590d
--- /dev/null
+++ b/doc/fr/screenshots/automatic-action-creation.png
Binary files differ
diff --git a/doc/fr/screenshots/board-collapsed-mode.png b/doc/fr/screenshots/board-collapsed-mode.png
new file mode 100644
index 00000000..a496faff
--- /dev/null
+++ b/doc/fr/screenshots/board-collapsed-mode.png
Binary files differ
diff --git a/doc/fr/screenshots/board-compact-mode.png b/doc/fr/screenshots/board-compact-mode.png
new file mode 100644
index 00000000..872ceae5
--- /dev/null
+++ b/doc/fr/screenshots/board-compact-mode.png
Binary files differ
diff --git a/doc/fr/screenshots/board-expanded-mode.png b/doc/fr/screenshots/board-expanded-mode.png
new file mode 100644
index 00000000..19f61451
--- /dev/null
+++ b/doc/fr/screenshots/board-expanded-mode.png
Binary files differ
diff --git a/doc/fr/screenshots/board-task-limit.png b/doc/fr/screenshots/board-task-limit.png
new file mode 100644
index 00000000..8353f33c
--- /dev/null
+++ b/doc/fr/screenshots/board-task-limit.png
Binary files differ
diff --git a/doc/fr/screenshots/board-view.png b/doc/fr/screenshots/board-view.png
new file mode 100644
index 00000000..0d1e18ea
--- /dev/null
+++ b/doc/fr/screenshots/board-view.png
Binary files differ
diff --git a/doc/fr/screenshots/calendar-view.png b/doc/fr/screenshots/calendar-view.png
new file mode 100644
index 00000000..1226162b
--- /dev/null
+++ b/doc/fr/screenshots/calendar-view.png
Binary files differ
diff --git a/doc/fr/screenshots/gantt-view.png b/doc/fr/screenshots/gantt-view.png
new file mode 100644
index 00000000..3caafa98
--- /dev/null
+++ b/doc/fr/screenshots/gantt-view.png
Binary files differ
diff --git a/doc/fr/screenshots/hide-column.png b/doc/fr/screenshots/hide-column.png
new file mode 100644
index 00000000..61015f9a
--- /dev/null
+++ b/doc/fr/screenshots/hide-column.png
Binary files differ
diff --git a/doc/fr/screenshots/list-view.png b/doc/fr/screenshots/list-view.png
new file mode 100644
index 00000000..c40e807a
--- /dev/null
+++ b/doc/fr/screenshots/list-view.png
Binary files differ
diff --git a/doc/fr/screenshots/new-project.png b/doc/fr/screenshots/new-project.png
new file mode 100644
index 00000000..42e5f196
--- /dev/null
+++ b/doc/fr/screenshots/new-project.png
Binary files differ
diff --git a/doc/fr/screenshots/new-user.png b/doc/fr/screenshots/new-user.png
new file mode 100644
index 00000000..116e9074
--- /dev/null
+++ b/doc/fr/screenshots/new-user.png
Binary files differ
diff --git a/doc/fr/screenshots/project-disable-sharing.png b/doc/fr/screenshots/project-disable-sharing.png
new file mode 100644
index 00000000..58832045
--- /dev/null
+++ b/doc/fr/screenshots/project-disable-sharing.png
Binary files differ
diff --git a/doc/fr/screenshots/project-edition.png b/doc/fr/screenshots/project-edition.png
new file mode 100644
index 00000000..ce8594fe
--- /dev/null
+++ b/doc/fr/screenshots/project-edition.png
Binary files differ
diff --git a/doc/fr/screenshots/project-enable-sharing.png b/doc/fr/screenshots/project-enable-sharing.png
new file mode 100644
index 00000000..147ccc53
--- /dev/null
+++ b/doc/fr/screenshots/project-enable-sharing.png
Binary files differ
diff --git a/doc/fr/screenshots/project-permissions.png b/doc/fr/screenshots/project-permissions.png
new file mode 100644
index 00000000..54f38690
--- /dev/null
+++ b/doc/fr/screenshots/project-permissions.png
Binary files differ
diff --git a/doc/fr/screenshots/project-view.png b/doc/fr/screenshots/project-view.png
new file mode 100644
index 00000000..ff9a7f76
--- /dev/null
+++ b/doc/fr/screenshots/project-view.png
Binary files differ
diff --git a/doc/fr/screenshots/show-column.png b/doc/fr/screenshots/show-column.png
new file mode 100644
index 00000000..51f78ac8
--- /dev/null
+++ b/doc/fr/screenshots/show-column.png
Binary files differ
diff --git a/doc/fr/screenshots/swimlane-configuration.png b/doc/fr/screenshots/swimlane-configuration.png
new file mode 100644
index 00000000..d0b25e9c
--- /dev/null
+++ b/doc/fr/screenshots/swimlane-configuration.png
Binary files differ
diff --git a/doc/fr/screenshots/swimlanes.png b/doc/fr/screenshots/swimlanes.png
new file mode 100644
index 00000000..e24a5b85
--- /dev/null
+++ b/doc/fr/screenshots/swimlanes.png
Binary files differ
diff --git a/doc/fr/sharing-projects.markdown b/doc/fr/sharing-projects.markdown
index af3d43b0..f3db3c68 100644
--- a/doc/fr/sharing-projects.markdown
+++ b/doc/fr/sharing-projects.markdown
@@ -1,5 +1,5 @@
Partager des tableaux et des tâches
-========================
+===================================
Par défaut, les tableaux sont privés, mais il est possible de rendre un tableau public.
@@ -15,11 +15,11 @@ Exemples d'utilisation :
- Afficher le tableau sur un grand écran dans votre bureau
Activer l'accès public
--------------------
+----------------------
Choisissez votre projet, puis cliquez sur « Accès public » et enfin sur le bouton « Activer l'accès public ».
-![Activer l'accès public](captures/kanboard-acces-prive.png)
+![Activer l'accès public](screenshots/project-enable-sharing.png)
Lorsque l'accès public est activé, plusieurs liens sont créés :
@@ -27,11 +27,9 @@ Lorsque l'accès public est activé, plusieurs liens sont créés :
- Lien de souscription au fil RSS
- Lien d'abonnement à iCalendar
-![Désactiver l'accès public](captures/kanboard-acces-public.png)
+![Désactiver l'accès public](screenshots/project-disable-sharing.png)
Vous pouvez désactiver l'accès public à tout moment.
À chaque fois que vous activez ou désactivez l'accès public, un nouveau jeton aléatoire est créé.
**Les liens précédents ne fonctionneront pas**.
-
-
diff --git a/doc/fr/swimlanes.markdown b/doc/fr/swimlanes.markdown
index 48772117..92b4a9fa 100644
--- a/doc/fr/swimlanes.markdown
+++ b/doc/fr/swimlanes.markdown
@@ -2,12 +2,13 @@ Swimlanes
=========
Les *swimlanes* sont des séparations horizontales de votre tableau (pensez à des « couloirs » ou « lignes d'eau » dans une piscine).
+
Par exemple, cela peut servir à séparer les sorties des différentes versions d'un logiciel, à diviser vos tâches selon différents produits, équipes ou tout autre critère de votre choix.
Tableau avec des swimlanes
---------------------
+--------------------------
-![Configuration des swimlanes](http://kanboard.net/screenshots/documentation/swimlanes.png)
+![Swimlanes](screenshots/swimlanes.png)
Gestion des swimlanes
------------------
@@ -18,7 +19,7 @@ Gestion des swimlanes
Pour configurer les swimlanes allez sur la page de **Configuration du projet** et choisissez la section **Swimlanes**.
-![Swimlanes Configuration](http://kanboard.net/screenshots/documentation/swimlanes-configuration.png)
+![Swimlanes Configuration](screenshots/swimlane-configuration.png)
À partir de cet endroit, vous pouvez ajouter une nouvelle swimlane ou renommer celle qui existe par défaut.
Vous pouvez aussi désactiver et modifier la position des diverses swimlanes.
diff --git a/doc/fr/user-management.markdown b/doc/fr/user-management.markdown
index 9fd03c43..bb9b0731 100644
--- a/doc/fr/user-management.markdown
+++ b/doc/fr/user-management.markdown
@@ -1,72 +1,26 @@
Gestion des utilisateurs
-===============
-
-Rôles au niveau de l'application
-------------------------------
-
-Kanboard utilise un système de permissions basique qui reconnaît 3 types d'utilisateurs :
-
-### Administrateur
-
-- Peut accéder à tout
-
-### Administrateur de projets
-
-- Peut créer des projets multi-utilisateurs et privés
-- Peut convertir les projets multi-utilisateurs et privés
-- Peut voir seulement ses propres projets
-- Ne peut pas modifier les paramètres de l'application
-- Ne peut pas gérer les utilisateurs
-
-### Utilisateur standard
-
-- Peut créer seulement des projets privés
-- Peut voir seulement ses propres projets
-- Ne peut pas supprimer de projets
-
-Rôles au niveau des projets
---------------------------
-
-Ces rôles sont liés aux permissions du projet.
-
-### Gestionnaire de projet
-
-- Peut gérer seulement ses propres projets
-- Peut accéder aux rapports et à la section budget
-
-### Membre du projet
-
-- Peut pratiquer toutes les opérations quotidiennes sur son projet (créer et déplacer des tâches…)
-- Ne peut pas configurer les projets
-
-Remarque : n'importe quel « utilisateur de base » peut être promu « Gestionnaire de projet » pour un projet donné, il n'est pas nécessaire d'être « Administrateur de projets ».
-
-Utilisateurs locaux et distants
-----------------------
-
-- Un utilisateur local est un compte qui utilise la base de données pour stocker ses identifiants. Les utilisateurs locaux utilisent le formulaire de connexion pour s'identifier.
-- Un utilisateur distant est un compte qui utilise un système externe pour stocker ses identifiants. Par exemple, ce peut être un compte LDAP, Github ou Google. L'authentification de ces utilisateurs peut s'effectuer ou non avec le formulaire de connexion.
+========================
Ajouter un nouvel utilisateur
---------------
+-----------------------------
Pour ajouter un nouvel utilisateur, vous devez être administrateur.
-1. Depuis le tableau de bord, allez au menu **Gestion des utilisateurs**
+1. Depuis le menu déroulant situé en haut à droite, cliquez sur **Gestion des utilisateurs**
2. Dans la partie haute vous avez un lien **Créer un utilisateur local** ou **Créer un utilisateur distant**
3. Informez les champs de saisie et enregistrez
-![Nouvel utilisateur](captures/kanboard-creer-utilisateur.png)
+![Nouvel utilisateur](screenshots/new-user.png)
Quand vous créez un **utilisateur local**, vous devez préciser au moins deux valeurs :
- **nom d'utilisateur** : c'est l'identifiant unique de votre utilisateur (login)
- **mot de passe** : le mot de passe de votre utilisateur doit comporter au moins 6 caractères
-Pour les **utilisateurs distants**, seul le nom d'utilisateur est obligatoire. Vous pouvez aussi leur associer leur compte Github ou Google si vous connaissez déjà leur id unique.
+Pour les **utilisateurs distants**, seul le nom d'utilisateur est obligatoire.
Modifier des utilisateurs
-----------
+-------------------------
Quand vous allez au menu **utilisateurs**, vous disposez d'une liste d'utilisateurs. Pour modifier un utilisateur cliquez sur le lien **Modifier**.
@@ -74,7 +28,7 @@ Quand vous allez au menu **utilisateurs**, vous disposez d'une liste d'utilisate
- vous devez être administrateur pour pouvoir modifier n'importe quel utilisateur
Supprimer des utilisateurs
-------------
+--------------------------
Depuis le menu **utilisateurs**, cliquez sur le lien **supprimer**. Ce lien n'est visible que si vous êtes administrateur.
diff --git a/doc/installation.markdown b/doc/installation.markdown
index dd4283f8..c796ac65 100644
--- a/doc/installation.markdown
+++ b/doc/installation.markdown
@@ -29,7 +29,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`
-2. `composer install`
+2. `composer install --no-dev`
3. Go to the third step just above
Note: This method will install the **current development version**, use at your own risk.
diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown
index 9b511c78..a00aba16 100644
--- a/doc/plugin-hooks.markdown
+++ b/doc/plugin-hooks.markdown
@@ -28,15 +28,6 @@ Some hooks can have only one listener:
- `$start` (DateTime)
- `$end` (DateTime)
-#### model:subtask-time-tracking:calendar:events
-
-- Override subtask time tracking events to display the calendar
-- Arguments:
- - `$user_id` (integer)
- - `$events` (array)
- - `$start` (string, ISO-8601 format)
- - `$end` (string, ISO-8601 format)
-
### Merge hooks
"Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one.
@@ -163,6 +154,7 @@ List of template hooks:
| `template:board:public:task:before-title` | Task in public board: before title |
| `template:board:public:task:after-title` | Task in public board: after title |
| `template:board:task:footer` | Task in board: footer |
+| `template:board:task:icons` | Task in board: tooltip icon |
| `template:config:sidebar` | Sidebar on settings page |
| `template:config:application ` | Application settings form |
| `template:config:integrations` | Integration page in global settings |
@@ -177,9 +169,9 @@ List of template hooks:
| `template:project:integrations` | Integration page in projects settings |
| `template:project:sidebar` | Sidebar in project settings |
| `template:project-user:sidebar` | Sidebar on project user overview page |
-| `template:task:menu` | "Actions" menu on left in different task views |
| `template:task:dropdown` | Task dropdown menu in listing pages |
| `template:task:sidebar` | Sidebar on task page |
+| `template:task:form:left-column` | Left column in task form |
| `template:task:form:right-column` | Right column in task form |
| `template:task:show:top ` | Show task page: top |
| `template:task:show:bottom` | Show task page: bottom |
diff --git a/doc/plugin-overrides.markdown b/doc/plugin-overrides.markdown
index 722b4126..96a09e47 100644
--- a/doc/plugin-overrides.markdown
+++ b/doc/plugin-overrides.markdown
@@ -34,3 +34,9 @@ $this->template->setTemplateOverride('header', 'theme:layout/header');
```
The first argument is the original template name and the second argument the template to use as replacement.
+
+You can still use the original template using the "kanboard:" prefix:
+
+```php
+<?= $this->render('kanboard:header') ?>
+```
diff --git a/doc/project-views.markdown b/doc/project-views.markdown
index b45f8178..4ed464de 100644
--- a/doc/project-views.markdown
+++ b/doc/project-views.markdown
@@ -8,13 +8,13 @@ The search engine uses the [advanced syntax](search.markdown).
Board View
----------
-![Board view](http://kanboard.net/screenshots/documentation/board-view.png)
+![Board view](screenshots/board-view.png)
- With this view you can drag and drop tasks between columns easily.
- You can also use the keyboard shortcut **"v b"** to switch to the board view.
- Tasks with a shadow are recently modified.
-![Board Task Limit](http://kanboard.net/screenshots/documentation/board-task-limit.png)
+![Board Task Limit](screenshots/board-task-limit.png)
When the task limit is reached for a column, the background becomes red. That means there are too many tasks in progress at the same time.
@@ -23,7 +23,7 @@ When the task limit is reached for a column, the background becomes red. That me
Calendar View
--------------
-![Calendar view](http://kanboard.net/screenshots/documentation/calendar-view.png)
+![Calendar view](screenshots/calendar-view.png)
- With this view you can visualize tasks with a due date.
- Depending of the settings, you can also see tasks in progress.
@@ -41,8 +41,18 @@ List View
Gantt View
----------
-![Gantt view](http://kanboard.net/screenshots/documentation/gantt-view.png)
+![Gantt view](screenshots/gantt-view.png)
- The Gantt view displays tasks on a horizontal timeline
- The start date and the due date are used to display the chart
- For quick access, use the keyboard shortcut: **v g**
+
+Project Overview
+----------------
+
+![Project overview](screenshots/project-view.png)
+
+- View the description of the project
+- Attach and upload documents to the project
+- View list of project members
+- View the last activities of the project
diff --git a/doc/roles.markdown b/doc/roles.markdown
index 1b65a8c4..181904d5 100644
--- a/doc/roles.markdown
+++ b/doc/roles.markdown
@@ -22,4 +22,3 @@ Each individual team project can assign a different role to each user and group:
| Project Manager | Can change project settings, access to the Gantt chart and reports |
| Project Member | Can create tasks and use the board |
| Project Viewer | Read-only access to the board and tasks |
-
diff --git a/doc/screenshots/automatic-action-creation.png b/doc/screenshots/automatic-action-creation.png
new file mode 100644
index 00000000..5ce35dc6
--- /dev/null
+++ b/doc/screenshots/automatic-action-creation.png
Binary files differ
diff --git a/doc/screenshots/board-collapsed-mode.png b/doc/screenshots/board-collapsed-mode.png
new file mode 100644
index 00000000..ef51f811
--- /dev/null
+++ b/doc/screenshots/board-collapsed-mode.png
Binary files differ
diff --git a/doc/screenshots/board-compact-mode.png b/doc/screenshots/board-compact-mode.png
new file mode 100644
index 00000000..b9f06f08
--- /dev/null
+++ b/doc/screenshots/board-compact-mode.png
Binary files differ
diff --git a/doc/screenshots/board-expanded-mode.png b/doc/screenshots/board-expanded-mode.png
new file mode 100644
index 00000000..727dd00f
--- /dev/null
+++ b/doc/screenshots/board-expanded-mode.png
Binary files differ
diff --git a/doc/screenshots/board-task-limit.png b/doc/screenshots/board-task-limit.png
new file mode 100644
index 00000000..87d58f21
--- /dev/null
+++ b/doc/screenshots/board-task-limit.png
Binary files differ
diff --git a/doc/screenshots/board-view.png b/doc/screenshots/board-view.png
new file mode 100644
index 00000000..ce5817a4
--- /dev/null
+++ b/doc/screenshots/board-view.png
Binary files differ
diff --git a/doc/screenshots/calendar-view.png b/doc/screenshots/calendar-view.png
new file mode 100644
index 00000000..67321c07
--- /dev/null
+++ b/doc/screenshots/calendar-view.png
Binary files differ
diff --git a/doc/screenshots/gantt-view.png b/doc/screenshots/gantt-view.png
new file mode 100644
index 00000000..084d17c5
--- /dev/null
+++ b/doc/screenshots/gantt-view.png
Binary files differ
diff --git a/doc/screenshots/hide-column.png b/doc/screenshots/hide-column.png
new file mode 100644
index 00000000..66c58f2a
--- /dev/null
+++ b/doc/screenshots/hide-column.png
Binary files differ
diff --git a/doc/screenshots/list-view.png b/doc/screenshots/list-view.png
new file mode 100644
index 00000000..0e737dfd
--- /dev/null
+++ b/doc/screenshots/list-view.png
Binary files differ
diff --git a/doc/screenshots/new-project.png b/doc/screenshots/new-project.png
new file mode 100644
index 00000000..be868a50
--- /dev/null
+++ b/doc/screenshots/new-project.png
Binary files differ
diff --git a/doc/screenshots/new-user.png b/doc/screenshots/new-user.png
new file mode 100644
index 00000000..c34fc321
--- /dev/null
+++ b/doc/screenshots/new-user.png
Binary files differ
diff --git a/doc/screenshots/project-disable-sharing.png b/doc/screenshots/project-disable-sharing.png
new file mode 100644
index 00000000..fcff064a
--- /dev/null
+++ b/doc/screenshots/project-disable-sharing.png
Binary files differ
diff --git a/doc/screenshots/project-edition.png b/doc/screenshots/project-edition.png
new file mode 100644
index 00000000..3db21116
--- /dev/null
+++ b/doc/screenshots/project-edition.png
Binary files differ
diff --git a/doc/screenshots/project-enable-sharing.png b/doc/screenshots/project-enable-sharing.png
new file mode 100644
index 00000000..d95a8f25
--- /dev/null
+++ b/doc/screenshots/project-enable-sharing.png
Binary files differ
diff --git a/doc/screenshots/project-permissions.png b/doc/screenshots/project-permissions.png
index c4450755..139833e2 100644
--- a/doc/screenshots/project-permissions.png
+++ b/doc/screenshots/project-permissions.png
Binary files differ
diff --git a/doc/screenshots/project-view.png b/doc/screenshots/project-view.png
new file mode 100644
index 00000000..98209de5
--- /dev/null
+++ b/doc/screenshots/project-view.png
Binary files differ
diff --git a/doc/screenshots/show-column.png b/doc/screenshots/show-column.png
new file mode 100644
index 00000000..c45f1dd9
--- /dev/null
+++ b/doc/screenshots/show-column.png
Binary files differ
diff --git a/doc/screenshots/swimlane-configuration.png b/doc/screenshots/swimlane-configuration.png
new file mode 100644
index 00000000..4bc13ae0
--- /dev/null
+++ b/doc/screenshots/swimlane-configuration.png
Binary files differ
diff --git a/doc/screenshots/swimlanes.png b/doc/screenshots/swimlanes.png
new file mode 100644
index 00000000..700bc020
--- /dev/null
+++ b/doc/screenshots/swimlanes.png
Binary files differ
diff --git a/doc/search.markdown b/doc/search.markdown
index 1a97a7fc..37bb8625 100644
--- a/doc/search.markdown
+++ b/doc/search.markdown
@@ -1,7 +1,8 @@
Advanced Search Syntax
======================
-Kanboard uses a simple query language for advanced search.
+Kanboard uses a simple query language for advanced search.
+You can search in tasks, comments, subtasks, links but also in the activity stream.
Example of query
----------------
@@ -12,23 +13,23 @@ This example will return all tasks assigned to me with a due date for tomorrow a
assigne:me due:tomorrow my title
```
-Search by task id or title
---------------------------
+Global search
+-------------
+
+### Search by task id or title
- Search by task id: `#123`
- Search by task id and task title: `123`
- Search by task title: anything that doesn't match any search attributes
-Search by status
-----------------
+### Search by status
Attribute: **status**
- Query to find open tasks: `status:open`
- Query to find closed tasks: `status:closed`
-Search by assignees
--------------------
+### Search by assignee
Attribute: **assignee**
@@ -38,18 +39,28 @@ Attribute: **assignee**
- Query for unassigned tasks: `assignee:nobody`
- Query for my assigned tasks: `assignee:me`
-Note: Kanboard will also search in assigned subtasks with the status todo and in progress.
+### Search by task creator
+
+Attribute: **creator**
+
+- Tasks created by myself: `creator:me`
+- Tasks created by John Doe: `creator:"John Doe"`
+- Tasks created by the user id #1: `creator:1`
+
+### Search by subtask assignee
+
+Attribute: **subtask:assignee**
+
+- Example: `subtask:assignee:"John Doe"`
-Search by color
----------------
+### Search by color
Attribute: **color**
- Query to search by color id: `color:blue`
- Query to search by color name: `color:"Deep Orange"`
-Search by the due date
-----------------------
+### Search by the due date
Attribute: **due**
@@ -69,8 +80,7 @@ Operators supported with a date:
- Greater than or equal: **due:>=2015-06-29**
- Lower than or equal: **due:<=2015-06-29**
-Search by modification date
----------------------------
+### Search by modification date
Attribute: **modified** or **updated**
@@ -80,29 +90,25 @@ There is also a filter by recently modified tasks: `modified:recently`.
This query will use the same value as the board highlight period configured in settings.
-Search by creation date
------------------------
+### Search by creation date
Attribute: **created**
Works in the same way as the modification date queries.
-Search by description
----------------------
+### Search by description
-Attribute: **description**
+Attribute: **description** or **desc**
Example: `description:"text search"`
-Search by external reference
-----------------------------
+### Search by external reference
The task reference is an external id of your task, by example a ticket number from another software.
- Find tasks with a reference: `ref:1234` or `reference:TICKET-1234`
-Search by category
-------------------
+### Search by category
Attribute: **category**
@@ -110,8 +116,7 @@ Attribute: **category**
- Find all tasks that have those categories: `category:"Bug" category:"Improvements"`
- Find tasks with no category assigned: `category:none`
-Search by project
------------------
+### Search by project
Attribute: **project**
@@ -119,28 +124,56 @@ Attribute: **project**
- Find tasks by project id: `project:23`
- Find tasks for several projects: `project:"My project A" project:"My project B"`
-Search by columns
------------------
+### Search by columns
Attribute: **column**
- Find tasks by column name: `column:"Work in progress"`
- Find tasks for several columns: `column:"Backlog" column:ready`
-Search by swim lane
--------------------
+### Search by swim-lane
Attribute: **swimlane**
-- Find tasks by swim lane: `swimlane:"Version 42"`
-- Find tasks in the default swim lane: `swimlane:default`
-- Find tasks into several swim lanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"`
+- Find tasks by swim-lane: `swimlane:"Version 42"`
+- Find tasks in the default swim-lane: `swimlane:default`
+- Find tasks into several swim-lanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"`
-Search by task link
-------------------
+### Search by task link
Attribute: **link**
- Find tasks by link name: `link:"is a milestone of"`
- Find tasks into several links: `link:"is a milestone of" link:"relates to"`
+### Search by comment
+
+Attribute: **comment**
+
+- Find comments that contains this title: `comment:"My comment message"`
+
+Activity stream search
+----------------------
+
+### Search events by task title
+
+Attribute: **title** or none (default)
+
+- Example: `title:"My task"`
+- Search by task id: `#123`
+
+### Search events by task status
+
+Attribute: **status**
+
+### Search by event creator
+
+Attribute: **creator**
+
+### Search by event creation date
+
+Attribute: **created**
+
+### Search events by project
+
+Attribute: **project**
diff --git a/doc/sharing-projects.markdown b/doc/sharing-projects.markdown
index c10b0f39..614c3230 100644
--- a/doc/sharing-projects.markdown
+++ b/doc/sharing-projects.markdown
@@ -15,11 +15,11 @@ Usage examples:
- Display the board on a large screen in your office
Enable public access
--------------------
+--------------------
Select your project, then click on "Public access" and finally click on the button "Enable public access".
-![Enable public access](http://kanboard.net/screenshots/documentation/project-enable-sharing.png)
+![Enable public access](screenshots/project-enable-sharing.png)
When the public access is enabled, a couple of links are generated:
@@ -27,7 +27,7 @@ When the public access is enabled, a couple of links are generated:
- RSS feed subscription link
- iCalendar subscription link
-![Disable public access](http://kanboard.net/screenshots/documentation/project-disable-sharing.png)
+![Disable public access](screenshots/project-disable-sharing.png)
You can also disable the public access whenever you want.
diff --git a/doc/swimlanes.markdown b/doc/swimlanes.markdown
index 3a67fb89..f76336b9 100644
--- a/doc/swimlanes.markdown
+++ b/doc/swimlanes.markdown
@@ -1,13 +1,16 @@
-Swim lanes
-==========
+Swimlanes
+=========
Swimlanes are horizontal separations in your board.
-By example, it's useful to separate software releases, divide your tasks in different products, teams or whatever you want.
+For example, it's useful to separate software releases, divide your tasks in different products, teams or whatever you want.
Board with swimlanes
--------------------
-![Swimlanes Configuration](http://kanboard.net/screenshots/documentation/swimlanes.png)
+![Swimlanes](screenshots/swimlanes.png)
+
+- You can collapse swimlanes by clicking on the icon on the left
+- The default swimlane is always shown at the top
Managing swimlanes
------------------
@@ -18,7 +21,7 @@ Managing swimlanes
To configure swimlanes go to the **project configuration page** and choose the section **Swimlanes**.
-![Swimlanes Configuration](http://kanboard.net/screenshots/documentation/swimlanes-configuration.png)
+![Swimlanes Configuration](screenshots/swimlane-configuration.png)
From there, you can add a new swimlane or rename the default one.
You can also disable and change the position of the different swimlanes.
diff --git a/doc/ubuntu-installation.markdown b/doc/ubuntu-installation.markdown
index ab4dfe7c..25ee2069 100644
--- a/doc/ubuntu-installation.markdown
+++ b/doc/ubuntu-installation.markdown
@@ -1,20 +1,35 @@
How to install Kanboard on Ubuntu?
==================================
-Ubuntu Trusty 14.04 LTS
+Ubuntu Xenial 16.04 LTS
-----------------------
Install Apache and PHP:
```bash
sudo apt-get update
-sudo apt-get install -y php5 php5-sqlite php5-gd php5-json php5-mcrypt unzip
+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
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www/html
+sudo wget http://kanboard.net/kanboard-latest.zip
+sudo unzip kanboard-latest.zip
+sudo chown -R www-data:www-data kanboard/data
+sudo rm kanboard-latest.zip
```
-In case your web server was running restart to make sure the php modules are reloaded:
+Ubuntu Trusty 14.04 LTS
+-----------------------
+
+Install Apache and PHP:
```bash
-service apache2 restart
+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
```
Install Kanboard:
@@ -27,4 +42,24 @@ sudo chown -R www-data:www-data kanboard/data
sudo rm kanboard-latest.zip
```
+Ubuntu Precise 12.04 LTS
+------------------------
+
+Install Apache and PHP:
+
+```bash
+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
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www
+sudo wget http://kanboard.net/kanboard-latest.zip
+sudo unzip kanboard-latest.zip
+sudo chown -R www-data:www-data kanboard/data
+sudo rm kanboard-latest.zip
+```
+
Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
diff --git a/doc/update.markdown b/doc/update.markdown
index 7be8a65a..12ac152d 100644
--- a/doc/update.markdown
+++ b/doc/update.markdown
@@ -27,7 +27,7 @@ From the repository (development version)
-----------------------------------------
1. `git pull`
-2. `composer install`
+2. `composer install --no-dev`
3. Login and check if everything is ok
Note: This method will install the **current development version**, use at your own risk.
diff --git a/doc/user-management.markdown b/doc/user-management.markdown
index 38033eb3..3c53db99 100644
--- a/doc/user-management.markdown
+++ b/doc/user-management.markdown
@@ -1,4 +1,4 @@
-User management
+User Management
===============
Add a new user
@@ -6,18 +6,18 @@ Add a new user
To add a new user, you must be an administrator.
-1. From the dashboard, go to the menu **User Management**
+1. From the dropdown menu in the top right corner, go to the menu **Users Management**
2. On the top, you have a link **New local user** or **New remote user**
-3. Fill out the form and save
+3. Fill the form and save
-![New user](http://kanboard.net/screenshots/documentation/new-user.png)
+![New user](screenshots/new-user.png)
When you create a **local user**, you have to specify at least those values:
- **username**: This is the unique identifier of your user (login)
- **password**: The password of your user must have at least 6 characters
-For **remote users**, only the username is mandatory. You can also pre-link Github or Google accounts if you already know their unique id.
+For **remote users**, only the username is mandatory.
Edit users
----------
diff --git a/doc/vagrant.markdown b/doc/vagrant.markdown
index d11bca84..864f37b8 100644
--- a/doc/vagrant.markdown
+++ b/doc/vagrant.markdown
@@ -1,64 +1,10 @@
Run Kanboard with Vagrant
=========================
-Vagrant is used to test Kanboard in different environments.
+You can try Kanboard with Vagrant very easily:
-Several configurations are available:
+- Clone the project from the git repository
+- Run `vagrant up`
+- You can access to the application by using the URL `http://localhost:8001/`
-- Ubuntu 14.04 LTS
-- Debian 8
-- Debian 7
-- Debian 6
-- Centos 7
-- Centos 6.7
-- Freebsd 10
-
-The installation process is not fully automated for all VM, manual configuration can be required.
-
-To use those configurations, you have to install the **last version of Virtualbox and Vagrant**.
-
-Standard boxes can be downloaded from Vagrant:
-
-```bash
-vagrant box add ubuntu/trusty64
-vagrant box add debian/jessie64
-vagrant box add debian/wheezy64
-vagrant box add bento/debian-6.0.10
-vagrant box add centos/7
-vagrant box add bento/centos-6.7
-vagrant box add freebsd/FreeBSD-10.2-STABLE
-```
-
-### Example with Ubuntu and Sqlite
-
-If you want to test Kanboard on Ubuntu with Sqlite:
-
-```bash
-vagrant up sqlite
-```
-
-The current directory is synced to the Apache document root.
-
-Composer dependencies have to be there, so if you didn't run `composer install` on your host machine you can also do it on the guest machine.
-
-Each box has its own TCP port:
-
-- ubuntu: http://localhost:8001/
-- debian8: http://localhost:8002/
-- debian7: http://localhost:8003/
-- debian6: http://localhost:8004/
-- centos7: http://localhost:8005/
-- centos6: http://localhost:8006/
-- freebsd10: http://localhost:8007/
-
-Available boxes are:
-
-- `vagrant up ubuntu`
-- `vagrant up debian8`
-- `vagrant up debian7`
-- `vagrant up debian6`
-- `vagrant up centos7`
-- `vagrant up centos6`
-- `vagrant up freebsd10`
-
-Any specific configuration has to be done manually (Postgres or Mysql).
+The virtual machine is based on Ubuntu 14.04 with PHP 5.5.
diff --git a/kanboard b/kanboard
index 5046181d..6a51c937 100755
--- a/kanboard
+++ b/kanboard
@@ -3,30 +3,34 @@
require __DIR__.'/app/common.php';
+use Kanboard\Console\ResetPasswordCommand;
+use Kanboard\Console\ResetTwoFactorCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\EventDispatcher\Event;
-use Kanboard\Console\TaskOverdueNotification;
-use Kanboard\Console\SubtaskExport;
-use Kanboard\Console\TaskExport;
-use Kanboard\Console\ProjectDailyStatsCalculation;
-use Kanboard\Console\ProjectDailyColumnStatsExport;
-use Kanboard\Console\TransitionExport;
-use Kanboard\Console\LocaleSync;
-use Kanboard\Console\LocaleComparator;
-use Kanboard\Console\TaskTrigger;
-use Kanboard\Console\Cronjob;
+use Kanboard\Console\TaskOverdueNotificationCommand;
+use Kanboard\Console\SubtaskExportCommand;
+use Kanboard\Console\TaskExportCommand;
+use Kanboard\Console\ProjectDailyStatsCalculationCommand;
+use Kanboard\Console\ProjectDailyColumnStatsExportCommand;
+use Kanboard\Console\TransitionExportCommand;
+use Kanboard\Console\LocaleSyncCommand;
+use Kanboard\Console\LocaleComparatorCommand;
+use Kanboard\Console\TaskTriggerCommand;
+use Kanboard\Console\CronjobCommand;
$container['dispatcher']->dispatch('app.bootstrap', new Event);
$application = new Application('Kanboard', APP_VERSION);
-$application->add(new TaskOverdueNotification($container));
-$application->add(new SubtaskExport($container));
-$application->add(new TaskExport($container));
-$application->add(new ProjectDailyStatsCalculation($container));
-$application->add(new ProjectDailyColumnStatsExport($container));
-$application->add(new TransitionExport($container));
-$application->add(new LocaleSync($container));
-$application->add(new LocaleComparator($container));
-$application->add(new TaskTrigger($container));
-$application->add(new Cronjob($container));
+$application->add(new TaskOverdueNotificationCommand($container));
+$application->add(new SubtaskExportCommand($container));
+$application->add(new TaskExportCommand($container));
+$application->add(new ProjectDailyStatsCalculationCommand($container));
+$application->add(new ProjectDailyColumnStatsExportCommand($container));
+$application->add(new TransitionExportCommand($container));
+$application->add(new LocaleSyncCommand($container));
+$application->add(new LocaleComparatorCommand($container));
+$application->add(new TaskTriggerCommand($container));
+$application->add(new CronjobCommand($container));
+$application->add(new ResetPasswordCommand($container));
+$application->add(new ResetTwoFactorCommand($container));
$application->run();
diff --git a/pull_request_template.md b/pull_request_template.md
new file mode 100644
index 00000000..be59deac
--- /dev/null
+++ b/pull_request_template.md
@@ -0,0 +1,8 @@
+Before to submit your pull-request:
+
+- Be sure that the unit tests pass
+- If you create a new feature, test your code, do not introduce new bugs
+- Avoid code duplication
+- Small pull-requests are easier to review and can be merged quickly
+- 1 pull-request == 1 feature/improvement
+- Non necessary features should be implemented as plugin
diff --git a/tests/units/Base.php b/tests/units/Base.php
index 6af14ba5..5125ffb9 100644
--- a/tests/units/Base.php
+++ b/tests/units/Base.php
@@ -39,6 +39,8 @@ abstract class Base extends PHPUnit_Framework_TestCase
$this->container->register(new Kanboard\ServiceProvider\ClassProvider);
$this->container->register(new Kanboard\ServiceProvider\NotificationProvider);
$this->container->register(new Kanboard\ServiceProvider\RouteProvider);
+ $this->container->register(new Kanboard\ServiceProvider\AvatarProvider);
+ $this->container->register(new Kanboard\ServiceProvider\FilterProvider);
$this->container['dispatcher'] = new TraceableEventDispatcher(
new EventDispatcher,
diff --git a/tests/units/Core/Filter/LexerBuilderTest.php b/tests/units/Core/Filter/LexerBuilderTest.php
new file mode 100644
index 00000000..ac5315bb
--- /dev/null
+++ b/tests/units/Core/Filter/LexerBuilderTest.php
@@ -0,0 +1,106 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Filter\LexerBuilder;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+
+class LexerBuilderTest extends Base
+{
+ public function testBuilderThatReturnResult()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($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 testBuilderThatReturnNothing()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($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('something')->toArray();
+
+ $this->assertCount(0, $tasks);
+ }
+
+ public function testBuilderWithEmptyInput()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($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('')->toArray();
+
+ $this->assertCount(1, $tasks);
+ }
+
+ public function testBuilderWithMultipleMatches()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $project->create(array('name' => 'Project')));
+ $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'ABC', 'owner_id' => 1)));
+ $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'DEF')));
+
+ $builder = new LexerBuilder();
+ $builder->withFilter(new TaskAssigneeFilter());
+ $builder->withFilter(new TaskTitleFilter(), true);
+ $builder->withQuery($query);
+ $tasks = $builder->build('assignee:nobody assignee:1')->toArray();
+
+ $this->assertCount(2, $tasks);
+ }
+
+ public function testClone()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $builder = new LexerBuilder();
+ $builder->withFilter(new TaskAssigneeFilter());
+ $builder->withFilter(new TaskTitleFilter());
+ $builder->withQuery($query);
+
+ $clone = clone($builder);
+ $this->assertFalse($builder === $clone);
+ $this->assertFalse($builder->build('test')->getQuery() === $clone->build('test')->getQuery());
+ }
+}
diff --git a/tests/units/Core/Filter/LexerTest.php b/tests/units/Core/Filter/LexerTest.php
new file mode 100644
index 00000000..3f3e368e
--- /dev/null
+++ b/tests/units/Core/Filter/LexerTest.php
@@ -0,0 +1,100 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Filter\Lexer;
+
+class LexerTest extends Base
+{
+ public function testTokenizeWithNoDefaultToken()
+ {
+ $lexer = new Lexer();
+ $this->assertSame(array(), $lexer->tokenize('This is Kanboard'));
+ }
+
+ public function testTokenizeWithDefaultToken()
+ {
+ $lexer = new Lexer();
+ $lexer->setDefaultToken('myDefaultToken');
+
+ $expected = array(
+ 'myDefaultToken' => array('This is Kanboard'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('This is Kanboard'));
+ }
+
+ public function testTokenizeWithCustomToken()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(assignee:)/", 'T_USER');
+
+ $expected = array(
+ 'T_USER' => array('admin'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('assignee:admin something else'));
+ }
+
+ public function testTokenizeWithCustomTokenAndDefaultToken()
+ {
+ $lexer = new Lexer();
+ $lexer->setDefaultToken('myDefaultToken');
+ $lexer->addToken("/^(assignee:)/", 'T_USER');
+
+ $expected = array(
+ 'T_USER' => array('admin'),
+ 'myDefaultToken' => array('something else'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('assignee:admin something else'));
+ }
+
+ public function testTokenizeWithQuotedString()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(assignee:)/", 'T_USER');
+
+ $expected = array(
+ 'T_USER' => array('Foo Bar'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('assignee:"Foo Bar" something else'));
+ }
+
+ public function testTokenizeWithNumber()
+ {
+ $lexer = new Lexer();
+ $lexer->setDefaultToken('myDefaultToken');
+
+ $expected = array(
+ 'myDefaultToken' => array('#123'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('#123'));
+ }
+
+ public function testTokenizeWithStringDate()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(date:)/", 'T_DATE');
+
+ $expected = array(
+ 'T_DATE' => array('today'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('date:today something else'));
+ }
+
+ public function testTokenizeWithIsoDate()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(date:)/", 'T_DATE');
+
+ $expected = array(
+ 'T_DATE' => array('<=2016-01-01'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('date:<=2016-01-01 something else'));
+ }
+}
diff --git a/tests/units/Core/Filter/OrCriteriaTest.php b/tests/units/Core/Filter/OrCriteriaTest.php
new file mode 100644
index 00000000..787d3461
--- /dev/null
+++ b/tests/units/Core/Filter/OrCriteriaTest.php
@@ -0,0 +1,58 @@
+<?php
+
+use Kanboard\Core\Filter\OrCriteria;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\User;
+
+require_once __DIR__.'/../../Base.php';
+
+class OrCriteriaTest extends Base
+{
+ public function testWithSameFilter()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $userModel = new User($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', 'project_id' => 1, 'owner_id' => 2)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $criteria = new OrCriteria();
+ $criteria->withQuery($query);
+ $criteria->withFilter(TaskAssigneeFilter::getInstance(1));
+ $criteria->withFilter(TaskAssigneeFilter::getInstance(2));
+ $criteria->apply();
+
+ $this->assertCount(2, $query->findAll());
+ }
+
+ public function testWithDifferentFilter()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $userModel = new User($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' => 'ABC', 'project_id' => 1, 'owner_id' => 2)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'DEF', 'project_id' => 1, 'owner_id' => 1)));
+
+ $criteria = new OrCriteria();
+ $criteria->withQuery($query);
+ $criteria->withFilter(TaskAssigneeFilter::getInstance(1));
+ $criteria->withFilter(TaskTitleFilter::getInstance('ABC'));
+ $criteria->apply();
+
+ $this->assertCount(2, $query->findAll());
+ }
+}
diff --git a/tests/units/Core/Http/OAuth2Test.php b/tests/units/Core/Http/OAuth2Test.php
index c68ae116..5a9c0ac1 100644
--- a/tests/units/Core/Http/OAuth2Test.php
+++ b/tests/units/Core/Http/OAuth2Test.php
@@ -10,7 +10,8 @@ class OAuth2Test extends Base
{
$oauth = new OAuth2($this->container);
$oauth->createService('A', 'B', 'C', 'D', 'E', array('f', 'g'));
- $this->assertEquals('D?response_type=code&client_id=A&redirect_uri=C&scope=f+g', $oauth->getAuthorizationUrl());
+ $state = $oauth->getState();
+ $this->assertEquals('D?response_type=code&client_id=A&redirect_uri=C&scope=f+g&state='.$state, $oauth->getAuthorizationUrl());
}
public function testAuthHeader()
@@ -27,12 +28,15 @@ class OAuth2Test extends Base
public function testAccessToken()
{
+ $oauth = new OAuth2($this->container);
+
$params = array(
'code' => 'something',
'client_id' => 'A',
'client_secret' => 'B',
'redirect_uri' => 'C',
'grant_type' => 'authorization_code',
+ 'state' => $oauth->getState(),
);
$response = json_encode(array(
@@ -46,7 +50,6 @@ class OAuth2Test extends Base
->with('E', $params, array('Accept: application/json'))
->will($this->returnValue($response));
- $oauth = new OAuth2($this->container);
$oauth->createService('A', 'B', 'C', 'D', 'E', array('f', 'g'));
$oauth->getAccessToken('something');
}
diff --git a/tests/units/Core/LexerTest.php b/tests/units/Core/LexerTest.php
deleted file mode 100644
index 55370aab..00000000
--- a/tests/units/Core/LexerTest.php
+++ /dev/null
@@ -1,468 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Core\Lexer;
-
-class LexerTest extends Base
-{
- public function testSwimlaneQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'Version 42', 'token' => 'T_STRING')),
- $lexer->tokenize('swimlane:"Version 42"')
- );
-
- $this->assertEquals(
- array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'v3', 'token' => 'T_STRING')),
- $lexer->tokenize('swimlane:v3')
- );
-
- $this->assertEquals(
- array('T_SWIMLANE' => array('v3')),
- $lexer->map($lexer->tokenize('swimlane:v3'))
- );
-
- $this->assertEquals(
- array('T_SWIMLANE' => array('Version 42', 'v3')),
- $lexer->map($lexer->tokenize('swimlane:"Version 42" swimlane:v3'))
- );
- }
-
- public function testAssigneeQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'me', 'token' => 'T_STRING')),
- $lexer->tokenize('assignee:me')
- );
-
- $this->assertEquals(
- array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'everybody', 'token' => 'T_STRING')),
- $lexer->tokenize('assignee:everybody')
- );
-
- $this->assertEquals(
- array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'nobody', 'token' => 'T_STRING')),
- $lexer->tokenize('assignee:nobody')
- );
-
- $this->assertEquals(
- array('T_ASSIGNEE' => array('nobody')),
- $lexer->map($lexer->tokenize('assignee:nobody'))
- );
-
- $this->assertEquals(
- array('T_ASSIGNEE' => array('John Doe', 'me')),
- $lexer->map($lexer->tokenize('assignee:"John Doe" assignee:me'))
- );
- }
-
- public function testColorQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'color:', 'token' => 'T_COLOR'), array('match' => 'Blue', 'token' => 'T_STRING')),
- $lexer->tokenize('color:Blue')
- );
-
- $this->assertEquals(
- array(array('match' => 'color:', 'token' => 'T_COLOR'), array('match' => 'Dark Grey', 'token' => 'T_STRING')),
- $lexer->tokenize('color:"Dark Grey"')
- );
-
- $this->assertEquals(
- array('T_COLOR' => array('Blue')),
- $lexer->map($lexer->tokenize('color:Blue'))
- );
-
- $this->assertEquals(
- array('T_COLOR' => array('Dark Grey')),
- $lexer->map($lexer->tokenize('color:"Dark Grey"'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('color: '))
- );
- }
-
- public function testCategoryQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'category:', 'token' => 'T_CATEGORY'), array('match' => 'Feature Request', 'token' => 'T_STRING')),
- $lexer->tokenize('category:"Feature Request"')
- );
-
- $this->assertEquals(
- array('T_CATEGORY' => array('Feature Request')),
- $lexer->map($lexer->tokenize('category:"Feature Request"'))
- );
-
- $this->assertEquals(
- array('T_CATEGORY' => array('Feature Request', 'Bug')),
- $lexer->map($lexer->tokenize('category:"Feature Request" category:Bug'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('category: '))
- );
- }
-
- public function testLinkQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'link:', 'token' => 'T_LINK'), array('match' => 'is a milestone of', 'token' => 'T_STRING')),
- $lexer->tokenize('link:"is a milestone of"')
- );
-
- $this->assertEquals(
- array('T_LINK' => array('is a milestone of')),
- $lexer->map($lexer->tokenize('link:"is a milestone of"'))
- );
-
- $this->assertEquals(
- array('T_LINK' => array('is a milestone of', 'fixes')),
- $lexer->map($lexer->tokenize('link:"is a milestone of" link:fixes'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('link: '))
- );
- }
-
- public function testColumnQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'column:', 'token' => 'T_COLUMN'), array('match' => 'Feature Request', 'token' => 'T_STRING')),
- $lexer->tokenize('column:"Feature Request"')
- );
-
- $this->assertEquals(
- array('T_COLUMN' => array('Feature Request')),
- $lexer->map($lexer->tokenize('column:"Feature Request"'))
- );
-
- $this->assertEquals(
- array('T_COLUMN' => array('Feature Request', 'Bug')),
- $lexer->map($lexer->tokenize('column:"Feature Request" column:Bug'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('column: '))
- );
- }
-
- public function testProjectQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'project:', 'token' => 'T_PROJECT'), array('match' => 'My project', 'token' => 'T_STRING')),
- $lexer->tokenize('project:"My project"')
- );
-
- $this->assertEquals(
- array('T_PROJECT' => array('My project')),
- $lexer->map($lexer->tokenize('project:"My project"'))
- );
-
- $this->assertEquals(
- array('T_PROJECT' => array('My project', 'plop')),
- $lexer->map($lexer->tokenize('project:"My project" project:plop'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('project: '))
- );
- }
-
- public function testStatusQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'status:', 'token' => 'T_STATUS'), array('match' => 'open', 'token' => 'T_STRING')),
- $lexer->tokenize('status:open')
- );
-
- $this->assertEquals(
- array(array('match' => 'status:', 'token' => 'T_STATUS'), array('match' => 'closed', 'token' => 'T_STRING')),
- $lexer->tokenize('status:closed')
- );
-
- $this->assertEquals(
- array('T_STATUS' => 'open'),
- $lexer->map($lexer->tokenize('status:open'))
- );
-
- $this->assertEquals(
- array('T_STATUS' => 'closed'),
- $lexer->map($lexer->tokenize('status:closed'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('status: '))
- );
- }
-
- public function testReferenceQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'ref:', 'token' => 'T_REFERENCE'), array('match' => '123', 'token' => 'T_STRING')),
- $lexer->tokenize('ref:123')
- );
-
- $this->assertEquals(
- array(array('match' => 'reference:', 'token' => 'T_REFERENCE'), array('match' => '456', 'token' => 'T_STRING')),
- $lexer->tokenize('reference:456')
- );
-
- $this->assertEquals(
- array('T_REFERENCE' => '123'),
- $lexer->map($lexer->tokenize('reference:123'))
- );
-
- $this->assertEquals(
- array('T_REFERENCE' => '456'),
- $lexer->map($lexer->tokenize('ref:456'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('ref: '))
- );
- }
-
- public function testDescriptionQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'description:', 'token' => 'T_DESCRIPTION'), array('match' => 'my text search', 'token' => 'T_STRING')),
- $lexer->tokenize('description:"my text search"')
- );
-
- $this->assertEquals(
- array('T_DESCRIPTION' => 'my text search'),
- $lexer->map($lexer->tokenize('description:"my text search"'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('description: '))
- );
- }
-
- public function testDueDateQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '<2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:<2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '>2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:>2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '<=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:<=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '>=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:>=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => 'yesterday', 'token' => 'T_DATE')),
- $lexer->tokenize('due:yesterday')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => 'tomorrow', 'token' => 'T_DATE')),
- $lexer->tokenize('due:tomorrow')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('due:#2015-05-01')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('due:01-05-1024')
- );
-
- $this->assertEquals(
- array('T_DUE' => '2015-05-01'),
- $lexer->map($lexer->tokenize('due:2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_DUE' => '<2015-05-01'),
- $lexer->map($lexer->tokenize('due:<2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_DUE' => 'today'),
- $lexer->map($lexer->tokenize('due:today'))
- );
- }
-
- public function testModifiedQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('modified:2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '<2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('modified:<2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '>2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('modified:>2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => '<=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:<=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => '>=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:>=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => 'yesterday', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:yesterday')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => 'tomorrow', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:tomorrow')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('updated:#2015-05-01')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('modified:01-05-1024')
- );
-
- $this->assertEquals(
- array('T_UPDATED' => '2015-05-01'),
- $lexer->map($lexer->tokenize('modified:2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_UPDATED' => '<2015-05-01'),
- $lexer->map($lexer->tokenize('modified:<2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_UPDATED' => 'today'),
- $lexer->map($lexer->tokenize('modified:today'))
- );
- }
-
- public function testMultipleCriterias()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array('T_COLOR' => array('Dark Grey'), 'T_ASSIGNEE' => array('Fred G'), 'T_TITLE' => 'my task title'),
- $lexer->map($lexer->tokenize('color:"Dark Grey" assignee:"Fred G" my task title'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow')),
- $lexer->map($lexer->tokenize('my title color:yellow'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_DUE' => '2015-04-01'),
- $lexer->map($lexer->tokenize('my title due:2015-04-01'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'awesome', 'T_DUE' => '<=2015-04-01'),
- $lexer->map($lexer->tokenize('due:<=2015-04-01 awesome'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'awesome', 'T_DUE' => 'today'),
- $lexer->map($lexer->tokenize('due:today awesome'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow'), 'T_DUE' => '2015-04-01'),
- $lexer->map($lexer->tokenize('my title color:yellow due:2015-04-01'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow'), 'T_DUE' => '2015-04-01', 'T_ASSIGNEE' => array('John Doe')),
- $lexer->map($lexer->tokenize('my title color:yellow due:2015-04-01 assignee:"John Doe"'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title'),
- $lexer->map($lexer->tokenize('my title color:'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title'),
- $lexer->map($lexer->tokenize('my title color:assignee:'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title'),
- $lexer->map($lexer->tokenize('my title '))
- );
-
- $this->assertEquals(
- array('T_TITLE' => '#123'),
- $lexer->map($lexer->tokenize('#123'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('color:assignee:'))
- );
- }
-}
diff --git a/tests/units/Core/TemplateTest.php b/tests/units/Core/TemplateTest.php
index bd476c51..9584c831 100644
--- a/tests/units/Core/TemplateTest.php
+++ b/tests/units/Core/TemplateTest.php
@@ -8,35 +8,41 @@ class TemplateTest extends Base
{
public function testGetTemplateFile()
{
- $t = new Template($this->container['helper']);
+ $template = new Template($this->container['helper']);
+
+ $this->assertStringEndsWith(
+ implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'a', 'b.php')),
+ $template->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b')
+ );
+
$this->assertStringEndsWith(
- 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'a'.DIRECTORY_SEPARATOR.'b.php',
- $t->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b')
+ implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'a', 'b.php')),
+ $template->getTemplateFile('kanboard:a'.DIRECTORY_SEPARATOR.'b')
);
}
public function testGetPluginTemplateFile()
{
- $t = new Template($this->container['helper']);
+ $template = new Template($this->container['helper']);
$this->assertStringEndsWith(
- 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'.DIRECTORY_SEPARATOR.'Myplugin'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'a'.DIRECTORY_SEPARATOR.'b.php',
- $t->getTemplateFile('myplugin:a'.DIRECTORY_SEPARATOR.'b')
+ implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', '..', 'plugins', 'Myplugin', 'Template', 'a', 'b.php')),
+ $template->getTemplateFile('myplugin:a'.DIRECTORY_SEPARATOR.'b')
);
}
public function testGetOverridedTemplateFile()
{
- $t = new Template($this->container['helper']);
- $t->setTemplateOverride('a'.DIRECTORY_SEPARATOR.'b', 'myplugin:c');
+ $template = new Template($this->container['helper']);
+ $template->setTemplateOverride('a'.DIRECTORY_SEPARATOR.'b', 'myplugin:c');
$this->assertStringEndsWith(
- 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'.DIRECTORY_SEPARATOR.'Myplugin'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'c.php',
- $t->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b')
+ implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', '..', 'plugins', 'Myplugin', 'Template', 'c.php')),
+ $template->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b')
);
$this->assertStringEndsWith(
- 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'d.php',
- $t->getTemplateFile('d')
+ implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'd.php')),
+ $template->getTemplateFile('d')
);
}
}
diff --git a/tests/units/Filter/ProjectActivityCreationDateFilterTest.php b/tests/units/Filter/ProjectActivityCreationDateFilterTest.php
new file mode 100644
index 00000000..d679f285
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityCreationDateFilterTest.php
@@ -0,0 +1,117 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityCreationDateFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityCreationDateFilterTest extends Base
+{
+ public function testWithToday()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('today');
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithYesterday()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('yesterday');
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+ }
+
+ public function testWithIsoDate()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter(date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithOperatorAndIsoDate()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('<'.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('>'.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityCreatorFilterTest.php b/tests/units/Filter/ProjectActivityCreatorFilterTest.php
new file mode 100644
index 00000000..99c70322
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityCreatorFilterTest.php
@@ -0,0 +1,91 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityCreatorFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityCreatorFilterTest extends Base
+{
+ public function testWithUsername()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('admin');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithAnotherUsername()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('John Doe');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+ }
+
+ public function testWithCurrentUser()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('me');
+ $filter->setCurrentUserId(1);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithAnotherCurrentUser()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('me');
+ $filter->setCurrentUserId(2);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityProjectIdFilterTest.php b/tests/units/Filter/ProjectActivityProjectIdFilterTest.php
new file mode 100644
index 00000000..193852e1
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityProjectIdFilterTest.php
@@ -0,0 +1,35 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityProjectIdFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityProjectIdFilterTest extends Base
+{
+ public function testFilterByProjectId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityProjectIdFilter(1);
+ $filter->withQuery($query)->apply();
+ $this->assertCount(1, $query->findAll());
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityProjectIdsFilterTest.php b/tests/units/Filter/ProjectActivityProjectIdsFilterTest.php
new file mode 100644
index 00000000..e99d2e2f
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityProjectIdsFilterTest.php
@@ -0,0 +1,63 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityProjectIdsFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityProjectIdsFilterTest extends Base
+{
+ public function testFilterByProjectIds()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+ $this->assertEquals(3, $projectModel->create(array('name' => 'P3')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2)));
+ $this->assertEquals(3, $taskCreation->create(array('title' => 'Test', 'project_id' => 3)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+ $this->assertNotFalse($projectActivityModel->createEvent(3, 3, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(3))));
+
+ $filter = new ProjectActivityProjectIdsFilter(array(1, 2));
+ $filter->withQuery($query)->apply();
+ $this->assertCount(2, $query->findAll());
+ }
+
+ public function testWithEmptyArgument()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+ $this->assertEquals(3, $projectModel->create(array('name' => 'P3')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2)));
+ $this->assertEquals(3, $taskCreation->create(array('title' => 'Test', 'project_id' => 3)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, $taskFinder->getById(1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, $taskFinder->getById(2)));
+ $this->assertNotFalse($projectActivityModel->createEvent(3, 3, 1, Task::EVENT_CREATE, $taskFinder->getById(3)));
+
+ $filter = new ProjectActivityProjectIdsFilter(array());
+ $filter->withQuery($query)->apply();
+ $this->assertCount(0, $query->findAll());
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityProjectNameFilterTest.php b/tests/units/Filter/ProjectActivityProjectNameFilterTest.php
new file mode 100644
index 00000000..de9d7d59
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityProjectNameFilterTest.php
@@ -0,0 +1,35 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityProjectNameFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityProjectNameFilterTest extends Base
+{
+ public function testFilterByProjectName()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityProjectNameFilter('P1');
+ $filter->withQuery($query)->apply();
+ $this->assertCount(1, $query->findAll());
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityTaskIdFilterTest.php b/tests/units/Filter/ProjectActivityTaskIdFilterTest.php
new file mode 100644
index 00000000..646cab1b
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityTaskIdFilterTest.php
@@ -0,0 +1,34 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityTaskIdFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityTaskIdFilterTest extends Base
+{
+ public function testFilterByTaskId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityTaskIdFilter(1);
+ $filter->withQuery($query)->apply();
+ $this->assertCount(1, $query->findAll());
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php b/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php
new file mode 100644
index 00000000..b8df6338
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php
@@ -0,0 +1,49 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityTaskStatusFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+use Kanboard\Model\TaskStatus;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityTaskStatusFilterTest extends Base
+{
+ public function testFilterByTaskStatus()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskStatus = new TaskStatus($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $this->assertTrue($taskStatus->close(1));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityTaskStatusFilter('open');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ $this->assertEquals(2, $events[0]['task_id']);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityTaskStatusFilter('closed');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ $this->assertEquals(1, $events[0]['task_id']);
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php
new file mode 100644
index 00000000..925a1ab2
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php
@@ -0,0 +1,79 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityTaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityTaskTitleFilterTest extends Base
+{
+ public function testWithFullTitle()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityTaskTitleFilter('test2');
+ $filter->withQuery($query)->apply();
+ $this->assertCount(1, $query->findAll());
+ }
+
+ public function testWithPartialTitle()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityTaskTitleFilter('test');
+ $filter->withQuery($query)->apply();
+ $this->assertCount(2, $query->findAll());
+ }
+
+ public function testWithId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityTaskTitleFilter('#2');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ $this->assertEquals(2, $events[0]['task_id']);
+ }
+}
diff --git a/tests/units/Filter/TaskAssigneeFilterTest.php b/tests/units/Filter/TaskAssigneeFilterTest.php
new file mode 100644
index 00000000..356342c5
--- /dev/null
+++ b/tests/units/Filter/TaskAssigneeFilterTest.php
@@ -0,0 +1,159 @@
+<?php
+
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\User;
+
+require_once __DIR__.'/../Base.php';
+
+class TaskAssigneeFilterTest extends Base
+{
+ public function testWithIntegerAssigneeId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue(1);
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue(123);
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithStringAssigneeId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('1');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue("123");
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithUsername()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('admin');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('foobar');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithName()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $userModel = new User($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', 'project_id' => 1, 'owner_id' => 2)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('foo bar');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('bob');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithNobody()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($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 TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('nobody');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+ }
+
+ public function testWithCurrentUser()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->setCurrentUserId(1);
+ $filter->withQuery($query);
+ $filter->withValue('me');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->setCurrentUserId(2);
+ $filter->withQuery($query);
+ $filter->withValue('me');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+}
diff --git a/tests/units/Filter/TaskCommentFilterTest.php b/tests/units/Filter/TaskCommentFilterTest.php
new file mode 100644
index 00000000..8d1b7f44
--- /dev/null
+++ b/tests/units/Filter/TaskCommentFilterTest.php
@@ -0,0 +1,52 @@
+<?php
+
+use Kanboard\Filter\TaskCommentFilter;
+use Kanboard\Model\Comment;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+
+require_once __DIR__.'/../Base.php';
+
+class TaskCommentFilterTest extends Base
+{
+ public function testMatch()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $commentModel = new Comment($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'user_id' => 1, 'comment' => 'This is a test')));
+
+ $filter = new TaskCommentFilter();
+ $filter->withQuery($query);
+ $filter->withValue('test');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+ }
+
+ public function testNoMatch()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $commentModel = new Comment($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'user_id' => 1, 'comment' => 'This is a test')));
+
+ $filter = new TaskCommentFilter();
+ $filter->withQuery($query);
+ $filter->withValue('foobar');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+}
diff --git a/tests/units/Filter/TaskCreatorFilterTest.php b/tests/units/Filter/TaskCreatorFilterTest.php
new file mode 100644
index 00000000..1c344de7
--- /dev/null
+++ b/tests/units/Filter/TaskCreatorFilterTest.php
@@ -0,0 +1,159 @@
+<?php
+
+use Kanboard\Filter\TaskCreatorFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\User;
+
+require_once __DIR__.'/../Base.php';
+
+class TaskCreatorFilterTest extends Base
+{
+ public function testWithIntegerAssigneeId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1)));
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue(1);
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue(123);
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithStringAssigneeId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1)));
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue('1');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue("123");
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithUsername()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1)));
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue('admin');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue('foobar');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithName()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $userModel = new User($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', 'project_id' => 1, 'creator_id' => 2)));
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue('foo bar');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue('bob');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithNobody()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($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 TaskCreatorFilter();
+ $filter->withQuery($query);
+ $filter->withValue('nobody');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+ }
+
+ public function testWithCurrentUser()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1)));
+
+ $filter = new TaskCreatorFilter();
+ $filter->setCurrentUserId(1);
+ $filter->withQuery($query);
+ $filter->withValue('me');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskCreatorFilter();
+ $filter->setCurrentUserId(2);
+ $filter->withQuery($query);
+ $filter->withValue('me');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+}
diff --git a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php b/tests/units/Formatter/TaskFilterCalendarFormatterTest.php
deleted file mode 100644
index 09dd0de6..00000000
--- a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Formatter\TaskFilterCalendarFormatter;
-
-class TaskFilterCalendarFormatterTest extends Base
-{
- public function testCopy()
- {
- $tf = new TaskFilterCalendarFormatter($this->container);
- $filter1 = $tf->create()->setFullDay();
- $filter2 = $tf->copy();
-
- $this->assertTrue($filter1 !== $filter2);
- $this->assertTrue($filter1->query !== $filter2->query);
- $this->assertTrue($filter1->query->condition !== $filter2->query->condition);
- $this->assertTrue($filter1->isFullDay());
- $this->assertFalse($filter2->isFullDay());
- }
-}
diff --git a/tests/units/Formatter/TaskFilterGanttFormatterTest.php b/tests/units/Formatter/TaskFilterGanttFormatterTest.php
deleted file mode 100644
index 14804784..00000000
--- a/tests/units/Formatter/TaskFilterGanttFormatterTest.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Formatter\TaskFilterGanttFormatter;
-use Kanboard\Model\Project;
-use Kanboard\Model\TaskCreation;
-use Kanboard\Core\DateParser;
-
-class TaskFilterGanttFormatterTest extends Base
-{
- public function testFormat()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilterGanttFormatter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
-
- $this->assertNotEmpty($tf->search('status:open')->format());
- }
-}
diff --git a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php b/tests/units/Formatter/TaskFilterICalendarFormatterTest.php
deleted file mode 100644
index 6de9cf0f..00000000
--- a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Eluceo\iCal\Component\Calendar;
-use Kanboard\Formatter\TaskFilterICalendarFormatter;
-use Kanboard\Model\Project;
-use Kanboard\Model\User;
-use Kanboard\Model\TaskCreation;
-use Kanboard\Core\DateParser;
-use Kanboard\Model\Config;
-
-class TaskFilterICalendarFormatterTest extends Base
-{
- public function testIcalEventsWithCreatorAndDueDate()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilterICalendarFormatter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'creator_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
-
- $ics = $tf->create()
- ->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month'))
- ->setFullDay()
- ->setCalendar(new Calendar('Kanboard'))
- ->setColumns('date_due')
- ->addFullDayEvents()
- ->format();
-
- $this->assertContains('UID:task-#1-date_due', $ics);
- $this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics);
- $this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics);
- $this->assertContains('URL:http://localhost/?controller=task&action=show&task_id=1&project_id=1', $ics);
- $this->assertContains('SUMMARY:#1 task1', $ics);
- $this->assertContains('ATTENDEE:MAILTO:admin@kanboard.local', $ics);
- $this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics);
- }
-
- public function testIcalEventsWithAssigneeAndDueDate()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilterICalendarFormatter($this->container);
- $u = new User($this->container);
- $c = new Config($this->container);
-
- $this->assertNotFalse($c->save(array('application_url' => 'http://kb/')));
- $this->assertEquals('http://kb/', $c->get('application_url'));
-
- $this->assertNotFalse($u->update(array('id' => 1, 'email' => 'bob@localhost')));
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('+5 days'))));
-
- $ics = $tf->create()
- ->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month'))
- ->setFullDay()
- ->setCalendar(new Calendar('Kanboard'))
- ->setColumns('date_due')
- ->addFullDayEvents()
- ->format();
-
- $this->assertContains('UID:task-#1-date_due', $ics);
- $this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics);
- $this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics);
- $this->assertContains('URL:http://kb/?controller=task&action=show&task_id=1&project_id=1', $ics);
- $this->assertContains('SUMMARY:#1 task1', $ics);
- $this->assertContains('ORGANIZER;CN=admin:MAILTO:bob@localhost', $ics);
- $this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics);
- }
-}
diff --git a/tests/units/Helper/ProjectActivityHelperTest.php b/tests/units/Helper/ProjectActivityHelperTest.php
new file mode 100644
index 00000000..88b2d352
--- /dev/null
+++ b/tests/units/Helper/ProjectActivityHelperTest.php
@@ -0,0 +1,97 @@
+<?php
+
+use Kanboard\Helper\ProjectActivityHelper;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\Task;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityHelperTest extends Base
+{
+ public function testGetProjectEvents()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(3, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 3, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(3))));
+
+ $helper = new ProjectActivityHelper($this->container);
+ $events = $helper->getProjectEvents(1);
+
+ $this->assertCount(3, $events);
+ $this->assertEquals(3, $events[0]['task_id']);
+ $this->assertNotEmpty($events[0]['event_content']);
+ $this->assertNotEmpty($events[0]['event_title']);
+ $this->assertNotEmpty($events[0]['author']);
+ $this->assertInternalType('array', $events[0]['task']);
+ }
+
+ public function testGetProjectsEvents()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+ $this->assertEquals(3, $projectModel->create(array('name' => 'P3')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2)));
+ $this->assertEquals(3, $taskCreation->create(array('title' => 'Test', 'project_id' => 3)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+ $this->assertNotFalse($projectActivityModel->createEvent(3, 3, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(3))));
+
+ $helper = new ProjectActivityHelper($this->container);
+ $events = $helper->getProjectsEvents(array(1, 2));
+
+ $this->assertCount(2, $events);
+ $this->assertEquals(2, $events[0]['task_id']);
+ $this->assertNotEmpty($events[0]['event_content']);
+ $this->assertNotEmpty($events[0]['event_title']);
+ $this->assertNotEmpty($events[0]['author']);
+ $this->assertInternalType('array', $events[0]['task']);
+ }
+
+ public function testGetTaskEvents()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $helper = new ProjectActivityHelper($this->container);
+ $events = $helper->getTaskEvents(1);
+
+ $this->assertCount(1, $events);
+ $this->assertEquals(1, $events[0]['task_id']);
+ $this->assertNotEmpty($events[0]['event_content']);
+ $this->assertNotEmpty($events[0]['event_title']);
+ $this->assertNotEmpty($events[0]['author']);
+ $this->assertInternalType('array', $events[0]['task']);
+ }
+}
diff --git a/tests/units/Helper/UserHelperTest.php b/tests/units/Helper/UserHelperTest.php
index 7ee6e8bb..9a9832b2 100644
--- a/tests/units/Helper/UserHelperTest.php
+++ b/tests/units/Helper/UserHelperTest.php
@@ -15,6 +15,7 @@ class UserHelperTest extends Base
$helper = new UserHelper($this->container);
$this->assertEquals('CN', $helper->getInitials('chuck norris'));
+ $this->assertEquals('CN', $helper->getInitials('chuck norris #2'));
$this->assertEquals('A', $helper->getInitials('admin'));
}
diff --git a/tests/units/Model/ProjectActivityTest.php b/tests/units/Model/ProjectActivityTest.php
index 27ea039d..a624cd86 100644
--- a/tests/units/Model/ProjectActivityTest.php
+++ b/tests/units/Model/ProjectActivityTest.php
@@ -10,90 +10,51 @@ use Kanboard\Model\Project;
class ProjectActivityTest extends Base
{
- public function testDecode()
- {
- $e = new ProjectActivity($this->container);
- $input = array('test');
- $serialized = serialize($input);
- $json = json_encode($input);
-
- $this->assertEquals($input, $e->decode($serialized));
- $this->assertEquals($input, $e->decode($json));
- }
-
public function testCreation()
{
- $e = new ProjectActivity($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFinder($this->container);
- $p = new Project($this->container);
+ $projectActivity = new ProjectActivity($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($this->container);
+ $projectModel = new Project($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
- $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1)));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Task #2', 'project_id' => 1)));
- $this->assertTrue($e->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $tf->getbyId(1))));
- $this->assertTrue($e->createEvent(1, 2, 1, Task::EVENT_UPDATE, array('task' => $tf->getById(2))));
- $this->assertFalse($e->createEvent(1, 1, 0, Task::EVENT_OPEN, array('task' => $tf->getbyId(1))));
+ $this->assertTrue($projectActivity->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $taskFinder->getbyId(1))));
+ $this->assertTrue($projectActivity->createEvent(1, 2, 1, Task::EVENT_UPDATE, array('task' => $taskFinder->getById(2))));
+ $this->assertFalse($projectActivity->createEvent(1, 1, 0, Task::EVENT_OPEN, array('task' => $taskFinder->getbyId(1))));
- $events = $e->getProject(1);
+ $events = $projectActivity->getQuery()->desc('id')->findAll();
- $this->assertNotEmpty($events);
- $this->assertTrue(is_array($events));
- $this->assertEquals(2, count($events));
+ $this->assertCount(2, $events);
$this->assertEquals(time(), $events[0]['date_creation'], '', 1);
$this->assertEquals(Task::EVENT_UPDATE, $events[0]['event_name']);
$this->assertEquals(Task::EVENT_CLOSE, $events[1]['event_name']);
}
- public function testFetchAllContent()
- {
- $e = new ProjectActivity($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFinder($this->container);
- $p = new Project($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
-
- $nb_events = 80;
-
- for ($i = 0; $i < $nb_events; $i++) {
- $this->assertTrue($e->createEvent(1, 1, 1, Task::EVENT_UPDATE, array('task' => $tf->getbyId(1))));
- }
-
- $events = $e->getProject(1);
-
- $this->assertNotEmpty($events);
- $this->assertTrue(is_array($events));
- $this->assertEquals(50, count($events));
- $this->assertEquals('admin', $events[0]['author']);
- $this->assertNotEmpty($events[0]['event_title']);
- $this->assertNotEmpty($events[0]['event_content']);
- }
-
public function testCleanup()
{
- $e = new ProjectActivity($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFinder($this->container);
- $p = new Project($this->container);
+ $projectActivity = new ProjectActivity($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($this->container);
+ $projectModel = new Project($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Task #1', 'project_id' => 1)));
$max = 15;
$nb_events = 100;
- $task = $tf->getbyId(1);
+ $task = $taskFinder->getbyId(1);
for ($i = 0; $i < $nb_events; $i++) {
- $this->assertTrue($e->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $task)));
+ $this->assertTrue($projectActivity->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $task)));
}
$this->assertEquals($nb_events, $this->container['db']->table('project_activities')->count());
- $e->cleanup($max);
+ $projectActivity->cleanup($max);
- $events = $e->getProject(1);
+ $events = $projectActivity->getQuery()->desc('id')->findAll();
$this->assertNotEmpty($events);
$this->assertCount($max, $events);
diff --git a/tests/units/Model/ProjectFileTest.php b/tests/units/Model/ProjectFileTest.php
index d9b37fbe..0d7a9377 100644
--- a/tests/units/Model/ProjectFileTest.php
+++ b/tests/units/Model/ProjectFileTest.php
@@ -278,7 +278,7 @@ class ProjectFileTest extends Base
$fileModel = $this
->getMockBuilder('\Kanboard\Model\ProjectFile')
->setConstructorArgs(array($this->container))
- ->setMethods(array('generateThumbnailFromFile'))
+ ->setMethods(array('generateThumbnailFromData'))
->getMock();
$projectModel = new Project($this->container);
@@ -288,7 +288,7 @@ class ProjectFileTest extends Base
$fileModel
->expects($this->once())
- ->method('generateThumbnailFromFile');
+ ->method('generateThumbnailFromData');
$this->container['objectStorage']
->expects($this->once())
diff --git a/tests/units/Model/SubtaskTimeTrackingTest.php b/tests/units/Model/SubtaskTimeTrackingTest.php
index 9fa8d5b0..2545dcb2 100644
--- a/tests/units/Model/SubtaskTimeTrackingTest.php
+++ b/tests/units/Model/SubtaskTimeTrackingTest.php
@@ -240,81 +240,4 @@ class SubtaskTimeTrackingTest extends Base
$this->assertEquals(0, $task['time_estimated']);
$this->assertEquals(0, $task['time_spent']);
}
-
- public function testGetCalendarEvents()
- {
- $tf = new TaskFinder($this->container);
- $tc = new TaskCreation($this->container);
- $s = new Subtask($this->container);
- $st = new SubtaskTimeTracking($this->container);
- $p = new Project($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1)));
- $this->assertEquals(2, $tc->create(array('title' => 'test 1', 'project_id' => 2)));
-
- $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1)));
- $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1)));
- $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 1)));
-
- $this->assertEquals(4, $s->create(array('title' => 'subtask #4', 'task_id' => 2)));
- $this->assertEquals(5, $s->create(array('title' => 'subtask #5', 'task_id' => 2)));
- $this->assertEquals(6, $s->create(array('title' => 'subtask #6', 'task_id' => 2)));
- $this->assertEquals(7, $s->create(array('title' => 'subtask #7', 'task_id' => 2)));
- $this->assertEquals(8, $s->create(array('title' => 'subtask #8', 'task_id' => 2)));
-
- // Slot start before and finish inside the calendar time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 1, 'start' => strtotime('-1 day'), 'end' => strtotime('+1 hour')));
-
- // Slot start inside time range and finish after the time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 2, 'start' => strtotime('+1 hour'), 'end' => strtotime('+2 days')));
-
- // Start before time range and finish inside time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 3, 'start' => strtotime('-1 day'), 'end' => strtotime('+1.5 days')));
-
- // Start and finish inside time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 4, 'start' => strtotime('+1 hour'), 'end' => strtotime('+2 hours')));
-
- // Start and finish after the time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 5, 'start' => strtotime('+2 days'), 'end' => strtotime('+3 days')));
-
- // Start and finish before the time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 6, 'start' => strtotime('-2 days'), 'end' => strtotime('-1 day')));
-
- // Start before time range and not finished
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 7, 'start' => strtotime('-1 day')));
-
- // Start inside time range and not finish
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 8, 'start' => strtotime('+3200 seconds')));
-
- $timesheet = $st->getUserTimesheet(1);
- $this->assertNotEmpty($timesheet);
- $this->assertCount(8, $timesheet);
-
- $events = $st->getUserCalendarEvents(1, date('Y-m-d'), date('Y-m-d', strtotime('+2 day')));
- $this->assertNotEmpty($events);
- $this->assertCount(6, $events);
- $this->assertEquals(1, $events[0]['subtask_id']);
- $this->assertEquals(2, $events[1]['subtask_id']);
- $this->assertEquals(3, $events[2]['subtask_id']);
- $this->assertEquals(4, $events[3]['subtask_id']);
- $this->assertEquals(7, $events[4]['subtask_id']);
- $this->assertEquals(8, $events[5]['subtask_id']);
-
- $events = $st->getProjectCalendarEvents(1, date('Y-m-d'), date('Y-m-d', strtotime('+2 days')));
- $this->assertNotEmpty($events);
- $this->assertCount(3, $events);
- $this->assertEquals(1, $events[0]['subtask_id']);
- $this->assertEquals(2, $events[1]['subtask_id']);
- $this->assertEquals(3, $events[2]['subtask_id']);
-
- $events = $st->getProjectCalendarEvents(2, date('Y-m-d'), date('Y-m-d', strtotime('+2 days')));
- $this->assertNotEmpty($events);
- $this->assertCount(3, $events);
- $this->assertEquals(4, $events[0]['subtask_id']);
- $this->assertEquals(7, $events[1]['subtask_id']);
- $this->assertEquals(8, $events[2]['subtask_id']);
- }
}
diff --git a/tests/units/Model/TaskFileTest.php b/tests/units/Model/TaskFileTest.php
index b900e8f3..e44e092d 100644
--- a/tests/units/Model/TaskFileTest.php
+++ b/tests/units/Model/TaskFileTest.php
@@ -331,7 +331,7 @@ class TaskFileTest extends Base
$fileModel = $this
->getMockBuilder('\Kanboard\Model\TaskFile')
->setConstructorArgs(array($this->container))
- ->setMethods(array('generateThumbnailFromFile'))
+ ->setMethods(array('generateThumbnailFromData'))
->getMock();
$projectModel = new Project($this->container);
@@ -343,7 +343,7 @@ class TaskFileTest extends Base
$fileModel
->expects($this->once())
- ->method('generateThumbnailFromFile');
+ ->method('generateThumbnailFromData');
$this->container['objectStorage']
->expects($this->once())
diff --git a/tests/units/Model/TaskFilterTest.php b/tests/units/Model/TaskFilterTest.php
deleted file mode 100644
index 9e291c31..00000000
--- a/tests/units/Model/TaskFilterTest.php
+++ /dev/null
@@ -1,624 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\Project;
-use Kanboard\Model\User;
-use Kanboard\Model\TaskFilter;
-use Kanboard\Model\TaskCreation;
-use Kanboard\Model\TaskLink;
-use Kanboard\Core\DateParser;
-use Kanboard\Model\Category;
-use Kanboard\Model\Subtask;
-use Kanboard\Model\Swimlane;
-
-class TaskFilterTest extends Base
-{
- public function testSearchWithEmptyResult()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time()))));
-
- $this->assertEmpty($tf->search('search something')->findAll());
- }
-
- public function testSearchWithEmptyInput()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time()))));
-
- $result = $tf->search('')->findAll();
- $this->assertNotEmpty($result);
- $this->assertCount(4, $result);
- }
-
- public function testSearchById()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task 43')));
-
- $tf->search('#2');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('1');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
-
- $tf->search('something');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('#');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('#abcd');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('task1');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
-
- $tf->search('43');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task 43', $tasks[0]['title']);
- }
-
- public function testSearchWithReference()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'reference' => 123)));
-
- $tf->search('ref:123');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('reference:123');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('ref:plop');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('ref:');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithStatus()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'is_active' => 0)));
-
- $tf->search('status:open');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
-
- $tf->search('status:plop');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
-
- $tf->search('status:closed');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- }
-
- public function testSearchWithDescription()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'description' => '**something to do**')));
-
- $tf->search('description:"something"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('description:"rainy day"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithCategory()
- {
- $p = new Project($this->container);
- $c = new Category($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $c->create(array('name' => 'Feature request', 'project_id' => 1)));
- $this->assertEquals(2, $c->create(array('name' => 'hé hé', 'project_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'category_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task3', 'category_id' => 2)));
-
- $tf->search('category:"Feature request"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('Feature request', $tasks[0]['category_name']);
-
- $tf->search('category:"hé hé"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- $this->assertEquals('hé hé', $tasks[0]['category_name']);
-
- $tf->search('category:"Feature request" category:"hé hé"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('Feature request', $tasks[0]['category_name']);
- $this->assertEquals('task3', $tasks[1]['title']);
- $this->assertEquals('hé hé', $tasks[1]['category_name']);
-
- $tf->search('category:none');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('', $tasks[0]['category_name']);
-
- $tf->search('category:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithProject()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'My project A')));
- $this->assertEquals(2, $p->create(array('name' => 'My project B')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 2, 'title' => 'task2')));
-
- $tf->search('project:"My project A"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('My project A', $tasks[0]['project_name']);
-
- $tf->search('project:2');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('My project B', $tasks[0]['project_name']);
-
- $tf->search('project:"My project A" project:"my project b"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('My project A', $tasks[0]['project_name']);
- $this->assertEquals('task2', $tasks[1]['title']);
- $this->assertEquals('My project B', $tasks[1]['project_name']);
-
- $tf->search('project:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithSwimlane()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
- $s = new Swimlane($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'My project A')));
- $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'Version 1.1')));
- $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'Version 1.2')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'swimlane_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'swimlane_id' => 2)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task3', 'swimlane_id' => 0)));
-
- $tf->search('swimlane:"Version 1.1"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:"versioN 1.2"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('Version 1.2', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:"Default swimlane"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']);
- $this->assertEquals('', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:default');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']);
- $this->assertEquals('', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:"Version 1.1" swimlane:"Version 1.2"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']);
- $this->assertEquals('task2', $tasks[1]['title']);
- $this->assertEquals('Version 1.2', $tasks[1]['swimlane_name']);
-
- $tf->search('swimlane:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithColumn()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'My project A')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'column_id' => 3)));
-
- $tf->search('column:Backlog');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Backlog', $tasks[0]['column_name']);
-
- $tf->search('column:backlog column:"Work in progress"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Backlog', $tasks[0]['column_name']);
- $this->assertEquals('task2', $tasks[1]['title']);
- $this->assertEquals('Work in progress', $tasks[1]['column_name']);
-
- $tf->search('column:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithDueDate()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time()))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'no due date')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'due date at 0', 'date_due' => 0)));
-
- $tf->search('due:>'.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('due:>='.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
- $this->assertEquals('youpi', $tasks[1]['title']);
-
- $tf->search('due:<'.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
-
- $tf->search('due:<='.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
- $this->assertEquals('youpi', $tasks[2]['title']);
-
- $tf->search('due:tomorrow');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('due:yesterday');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('Bob at work', $tasks[0]['title']);
-
- $tf->search('due:today');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('youpi', $tasks[0]['title']);
- }
-
- public function testSearchWithColor()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'color_id' => 'light_green')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'color_id' => 'blue')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work')));
-
- $tf->search('color:"Light Green"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
-
- $tf->search('color:"Light Green" amazing');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('color:"plop');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('color:unknown');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
-
- $tf->search('color:blue amazing');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('color:blue color:Yellow');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
- }
-
- public function testSearchWithAssignee()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'owner_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'owner_id' => 0)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'owner_id' => 2)));
-
- $tf->search('assignee:john');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('assignee:admin my task title');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
-
- $tf->search('my task title');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('my task title is amazing', $tasks[1]['title']);
-
- $tf->search('my task title assignee:nobody');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('assignee:"Bob ryan" assignee:nobody');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
- }
-
- public function testSearchWithAssigneeIncludingSubtasks()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $s = new Subtask($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Paul Ryan')));
-
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 2)));
- $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'status' => 1, 'user_id' => 0)));
-
- $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'task2', 'owner_id' => 0)));
- $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 2, 'status' => 1, 'user_id' => 2)));
-
- $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'task3', 'owner_id' => 0)));
- $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 3, 'user_id' => 1)));
-
- $tf->search('assignee:bob');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('task2', $tasks[1]['title']);
-
- $tf->search('assignee:"Paul Ryan"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('task2', $tasks[1]['title']);
-
- $tf->search('assignee:nobody');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('task3', $tasks[1]['title']);
-
- $tf->search('assignee:admin');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- }
-
- public function testSearchWithLink()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $tl = new TaskLink($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'color_id' => 'light_green')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'color_id' => 'blue')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'I have a bad feeling about that')));
- $this->assertEquals(1, $tl->create(1, 2, 9)); // #1 is a milestone of #2
- $this->assertEquals(3, $tl->create(2, 1, 2)); // #2 blocks #1
- $this->assertEquals(5, $tl->create(3, 2, 2)); // #3 blocks #2
-
- $tf->search('link:"is a milestone of"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
-
- $tf->search('link:"is a milestone of" amazing');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('link:"unknown"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('link:unknown');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('link:blocks amazing');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('link:"is a milestone of" link:blocks');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('my task title is amazing', $tasks[1]['title']);
- $this->assertEquals('Bob at work', $tasks[2]['title']);
-
- $tf->search('link:"is a milestone of" link:blocks link:unknown');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('my task title is amazing', $tasks[1]['title']);
- $this->assertEquals('Bob at work', $tasks[2]['title']);
- }
-
- public function testCopy()
- {
- $tf = new TaskFilter($this->container);
- $filter1 = $tf->create();
- $filter2 = $tf->copy();
-
- $this->assertTrue($filter1 !== $filter2);
- $this->assertTrue($filter1->query !== $filter2->query);
- $this->assertTrue($filter1->query->condition !== $filter2->query->condition);
- }
-}
diff --git a/tests/units/Model/UserNotificationFilterTest.php b/tests/units/Model/UserNotificationFilterTest.php
index 0b5f1d98..924f0883 100644
--- a/tests/units/Model/UserNotificationFilterTest.php
+++ b/tests/units/Model/UserNotificationFilterTest.php
@@ -26,10 +26,11 @@ class UserNotificationFilterTest extends Base
$this->assertEquals(1, $p->create(array('name' => 'UnitTest1')));
$this->assertEquals(2, $p->create(array('name' => 'UnitTest2')));
+ $this->assertEquals(3, $p->create(array('name' => 'UnitTest3')));
$this->assertEmpty($nf->getSelectedProjects(1));
- $nf->saveSelectedProjects(1, array(1, 2));
- $this->assertEquals(array(1, 2), $nf->getSelectedProjects(1));
+ $this->assertTrue($nf->saveSelectedProjects(1, array(1, 2, 3)));
+ $this->assertEquals(array(1, 2, 3), $nf->getSelectedProjects(1));
}
public function testSaveUserFilter()
diff --git a/tests/units/User/Avatar/LetterAvatarProviderTest.php b/tests/units/User/Avatar/LetterAvatarProviderTest.php
index 0c1bfc4b..39e51c98 100644
--- a/tests/units/User/Avatar/LetterAvatarProviderTest.php
+++ b/tests/units/User/Avatar/LetterAvatarProviderTest.php
@@ -23,7 +23,7 @@ class LetterAvatarProviderTest extends Base
{
$provider = new LetterAvatarProvider($this->container);
$user = array('id' => 123, 'name' => 'Kanboard Admin', 'username' => 'bob', 'email' => '');
- $expected = '<div class="avatar-letter" style="background-color: rgb(187, 224, 108)" title="Kanboard Admin">KA</div>';
+ $expected = '<div class="avatar-letter" style="background-color: rgb(131, 224, 108)" title="Kanboard Admin">KA</div>';
$this->assertEquals($expected, $provider->render($user, 48));
}
@@ -31,7 +31,7 @@ class LetterAvatarProviderTest extends Base
{
$provider = new LetterAvatarProvider($this->container);
$user = array('id' => 123, 'name' => '', 'username' => 'admin', 'email' => '');
- $expected = '<div class="avatar-letter" style="background-color: rgb(210, 97, 45)" title="admin">A</div>';
+ $expected = '<div class="avatar-letter" style="background-color: rgb(134, 45, 132)" title="admin">A</div>';
$this->assertEquals($expected, $provider->render($user, 48));
}
}