summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGerardo Zamudio <gerardozamudio@users.noreply.github.com>2016-02-24 23:48:50 -0600
committerGerardo Zamudio <gerardozamudio@users.noreply.github.com>2016-02-24 23:48:50 -0600
commite4de6b3898b64b26d29aff31f21df5fda8055686 (patch)
tree575f8a65440f291d70a070d168eafca8c82a6459 /app
parentd9ffbea174ea6524d0a22f8375ca8b3aa04a3c96 (diff)
parenta6540bc604c837d92c9368540c145606723e97f7 (diff)
Merge pull request #1 from fguillot/master
Update from upstream
Diffstat (limited to 'app')
-rw-r--r--app/Action/Base.php132
-rw-r--r--app/Action/CommentCreation.php30
-rw-r--r--app/Action/CommentCreationMoveTaskColumn.php (renamed from app/Action/TaskLogMoveAnotherColumn.php)17
-rw-r--r--app/Action/TaskAssignCategoryColor.php11
-rw-r--r--app/Action/TaskAssignCategoryLabel.php21
-rw-r--r--app/Action/TaskAssignCategoryLink.php101
-rw-r--r--app/Action/TaskAssignColorCategory.php13
-rw-r--r--app/Action/TaskAssignColorColumn.php13
-rw-r--r--app/Action/TaskAssignColorLink.php13
-rw-r--r--app/Action/TaskAssignColorUser.php13
-rw-r--r--app/Action/TaskAssignCurrentUser.php19
-rw-r--r--app/Action/TaskAssignCurrentUserColumn.php98
-rw-r--r--app/Action/TaskAssignSpecificUser.php11
-rw-r--r--app/Action/TaskAssignUser.php21
-rw-r--r--app/Action/TaskClose.php62
-rw-r--r--app/Action/TaskCloseColumn.php84
-rw-r--r--app/Action/TaskCloseNoActivity.php95
-rw-r--r--app/Action/TaskCreation.php21
-rw-r--r--app/Action/TaskDuplicateAnotherProject.php15
-rw-r--r--app/Action/TaskEmail.php11
-rw-r--r--app/Action/TaskEmailNoActivity.php124
-rw-r--r--app/Action/TaskMoveAnotherProject.php11
-rw-r--r--app/Action/TaskMoveColumnAssigned.php12
-rw-r--r--app/Action/TaskMoveColumnCategoryChange.php15
-rw-r--r--app/Action/TaskMoveColumnUnAssigned.php12
-rw-r--r--app/Action/TaskOpen.php19
-rw-r--r--app/Action/TaskUpdateStartDate.php13
-rw-r--r--app/Analytic/AverageLeadCycleTimeAnalytic.php114
-rw-r--r--app/Analytic/AverageTimeSpentColumnAnalytic.php153
-rw-r--r--app/Analytic/EstimatedTimeComparisonAnalytic.php50
-rw-r--r--app/Analytic/TaskDistributionAnalytic.php48
-rw-r--r--app/Analytic/UserDistributionAnalytic.php56
-rw-r--r--app/Api/Action.php30
-rw-r--r--app/Api/App.php10
-rw-r--r--app/Api/Auth.php52
-rw-r--r--app/Api/Board.php35
-rw-r--r--app/Api/Category.php4
-rw-r--r--app/Api/Column.php42
-rw-r--r--app/Api/Comment.php7
-rw-r--r--app/Api/File.php58
-rw-r--r--app/Api/Group.php49
-rw-r--r--app/Api/GroupMember.php32
-rw-r--r--app/Api/Link.php4
-rw-r--r--app/Api/Me.php16
-rw-r--r--app/Api/Project.php4
-rw-r--r--app/Api/ProjectPermission.php53
-rw-r--r--app/Api/Subtask.php4
-rw-r--r--app/Api/Swimlane.php11
-rw-r--r--app/Api/Task.php44
-rw-r--r--app/Api/User.php81
-rw-r--r--app/Auth/Database.php49
-rw-r--r--app/Auth/DatabaseAuth.php126
-rw-r--r--app/Auth/Github.php123
-rw-r--r--app/Auth/Gitlab.php123
-rw-r--r--app/Auth/Google.php124
-rw-r--r--app/Auth/Ldap.php521
-rw-r--r--app/Auth/LdapAuth.php172
-rw-r--r--app/Auth/RememberMe.php323
-rw-r--r--app/Auth/RememberMeAuth.php79
-rw-r--r--app/Auth/ReverseProxy.php83
-rw-r--r--app/Auth/ReverseProxyAuth.php76
-rw-r--r--app/Auth/TotpAuth.php144
-rw-r--r--app/Console/Base.php25
-rw-r--r--app/Console/Cronjob.php32
-rw-r--r--app/Console/TaskTrigger.php51
-rw-r--r--app/Controller/Action.php47
-rw-r--r--app/Controller/Activity.php5
-rw-r--r--app/Controller/Analytic.php108
-rw-r--r--app/Controller/App.php79
-rw-r--r--app/Controller/Auth.php43
-rw-r--r--app/Controller/Base.php259
-rw-r--r--app/Controller/Board.php220
-rw-r--r--app/Controller/BoardPopover.php135
-rw-r--r--app/Controller/BoardTooltip.php126
-rw-r--r--app/Controller/Calendar.php2
-rw-r--r--app/Controller/Captcha.php29
-rw-r--r--app/Controller/Category.php24
-rw-r--r--app/Controller/Column.php92
-rw-r--r--app/Controller/Comment.php49
-rw-r--r--app/Controller/Config.php58
-rw-r--r--app/Controller/Currency.php30
-rw-r--r--app/Controller/Customfilter.php41
-rw-r--r--app/Controller/Doc.php4
-rw-r--r--app/Controller/Export.php6
-rw-r--r--app/Controller/Feed.php4
-rw-r--r--app/Controller/File.php192
-rw-r--r--app/Controller/FileViewer.php116
-rw-r--r--app/Controller/Gantt.php33
-rw-r--r--app/Controller/Group.php250
-rw-r--r--app/Controller/GroupHelper.php24
-rw-r--r--app/Controller/Ical.php3
-rw-r--r--app/Controller/Link.php38
-rw-r--r--app/Controller/Listing.php5
-rw-r--r--app/Controller/Oauth.php83
-rw-r--r--app/Controller/PasswordReset.php120
-rw-r--r--app/Controller/Project.php261
-rw-r--r--app/Controller/ProjectCreation.php125
-rw-r--r--app/Controller/ProjectEdit.php125
-rw-r--r--app/Controller/ProjectFile.php79
-rw-r--r--app/Controller/ProjectOverview.php29
-rw-r--r--app/Controller/ProjectPermission.php191
-rw-r--r--app/Controller/Projectuser.php51
-rw-r--r--app/Controller/Search.php5
-rw-r--r--app/Controller/Subtask.php160
-rw-r--r--app/Controller/SubtaskRestriction.php61
-rw-r--r--app/Controller/SubtaskStatus.php72
-rw-r--r--app/Controller/Swimlane.php129
-rw-r--r--app/Controller/Task.php32
-rw-r--r--app/Controller/TaskExternalLink.php185
-rw-r--r--app/Controller/TaskFile.php98
-rw-r--r--app/Controller/TaskHelper.php57
-rw-r--r--app/Controller/TaskImport.php6
-rw-r--r--app/Controller/TaskRecurrence.php61
-rw-r--r--app/Controller/Taskcreation.php44
-rw-r--r--app/Controller/Taskduplication.php24
-rw-r--r--app/Controller/Tasklink.php65
-rw-r--r--app/Controller/Taskmodification.php142
-rw-r--r--app/Controller/Taskstatus.php46
-rw-r--r--app/Controller/Timer.php34
-rw-r--r--app/Controller/Twofactor.php113
-rw-r--r--app/Controller/User.php178
-rw-r--r--app/Controller/UserHelper.php37
-rw-r--r--app/Controller/UserImport.php6
-rw-r--r--app/Controller/UserStatus.php111
-rw-r--r--app/Controller/Webhook.php53
-rw-r--r--app/Core/Action/ActionManager.php142
-rw-r--r--app/Core/Base.php94
-rw-r--r--app/Core/Cache/MemoryCache.php2
-rw-r--r--app/Core/Csv.php13
-rw-r--r--app/Core/DateParser.php215
-rw-r--r--app/Core/Event/EventManager.php63
-rw-r--r--app/Core/ExternalLink/ExternalLinkInterface.php36
-rw-r--r--app/Core/ExternalLink/ExternalLinkManager.php173
-rw-r--r--app/Core/ExternalLink/ExternalLinkProviderInterface.php71
-rw-r--r--app/Core/ExternalLink/ExternalLinkProviderNotFound.php15
-rw-r--r--app/Core/Group/GroupBackendProviderInterface.php21
-rw-r--r--app/Core/Group/GroupManager.php71
-rw-r--r--app/Core/Group/GroupProviderInterface.php40
-rw-r--r--app/Core/Helper.php1
-rw-r--r--app/Core/Http/Client.php (renamed from app/Core/HttpClient.php)69
-rw-r--r--app/Core/Http/OAuth2.php (renamed from app/Core/OAuth2.php)8
-rw-r--r--app/Core/Http/RememberMeCookie.php120
-rw-r--r--app/Core/Http/Request.php337
-rw-r--r--app/Core/Http/Response.php (renamed from app/Core/Response.php)23
-rw-r--r--app/Core/Http/Route.php187
-rw-r--r--app/Core/Http/Router.php169
-rw-r--r--app/Core/Ldap/Client.php165
-rw-r--r--app/Core/Ldap/ClientException.php15
-rw-r--r--app/Core/Ldap/Entries.php63
-rw-r--r--app/Core/Ldap/Entry.php91
-rw-r--r--app/Core/Ldap/Group.php131
-rw-r--r--app/Core/Ldap/Query.php87
-rw-r--r--app/Core/Ldap/User.php224
-rw-r--r--app/Core/Lexer.php2
-rw-r--r--app/Core/Mail/Client.php20
-rw-r--r--app/Core/Mail/Transport/Mail.php2
-rw-r--r--app/Core/Mail/Transport/Sendmail.php2
-rw-r--r--app/Core/Mail/Transport/Smtp.php2
-rw-r--r--app/Core/Markdown.php80
-rw-r--r--app/Core/Plugin/Loader.php6
-rw-r--r--app/Core/Request.php240
-rw-r--r--app/Core/Router.php229
-rw-r--r--app/Core/Security.php86
-rw-r--r--app/Core/Security/AccessMap.php175
-rw-r--r--app/Core/Security/AuthenticationManager.php187
-rw-r--r--app/Core/Security/AuthenticationProviderInterface.php28
-rw-r--r--app/Core/Security/Authorization.php46
-rw-r--r--app/Core/Security/OAuthAuthenticationProviderInterface.php46
-rw-r--r--app/Core/Security/PasswordAuthenticationProviderInterface.php36
-rw-r--r--app/Core/Security/PostAuthenticationProviderInterface.php69
-rw-r--r--app/Core/Security/PreAuthenticationProviderInterface.php20
-rw-r--r--app/Core/Security/Role.php64
-rw-r--r--app/Core/Security/SessionCheckProviderInterface.php20
-rw-r--r--app/Core/Security/Token.php61
-rw-r--r--app/Core/Session.php143
-rw-r--r--app/Core/Session/FlashMessage.php71
-rw-r--r--app/Core/Session/SessionManager.php110
-rw-r--r--app/Core/Session/SessionStorage.php89
-rw-r--r--app/Core/Template.php53
-rw-r--r--app/Core/Tool.php3
-rw-r--r--app/Core/Translator.php28
-rw-r--r--app/Core/User/GroupSync.php32
-rw-r--r--app/Core/User/UserProfile.php62
-rw-r--r--app/Core/User/UserProperty.php70
-rw-r--r--app/Core/User/UserProviderInterface.php103
-rw-r--r--app/Core/User/UserSession.php204
-rw-r--r--app/Core/User/UserSync.php76
-rw-r--r--app/Event/AuthEvent.php27
-rw-r--r--app/Event/AuthFailureEvent.php44
-rw-r--r--app/Event/AuthSuccessEvent.php43
-rw-r--r--app/Event/GenericEvent.php2
-rw-r--r--app/Event/TaskListEvent.php11
-rw-r--r--app/ExternalLink/AttachmentLink.php26
-rw-r--r--app/ExternalLink/AttachmentLinkProvider.php117
-rw-r--r--app/ExternalLink/BaseLink.php44
-rw-r--r--app/ExternalLink/BaseLinkProvider.php33
-rw-r--r--app/ExternalLink/WebLink.php37
-rw-r--r--app/ExternalLink/WebLinkProvider.php77
-rw-r--r--app/Formatter/GroupAutoCompleteFormatter.php55
-rw-r--r--app/Formatter/ProjectGanttFormatter.php2
-rw-r--r--app/Formatter/TaskFilterGanttFormatter.php2
-rw-r--r--app/Formatter/UserFilterAutoCompleteFormatter.php38
-rw-r--r--app/Group/DatabaseBackendGroupProvider.php34
-rw-r--r--app/Group/DatabaseGroupProvider.php66
-rw-r--r--app/Group/LdapBackendGroupProvider.php54
-rw-r--r--app/Group/LdapGroupProvider.php76
-rw-r--r--app/Helper/App.php73
-rw-r--r--app/Helper/Dt.php46
-rw-r--r--app/Helper/File.php57
-rw-r--r--app/Helper/Form.php6
-rw-r--r--app/Helper/Layout.php171
-rw-r--r--app/Helper/Subtask.php88
-rw-r--r--app/Helper/Task.php151
-rw-r--r--app/Helper/Text.php30
-rw-r--r--app/Helper/Url.php25
-rw-r--r--app/Helper/User.php82
-rw-r--r--app/Integration/BitbucketWebhook.php312
-rw-r--r--app/Integration/GithubWebhook.php380
-rw-r--r--app/Integration/GitlabWebhook.php265
-rw-r--r--app/Locale/bs_BA/translations.php1151
-rw-r--r--app/Locale/cs_CZ/translations.php326
-rw-r--r--app/Locale/da_DK/translations.php326
-rw-r--r--app/Locale/de_DE/translations.php406
-rw-r--r--app/Locale/el_GR/translations.php1151
-rw-r--r--app/Locale/es_ES/translations.php326
-rw-r--r--app/Locale/fi_FI/translations.php326
-rw-r--r--app/Locale/fr_FR/translations.php333
-rw-r--r--app/Locale/hu_HU/translations.php326
-rw-r--r--app/Locale/id_ID/translations.php326
-rw-r--r--app/Locale/it_IT/translations.php1514
-rw-r--r--app/Locale/ja_JP/translations.php326
-rw-r--r--app/Locale/my_MY/translations.php1151
-rw-r--r--app/Locale/nb_NO/translations.php326
-rw-r--r--app/Locale/nl_NL/translations.php326
-rw-r--r--app/Locale/pl_PL/translations.php738
-rw-r--r--app/Locale/pt_BR/translations.php480
-rw-r--r--app/Locale/pt_PT/translations.php418
-rw-r--r--app/Locale/ru_RU/translations.php546
-rw-r--r--app/Locale/sr_Latn_RS/translations.php326
-rw-r--r--app/Locale/sv_SE/translations.php326
-rw-r--r--app/Locale/th_TH/translations.php937
-rw-r--r--app/Locale/tr_TR/translations.php1126
-rw-r--r--app/Locale/zh_CN/translations.php900
-rw-r--r--app/Model/Acl.php289
-rw-r--r--app/Model/Action.php350
-rw-r--r--app/Model/ActionParameter.php162
-rw-r--r--app/Model/Authentication.php202
-rw-r--r--app/Model/Board.php315
-rw-r--r--app/Model/Category.php73
-rw-r--r--app/Model/Column.php209
-rw-r--r--app/Model/Comment.php70
-rw-r--r--app/Model/Config.php125
-rw-r--r--app/Model/Currency.php53
-rw-r--r--app/Model/CustomFilter.php62
-rw-r--r--app/Model/File.php300
-rw-r--r--app/Model/Group.php117
-rw-r--r--app/Model/GroupMember.php111
-rw-r--r--app/Model/LastLogin.php42
-rw-r--r--app/Model/Link.php45
-rw-r--r--app/Model/Metadata.php13
-rw-r--r--app/Model/Notification.php58
-rw-r--r--app/Model/PasswordReset.php93
-rw-r--r--app/Model/Project.php142
-rw-r--r--app/Model/ProjectActivity.php14
-rw-r--r--app/Model/ProjectAnalytic.php182
-rw-r--r--app/Model/ProjectDailyColumnStats.php280
-rw-r--r--app/Model/ProjectDailyStats.php45
-rw-r--r--app/Model/ProjectDuplication.php140
-rw-r--r--app/Model/ProjectFile.php40
-rw-r--r--app/Model/ProjectGroupRole.php190
-rw-r--r--app/Model/ProjectGroupRoleFilter.php89
-rw-r--r--app/Model/ProjectPermission.php448
-rw-r--r--app/Model/ProjectUserRole.php276
-rw-r--r--app/Model/ProjectUserRoleFilter.php88
-rw-r--r--app/Model/RememberMeSession.php151
-rw-r--r--app/Model/Setting.php6
-rw-r--r--app/Model/Subtask.php192
-rw-r--r--app/Model/SubtaskTimeTracking.php10
-rw-r--r--app/Model/Swimlane.php321
-rw-r--r--app/Model/Task.php25
-rw-r--r--app/Model/TaskAnalytic.php2
-rw-r--r--app/Model/TaskCreation.php21
-rw-r--r--app/Model/TaskDuplication.php8
-rw-r--r--app/Model/TaskExport.php6
-rw-r--r--app/Model/TaskExternalLink.php99
-rw-r--r--app/Model/TaskFile.php54
-rw-r--r--app/Model/TaskFilter.php43
-rw-r--r--app/Model/TaskFinder.php41
-rw-r--r--app/Model/TaskImport.php2
-rw-r--r--app/Model/TaskLink.php64
-rw-r--r--app/Model/TaskModification.php17
-rw-r--r--app/Model/TaskPermission.php4
-rw-r--r--app/Model/TaskPosition.php17
-rw-r--r--app/Model/TaskStatus.php32
-rw-r--r--app/Model/Transition.php8
-rw-r--r--app/Model/User.php321
-rw-r--r--app/Model/UserFilter.php80
-rw-r--r--app/Model/UserImport.php18
-rw-r--r--app/Model/UserLocking.php103
-rw-r--r--app/Model/UserMention.php61
-rw-r--r--app/Model/UserNotification.php50
-rw-r--r--app/Model/UserSession.php177
-rw-r--r--app/Notification/Mail.php15
-rw-r--r--app/Notification/Web.php7
-rw-r--r--app/Schema/Mysql.php212
-rw-r--r--app/Schema/Postgres.php206
-rw-r--r--app/Schema/Sql/mysql.sql101
-rw-r--r--app/Schema/Sql/postgres.sql283
-rw-r--r--app/Schema/Sqlite.php186
-rw-r--r--app/ServiceProvider/ActionProvider.php82
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php143
-rw-r--r--app/ServiceProvider/ClassProvider.php108
-rw-r--r--app/ServiceProvider/DatabaseProvider.php2
-rw-r--r--app/ServiceProvider/EventDispatcherProvider.php5
-rw-r--r--app/ServiceProvider/ExternalLinkProvider.php34
-rw-r--r--app/ServiceProvider/GroupProvider.php40
-rw-r--r--app/ServiceProvider/LoggingProvider.php2
-rw-r--r--app/ServiceProvider/NotificationProvider.php45
-rw-r--r--app/ServiceProvider/PluginProvider.php31
-rw-r--r--app/ServiceProvider/RouteProvider.php201
-rw-r--r--app/ServiceProvider/SessionProvider.php42
-rw-r--r--app/Subscriber/AuthSubscriber.php95
-rw-r--r--app/Subscriber/BaseSubscriber.php40
-rw-r--r--app/Subscriber/BootstrapSubscriber.php29
-rw-r--r--app/Subscriber/NotificationSubscriber.php49
-rw-r--r--app/Subscriber/ProjectDailySummarySubscriber.php15
-rw-r--r--app/Subscriber/ProjectModificationDateSubscriber.php21
-rw-r--r--app/Subscriber/RecurringTaskSubscriber.php14
-rw-r--r--app/Subscriber/SubtaskTimeTrackingSubscriber.php8
-rw-r--r--app/Subscriber/TaskMovedDateSubscriber.php25
-rw-r--r--app/Subscriber/TransitionSubscriber.php6
-rw-r--r--app/Template/action/event.php2
-rw-r--r--app/Template/action/index.php36
-rw-r--r--app/Template/action/params.php15
-rw-r--r--app/Template/action/remove.php2
-rw-r--r--app/Template/activity/project.php2
-rw-r--r--app/Template/analytic/compare_hours.php62
-rw-r--r--app/Template/analytic/layout.php7
-rw-r--r--app/Template/analytic/sidebar.php22
-rw-r--r--app/Template/app/filters_helper.php8
-rw-r--r--app/Template/app/layout.php20
-rw-r--r--app/Template/app/notifications.php2
-rw-r--r--app/Template/app/overview.php11
-rw-r--r--app/Template/app/projects.php4
-rw-r--r--app/Template/app/sidebar.php16
-rw-r--r--app/Template/app/subtasks.php8
-rw-r--r--app/Template/app/tasks.php8
-rw-r--r--app/Template/auth/index.php28
-rw-r--r--app/Template/board/popover_assignee.php29
-rw-r--r--app/Template/board/popover_category.php29
-rw-r--r--app/Template/board/popover_close_all_tasks_column.php18
-rw-r--r--app/Template/board/table_column.php28
-rw-r--r--app/Template/board/table_swimlane.php2
-rw-r--r--app/Template/board/table_tasks.php4
-rw-r--r--app/Template/board/task_footer.php24
-rw-r--r--app/Template/board/task_menu.php11
-rw-r--r--app/Template/board/task_private.php42
-rw-r--r--app/Template/board/task_public.php1
-rw-r--r--app/Template/board/tooltip_comments.php2
-rw-r--r--app/Template/board/tooltip_external_links.php20
-rw-r--r--app/Template/board/tooltip_files.php4
-rw-r--r--app/Template/board/tooltip_subtasks.php17
-rw-r--r--app/Template/board/tooltip_tasklinks.php37
-rw-r--r--app/Template/board/view_private.php2
-rw-r--r--app/Template/calendar/show.php4
-rw-r--r--app/Template/category/edit.php6
-rw-r--r--app/Template/category/index.php9
-rw-r--r--app/Template/category/remove.php2
-rw-r--r--app/Template/column/create.php41
-rw-r--r--app/Template/column/edit.php6
-rw-r--r--app/Template/column/index.php107
-rw-r--r--app/Template/column/remove.php2
-rw-r--r--app/Template/comment/create.php22
-rw-r--r--app/Template/comment/edit.php6
-rw-r--r--app/Template/comment/forbidden.php7
-rw-r--r--app/Template/comment/remove.php2
-rw-r--r--app/Template/comment/show.php8
-rw-r--r--app/Template/config/about.php1
-rw-r--r--app/Template/config/application.php24
-rw-r--r--app/Template/config/board.php10
-rw-r--r--app/Template/config/integrations.php20
-rw-r--r--app/Template/config/layout.php4
-rw-r--r--app/Template/config/project.php9
-rw-r--r--app/Template/config/sidebar.php27
-rw-r--r--app/Template/currency/index.php10
-rw-r--r--app/Template/custom_filter/add.php4
-rw-r--r--app/Template/custom_filter/edit.php10
-rw-r--r--app/Template/custom_filter/index.php21
-rw-r--r--app/Template/custom_filter/remove.php17
-rw-r--r--app/Template/event/events.php2
-rw-r--r--app/Template/event/task_file_create.php11
-rw-r--r--app/Template/export/sidebar.php2
-rw-r--r--app/Template/export/subtasks.php8
-rw-r--r--app/Template/export/summary.php8
-rw-r--r--app/Template/export/tasks.php8
-rw-r--r--app/Template/export/transitions.php8
-rw-r--r--app/Template/file/new.php14
-rw-r--r--app/Template/file/open.php6
-rw-r--r--app/Template/file/show.php56
-rw-r--r--app/Template/file_viewer/show.php14
-rw-r--r--app/Template/gantt/project.php2
-rw-r--r--app/Template/gantt/projects.php8
-rw-r--r--app/Template/gantt/task_creation.php27
-rw-r--r--app/Template/group/associate.php25
-rw-r--r--app/Template/group/create.php19
-rw-r--r--app/Template/group/dissociate.php17
-rw-r--r--app/Template/group/edit.php22
-rw-r--r--app/Template/group/index.php46
-rw-r--r--app/Template/group/remove.php17
-rw-r--r--app/Template/group/users.php42
-rw-r--r--app/Template/header.php80
-rw-r--r--app/Template/layout.php2
-rw-r--r--app/Template/listing/show.php11
-rw-r--r--app/Template/notification/comment_user_mention.php7
-rw-r--r--app/Template/notification/task_create.php4
-rw-r--r--app/Template/notification/task_file_create.php (renamed from app/Template/notification/file_create.php)0
-rw-r--r--app/Template/notification/task_overdue.php2
-rw-r--r--app/Template/notification/task_user_mention.php7
-rw-r--r--app/Template/password_reset/change.php16
-rw-r--r--app/Template/password_reset/create.php17
-rw-r--r--app/Template/password_reset/email.php6
-rw-r--r--app/Template/project/dropdown.php11
-rw-r--r--app/Template/project/duplicate.php6
-rw-r--r--app/Template/project/edit.php50
-rw-r--r--app/Template/project/filters.php101
-rw-r--r--app/Template/project/index.php41
-rw-r--r--app/Template/project/integrations.php24
-rw-r--r--app/Template/project/layout.php2
-rw-r--r--app/Template/project/new.php24
-rw-r--r--app/Template/project/notifications.php2
-rw-r--r--app/Template/project/show.php10
-rw-r--r--app/Template/project/sidebar.php49
-rw-r--r--app/Template/project/users.php82
-rw-r--r--app/Template/project_creation/create.php42
-rw-r--r--app/Template/project_edit/dates.php26
-rw-r--r--app/Template/project_edit/description.php37
-rw-r--r--app/Template/project_edit/general.php36
-rw-r--r--app/Template/project_edit/task_priority.php29
-rw-r--r--app/Template/project_file/create.php33
-rw-r--r--app/Template/project_file/remove.php15
-rw-r--r--app/Template/project_header/dropdown.php34
-rw-r--r--app/Template/project_header/header.php15
-rw-r--r--app/Template/project_header/search.php45
-rw-r--r--app/Template/project_header/views.php24
-rw-r--r--app/Template/project_overview/columns.php8
-rw-r--r--app/Template/project_overview/description.php8
-rw-r--r--app/Template/project_overview/files.php98
-rw-r--r--app/Template/project_overview/information.php35
-rw-r--r--app/Template/project_overview/show.php16
-rw-r--r--app/Template/project_permission/index.php141
-rw-r--r--app/Template/project_user/layout.php11
-rw-r--r--app/Template/project_user/sidebar.php10
-rw-r--r--app/Template/project_user/tasks.php4
-rw-r--r--app/Template/project_user/tooltip_users.php14
-rw-r--r--app/Template/search/index.php5
-rw-r--r--app/Template/search/results.php2
-rw-r--r--app/Template/subtask/create.php19
-rw-r--r--app/Template/subtask/edit.php23
-rw-r--r--app/Template/subtask/icons.php7
-rw-r--r--app/Template/subtask/menu.php11
-rw-r--r--app/Template/subtask/remove.php2
-rw-r--r--app/Template/subtask/show.php94
-rw-r--r--app/Template/subtask/table.php71
-rw-r--r--app/Template/subtask_restriction/popover.php (renamed from app/Template/subtask/restriction_change_status.php)6
-rw-r--r--app/Template/swimlane/create.php37
-rw-r--r--app/Template/swimlane/edit.php6
-rw-r--r--app/Template/swimlane/edit_default.php18
-rw-r--r--app/Template/swimlane/index.php87
-rw-r--r--app/Template/swimlane/remove.php2
-rw-r--r--app/Template/swimlane/table.php110
-rw-r--r--app/Template/task/changes.php4
-rw-r--r--app/Template/task/comments.php6
-rw-r--r--app/Template/task/details.php231
-rw-r--r--app/Template/task/dropdown.php60
-rw-r--r--app/Template/task/layout.php11
-rw-r--r--app/Template/task/menu.php78
-rw-r--r--app/Template/task/public.php11
-rw-r--r--app/Template/task/remove.php2
-rw-r--r--app/Template/task/show.php39
-rw-r--r--app/Template/task/sidebar.php69
-rw-r--r--app/Template/task/time_tracking_details.php6
-rw-r--r--app/Template/task/transitions.php2
-rw-r--r--app/Template/task_creation/form.php59
-rw-r--r--app/Template/task_duplication/copy.php6
-rw-r--r--app/Template/task_duplication/duplicate.php2
-rw-r--r--app/Template/task_duplication/move.php6
-rw-r--r--app/Template/task_external_link/create.php13
-rw-r--r--app/Template/task_external_link/edit.php13
-rw-r--r--app/Template/task_external_link/find.php28
-rw-r--r--app/Template/task_external_link/form.php13
-rw-r--r--app/Template/task_external_link/remove.php15
-rw-r--r--app/Template/task_external_link/show.php50
-rw-r--r--app/Template/task_file/create.php33
-rw-r--r--app/Template/task_file/remove.php (renamed from app/Template/file/remove.php)4
-rw-r--r--app/Template/task_file/screenshot.php (renamed from app/Template/file/screenshot.php)2
-rw-r--r--app/Template/task_file/show.php90
-rw-r--r--app/Template/task_modification/edit_description.php20
-rw-r--r--app/Template/task_modification/edit_task.php58
-rw-r--r--app/Template/task_modification/edit_time.php20
-rw-r--r--app/Template/task_recurrence/edit.php (renamed from app/Template/task_modification/edit_recurrence.php)8
-rw-r--r--app/Template/task_recurrence/info.php (renamed from app/Template/task/recurring_info.php)0
-rw-r--r--app/Template/task_status/close.php2
-rw-r--r--app/Template/task_status/open.php2
-rw-r--r--app/Template/tasklink/create.php12
-rw-r--r--app/Template/tasklink/edit.php4
-rw-r--r--app/Template/tasklink/show.php37
-rw-r--r--app/Template/twofactor/index.php36
-rw-r--r--app/Template/twofactor/show.php31
-rw-r--r--app/Template/user/authentication.php9
-rw-r--r--app/Template/user/create_local.php23
-rw-r--r--app/Template/user/create_remote.php26
-rw-r--r--app/Template/user/dropdown.php27
-rw-r--r--app/Template/user/edit.php14
-rw-r--r--app/Template/user/external.php52
-rw-r--r--app/Template/user/index.php40
-rw-r--r--app/Template/user/last.php2
-rw-r--r--app/Template/user/layout.php4
-rw-r--r--app/Template/user/password.php8
-rw-r--r--app/Template/user/password_reset.php26
-rw-r--r--app/Template/user/profile.php8
-rw-r--r--app/Template/user/sessions.php6
-rw-r--r--app/Template/user/show.php3
-rw-r--r--app/Template/user/sidebar.php56
-rw-r--r--app/Template/user/timesheet.php4
-rw-r--r--app/Template/user_import/step1.php2
-rw-r--r--app/Template/user_status/disable.php13
-rw-r--r--app/Template/user_status/enable.php13
-rw-r--r--app/Template/user_status/remove.php (renamed from app/Template/user/remove.php)6
-rw-r--r--app/User/DatabaseUserProvider.php143
-rw-r--r--app/User/LdapUserProvider.php206
-rw-r--r--app/User/OAuthUserProvider.php140
-rw-r--r--app/User/ReverseProxyUserProvider.php147
-rw-r--r--app/Validator/ActionValidator.php38
-rw-r--r--app/Validator/AuthValidator.php119
-rw-r--r--app/Validator/Base.php54
-rw-r--r--app/Validator/CategoryValidator.php74
-rw-r--r--app/Validator/ColumnValidator.php75
-rw-r--r--app/Validator/CommentValidator.php74
-rw-r--r--app/Validator/CurrencyValidator.php36
-rw-r--r--app/Validator/CustomFilterValidator.php74
-rw-r--r--app/Validator/ExternalLinkValidator.php76
-rw-r--r--app/Validator/GroupValidator.php71
-rw-r--r--app/Validator/LinkValidator.php59
-rw-r--r--app/Validator/PasswordResetValidator.php92
-rw-r--r--app/Validator/ProjectValidator.php86
-rw-r--r--app/Validator/SubtaskValidator.php101
-rw-r--r--app/Validator/SwimlaneValidator.php96
-rw-r--r--app/Validator/TaskLinkValidator.php71
-rw-r--r--app/Validator/TaskValidator.php (renamed from app/Model/TaskValidator.php)11
-rw-r--r--app/Validator/UserValidator.php128
-rw-r--r--app/check_setup.php27
-rw-r--r--app/common.php21
-rw-r--r--app/constants.php53
-rw-r--r--app/functions.php10
-rw-r--r--app/routes.php117
555 files changed, 30469 insertions, 15599 deletions
diff --git a/app/Action/Base.php b/app/Action/Base.php
index 4d2d6da6..e8449d0c 100644
--- a/app/Action/Base.php
+++ b/app/Action/Base.php
@@ -3,7 +3,6 @@
namespace Kanboard\Action;
use Kanboard\Event\GenericEvent;
-use Pimple\Container;
/**
* Base class for automatic actions
@@ -14,6 +13,14 @@ use Pimple\Container;
abstract class Base extends \Kanboard\Core\Base
{
/**
+ * Extended events
+ *
+ * @access private
+ * @var array
+ */
+ private $compatibleEvents = array();
+
+ /**
* Flag for called listener
*
* @access private
@@ -27,7 +34,7 @@ abstract class Base extends \Kanboard\Core\Base
* @access private
* @var integer
*/
- private $project_id = 0;
+ private $projectId = 0;
/**
* User parameters
@@ -38,20 +45,25 @@ abstract class Base extends \Kanboard\Core\Base
private $params = array();
/**
- * Attached event name
+ * Get automatic action name
*
- * @access protected
- * @var string
+ * @final
+ * @access public
+ * @return string
*/
- protected $event_name = '';
+ final public function getName()
+ {
+ return '\\'.get_called_class();
+ }
/**
- * Container instance
+ * Get automatic action description
*
- * @access protected
- * @var \Pimple\Container
+ * @abstract
+ * @access public
+ * @return string
*/
- protected $container;
+ abstract public function getDescription();
/**
* Execute the action
@@ -100,30 +112,32 @@ abstract class Base extends \Kanboard\Core\Base
abstract public function hasRequiredCondition(array $data);
/**
- * Constructor
+ * Return class information
*
* @access public
- * @param \Pimple\Container $container Container
- * @param integer $project_id Project id
- * @param string $event_name Attached event name
+ * @return string
*/
- public function __construct(Container $container, $project_id, $event_name)
+ public function __toString()
{
- $this->container = $container;
- $this->project_id = $project_id;
- $this->event_name = $event_name;
- $this->called = false;
+ $params = array();
+
+ foreach ($this->params as $key => $value) {
+ $params[] = $key.'='.var_export($value, true);
+ }
+
+ return $this->getName().'('.implode('|', $params).')';
}
/**
- * Return class information
+ * Set project id
*
* @access public
- * @return string
+ * @return Base
*/
- public function __toString()
+ public function setProjectId($project_id)
{
- return get_called_class();
+ $this->projectId = $project_id;
+ return $this;
}
/**
@@ -134,7 +148,7 @@ abstract class Base extends \Kanboard\Core\Base
*/
public function getProjectId()
{
- return $this->project_id;
+ return $this->projectId;
}
/**
@@ -143,10 +157,12 @@ abstract class Base extends \Kanboard\Core\Base
* @access public
* @param string $name Parameter name
* @param mixed $value Value
+ * @param Base
*/
public function setParam($name, $value)
{
$this->params[$name] = $value;
+ return $this;
}
/**
@@ -154,24 +170,25 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access public
* @param string $name Parameter name
- * @param mixed $default_value Default value
+ * @param mixed $default Default value
* @return mixed
*/
- public function getParam($name, $default_value = null)
+ public function getParam($name, $default = null)
{
- return isset($this->params[$name]) ? $this->params[$name] : $default_value;
+ return isset($this->params[$name]) ? $this->params[$name] : $default;
}
/**
* Check if an action is executable (right project and required parameters)
*
* @access public
- * @param array $data Event data dictionary
- * @return bool True if the action is executable
+ * @param array $data
+ * @param string $eventName
+ * @return bool
*/
- public function isExecutable(array $data)
+ public function isExecutable(array $data, $eventName)
{
- return $this->hasCompatibleEvent() &&
+ return $this->hasCompatibleEvent($eventName) &&
$this->hasRequiredProject($data) &&
$this->hasRequiredParameters($data) &&
$this->hasRequiredCondition($data);
@@ -181,11 +198,12 @@ abstract class Base extends \Kanboard\Core\Base
* Check if the event is compatible with the action
*
* @access public
+ * @param string $eventName
* @return bool
*/
- public function hasCompatibleEvent()
+ public function hasCompatibleEvent($eventName)
{
- return in_array($this->event_name, $this->getCompatibleEvents());
+ return in_array($eventName, $this->getEvents());
}
/**
@@ -197,7 +215,7 @@ abstract class Base extends \Kanboard\Core\Base
*/
public function hasRequiredProject(array $data)
{
- return isset($data['project_id']) && $data['project_id'] == $this->project_id;
+ return isset($data['project_id']) && $data['project_id'] == $this->getProjectId();
}
/**
@@ -222,10 +240,11 @@ abstract class Base extends \Kanboard\Core\Base
* Execute the action
*
* @access public
- * @param \Event\GenericEvent $event Event data dictionary
- * @return bool True if the action was executed or false when not executed
+ * @param \Kanboard\Event\GenericEvent $event
+ * @param string $eventName
+ * @return bool
*/
- public function execute(GenericEvent $event)
+ public function execute(GenericEvent $event, $eventName)
{
// Avoid infinite loop, a listener instance can be called only one time
if ($this->called) {
@@ -233,17 +252,44 @@ abstract class Base extends \Kanboard\Core\Base
}
$data = $event->getAll();
- $result = false;
+ $executable = $this->isExecutable($data, $eventName);
+ $executed = false;
- if ($this->isExecutable($data)) {
+ if ($executable) {
$this->called = true;
- $result = $this->doAction($data);
+ $executed = $this->doAction($data);
}
- if (DEBUG) {
- $this->container['logger']->debug(get_called_class().' => '.($result ? 'true' : 'false'));
+ $this->logger->debug($this.' ['.$eventName.'] => executable='.var_export($executable, true).' exec_success='.var_export($executed, true));
+
+ return $executed;
+ }
+
+ /**
+ * Register a new event for the automatic action
+ *
+ * @access public
+ * @param string $event
+ * @param string $description
+ */
+ public function addEvent($event, $description = '')
+ {
+ if ($description !== '') {
+ $this->eventManager->register($event, $description);
}
- return $result;
+ $this->compatibleEvents[] = $event;
+ return $this;
+ }
+
+ /**
+ * Get all compatible events of an automatic action
+ *
+ * @access public
+ * @return array
+ */
+ public function getEvents()
+ {
+ return array_unique(array_merge($this->getCompatibleEvents(), $this->compatibleEvents));
}
}
diff --git a/app/Action/CommentCreation.php b/app/Action/CommentCreation.php
index 73fedc3b..b91e39e2 100644
--- a/app/Action/CommentCreation.php
+++ b/app/Action/CommentCreation.php
@@ -2,10 +2,6 @@
namespace Kanboard\Action;
-use Kanboard\Integration\BitbucketWebhook;
-use Kanboard\Integration\GithubWebhook;
-use Kanboard\Integration\GitlabWebhook;
-
/**
* Create automatically a comment from a webhook
*
@@ -15,6 +11,17 @@ use Kanboard\Integration\GitlabWebhook;
class CommentCreation extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Create a comment from an external provider');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -22,14 +29,7 @@ class CommentCreation extends Base
*/
public function getCompatibleEvents()
{
- return array(
- GithubWebhook::EVENT_ISSUE_COMMENT,
- GithubWebhook::EVENT_COMMIT,
- BitbucketWebhook::EVENT_ISSUE_COMMENT,
- BitbucketWebhook::EVENT_COMMIT,
- GitlabWebhook::EVENT_COMMIT,
- GitlabWebhook::EVENT_ISSUE_COMMENT,
- );
+ return array();
}
/**
@@ -67,9 +67,9 @@ class CommentCreation extends Base
{
return (bool) $this->comment->create(array(
'reference' => isset($data['reference']) ? $data['reference'] : '',
- 'comment' => empty($data['comment']) ? $data['commit_comment'] : $data['comment'],
+ 'comment' => $data['comment'],
'task_id' => $data['task_id'],
- 'user_id' => empty($data['user_id']) ? 0 : $data['user_id'],
+ 'user_id' => isset($data['user_id']) && $this->projectPermission->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0,
));
}
@@ -82,6 +82,6 @@ class CommentCreation extends Base
*/
public function hasRequiredCondition(array $data)
{
- return ! empty($data['comment']) || ! empty($data['commit_comment']);
+ return ! empty($data['comment']);
}
}
diff --git a/app/Action/TaskLogMoveAnotherColumn.php b/app/Action/CommentCreationMoveTaskColumn.php
index a699c4ab..11224d67 100644
--- a/app/Action/TaskLogMoveAnotherColumn.php
+++ b/app/Action/CommentCreationMoveTaskColumn.php
@@ -5,14 +5,25 @@ namespace Kanboard\Action;
use Kanboard\Model\Task;
/**
- * Add a log of the triggering event to the task description.
+ * Add a comment of the triggering event to the task description.
*
* @package action
* @author Oren Ben-Kiki
*/
-class TaskLogMoveAnotherColumn extends Base
+class CommentCreationMoveTaskColumn extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Add a comment log when moving the task between columns');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -60,7 +71,7 @@ class TaskLogMoveAnotherColumn extends Base
return false;
}
- $column = $this->board->getColumn($data['column_id']);
+ $column = $this->column->getById($data['column_id']);
return (bool) $this->comment->create(array(
'comment' => t('Moved to column %s', $column['title']),
diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php
index ffa1ac2a..f5085cb0 100644
--- a/app/Action/TaskAssignCategoryColor.php
+++ b/app/Action/TaskAssignCategoryColor.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskAssignCategoryColor extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign automatically a category based on a color');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
diff --git a/app/Action/TaskAssignCategoryLabel.php b/app/Action/TaskAssignCategoryLabel.php
index 0ef474b6..95fa116e 100644
--- a/app/Action/TaskAssignCategoryLabel.php
+++ b/app/Action/TaskAssignCategoryLabel.php
@@ -2,8 +2,6 @@
namespace Kanboard\Action;
-use Kanboard\Integration\GithubWebhook;
-
/**
* Set a category automatically according to a label
*
@@ -13,6 +11,17 @@ use Kanboard\Integration\GithubWebhook;
class TaskAssignCategoryLabel extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Change the category based on an external label');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -20,9 +29,7 @@ class TaskAssignCategoryLabel extends Base
*/
public function getCompatibleEvents()
{
- return array(
- GithubWebhook::EVENT_ISSUE_LABEL_CHANGE,
- );
+ return array();
}
/**
@@ -64,7 +71,7 @@ class TaskAssignCategoryLabel extends Base
{
$values = array(
'id' => $data['task_id'],
- 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'),
+ 'category_id' => $this->getParam('category_id'),
);
return $this->taskModification->update($values);
@@ -79,6 +86,6 @@ class TaskAssignCategoryLabel extends Base
*/
public function hasRequiredCondition(array $data)
{
- return $data['label'] == $this->getParam('label');
+ return $data['label'] == $this->getParam('label') && empty($data['category_id']);
}
}
diff --git a/app/Action/TaskAssignCategoryLink.php b/app/Action/TaskAssignCategoryLink.php
new file mode 100644
index 00000000..b39e41b4
--- /dev/null
+++ b/app/Action/TaskAssignCategoryLink.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\TaskLink;
+
+/**
+ * Set a category automatically according to a task link
+ *
+ * @package action
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class TaskAssignCategoryLink extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign automatically a category based on a link');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskLink::EVENT_CREATE_UPDATE,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'category_id' => t('Category'),
+ 'link_id' => t('Link type'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'link_id',
+ );
+ }
+
+ /**
+ * Execute the action (change the category)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $values = array(
+ 'id' => $data['task_id'],
+ 'category_id' => $this->getParam('category_id'),
+ );
+
+ return $this->taskModification->update($values);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ if ($data['link_id'] == $this->getParam('link_id')) {
+ $task = $this->taskFinder->getById($data['task_id']);
+ return empty($task['category_id']);
+ }
+
+ return false;
+ }
+}
diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php
index a2332f78..139c24cb 100644
--- a/app/Action/TaskAssignColorCategory.php
+++ b/app/Action/TaskAssignColorCategory.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskAssignColorCategory extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign automatically a color based on a category');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -67,7 +78,7 @@ class TaskAssignColorCategory extends Base
'color_id' => $this->getParam('color_id'),
);
- return $this->taskModification->update($values);
+ return $this->taskModification->update($values, false);
}
/**
diff --git a/app/Action/TaskAssignColorColumn.php b/app/Action/TaskAssignColorColumn.php
index 53140733..92412739 100644
--- a/app/Action/TaskAssignColorColumn.php
+++ b/app/Action/TaskAssignColorColumn.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskAssignColorColumn extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign a color when the task is moved to a specific column');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -68,7 +79,7 @@ class TaskAssignColorColumn extends Base
'color_id' => $this->getParam('color_id'),
);
- return $this->taskModification->update($values);
+ return $this->taskModification->update($values, false);
}
/**
diff --git a/app/Action/TaskAssignColorLink.php b/app/Action/TaskAssignColorLink.php
index 67b2ef62..12ceabb3 100644
--- a/app/Action/TaskAssignColorLink.php
+++ b/app/Action/TaskAssignColorLink.php
@@ -13,6 +13,17 @@ use Kanboard\Model\TaskLink;
class TaskAssignColorLink extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Change task color when using a specific task link');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -67,7 +78,7 @@ class TaskAssignColorLink extends Base
'color_id' => $this->getParam('color_id'),
);
- return $this->taskModification->update($values);
+ return $this->taskModification->update($values, false);
}
/**
diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php
index 6bf02c36..6ec8ce95 100644
--- a/app/Action/TaskAssignColorUser.php
+++ b/app/Action/TaskAssignColorUser.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskAssignColorUser extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign a color to a specific user');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -68,7 +79,7 @@ class TaskAssignColorUser extends Base
'color_id' => $this->getParam('color_id'),
);
- return $this->taskModification->update($values);
+ return $this->taskModification->update($values, false);
}
/**
diff --git a/app/Action/TaskAssignCurrentUser.php b/app/Action/TaskAssignCurrentUser.php
index f34c4f36..192a120c 100644
--- a/app/Action/TaskAssignCurrentUser.php
+++ b/app/Action/TaskAssignCurrentUser.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskAssignCurrentUser extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign the task to the person who does the action');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -22,7 +33,6 @@ class TaskAssignCurrentUser extends Base
{
return array(
Task::EVENT_CREATE,
- Task::EVENT_MOVE_COLUMN,
);
}
@@ -34,9 +44,7 @@ class TaskAssignCurrentUser extends Base
*/
public function getActionRequiredParameters()
{
- return array(
- 'column_id' => t('Column'),
- );
+ return array();
}
/**
@@ -49,7 +57,6 @@ class TaskAssignCurrentUser extends Base
{
return array(
'task_id',
- 'column_id',
);
}
@@ -83,6 +90,6 @@ class TaskAssignCurrentUser extends Base
*/
public function hasRequiredCondition(array $data)
{
- return $data['column_id'] == $this->getParam('column_id');
+ return true;
}
}
diff --git a/app/Action/TaskAssignCurrentUserColumn.php b/app/Action/TaskAssignCurrentUserColumn.php
new file mode 100644
index 00000000..05d08dd3
--- /dev/null
+++ b/app/Action/TaskAssignCurrentUserColumn.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\Task;
+
+/**
+ * Assign a task to the logged user on column change
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskAssignCurrentUserColumn extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign the task to the person who does the action when the column is changed');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ Task::EVENT_MOVE_COLUMN,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'column_id' => t('Column'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'column_id',
+ );
+ }
+
+ /**
+ * Execute the action
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ if (! $this->userSession->isLogged()) {
+ return false;
+ }
+
+ $values = array(
+ 'id' => $data['task_id'],
+ 'owner_id' => $this->userSession->getId(),
+ );
+
+ return $this->taskModification->update($values);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php
index dfcb281b..2dc3e966 100644
--- a/app/Action/TaskAssignSpecificUser.php
+++ b/app/Action/TaskAssignSpecificUser.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskAssignSpecificUser extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign the task to a specific user');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
diff --git a/app/Action/TaskAssignUser.php b/app/Action/TaskAssignUser.php
index a5821729..da54d186 100644
--- a/app/Action/TaskAssignUser.php
+++ b/app/Action/TaskAssignUser.php
@@ -2,9 +2,6 @@
namespace Kanboard\Action;
-use Kanboard\Integration\GithubWebhook;
-use Kanboard\Integration\BitbucketWebhook;
-
/**
* Assign a task to someone
*
@@ -14,6 +11,17 @@ use Kanboard\Integration\BitbucketWebhook;
class TaskAssignUser extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Change the assignee based on an external username');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -21,10 +29,7 @@ class TaskAssignUser extends Base
*/
public function getCompatibleEvents()
{
- return array(
- GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE,
- BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE,
- );
+ return array();
}
/**
@@ -78,6 +83,6 @@ class TaskAssignUser extends Base
*/
public function hasRequiredCondition(array $data)
{
- return true;
+ return $this->projectPermission->isAssignable($this->getProjectId(), $data['owner_id']);
}
}
diff --git a/app/Action/TaskClose.php b/app/Action/TaskClose.php
index d80bd023..cf91e83e 100644
--- a/app/Action/TaskClose.php
+++ b/app/Action/TaskClose.php
@@ -2,11 +2,6 @@
namespace Kanboard\Action;
-use Kanboard\Integration\GitlabWebhook;
-use Kanboard\Integration\GithubWebhook;
-use Kanboard\Integration\BitbucketWebhook;
-use Kanboard\Model\Task;
-
/**
* Close automatically a task
*
@@ -16,6 +11,17 @@ use Kanboard\Model\Task;
class TaskClose extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Close a task');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -23,15 +29,7 @@ class TaskClose extends Base
*/
public function getCompatibleEvents()
{
- return array(
- Task::EVENT_MOVE_COLUMN,
- GithubWebhook::EVENT_COMMIT,
- GithubWebhook::EVENT_ISSUE_CLOSED,
- GitlabWebhook::EVENT_COMMIT,
- GitlabWebhook::EVENT_ISSUE_CLOSED,
- BitbucketWebhook::EVENT_COMMIT,
- BitbucketWebhook::EVENT_ISSUE_CLOSED,
- );
+ return array();
}
/**
@@ -42,17 +40,7 @@ class TaskClose extends Base
*/
public function getActionRequiredParameters()
{
- switch ($this->event_name) {
- case GithubWebhook::EVENT_COMMIT:
- case GithubWebhook::EVENT_ISSUE_CLOSED:
- case GitlabWebhook::EVENT_COMMIT:
- case GitlabWebhook::EVENT_ISSUE_CLOSED:
- case BitbucketWebhook::EVENT_COMMIT:
- case BitbucketWebhook::EVENT_ISSUE_CLOSED:
- return array();
- default:
- return array('column_id' => t('Column'));
- }
+ return array();
}
/**
@@ -63,17 +51,7 @@ class TaskClose extends Base
*/
public function getEventRequiredParameters()
{
- switch ($this->event_name) {
- case GithubWebhook::EVENT_COMMIT:
- case GithubWebhook::EVENT_ISSUE_CLOSED:
- case GitlabWebhook::EVENT_COMMIT:
- case GitlabWebhook::EVENT_ISSUE_CLOSED:
- case BitbucketWebhook::EVENT_COMMIT:
- case BitbucketWebhook::EVENT_ISSUE_CLOSED:
- return array('task_id');
- default:
- return array('task_id', 'column_id');
- }
+ return array('task_id');
}
/**
@@ -97,16 +75,6 @@ class TaskClose extends Base
*/
public function hasRequiredCondition(array $data)
{
- switch ($this->event_name) {
- case GithubWebhook::EVENT_COMMIT:
- case GithubWebhook::EVENT_ISSUE_CLOSED:
- case GitlabWebhook::EVENT_COMMIT:
- case GitlabWebhook::EVENT_ISSUE_CLOSED:
- case BitbucketWebhook::EVENT_COMMIT:
- case BitbucketWebhook::EVENT_ISSUE_CLOSED:
- return true;
- default:
- return $data['column_id'] == $this->getParam('column_id');
- }
+ return true;
}
}
diff --git a/app/Action/TaskCloseColumn.php b/app/Action/TaskCloseColumn.php
new file mode 100644
index 00000000..09af3b96
--- /dev/null
+++ b/app/Action/TaskCloseColumn.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\Task;
+
+/**
+ * Close automatically a task in a specific column
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskCloseColumn extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Close a task in a specific column');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ Task::EVENT_MOVE_COLUMN,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array('column_id' => t('Column'));
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('task_id', 'column_id');
+ }
+
+ /**
+ * Execute the action (close the task)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ return $this->taskStatus->close($data['task_id']);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/app/Action/TaskCloseNoActivity.php b/app/Action/TaskCloseNoActivity.php
new file mode 100644
index 00000000..59f7f56a
--- /dev/null
+++ b/app/Action/TaskCloseNoActivity.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\Task;
+
+/**
+ * Close automatically a task after when inactive
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskCloseNoActivity extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Close a task when there is no activity');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(Task::EVENT_DAILY_CRONJOB);
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'duration' => t('Duration in days')
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+ }
+
+ /**
+ * Execute the action (close the task)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+
+ foreach ($data['tasks'] as $task) {
+ $duration = time() - $task['date_modification'];
+
+ if ($duration > $max) {
+ $results[] = $this->taskStatus->close($task['id']);
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+}
diff --git a/app/Action/TaskCreation.php b/app/Action/TaskCreation.php
index af1403f0..290c31e1 100644
--- a/app/Action/TaskCreation.php
+++ b/app/Action/TaskCreation.php
@@ -2,10 +2,6 @@
namespace Kanboard\Action;
-use Kanboard\Integration\GithubWebhook;
-use Kanboard\Integration\GitlabWebhook;
-use Kanboard\Integration\BitbucketWebhook;
-
/**
* Create automatically a task from a webhook
*
@@ -15,6 +11,17 @@ use Kanboard\Integration\BitbucketWebhook;
class TaskCreation extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Create a task from an external provider');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -22,11 +29,7 @@ class TaskCreation extends Base
*/
public function getCompatibleEvents()
{
- return array(
- GithubWebhook::EVENT_ISSUE_OPENED,
- GitlabWebhook::EVENT_ISSUE_OPENED,
- BitbucketWebhook::EVENT_ISSUE_OPENED,
- );
+ return array();
}
/**
diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php
index 1f6684dd..5f05136e 100644
--- a/app/Action/TaskDuplicateAnotherProject.php
+++ b/app/Action/TaskDuplicateAnotherProject.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskDuplicateAnotherProject extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Duplicate the task to another project');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -51,7 +62,6 @@ class TaskDuplicateAnotherProject extends Base
return array(
'task_id',
'column_id',
- 'project_id',
);
}
@@ -64,8 +74,7 @@ class TaskDuplicateAnotherProject extends Base
*/
public function doAction(array $data)
{
- $destination_column_id = $this->board->getFirstColumn($this->getParam('project_id'));
-
+ $destination_column_id = $this->column->getFirstColumnId($this->getParam('project_id'));
return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id);
}
diff --git a/app/Action/TaskEmail.php b/app/Action/TaskEmail.php
index 7fb76c4c..4e0e06a6 100644
--- a/app/Action/TaskEmail.php
+++ b/app/Action/TaskEmail.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskEmail extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Send a task by email to someone');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
diff --git a/app/Action/TaskEmailNoActivity.php b/app/Action/TaskEmailNoActivity.php
new file mode 100644
index 00000000..c5d7a797
--- /dev/null
+++ b/app/Action/TaskEmailNoActivity.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\Task;
+
+/**
+ * Email a task with no activity
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskEmailNoActivity extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Send email when there is no activity on a task');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ Task::EVENT_DAILY_CRONJOB,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'user_id' => t('User that will receive the email'),
+ 'subject' => t('Email subject'),
+ 'duration' => t('Duration in days'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+
+ /**
+ * Execute the action (move the task to another column)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+ $user = $this->user->getById($this->getParam('user_id'));
+
+ if (! empty($user['email'])) {
+ foreach ($data['tasks'] as $task) {
+ $duration = time() - $task['date_modification'];
+
+ if ($duration > $max) {
+ $results[] = $this->sendEmail($task['id'], $user);
+ }
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+
+ /**
+ * Send email
+ *
+ * @access private
+ * @param integer $task_id
+ * @param array $user
+ * @return boolean
+ */
+ private function sendEmail($task_id, array $user)
+ {
+ $task = $this->taskFinder->getDetails($task_id);
+
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url')))
+ );
+
+ return true;
+ }
+}
diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php
index 476e2036..fdff0d8c 100644
--- a/app/Action/TaskMoveAnotherProject.php
+++ b/app/Action/TaskMoveAnotherProject.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskMoveAnotherProject extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Move the task to another project');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
diff --git a/app/Action/TaskMoveColumnAssigned.php b/app/Action/TaskMoveColumnAssigned.php
index 16622ee4..1b23a591 100644
--- a/app/Action/TaskMoveColumnAssigned.php
+++ b/app/Action/TaskMoveColumnAssigned.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskMoveColumnAssigned extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Move the task to another column when assigned to a user');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -51,7 +62,6 @@ class TaskMoveColumnAssigned extends Base
return array(
'task_id',
'column_id',
- 'project_id',
'owner_id'
);
}
diff --git a/app/Action/TaskMoveColumnCategoryChange.php b/app/Action/TaskMoveColumnCategoryChange.php
index 1e12be4a..0f591eda 100644
--- a/app/Action/TaskMoveColumnCategoryChange.php
+++ b/app/Action/TaskMoveColumnCategoryChange.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskMoveColumnCategoryChange extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Move the task to another column when the category is changed');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -50,7 +61,6 @@ class TaskMoveColumnCategoryChange extends Base
return array(
'task_id',
'column_id',
- 'project_id',
'category_id',
);
}
@@ -71,7 +81,8 @@ class TaskMoveColumnCategoryChange extends Base
$data['task_id'],
$this->getParam('dest_column_id'),
$original_task['position'],
- $original_task['swimlane_id']
+ $original_task['swimlane_id'],
+ false
);
}
diff --git a/app/Action/TaskMoveColumnUnAssigned.php b/app/Action/TaskMoveColumnUnAssigned.php
index 617c75a8..99ef9351 100644
--- a/app/Action/TaskMoveColumnUnAssigned.php
+++ b/app/Action/TaskMoveColumnUnAssigned.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskMoveColumnUnAssigned extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Move the task to another column when assignee is cleared');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -51,7 +62,6 @@ class TaskMoveColumnUnAssigned extends Base
return array(
'task_id',
'column_id',
- 'project_id',
'owner_id'
);
}
diff --git a/app/Action/TaskOpen.php b/app/Action/TaskOpen.php
index 2e53efae..ec0f96f7 100644
--- a/app/Action/TaskOpen.php
+++ b/app/Action/TaskOpen.php
@@ -2,9 +2,6 @@
namespace Kanboard\Action;
-use Kanboard\Integration\GithubWebhook;
-use Kanboard\Integration\BitbucketWebhook;
-
/**
* Open automatically a task
*
@@ -14,6 +11,17 @@ use Kanboard\Integration\BitbucketWebhook;
class TaskOpen extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Open a task');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -21,10 +29,7 @@ class TaskOpen extends Base
*/
public function getCompatibleEvents()
{
- return array(
- GithubWebhook::EVENT_ISSUE_REOPENED,
- BitbucketWebhook::EVENT_ISSUE_REOPENED,
- );
+ return array();
}
/**
diff --git a/app/Action/TaskUpdateStartDate.php b/app/Action/TaskUpdateStartDate.php
index 4cd548af..e5cea01b 100644
--- a/app/Action/TaskUpdateStartDate.php
+++ b/app/Action/TaskUpdateStartDate.php
@@ -13,6 +13,17 @@ use Kanboard\Model\Task;
class TaskUpdateStartDate extends Base
{
/**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Automatically update the start date');
+ }
+
+ /**
* Get the list of compatible events
*
* @access public
@@ -66,7 +77,7 @@ class TaskUpdateStartDate extends Base
'date_started' => time(),
);
- return $this->taskModification->update($values);
+ return $this->taskModification->update($values, false);
}
/**
diff --git a/app/Analytic/AverageLeadCycleTimeAnalytic.php b/app/Analytic/AverageLeadCycleTimeAnalytic.php
new file mode 100644
index 00000000..fd85f864
--- /dev/null
+++ b/app/Analytic/AverageLeadCycleTimeAnalytic.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Kanboard\Analytic;
+
+use Kanboard\Core\Base;
+use Kanboard\Model\Task;
+
+/**
+ * Average Lead and Cycle Time
+ *
+ * @package analytic
+ * @author Frederic Guillot
+ */
+class AverageLeadCycleTimeAnalytic extends Base
+{
+ /**
+ * Build report
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function build($project_id)
+ {
+ $stats = array(
+ 'count' => 0,
+ 'total_lead_time' => 0,
+ 'total_cycle_time' => 0,
+ 'avg_lead_time' => 0,
+ 'avg_cycle_time' => 0,
+ );
+
+ $tasks = $this->getTasks($project_id);
+
+ foreach ($tasks as &$task) {
+ $stats['count']++;
+ $stats['total_lead_time'] += $this->calculateLeadTime($task);
+ $stats['total_cycle_time'] += $this->calculateCycleTime($task);
+ }
+
+ $stats['avg_lead_time'] = $this->calculateAverage($stats, 'total_lead_time');
+ $stats['avg_cycle_time'] = $this->calculateAverage($stats, 'total_cycle_time');
+
+ return $stats;
+ }
+
+ /**
+ * Calculate average
+ *
+ * @access private
+ * @param array &$stats
+ * @param string $field
+ * @return float
+ */
+ private function calculateAverage(array &$stats, $field)
+ {
+ if ($stats['count'] > 0) {
+ return (int) ($stats[$field] / $stats['count']);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Calculate lead time
+ *
+ * @access private
+ * @param array &$task
+ * @return integer
+ */
+ private function calculateLeadTime(array &$task)
+ {
+ $end = $task['date_completed'] ?: time();
+ $start = $task['date_creation'];
+
+ return $end - $start;
+ }
+
+ /**
+ * Calculate cycle time
+ *
+ * @access private
+ * @param array &$task
+ * @return integer
+ */
+ private function calculateCycleTime(array &$task)
+ {
+ if (empty($task['date_started'])) {
+ return 0;
+ }
+
+ $end = $task['date_completed'] ?: time();
+ $start = $task['date_started'];
+
+ return $end - $start;
+ }
+
+ /**
+ * Get the 1000 last created tasks
+ *
+ * @access private
+ * @return array
+ */
+ private function getTasks($project_id)
+ {
+ return $this->db
+ ->table(Task::TABLE)
+ ->columns('date_completed', 'date_creation', 'date_started')
+ ->eq('project_id', $project_id)
+ ->desc('id')
+ ->limit(1000)
+ ->findAll();
+ }
+}
diff --git a/app/Analytic/AverageTimeSpentColumnAnalytic.php b/app/Analytic/AverageTimeSpentColumnAnalytic.php
new file mode 100644
index 00000000..bef55419
--- /dev/null
+++ b/app/Analytic/AverageTimeSpentColumnAnalytic.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Kanboard\Analytic;
+
+use Kanboard\Core\Base;
+use Kanboard\Model\Task;
+
+/**
+ * Average Time Spent by Column
+ *
+ * @package analytic
+ * @author Frederic Guillot
+ */
+class AverageTimeSpentColumnAnalytic extends Base
+{
+ /**
+ * Build report
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function build($project_id)
+ {
+ $stats = $this->initialize($project_id);
+
+ $this->processTasks($stats, $project_id);
+ $this->calculateAverage($stats);
+
+ return $stats;
+ }
+
+ /**
+ * Initialize default values for each column
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function initialize($project_id)
+ {
+ $stats = array();
+ $columns = $this->column->getList($project_id);
+
+ foreach ($columns as $column_id => $column_title) {
+ $stats[$column_id] = array(
+ 'count' => 0,
+ 'time_spent' => 0,
+ 'average' => 0,
+ 'title' => $column_title,
+ );
+ }
+
+ return $stats;
+ }
+
+ /**
+ * Calculate time spent for each tasks for each columns
+ *
+ * @access private
+ * @param array $stats
+ * @param integer $project_id
+ */
+ private function processTasks(array &$stats, $project_id)
+ {
+ $tasks = $this->getTasks($project_id);
+
+ foreach ($tasks as &$task) {
+ foreach ($this->getTaskTimeByColumns($task) as $column_id => $time_spent) {
+ if (isset($stats[$column_id])) {
+ $stats[$column_id]['count']++;
+ $stats[$column_id]['time_spent'] += $time_spent;
+ }
+ }
+ }
+ }
+
+ /**
+ * Calculate averages
+ *
+ * @access private
+ * @param array $stats
+ */
+ private function calculateAverage(array &$stats)
+ {
+ foreach ($stats as &$column) {
+ $this->calculateColumnAverage($column);
+ }
+ }
+
+ /**
+ * Calculate column average
+ *
+ * @access private
+ * @param array $column
+ */
+ private function calculateColumnAverage(array &$column)
+ {
+ if ($column['count'] > 0) {
+ $column['average'] = (int) ($column['time_spent'] / $column['count']);
+ }
+ }
+
+ /**
+ * Get time spent for each column for a given task
+ *
+ * @access private
+ * @param array $task
+ * @return array
+ */
+ private function getTaskTimeByColumns(array &$task)
+ {
+ $columns = $this->transition->getTimeSpentByTask($task['id']);
+
+ if (! isset($columns[$task['column_id']])) {
+ $columns[$task['column_id']] = 0;
+ }
+
+ $columns[$task['column_id']] += $this->getTaskTimeSpentInCurrentColumn($task);
+
+ return $columns;
+ }
+
+ /**
+ * Calculate time spent of a task in the current column
+ *
+ * @access private
+ * @param array $task
+ */
+ private function getTaskTimeSpentInCurrentColumn(array &$task)
+ {
+ $end = $task['date_completed'] ?: time();
+ return $end - $task['date_moved'];
+ }
+
+ /**
+ * Fetch the last 1000 tasks
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getTasks($project_id)
+ {
+ return $this->db
+ ->table(Task::TABLE)
+ ->columns('id', 'date_completed', 'date_moved', 'column_id')
+ ->eq('project_id', $project_id)
+ ->desc('id')
+ ->limit(1000)
+ ->findAll();
+ }
+}
diff --git a/app/Analytic/EstimatedTimeComparisonAnalytic.php b/app/Analytic/EstimatedTimeComparisonAnalytic.php
new file mode 100644
index 00000000..490bcd50
--- /dev/null
+++ b/app/Analytic/EstimatedTimeComparisonAnalytic.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Kanboard\Analytic;
+
+use Kanboard\Core\Base;
+use Kanboard\Model\Task;
+
+/**
+ * Estimated/Spent Time Comparison
+ *
+ * @package analytic
+ * @author Frederic Guillot
+ */
+class EstimatedTimeComparisonAnalytic extends Base
+{
+ /**
+ * Build report
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function build($project_id)
+ {
+ $rows = $this->db->table(Task::TABLE)
+ ->columns('SUM(time_estimated) AS time_estimated', 'SUM(time_spent) AS time_spent', 'is_active')
+ ->eq('project_id', $project_id)
+ ->groupBy('is_active')
+ ->findAll();
+
+ $metrics = array(
+ 'open' => array(
+ 'time_spent' => 0,
+ 'time_estimated' => 0,
+ ),
+ 'closed' => array(
+ 'time_spent' => 0,
+ 'time_estimated' => 0,
+ ),
+ );
+
+ foreach ($rows as $row) {
+ $key = $row['is_active'] == Task::STATUS_OPEN ? 'open' : 'closed';
+ $metrics[$key]['time_spent'] = $row['time_spent'];
+ $metrics[$key]['time_estimated'] = $row['time_estimated'];
+ }
+
+ return $metrics;
+ }
+}
diff --git a/app/Analytic/TaskDistributionAnalytic.php b/app/Analytic/TaskDistributionAnalytic.php
new file mode 100644
index 00000000..838652e3
--- /dev/null
+++ b/app/Analytic/TaskDistributionAnalytic.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Kanboard\Analytic;
+
+use Kanboard\Core\Base;
+
+/**
+ * Task Distribution
+ *
+ * @package analytic
+ * @author Frederic Guillot
+ */
+class TaskDistributionAnalytic extends Base
+{
+ /**
+ * Build report
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function build($project_id)
+ {
+ $metrics = array();
+ $total = 0;
+ $columns = $this->column->getAll($project_id);
+
+ foreach ($columns as $column) {
+ $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
+ $total += $nb_tasks;
+
+ $metrics[] = array(
+ 'column_title' => $column['title'],
+ 'nb_tasks' => $nb_tasks,
+ );
+ }
+
+ if ($total === 0) {
+ return array();
+ }
+
+ foreach ($metrics as &$metric) {
+ $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
+ }
+
+ return $metrics;
+ }
+}
diff --git a/app/Analytic/UserDistributionAnalytic.php b/app/Analytic/UserDistributionAnalytic.php
new file mode 100644
index 00000000..e1815f9c
--- /dev/null
+++ b/app/Analytic/UserDistributionAnalytic.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Analytic;
+
+use Kanboard\Core\Base;
+
+/**
+ * User Distribution
+ *
+ * @package analytic
+ * @author Frederic Guillot
+ */
+class UserDistributionAnalytic extends Base
+{
+ /**
+ * Build Report
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function build($project_id)
+ {
+ $metrics = array();
+ $total = 0;
+ $tasks = $this->taskFinder->getAll($project_id);
+ $users = $this->projectUserRole->getAssignableUsersList($project_id);
+
+ foreach ($tasks as $task) {
+ $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
+ $total++;
+
+ if (! isset($metrics[$user])) {
+ $metrics[$user] = array(
+ 'nb_tasks' => 0,
+ 'percentage' => 0,
+ 'user' => $user,
+ );
+ }
+
+ $metrics[$user]['nb_tasks']++;
+ }
+
+ if ($total === 0) {
+ return array();
+ }
+
+ foreach ($metrics as &$metric) {
+ $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
+ }
+
+ ksort($metrics);
+
+ return array_values($metrics);
+ }
+}
diff --git a/app/Api/Action.php b/app/Api/Action.php
index 0ae91f10..9e3b86f6 100644
--- a/app/Api/Action.php
+++ b/app/Api/Action.php
@@ -12,17 +12,17 @@ class Action extends \Kanboard\Core\Base
{
public function getAvailableActions()
{
- return $this->action->getAvailableActions();
+ return $this->actionManager->getAvailableActions();
}
public function getAvailableActionEvents()
{
- return $this->action->getAvailableEvents();
+ return $this->eventManager->getAll();
}
public function getCompatibleActionEvents($action_name)
{
- return $this->action->getCompatibleEvents($action_name);
+ return $this->actionManager->getCompatibleEvents($action_name);
}
public function removeAction($action_id)
@@ -32,22 +32,10 @@ class Action extends \Kanboard\Core\Base
public function getActions($project_id)
{
- $actions = $this->action->getAllByProject($project_id);
-
- foreach ($actions as $index => $action) {
- $params = array();
-
- foreach ($action['params'] as $param) {
- $params[$param['name']] = $param['value'];
- }
-
- $actions[$index]['params'] = $params;
- }
-
- return $actions;
+ return $this->action->getAllByProject($project_id);
}
- public function createAction($project_id, $event_name, $action_name, $params)
+ public function createAction($project_id, $event_name, $action_name, array $params)
{
$values = array(
'project_id' => $project_id,
@@ -56,23 +44,23 @@ class Action extends \Kanboard\Core\Base
'params' => $params,
);
- list($valid, ) = $this->action->validateCreation($values);
+ list($valid, ) = $this->actionValidator->validateCreation($values);
if (! $valid) {
return false;
}
// Check if the action exists
- $actions = $this->action->getAvailableActions();
+ $actions = $this->actionManager->getAvailableActions();
if (! isset($actions[$action_name])) {
return false;
}
// Check the event
- $action = $this->action->load($action_name, $project_id, $event_name);
+ $action = $this->actionManager->getAction($action_name);
- if (! in_array($event_name, $action->getCompatibleEvents())) {
+ if (! in_array($event_name, $action->getEvents())) {
return false;
}
diff --git a/app/Api/App.php b/app/Api/App.php
index d082bcfb..635f1ce2 100644
--- a/app/Api/App.php
+++ b/app/Api/App.php
@@ -34,4 +34,14 @@ class App extends \Kanboard\Core\Base
{
return $this->color->getList();
}
+
+ public function getApplicationRoles()
+ {
+ return $this->role->getApplicationRoles();
+ }
+
+ public function getProjectRoles()
+ {
+ return $this->role->getProjectRoles();
+ }
}
diff --git a/app/Api/Auth.php b/app/Api/Auth.php
index b3627e4b..c7c5298c 100644
--- a/app/Api/Auth.php
+++ b/app/Api/Auth.php
@@ -3,7 +3,6 @@
namespace Kanboard\Api;
use JsonRPC\AuthenticationFailure;
-use Symfony\Component\EventDispatcher\Event;
/**
* Base class
@@ -24,15 +23,58 @@ class Auth extends Base
*/
public function checkCredentials($username, $password, $class, $method)
{
- $this->container['dispatcher']->dispatch('api.bootstrap', new Event);
+ $this->dispatcher->dispatch('app.bootstrap');
- if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) {
+ if ($this->isUserAuthenticated($username, $password)) {
$this->checkProcedurePermission(true, $method);
- $this->userSession->refresh($this->user->getByUsername($username));
- } elseif ($username === 'jsonrpc' && $password === $this->config->get('api_token')) {
+ $this->userSession->initialize($this->user->getByUsername($username));
+ } elseif ($this->isAppAuthenticated($username, $password)) {
$this->checkProcedurePermission(false, $method);
} else {
throw new AuthenticationFailure('Wrong credentials');
}
}
+
+ /**
+ * Check user credentials
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @return boolean
+ */
+ private function isUserAuthenticated($username, $password)
+ {
+ return $username !== 'jsonrpc' &&
+ ! $this->userLocking->isLocked($username) &&
+ $this->authenticationManager->passwordAuthentication($username, $password);
+ }
+
+ /**
+ * Check administrative credentials
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @return boolean
+ */
+ private function isAppAuthenticated($username, $password)
+ {
+ return $username === 'jsonrpc' && $password === $this->getApiToken();
+ }
+
+ /**
+ * Get API Token
+ *
+ * @access private
+ * @return string
+ */
+ private function getApiToken()
+ {
+ if (defined('API_AUTHENTICATION_TOKEN')) {
+ return API_AUTHENTICATION_TOKEN;
+ }
+
+ return $this->config->get('api_token');
+ }
}
diff --git a/app/Api/Board.php b/app/Api/Board.php
index d615b1dc..185ac51a 100644
--- a/app/Api/Board.php
+++ b/app/Api/Board.php
@@ -15,39 +15,4 @@ class Board extends Base
$this->checkProjectPermission($project_id);
return $this->board->getBoard($project_id);
}
-
- public function getColumns($project_id)
- {
- return $this->board->getColumns($project_id);
- }
-
- public function getColumn($column_id)
- {
- return $this->board->getColumn($column_id);
- }
-
- public function moveColumnUp($project_id, $column_id)
- {
- return $this->board->moveUp($project_id, $column_id);
- }
-
- public function moveColumnDown($project_id, $column_id)
- {
- return $this->board->moveDown($project_id, $column_id);
- }
-
- public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
- {
- return $this->board->updateColumn($column_id, $title, $task_limit, $description);
- }
-
- public function addColumn($project_id, $title, $task_limit = 0, $description = '')
- {
- return $this->board->addColumn($project_id, $title, $task_limit, $description);
- }
-
- public function removeColumn($column_id)
- {
- return $this->board->removeColumn($column_id);
- }
}
diff --git a/app/Api/Category.php b/app/Api/Category.php
index 458eaef6..fbd61c56 100644
--- a/app/Api/Category.php
+++ b/app/Api/Category.php
@@ -32,7 +32,7 @@ class Category extends \Kanboard\Core\Base
'name' => $name,
);
- list($valid, ) = $this->category->validateCreation($values);
+ list($valid, ) = $this->categoryValidator->validateCreation($values);
return $valid ? $this->category->create($values) : false;
}
@@ -43,7 +43,7 @@ class Category extends \Kanboard\Core\Base
'name' => $name,
);
- list($valid, ) = $this->category->validateModification($values);
+ list($valid, ) = $this->categoryValidator->validateModification($values);
return $valid && $this->category->update($values);
}
}
diff --git a/app/Api/Column.php b/app/Api/Column.php
new file mode 100644
index 00000000..ddc3a5d0
--- /dev/null
+++ b/app/Api/Column.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Kanboard\Api;
+
+/**
+ * Column API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Column extends Base
+{
+ public function getColumns($project_id)
+ {
+ return $this->column->getAll($project_id);
+ }
+
+ public function getColumn($column_id)
+ {
+ return $this->column->getById($column_id);
+ }
+
+ public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
+ {
+ return $this->column->update($column_id, $title, $task_limit, $description);
+ }
+
+ public function addColumn($project_id, $title, $task_limit = 0, $description = '')
+ {
+ return $this->column->create($project_id, $title, $task_limit, $description);
+ }
+
+ public function removeColumn($column_id)
+ {
+ return $this->column->remove($column_id);
+ }
+
+ public function changeColumnPosition($project_id, $column_id, $position)
+ {
+ return $this->column->changePosition($project_id, $column_id, $position);
+ }
+}
diff --git a/app/Api/Comment.php b/app/Api/Comment.php
index 26b632e9..1fc1c708 100644
--- a/app/Api/Comment.php
+++ b/app/Api/Comment.php
@@ -25,15 +25,16 @@ class Comment extends \Kanboard\Core\Base
return $this->comment->remove($comment_id);
}
- public function createComment($task_id, $user_id, $content)
+ public function createComment($task_id, $user_id, $content, $reference = '')
{
$values = array(
'task_id' => $task_id,
'user_id' => $user_id,
'comment' => $content,
+ 'reference' => $reference,
);
- list($valid, ) = $this->comment->validateCreation($values);
+ list($valid, ) = $this->commentValidator->validateCreation($values);
return $valid ? $this->comment->create($values) : false;
}
@@ -45,7 +46,7 @@ class Comment extends \Kanboard\Core\Base
'comment' => $content,
);
- list($valid, ) = $this->comment->validateModification($values);
+ list($valid, ) = $this->commentValidator->validateModification($values);
return $valid && $this->comment->update($values);
}
}
diff --git a/app/Api/File.php b/app/Api/File.php
index be415ecb..71c31c76 100644
--- a/app/Api/File.php
+++ b/app/Api/File.php
@@ -10,45 +10,81 @@ use Kanboard\Core\ObjectStorage\ObjectStorageException;
* @package api
* @author Frederic Guillot
*/
-class File extends \Kanboard\Core\Base
+class File extends Base
{
- public function getFile($file_id)
+ public function getTaskFile($file_id)
{
- return $this->file->getById($file_id);
+ return $this->taskFile->getById($file_id);
}
- public function getAllFiles($task_id)
+ public function getAllTaskFiles($task_id)
{
- return $this->file->getAll($task_id);
+ return $this->taskFile->getAll($task_id);
}
- public function downloadFile($file_id)
+ public function downloadTaskFile($file_id)
{
try {
- $file = $this->file->getById($file_id);
+ $file = $this->taskFile->getById($file_id);
if (! empty($file)) {
return base64_encode($this->objectStorage->get($file['path']));
}
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
+ return '';
+ }
+ }
+
+ public function createTaskFile($project_id, $task_id, $filename, $blob)
+ {
+ try {
+ return $this->taskFile->uploadContent($task_id, $filename, $blob);
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
}
+ }
+
+ public function removeTaskFile($file_id)
+ {
+ return $this->taskFile->remove($file_id);
+ }
- return '';
+ public function removeAllTaskFiles($task_id)
+ {
+ return $this->taskFile->removeAll($task_id);
+ }
+
+ // Deprecated procedures
+
+ public function getFile($file_id)
+ {
+ return $this->getTaskFile($file_id);
+ }
+
+ public function getAllFiles($task_id)
+ {
+ return $this->getAllTaskFiles($task_id);
+ }
+
+ public function downloadFile($file_id)
+ {
+ return $this->downloadTaskFile($file_id);
}
public function createFile($project_id, $task_id, $filename, $blob)
{
- return $this->file->uploadContent($project_id, $task_id, $filename, $blob);
+ return $this->createTaskFile($project_id, $task_id, $filename, $blob);
}
public function removeFile($file_id)
{
- return $this->file->remove($file_id);
+ return $this->removeTaskFile($file_id);
}
public function removeAllFiles($task_id)
{
- return $this->file->removeAll($task_id);
+ return $this->removeAllTaskFiles($task_id);
}
}
diff --git a/app/Api/Group.php b/app/Api/Group.php
new file mode 100644
index 00000000..a1e0a73d
--- /dev/null
+++ b/app/Api/Group.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Kanboard\Api;
+
+/**
+ * Group API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Group extends \Kanboard\Core\Base
+{
+ public function createGroup($name, $external_id = '')
+ {
+ return $this->group->create($name, $external_id);
+ }
+
+ public function updateGroup($group_id, $name = null, $external_id = null)
+ {
+ $values = array(
+ 'id' => $group_id,
+ 'name' => $name,
+ 'external_id' => $external_id,
+ );
+
+ foreach ($values as $key => $value) {
+ if (is_null($value)) {
+ unset($values[$key]);
+ }
+ }
+
+ return $this->group->update($values);
+ }
+
+ public function removeGroup($group_id)
+ {
+ return $this->group->remove($group_id);
+ }
+
+ public function getGroup($group_id)
+ {
+ return $this->group->getById($group_id);
+ }
+
+ public function getAllGroups()
+ {
+ return $this->group->getAll();
+ }
+}
diff --git a/app/Api/GroupMember.php b/app/Api/GroupMember.php
new file mode 100644
index 00000000..de62f0c6
--- /dev/null
+++ b/app/Api/GroupMember.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Kanboard\Api;
+
+/**
+ * Group Member API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class GroupMember extends \Kanboard\Core\Base
+{
+ public function getGroupMembers($group_id)
+ {
+ return $this->groupMember->getMembers($group_id);
+ }
+
+ public function addGroupMember($group_id, $user_id)
+ {
+ return $this->groupMember->addUser($group_id, $user_id);
+ }
+
+ public function removeGroupMember($group_id, $user_id)
+ {
+ return $this->groupMember->removeUser($group_id, $user_id);
+ }
+
+ public function isGroupMember($group_id, $user_id)
+ {
+ return $this->groupMember->isMember($group_id, $user_id);
+ }
+}
diff --git a/app/Api/Link.php b/app/Api/Link.php
index d4df18fe..23a9916d 100644
--- a/app/Api/Link.php
+++ b/app/Api/Link.php
@@ -72,7 +72,7 @@ class Link extends \Kanboard\Core\Base
'opposite_label' => $opposite_label,
);
- list($valid, ) = $this->link->validateCreation($values);
+ list($valid, ) = $this->linkValidator->validateCreation($values);
return $valid ? $this->link->create($label, $opposite_label) : false;
}
@@ -93,7 +93,7 @@ class Link extends \Kanboard\Core\Base
'label' => $label,
);
- list($valid, ) = $this->link->validateModification($values);
+ list($valid, ) = $this->linkValidator->validateModification($values);
return $valid && $this->link->update($values);
}
diff --git a/app/Api/Me.php b/app/Api/Me.php
index 2c332a8c..ccc809ed 100644
--- a/app/Api/Me.php
+++ b/app/Api/Me.php
@@ -14,13 +14,13 @@ class Me extends Base
{
public function getMe()
{
- return $this->session['user'];
+ return $this->sessionStorage->user;
}
public function getMyDashboard()
{
$user_id = $this->userSession->getId();
- $projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll();
+ $projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))->findAll();
$tasks = $this->taskFinder->getUserQuery($user_id)->findAll();
return array(
@@ -32,25 +32,29 @@ class Me extends Base
public function getMyActivityStream()
{
- $project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
return $this->projectActivity->getProjects($project_ids, 100);
}
public function createMyPrivateProject($name, $description = null)
{
+ if ($this->config->get('disable_private_project', 0) == 1) {
+ return false;
+ }
+
$values = array(
'name' => $name,
'description' => $description,
'is_private' => 1,
);
- list($valid, ) = $this->project->validateCreation($values);
+ list($valid, ) = $this->projectValidator->validateCreation($values);
return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false;
}
public function getMyProjectsList()
{
- return $this->projectPermission->getMemberProjects($this->userSession->getId());
+ return $this->projectUserRole->getProjectsByUser($this->userSession->getId());
}
public function getMyOverdueTasks()
@@ -60,7 +64,7 @@ class Me extends Base
public function getMyProjects()
{
- $project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
$projects = $this->project->getAllByIds($project_ids);
return $this->formatProjects($projects);
diff --git a/app/Api/Project.php b/app/Api/Project.php
index f934432d..8e311f7f 100644
--- a/app/Api/Project.php
+++ b/app/Api/Project.php
@@ -69,7 +69,7 @@ class Project extends Base
'description' => $description
);
- list($valid, ) = $this->project->validateCreation($values);
+ list($valid, ) = $this->projectValidator->validateCreation($values);
return $valid ? $this->project->create($values) : false;
}
@@ -81,7 +81,7 @@ class Project extends Base
'description' => $description
);
- list($valid, ) = $this->project->validateModification($values);
+ list($valid, ) = $this->projectValidator->validateModification($values);
return $valid && $this->project->update($values);
}
}
diff --git a/app/Api/ProjectPermission.php b/app/Api/ProjectPermission.php
index 80323395..11e92af0 100644
--- a/app/Api/ProjectPermission.php
+++ b/app/Api/ProjectPermission.php
@@ -2,26 +2,71 @@
namespace Kanboard\Api;
+use Kanboard\Core\Security\Role;
+
/**
- * ProjectPermission API controller
+ * Project Permission API controller
*
* @package api
* @author Frederic Guillot
*/
class ProjectPermission extends \Kanboard\Core\Base
{
+ public function getProjectUsers($project_id)
+ {
+ return $this->projectUserRole->getAllUsers($project_id);
+ }
+
+ public function getAssignableUsers($project_id, $prepend_unassigned = false)
+ {
+ return $this->projectUserRole->getAssignableUsersList($project_id, $prepend_unassigned);
+ }
+
+ public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER)
+ {
+ return $this->projectUserRole->addUser($project_id, $user_id, $role);
+ }
+
+ public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER)
+ {
+ return $this->projectGroupRole->addGroup($project_id, $group_id, $role);
+ }
+
+ public function removeProjectUser($project_id, $user_id)
+ {
+ return $this->projectUserRole->removeUser($project_id, $user_id);
+ }
+
+ public function removeProjectGroup($project_id, $group_id)
+ {
+ return $this->projectGroupRole->removeGroup($project_id, $group_id);
+ }
+
+ public function changeProjectUserRole($project_id, $user_id, $role)
+ {
+ return $this->projectUserRole->changeUserRole($project_id, $user_id, $role);
+ }
+
+ public function changeProjectGroupRole($project_id, $group_id, $role)
+ {
+ return $this->projectGroupRole->changeGroupRole($project_id, $group_id, $role);
+ }
+
+ // Deprecated
public function getMembers($project_id)
{
- return $this->projectPermission->getMembers($project_id);
+ return $this->getProjectUsers($project_id);
}
+ // Deprecated
public function revokeUser($project_id, $user_id)
{
- return $this->projectPermission->revokeMember($project_id, $user_id);
+ return $this->removeProjectUser($project_id, $user_id);
}
+ // Deprecated
public function allowUser($project_id, $user_id)
{
- return $this->projectPermission->addMember($project_id, $user_id);
+ return $this->addProjectUser($project_id, $user_id);
}
}
diff --git a/app/Api/Subtask.php b/app/Api/Subtask.php
index 7baee3d3..782fdb02 100644
--- a/app/Api/Subtask.php
+++ b/app/Api/Subtask.php
@@ -36,7 +36,7 @@ class Subtask extends \Kanboard\Core\Base
'status' => $status,
);
- list($valid, ) = $this->subtask->validateCreation($values);
+ list($valid, ) = $this->subtaskValidator->validateCreation($values);
return $valid ? $this->subtask->create($values) : false;
}
@@ -58,7 +58,7 @@ class Subtask extends \Kanboard\Core\Base
}
}
- list($valid, ) = $this->subtask->validateApiModification($values);
+ list($valid, ) = $this->subtaskValidator->validateApiModification($values);
return $valid && $this->subtask->update($values);
}
}
diff --git a/app/Api/Swimlane.php b/app/Api/Swimlane.php
index 84c699ab..03a2819f 100644
--- a/app/Api/Swimlane.php
+++ b/app/Api/Swimlane.php
@@ -48,9 +48,11 @@ class Swimlane extends \Kanboard\Core\Base
public function updateSwimlane($swimlane_id, $name, $description = null)
{
$values = array('id' => $swimlane_id, 'name' => $name);
+
if (!is_null($description)) {
$values['description'] = $description;
}
+
return $this->swimlane->update($values);
}
@@ -69,13 +71,8 @@ class Swimlane extends \Kanboard\Core\Base
return $this->swimlane->enable($project_id, $swimlane_id);
}
- public function moveSwimlaneUp($project_id, $swimlane_id)
- {
- return $this->swimlane->moveUp($project_id, $swimlane_id);
- }
-
- public function moveSwimlaneDown($project_id, $swimlane_id)
+ public function changeSwimlanePosition($project_id, $swimlane_id, $position)
{
- return $this->swimlane->moveDown($project_id, $swimlane_id);
+ return $this->swimlane->changePosition($project_id, $swimlane_id, $position);
}
}
diff --git a/app/Api/Task.php b/app/Api/Task.php
index 0dceb209..177a09c6 100644
--- a/app/Api/Task.php
+++ b/app/Api/Task.php
@@ -64,13 +64,31 @@ class Task extends Base
return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id);
}
+ public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
+ {
+ return $this->taskDuplication->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
+ }
+
+ public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
+ {
+ return $this->taskDuplication->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
+ }
+
public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0,
- $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0,
- $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0,
- $recurrence_basedate = 0, $reference = '')
+ $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0,
+ $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0,
+ $recurrence_basedate = 0, $reference = '')
{
$this->checkProjectPermission($project_id);
+ if ($owner_id !== 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) {
+ return false;
+ }
+
+ if ($this->userSession->isLogged()) {
+ $creator_id = $this->userSession->getId();
+ }
+
$values = array(
'title' => $title,
'project_id' => $project_id,
@@ -96,20 +114,28 @@ class Task extends Base
return $valid ? $this->taskCreation->create($values) : false;
}
- public function updateTask($id, $title = null, $project_id = null, $color_id = null, $owner_id = null,
- $creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null,
- $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
- $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
+ public function updateTask($id, $title = null, $color_id = null, $owner_id = null,
+ $date_due = null, $description = null, $category_id = null, $score = null,
+ $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
+ $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
{
$this->checkTaskPermission($id);
+ $project_id = $this->taskFinder->getProjectId($id);
+
+ if ($project_id === 0) {
+ return false;
+ }
+
+ if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) {
+ return false;
+ }
+
$values = array(
'id' => $id,
'title' => $title,
- 'project_id' => $project_id,
'color_id' => $color_id,
'owner_id' => $owner_id,
- 'creator_id' => $creator_id,
'date_due' => $date_due,
'description' => $description,
'category_id' => $category_id,
diff --git a/app/Api/User.php b/app/Api/User.php
index 105723d3..48337ac6 100644
--- a/app/Api/User.php
+++ b/app/Api/User.php
@@ -2,7 +2,11 @@
namespace Kanboard\Api;
-use Kanboard\Auth\Ldap;
+use LogicException;
+use Kanboard\Core\Security\Role;
+use Kanboard\Core\Ldap\Client as LdapClient;
+use Kanboard\Core\Ldap\ClientException as LdapException;
+use Kanboard\Core\Ldap\User as LdapUser;
/**
* User API controller
@@ -17,6 +21,11 @@ class User extends \Kanboard\Core\Base
return $this->user->getById($user_id);
}
+ public function getUserByName($username)
+ {
+ return $this->user->getByUsername($username);
+ }
+
public function getAllUsers()
{
return $this->user->getAll();
@@ -27,7 +36,22 @@ class User extends \Kanboard\Core\Base
return $this->user->remove($user_id);
}
- public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $is_project_admin = 0)
+ public function disableUser($user_id)
+ {
+ return $this->user->disable($user_id);
+ }
+
+ public function enableUser($user_id)
+ {
+ return $this->user->enable($user_id);
+ }
+
+ public function isActiveUser($user_id)
+ {
+ return $this->user->isActive($user_id);
+ }
+
+ public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER)
{
$values = array(
'username' => $username,
@@ -35,44 +59,53 @@ class User extends \Kanboard\Core\Base
'confirmation' => $password,
'name' => $name,
'email' => $email,
- 'is_admin' => $is_admin,
- 'is_project_admin' => $is_project_admin,
+ 'role' => $role,
);
- list($valid, ) = $this->user->validateCreation($values);
+ list($valid, ) = $this->userValidator->validateCreation($values);
return $valid ? $this->user->create($values) : false;
}
- public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 0)
+ public function createLdapUser($username)
{
- $ldap = new Ldap($this->container);
- $user = $ldap->lookup($username, $email);
+ try {
- if (! $user) {
- return false;
- }
+ $ldap = LdapClient::connect();
+ $user = LdapUser::getUser($ldap, sprintf(LDAP_USER_FILTER, $username));
- $values = array(
- 'username' => $user['username'],
- 'name' => $user['name'],
- 'email' => $user['email'],
- 'is_ldap_user' => 1,
- 'is_admin' => $is_admin,
- 'is_project_admin' => $is_project_admin,
- );
+ if ($user === null) {
+ $this->logger->info('User not found in LDAP server');
+ return false;
+ }
- return $this->user->create($values);
+ if ($user->getUsername() === '') {
+ throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
+ }
+
+ $values = array(
+ 'username' => $user->getUsername(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail(),
+ 'role' => $user->getRole(),
+ 'is_ldap_user' => 1,
+ );
+
+ return $this->user->create($values);
+
+ } catch (LdapException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
}
- public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null)
+ public function updateUser($id, $username = null, $name = null, $email = null, $role = null)
{
$values = array(
'id' => $id,
'username' => $username,
'name' => $name,
'email' => $email,
- 'is_admin' => $is_admin,
- 'is_project_admin' => $is_project_admin,
+ 'role' => $role,
);
foreach ($values as $key => $value) {
@@ -81,7 +114,7 @@ class User extends \Kanboard\Core\Base
}
}
- list($valid, ) = $this->user->validateApiModification($values);
+ list($valid, ) = $this->userValidator->validateApiModification($values);
return $valid && $this->user->update($values);
}
}
diff --git a/app/Auth/Database.php b/app/Auth/Database.php
deleted file mode 100644
index 91b17a5f..00000000
--- a/app/Auth/Database.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Model\User;
-use Kanboard\Event\AuthEvent;
-
-/**
- * Database authentication
- *
- * @package auth
- * @author Frederic Guillot
- */
-class Database extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'Database';
-
- /**
- * Authenticate a user
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean
- */
- public function authenticate($username, $password)
- {
- $user = $this->db
- ->table(User::TABLE)
- ->eq('username', $username)
- ->eq('disable_login_form', 0)
- ->eq('is_ldap_user', 0)
- ->findOne();
-
- if (is_array($user) && password_verify($password, $user['password'])) {
- $this->userSession->refresh($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
- return true;
- }
-
- return false;
- }
-}
diff --git a/app/Auth/DatabaseAuth.php b/app/Auth/DatabaseAuth.php
new file mode 100644
index 00000000..c13af687
--- /dev/null
+++ b/app/Auth/DatabaseAuth.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
+use Kanboard\Core\Security\SessionCheckProviderInterface;
+use Kanboard\Model\User;
+use Kanboard\User\DatabaseUserProvider;
+
+/**
+ * Database Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterface, SessionCheckProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access protected
+ * @var array
+ */
+ protected $userInfo = array();
+
+ /**
+ * Username
+ *
+ * @access protected
+ * @var string
+ */
+ protected $username = '';
+
+ /**
+ * Password
+ *
+ * @access protected
+ * @var string
+ */
+ protected $password = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'Database';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $user = $this->db
+ ->table(User::TABLE)
+ ->columns('id', 'password')
+ ->eq('username', $this->username)
+ ->eq('disable_login_form', 0)
+ ->eq('is_ldap_user', 0)
+ ->eq('is_active', 1)
+ ->findOne();
+
+ if (! empty($user) && password_verify($this->password, $user['password'])) {
+ $this->userInfo = $user;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the user session is valid
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isValidSession()
+ {
+ return $this->user->isActive($this->userSession->getId());
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return \Kanboard\User\DatabaseUserProvider
+ */
+ public function getUser()
+ {
+ if (empty($this->userInfo)) {
+ return null;
+ }
+
+ return new DatabaseUserProvider($this->userInfo);
+ }
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+ }
+}
diff --git a/app/Auth/Github.php b/app/Auth/Github.php
deleted file mode 100644
index b89dc5b8..00000000
--- a/app/Auth/Github.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * Github backend
- *
- * @package auth
- */
-class Github extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'Github';
-
- /**
- * OAuth2 instance
- *
- * @access private
- * @var \Kanboard\Core\OAuth2
- */
- private $service;
-
- /**
- * Authenticate a Github user
- *
- * @access public
- * @param string $github_id Github user id
- * @return boolean
- */
- public function authenticate($github_id)
- {
- $user = $this->user->getByGithubId($github_id);
-
- if (! empty($user)) {
- $this->userSession->refresh($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
- return true;
- }
-
- return false;
- }
-
- /**
- * Unlink a Github account for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return boolean
- */
- public function unlink($user_id)
- {
- return $this->user->update(array(
- 'id' => $user_id,
- 'github_id' => '',
- ));
- }
-
- /**
- * Update the user table based on the Github profile information
- *
- * @access public
- * @param integer $user_id User id
- * @param array $profile Github profile
- * @return boolean
- */
- public function updateUser($user_id, array $profile)
- {
- $user = $this->user->getById($user_id);
-
- return $this->user->update(array(
- 'id' => $user_id,
- 'github_id' => $profile['id'],
- 'email' => empty($user['email']) ? $profile['email'] : $user['email'],
- 'name' => empty($user['name']) ? $profile['name'] : $user['name'],
- ));
- }
-
- /**
- * Get OAuth2 configured service
- *
- * @access public
- * @return Kanboard\Core\OAuth2
- */
- public function getService()
- {
- if (empty($this->service)) {
- $this->service = $this->oauth->createService(
- GITHUB_CLIENT_ID,
- GITHUB_CLIENT_SECRET,
- $this->helper->url->to('oauth', 'github', array(), '', true),
- GITHUB_OAUTH_AUTHORIZE_URL,
- GITHUB_OAUTH_TOKEN_URL,
- array()
- );
- }
-
- return $this->service;
- }
-
- /**
- * Get Github profile
- *
- * @access public
- * @param string $code
- * @return array
- */
- public function getProfile($code)
- {
- $this->getService()->getAccessToken($code);
-
- return $this->httpClient->getJson(
- GITHUB_API_URL.'user',
- array($this->getService()->getAuthorizationHeader())
- );
- }
-}
diff --git a/app/Auth/Gitlab.php b/app/Auth/Gitlab.php
deleted file mode 100644
index a59bc1fa..00000000
--- a/app/Auth/Gitlab.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * Gitlab backend
- *
- * @package auth
- */
-class Gitlab extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'Gitlab';
-
- /**
- * OAuth2 instance
- *
- * @access private
- * @var \Kanboard\Core\OAuth2
- */
- private $service;
-
- /**
- * Authenticate a Gitlab user
- *
- * @access public
- * @param string $gitlab_id Gitlab user id
- * @return boolean
- */
- public function authenticate($gitlab_id)
- {
- $user = $this->user->getByGitlabId($gitlab_id);
-
- if (! empty($user)) {
- $this->userSession->refresh($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
- return true;
- }
-
- return false;
- }
-
- /**
- * Unlink a Gitlab account for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return boolean
- */
- public function unlink($user_id)
- {
- return $this->user->update(array(
- 'id' => $user_id,
- 'gitlab_id' => '',
- ));
- }
-
- /**
- * Update the user table based on the Gitlab profile information
- *
- * @access public
- * @param integer $user_id User id
- * @param array $profile Gitlab profile
- * @return boolean
- */
- public function updateUser($user_id, array $profile)
- {
- $user = $this->user->getById($user_id);
-
- return $this->user->update(array(
- 'id' => $user_id,
- 'gitlab_id' => $profile['id'],
- 'email' => empty($user['email']) ? $profile['email'] : $user['email'],
- 'name' => empty($user['name']) ? $profile['name'] : $user['name'],
- ));
- }
-
- /**
- * Get OAuth2 configured service
- *
- * @access public
- * @return Kanboard\Core\OAuth2
- */
- public function getService()
- {
- if (empty($this->service)) {
- $this->service = $this->oauth->createService(
- GITLAB_CLIENT_ID,
- GITLAB_CLIENT_SECRET,
- $this->helper->url->to('oauth', 'gitlab', array(), '', true),
- GITLAB_OAUTH_AUTHORIZE_URL,
- GITLAB_OAUTH_TOKEN_URL,
- array()
- );
- }
-
- return $this->service;
- }
-
- /**
- * Get Gitlab profile
- *
- * @access public
- * @param string $code
- * @return array
- */
- public function getProfile($code)
- {
- $this->getService()->getAccessToken($code);
-
- return $this->httpClient->getJson(
- GITLAB_API_URL.'user',
- array($this->getService()->getAuthorizationHeader())
- );
- }
-}
diff --git a/app/Auth/Google.php b/app/Auth/Google.php
deleted file mode 100644
index 32bcb4b1..00000000
--- a/app/Auth/Google.php
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * Google backend
- *
- * @package auth
- * @author Frederic Guillot
- */
-class Google extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'Google';
-
- /**
- * OAuth2 instance
- *
- * @access private
- * @var \Kanboard\Core\OAuth2
- */
- private $service;
-
- /**
- * Authenticate a Google user
- *
- * @access public
- * @param string $google_id Google unique id
- * @return boolean
- */
- public function authenticate($google_id)
- {
- $user = $this->user->getByGoogleId($google_id);
-
- if (! empty($user)) {
- $this->userSession->refresh($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
- return true;
- }
-
- return false;
- }
-
- /**
- * Unlink a Google account for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return boolean
- */
- public function unlink($user_id)
- {
- return $this->user->update(array(
- 'id' => $user_id,
- 'google_id' => '',
- ));
- }
-
- /**
- * Update the user table based on the Google profile information
- *
- * @access public
- * @param integer $user_id User id
- * @param array $profile Google profile
- * @return boolean
- */
- public function updateUser($user_id, array $profile)
- {
- $user = $this->user->getById($user_id);
-
- return $this->user->update(array(
- 'id' => $user_id,
- 'google_id' => $profile['id'],
- 'email' => empty($user['email']) ? $profile['email'] : $user['email'],
- 'name' => empty($user['name']) ? $profile['name'] : $user['name'],
- ));
- }
-
- /**
- * Get OAuth2 configured service
- *
- * @access public
- * @return KanboardCore\OAuth2
- */
- public function getService()
- {
- if (empty($this->service)) {
- $this->service = $this->oauth->createService(
- GOOGLE_CLIENT_ID,
- GOOGLE_CLIENT_SECRET,
- $this->helper->url->to('oauth', 'google', array(), '', true),
- 'https://accounts.google.com/o/oauth2/auth',
- 'https://accounts.google.com/o/oauth2/token',
- array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile')
- );
- }
-
- return $this->service;
- }
-
- /**
- * Get Google profile
- *
- * @access public
- * @param string $code
- * @return array
- */
- public function getProfile($code)
- {
- $this->getService()->getAccessToken($code);
-
- return $this->httpClient->getJson(
- 'https://www.googleapis.com/oauth2/v1/userinfo',
- array($this->getService()->getAuthorizationHeader())
- );
- }
-}
diff --git a/app/Auth/Ldap.php b/app/Auth/Ldap.php
deleted file mode 100644
index c252be17..00000000
--- a/app/Auth/Ldap.php
+++ /dev/null
@@ -1,521 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * LDAP model
- *
- * @package auth
- * @author Frederic Guillot
- */
-class Ldap extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'LDAP';
-
- /**
- * Get LDAP server name
- *
- * @access public
- * @return string
- */
- public function getLdapServer()
- {
- return LDAP_SERVER;
- }
-
- /**
- * Get LDAP bind type
- *
- * @access public
- * @return integer
- */
- public function getLdapBindType()
- {
- return LDAP_BIND_TYPE;
- }
-
- /**
- * Get LDAP server port
- *
- * @access public
- * @return integer
- */
- public function getLdapPort()
- {
- return LDAP_PORT;
- }
-
- /**
- * Get LDAP username (proxy auth)
- *
- * @access public
- * @return string
- */
- public function getLdapUsername()
- {
- return LDAP_USERNAME;
- }
-
- /**
- * Get LDAP password (proxy auth)
- *
- * @access public
- * @return string
- */
- public function getLdapPassword()
- {
- return LDAP_PASSWORD;
- }
-
- /**
- * Get LDAP Base DN
- *
- * @access public
- * @return string
- */
- public function getLdapBaseDn()
- {
- return LDAP_ACCOUNT_BASE;
- }
-
- /**
- * Get LDAP account id attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountId()
- {
- return LDAP_ACCOUNT_ID;
- }
-
- /**
- * Get LDAP account email attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountEmail()
- {
- return LDAP_ACCOUNT_EMAIL;
- }
-
- /**
- * Get LDAP account name attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountName()
- {
- return LDAP_ACCOUNT_FULLNAME;
- }
-
- /**
- * Get LDAP account memberof attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountMemberOf()
- {
- return LDAP_ACCOUNT_MEMBEROF;
- }
-
- /**
- * Get LDAP admin group DN
- *
- * @access public
- * @return string
- */
- public function getLdapGroupAdmin()
- {
- return LDAP_GROUP_ADMIN_DN;
- }
-
- /**
- * Get LDAP project admin group DN
- *
- * @access public
- * @return string
- */
- public function getLdapGroupProjectAdmin()
- {
- return LDAP_GROUP_PROJECT_ADMIN_DN;
- }
-
- /**
- * Get LDAP username pattern
- *
- * @access public
- * @param string $username
- * @return string
- */
- public function getLdapUserPattern($username)
- {
- return sprintf(LDAP_USER_PATTERN, $username);
- }
-
- /**
- * Return true if the LDAP username is case sensitive
- *
- * @access public
- * @return boolean
- */
- public function isLdapAccountCaseSensitive()
- {
- return LDAP_USERNAME_CASE_SENSITIVE;
- }
-
- /**
- * Return true if the automatic account creation is enabled
- *
- * @access public
- * @return boolean
- */
- public function isLdapAccountCreationEnabled()
- {
- return LDAP_ACCOUNT_CREATION;
- }
-
- /**
- * Ge the list of attributes to fetch when reading the LDAP user entry
- *
- * Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
- *
- * @access public
- * @return array
- */
- public function getProfileAttributes()
- {
- return array_values(array_filter(array(
- $this->getLdapAccountId(),
- $this->getLdapAccountName(),
- $this->getLdapAccountEmail(),
- $this->getLdapAccountMemberOf()
- )));
- }
-
- /**
- * Authenticate the user
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean
- */
- public function authenticate($username, $password)
- {
- $username = $this->isLdapAccountCaseSensitive() ? $username : strtolower($username);
- $result = $this->findUser($username, $password);
-
- if (is_array($result)) {
- $user = $this->user->getByUsername($username);
-
- if (! empty($user)) {
-
- // There is already a local user with that name
- if ($user['is_ldap_user'] == 0) {
- return false;
- }
- } else {
-
- // We create automatically a new user
- if ($this->isLdapAccountCreationEnabled() && $this->user->create($result) !== false) {
- $user = $this->user->getByUsername($username);
- } else {
- return false;
- }
- }
-
- // We open the session
- $this->userSession->refresh($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Find the user from the LDAP server
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean|array
- */
- public function findUser($username, $password)
- {
- $ldap = $this->connect();
-
- if ($ldap !== false && $this->bind($ldap, $username, $password)) {
- return $this->getProfile($ldap, $username, $password);
- }
-
- return false;
- }
-
- /**
- * LDAP connection
- *
- * @access public
- * @return resource|boolean
- */
- public function connect()
- {
- if (! function_exists('ldap_connect')) {
- $this->logger->error('LDAP: The PHP LDAP extension is required');
- return false;
- }
-
- // Skip SSL certificate verification
- if (! LDAP_SSL_VERIFY) {
- putenv('LDAPTLS_REQCERT=never');
- }
-
- $ldap = ldap_connect($this->getLdapServer(), $this->getLdapPort());
-
- if ($ldap === false) {
- $this->logger->error('LDAP: Unable to connect to the LDAP server');
- return false;
- }
-
- ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
- ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
- ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
- ldap_set_option($ldap, LDAP_OPT_TIMELIMIT, 1);
-
- if (LDAP_START_TLS && ! @ldap_start_tls($ldap)) {
- $this->logger->error('LDAP: Unable to use ldap_start_tls()');
- return false;
- }
-
- return $ldap;
- }
-
- /**
- * LDAP authentication
- *
- * @access public
- * @param resource $ldap
- * @param string $username
- * @param string $password
- * @return boolean
- */
- public function bind($ldap, $username, $password)
- {
- if ($this->getLdapBindType() === 'user') {
- $ldap_username = sprintf($this->getLdapUsername(), $username);
- $ldap_password = $password;
- } elseif ($this->getLdapBindType() === 'proxy') {
- $ldap_username = $this->getLdapUsername();
- $ldap_password = $this->getLdapPassword();
- } else {
- $ldap_username = null;
- $ldap_password = null;
- }
-
- if (! @ldap_bind($ldap, $ldap_username, $ldap_password)) {
- $this->logger->error('LDAP: Unable to bind to server with: '.$ldap_username);
- $this->logger->error('LDAP: bind type='.$this->getLdapBindType());
- return false;
- }
-
- return true;
- }
-
- /**
- * Get LDAP user profile
- *
- * @access public
- * @param resource $ldap
- * @param string $username
- * @param string $password
- * @return boolean|array
- */
- public function getProfile($ldap, $username, $password)
- {
- $user_pattern = $this->getLdapUserPattern($username);
- $entries = $this->executeQuery($ldap, $user_pattern);
-
- if ($entries === false) {
- $this->logger->error('LDAP: Unable to get user profile: '.$user_pattern);
- return false;
- }
-
- if (@ldap_bind($ldap, $entries[0]['dn'], $password)) {
- return $this->prepareProfile($ldap, $entries, $username);
- }
-
- if (DEBUG) {
- $this->logger->debug('LDAP: wrong password for '.$entries[0]['dn']);
- }
-
- return false;
- }
-
- /**
- * Build user profile from LDAP information
- *
- * @access public
- * @param resource $ldap
- * @param array $entries
- * @param string $username
- * @return boolean|array
- */
- public function prepareProfile($ldap, array $entries, $username)
- {
- if ($this->getLdapAccountId() !== '') {
- $username = $this->getEntry($entries, $this->getLdapAccountId(), $username);
- }
-
- return array(
- 'username' => $username,
- 'name' => $this->getEntry($entries, $this->getLdapAccountName()),
- 'email' => $this->getEntry($entries, $this->getLdapAccountEmail()),
- 'is_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupAdmin()),
- 'is_project_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupProjectAdmin()),
- 'is_ldap_user' => 1,
- );
- }
-
- /**
- * Check group membership
- *
- * @access public
- * @param array $group_entries
- * @param string $group_dn
- * @return boolean
- */
- public function isMemberOf(array $group_entries, $group_dn)
- {
- if (! isset($group_entries['count']) || empty($group_dn)) {
- return false;
- }
-
- for ($i = 0; $i < $group_entries['count']; $i++) {
- if ($group_entries[$i] === $group_dn) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Retrieve info on LDAP user by username or email
- *
- * @access public
- * @param string $username
- * @param string $email
- * @return boolean|array
- */
- public function lookup($username = null, $email = null)
- {
- $query = $this->getLookupQuery($username, $email);
- if ($query === '') {
- return false;
- }
-
- // Connect and attempt anonymous or proxy binding
- $ldap = $this->connect();
- if ($ldap === false || ! $this->bind($ldap, null, null)) {
- return false;
- }
-
- // Try to find user
- $entries = $this->executeQuery($ldap, $query);
- if ($entries === false) {
- return false;
- }
-
- // User id not retrieved: LDAP_ACCOUNT_ID not properly configured
- if (empty($username) && ! isset($entries[0][$this->getLdapAccountId()][0])) {
- return false;
- }
-
- return $this->prepareProfile($ldap, $entries, $username);
- }
-
- /**
- * Execute LDAP query
- *
- * @access private
- * @param resource $ldap
- * @param string $query
- * @return boolean|array
- */
- private function executeQuery($ldap, $query)
- {
- $sr = @ldap_search($ldap, $this->getLdapBaseDn(), $query, $this->getProfileAttributes());
- if ($sr === false) {
- return false;
- }
-
- $entries = ldap_get_entries($ldap, $sr);
- if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
- return false;
- }
-
- return $entries;
- }
-
- /**
- * Get the LDAP query to find a user
- *
- * @access private
- * @param string $username
- * @param string $email
- * @return string
- */
- private function getLookupQuery($username, $email)
- {
- if (! empty($username) && ! empty($email)) {
- return '(&('.$this->getLdapUserPattern($username).')('.$this->getLdapAccountEmail().'='.$email.'))';
- } elseif (! empty($username)) {
- return $this->getLdapUserPattern($username);
- } elseif (! empty($email)) {
- return '('.$this->getLdapAccountEmail().'='.$email.')';
- }
-
- return '';
- }
-
- /**
- * Return one entry from a list of entries
- *
- * @access private
- * @param array $entries LDAP entries
- * @param string $key Key
- * @param string $default Default value if key not set in entry
- * @return string
- */
- private function getEntry(array $entries, $key, $default = '')
- {
- return isset($entries[0][$key][0]) ? $entries[0][$key][0] : $default;
- }
-
- /**
- * Return subset of entries
- *
- * @access private
- * @param array $entries
- * @param string $key
- * @param array $default
- * @return array
- */
- private function getEntries(array $entries, $key, $default = array())
- {
- return isset($entries[0][$key]) ? $entries[0][$key] : $default;
- }
-}
diff --git a/app/Auth/LdapAuth.php b/app/Auth/LdapAuth.php
new file mode 100644
index 00000000..b4efbb55
--- /dev/null
+++ b/app/Auth/LdapAuth.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use LogicException;
+use Kanboard\Core\Base;
+use Kanboard\Core\Ldap\Client as LdapClient;
+use Kanboard\Core\Ldap\ClientException as LdapException;
+use Kanboard\Core\Ldap\User as LdapUser;
+use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
+
+/**
+ * LDAP Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access protected
+ * @var \Kanboard\User\LdapUserProvider
+ */
+ protected $userInfo = null;
+
+ /**
+ * Username
+ *
+ * @access protected
+ * @var string
+ */
+ protected $username = '';
+
+ /**
+ * Password
+ *
+ * @access protected
+ * @var string
+ */
+ protected $password = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'LDAP';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ try {
+
+ $client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
+ $user = LdapUser::getUser($client, $this->username);
+
+ if ($user === null) {
+ $this->logger->info('User not found in LDAP server');
+ return false;
+ }
+
+ if ($user->getUsername() === '') {
+ throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
+ }
+
+ if ($client->authenticate($user->getDn(), $this->password)) {
+ $this->userInfo = $user;
+ return true;
+ }
+
+ } catch (LdapException $e) {
+ $this->logger->error($e->getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return \Kanboard\User\LdapUserProvider
+ */
+ public function getUser()
+ {
+ return $this->userInfo;
+ }
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+ }
+
+ /**
+ * Get LDAP username (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapUsername()
+ {
+ switch ($this->getLdapBindType()) {
+ case 'proxy':
+ return LDAP_USERNAME;
+ case 'user':
+ return sprintf(LDAP_USERNAME, $this->username);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get LDAP password (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapPassword()
+ {
+ switch ($this->getLdapBindType()) {
+ case 'proxy':
+ return LDAP_PASSWORD;
+ case 'user':
+ return $this->password;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get LDAP bind type
+ *
+ * @access public
+ * @return integer
+ */
+ public function getLdapBindType()
+ {
+ if (LDAP_BIND_TYPE !== 'user' && LDAP_BIND_TYPE !== 'proxy' && LDAP_BIND_TYPE !== 'anonymous') {
+ throw new LogicException('Wrong value for the parameter LDAP_BIND_TYPE');
+ }
+
+ return LDAP_BIND_TYPE;
+ }
+}
diff --git a/app/Auth/RememberMe.php b/app/Auth/RememberMe.php
deleted file mode 100644
index 0290e36c..00000000
--- a/app/Auth/RememberMe.php
+++ /dev/null
@@ -1,323 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Core\Request;
-use Kanboard\Event\AuthEvent;
-use Kanboard\Core\Security;
-
-/**
- * RememberMe model
- *
- * @package auth
- * @author Frederic Guillot
- */
-class RememberMe extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'RememberMe';
-
- /**
- * SQL table name
- *
- * @var string
- */
- const TABLE = 'remember_me';
-
- /**
- * Cookie name
- *
- * @var string
- */
- const COOKIE_NAME = '__R';
-
- /**
- * Expiration (60 days)
- *
- * @var integer
- */
- const EXPIRATION = 5184000;
-
- /**
- * Get a remember me record
- *
- * @access public
- * @param $token
- * @param $sequence
- * @return mixed
- */
- public function find($token, $sequence)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('token', $token)
- ->eq('sequence', $sequence)
- ->gt('expiration', time())
- ->findOne();
- }
-
- /**
- * Get all sessions for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return array
- */
- public function getAll($user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->desc('date_creation')
- ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
- ->findAll();
- }
-
- /**
- * Authenticate the user with the cookie
- *
- * @access public
- * @return bool
- */
- public function authenticate()
- {
- $credentials = $this->readCookie();
-
- if ($credentials !== false) {
- $record = $this->find($credentials['token'], $credentials['sequence']);
-
- if ($record) {
-
- // Update the sequence
- $this->writeCookie(
- $record['token'],
- $this->update($record['token']),
- $record['expiration']
- );
-
- // Create the session
- $this->userSession->refresh($this->user->getById($record['user_id']));
-
- // Do not ask 2FA for remember me session
- $this->session['2fa_validated'] = true;
-
- $this->container['dispatcher']->dispatch(
- 'auth.success',
- new AuthEvent(self::AUTH_NAME, $this->userSession->getId())
- );
-
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Remove a session record
- *
- * @access public
- * @param integer $session_id Session id
- * @return mixed
- */
- public function remove($session_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('id', $session_id)
- ->remove();
- }
-
- /**
- * Remove the current RememberMe session and the cookie
- *
- * @access public
- * @param integer $user_id User id
- */
- public function destroy($user_id)
- {
- $credentials = $this->readCookie();
-
- if ($credentials !== false) {
- $this->deleteCookie();
-
- $this->db
- ->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->eq('token', $credentials['token'])
- ->remove();
- }
- }
-
- /**
- * Create a new RememberMe session
- *
- * @access public
- * @param integer $user_id User id
- * @param string $ip IP Address
- * @param string $user_agent User Agent
- * @return array
- */
- public function create($user_id, $ip, $user_agent)
- {
- $token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken());
- $sequence = Security::generateToken();
- $expiration = time() + self::EXPIRATION;
-
- $this->cleanup($user_id);
-
- $this
- ->db
- ->table(self::TABLE)
- ->insert(array(
- 'user_id' => $user_id,
- 'ip' => $ip,
- 'user_agent' => $user_agent,
- 'token' => $token,
- 'sequence' => $sequence,
- 'expiration' => $expiration,
- 'date_creation' => time(),
- ));
-
- return array(
- 'token' => $token,
- 'sequence' => $sequence,
- 'expiration' => $expiration,
- );
- }
-
- /**
- * Remove old sessions for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return bool
- */
- public function cleanup($user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->lt('expiration', time())
- ->remove();
- }
-
- /**
- * Return a new sequence token and update the database
- *
- * @access public
- * @param string $token Session token
- * @return string
- */
- public function update($token)
- {
- $new_sequence = Security::generateToken();
-
- $this->db
- ->table(self::TABLE)
- ->eq('token', $token)
- ->update(array('sequence' => $new_sequence));
-
- return $new_sequence;
- }
-
- /**
- * Encode the cookie
- *
- * @access public
- * @param string $token Session token
- * @param string $sequence Sequence token
- * @return string
- */
- public function encodeCookie($token, $sequence)
- {
- return implode('|', array($token, $sequence));
- }
-
- /**
- * Decode the value of a cookie
- *
- * @access public
- * @param string $value Raw cookie data
- * @return array
- */
- public function decodeCookie($value)
- {
- list($token, $sequence) = explode('|', $value);
-
- return array(
- 'token' => $token,
- 'sequence' => $sequence,
- );
- }
-
- /**
- * Return true if the current user has a RememberMe cookie
- *
- * @access public
- * @return bool
- */
- public function hasCookie()
- {
- return ! empty($_COOKIE[self::COOKIE_NAME]);
- }
-
- /**
- * Write and encode the cookie
- *
- * @access public
- * @param string $token Session token
- * @param string $sequence Sequence token
- * @param string $expiration Cookie expiration
- */
- public function writeCookie($token, $sequence, $expiration)
- {
- setcookie(
- self::COOKIE_NAME,
- $this->encodeCookie($token, $sequence),
- $expiration,
- $this->helper->url->dir(),
- null,
- Request::isHTTPS(),
- true
- );
- }
-
- /**
- * Read and decode the cookie
- *
- * @access public
- * @return mixed
- */
- public function readCookie()
- {
- if (empty($_COOKIE[self::COOKIE_NAME])) {
- return false;
- }
-
- return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]);
- }
-
- /**
- * Remove the cookie
- *
- * @access public
- */
- public function deleteCookie()
- {
- setcookie(
- self::COOKIE_NAME,
- '',
- time() - 3600,
- $this->helper->url->dir(),
- null,
- Request::isHTTPS(),
- true
- );
- }
-}
diff --git a/app/Auth/RememberMeAuth.php b/app/Auth/RememberMeAuth.php
new file mode 100644
index 00000000..509a511d
--- /dev/null
+++ b/app/Auth/RememberMeAuth.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PreAuthenticationProviderInterface;
+use Kanboard\User\DatabaseUserProvider;
+
+/**
+ * Rember Me Cookie Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class RememberMeAuth extends Base implements PreAuthenticationProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access protected
+ * @var array
+ */
+ protected $userInfo = array();
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'RememberMe';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $credentials = $this->rememberMeCookie->read();
+
+ if ($credentials !== false) {
+ $session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']);
+
+ if (! empty($session)) {
+ $this->rememberMeCookie->write(
+ $session['token'],
+ $this->rememberMeSession->updateSequence($session['token']),
+ $session['expiration']
+ );
+
+ $this->userInfo = $this->user->getById($session['user_id']);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return DatabaseUserProvider
+ */
+ public function getUser()
+ {
+ if (empty($this->userInfo)) {
+ return null;
+ }
+
+ return new DatabaseUserProvider($this->userInfo);
+ }
+}
diff --git a/app/Auth/ReverseProxy.php b/app/Auth/ReverseProxy.php
deleted file mode 100644
index 1910ad35..00000000
--- a/app/Auth/ReverseProxy.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * ReverseProxy backend
- *
- * @package auth
- * @author Sylvain Veyrié
- */
-class ReverseProxy extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'ReverseProxy';
-
- /**
- * Get username from the reverse proxy
- *
- * @access public
- * @return string
- */
- public function getUsername()
- {
- return isset($_SERVER[REVERSE_PROXY_USER_HEADER]) ? $_SERVER[REVERSE_PROXY_USER_HEADER] : '';
- }
-
- /**
- * Authenticate the user with the HTTP header
- *
- * @access public
- * @return bool
- */
- public function authenticate()
- {
- if (isset($_SERVER[REVERSE_PROXY_USER_HEADER])) {
- $login = $_SERVER[REVERSE_PROXY_USER_HEADER];
- $user = $this->user->getByUsername($login);
-
- if (empty($user)) {
- $this->createUser($login);
- $user = $this->user->getByUsername($login);
- }
-
- $this->userSession->refresh($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Create automatically a new local user after the authentication
- *
- * @access private
- * @param string $login Username
- * @return bool
- */
- private function createUser($login)
- {
- $email = strpos($login, '@') !== false ? $login : '';
-
- if (REVERSE_PROXY_DEFAULT_DOMAIN !== '' && empty($email)) {
- $email = $login.'@'.REVERSE_PROXY_DEFAULT_DOMAIN;
- }
-
- return $this->user->create(array(
- 'email' => $email,
- 'username' => $login,
- 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login,
- 'is_ldap_user' => 1,
- 'disable_login_form' => 1,
- ));
- }
-}
diff --git a/app/Auth/ReverseProxyAuth.php b/app/Auth/ReverseProxyAuth.php
new file mode 100644
index 00000000..b9730c5c
--- /dev/null
+++ b/app/Auth/ReverseProxyAuth.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PreAuthenticationProviderInterface;
+use Kanboard\Core\Security\SessionCheckProviderInterface;
+use Kanboard\User\ReverseProxyUserProvider;
+
+/**
+ * Reverse-Proxy Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterface, SessionCheckProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access protected
+ * @var \Kanboard\User\ReverseProxyUserProvider
+ */
+ protected $userInfo = null;
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'ReverseProxy';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $username = $this->request->getRemoteUser();
+
+ if (! empty($username)) {
+ $this->userInfo = new ReverseProxyUserProvider($username);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the user session is valid
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isValidSession()
+ {
+ return $this->request->getRemoteUser() === $this->userSession->getUsername();
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return ReverseProxyUserProvider
+ */
+ public function getUser()
+ {
+ return $this->userInfo;
+ }
+}
diff --git a/app/Auth/TotpAuth.php b/app/Auth/TotpAuth.php
new file mode 100644
index 00000000..f4304930
--- /dev/null
+++ b/app/Auth/TotpAuth.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Otp\Otp;
+use Otp\GoogleAuthenticator;
+use Base32\Base32;
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PostAuthenticationProviderInterface;
+
+/**
+ * TOTP Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class TotpAuth extends Base implements PostAuthenticationProviderInterface
+{
+ /**
+ * User pin code
+ *
+ * @access protected
+ * @var string
+ */
+ protected $code = '';
+
+ /**
+ * Private key
+ *
+ * @access protected
+ * @var string
+ */
+ protected $secret = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return t('Time-based One-time Password Algorithm');
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $otp = new Otp;
+ return $otp->checkTotp(Base32::decode($this->secret), $this->code);
+ }
+
+ /**
+ * Called before to prompt the user
+ *
+ * @access public
+ */
+ public function beforeCode()
+ {
+
+ }
+
+ /**
+ * Set validation code
+ *
+ * @access public
+ * @param string $code
+ */
+ public function setCode($code)
+ {
+ $this->code = $code;
+ }
+
+ /**
+ * Generate secret
+ *
+ * @access public
+ * @return string
+ */
+ public function generateSecret()
+ {
+ $this->secret = GoogleAuthenticator::generateRandom();
+ return $this->secret;
+ }
+
+ /**
+ * Set secret token
+ *
+ * @access public
+ * @param string $secret
+ */
+ public function setSecret($secret)
+ {
+ $this->secret = $secret;
+ }
+
+ /**
+ * Get secret token
+ *
+ * @access public
+ * @return string
+ */
+ public function getSecret()
+ {
+ return $this->secret;
+ }
+
+ /**
+ * Get QR code url
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getQrCodeUrl($label)
+ {
+ if (empty($this->secret)) {
+ return '';
+ }
+
+ return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret);
+ }
+
+ /**
+ * Get key url (empty if no url can be provided)
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getKeyUrl($label)
+ {
+ if (empty($this->secret)) {
+ return '';
+ }
+
+ return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret);
+ }
+}
diff --git a/app/Console/Base.php b/app/Console/Base.php
index 4c5caf73..ac89207d 100644
--- a/app/Console/Base.php
+++ b/app/Console/Base.php
@@ -11,18 +11,19 @@ use Symfony\Component\Console\Command\Command;
* @package console
* @author Frederic Guillot
*
- * @property \Kanboard\Model\Notification $notification
- * @property \Kanboard\Model\Project $project
- * @property \Kanboard\Model\ProjectPermission $projectPermission
- * @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
- * @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats
- * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
- * @property \Kanboard\Model\SubtaskExport $subtaskExport
- * @property \Kanboard\Model\OverdueNotification $overdueNotification
- * @property \Kanboard\Model\Task $task
- * @property \Kanboard\Model\TaskExport $taskExport
- * @property \Kanboard\Model\TaskFinder $taskFinder
- * @property \Kanboard\Model\Transition $transition
+ * @property \Kanboard\Model\Notification $notification
+ * @property \Kanboard\Model\Project $project
+ * @property \Kanboard\Model\ProjectPermission $projectPermission
+ * @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
+ * @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats
+ * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
+ * @property \Kanboard\Model\SubtaskExport $subtaskExport
+ * @property \Kanboard\Model\OverdueNotification $overdueNotification
+ * @property \Kanboard\Model\Task $task
+ * @property \Kanboard\Model\TaskExport $taskExport
+ * @property \Kanboard\Model\TaskFinder $taskFinder
+ * @property \Kanboard\Model\Transition $transition
+ * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
abstract class Base extends Command
{
diff --git a/app/Console/Cronjob.php b/app/Console/Cronjob.php
new file mode 100644
index 00000000..3a5c5596
--- /dev/null
+++ b/app/Console/Cronjob.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Output\NullOutput;
+
+class Cronjob extends Base
+{
+ private $commands = array(
+ 'projects:daily-stats',
+ 'notification:overdue-tasks',
+ 'trigger:tasks',
+ );
+
+ protected function configure()
+ {
+ $this
+ ->setName('cronjob')
+ ->setDescription('Execute daily cronjob');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ foreach ($this->commands as $command) {
+ $job = $this->getApplication()->find($command);
+ $job->run(new ArrayInput(array('command' => $command)), new NullOutput());
+ }
+ }
+}
diff --git a/app/Console/TaskTrigger.php b/app/Console/TaskTrigger.php
new file mode 100644
index 00000000..8d707211
--- /dev/null
+++ b/app/Console/TaskTrigger.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Kanboard\Model\Task;
+use Kanboard\Event\TaskListEvent;
+
+class TaskTrigger extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('trigger:tasks')
+ ->setDescription('Trigger scheduler event for all tasks');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ foreach ($this->getProjectIds() as $project_id) {
+ $tasks = $this->taskFinder->getAll($project_id);
+ $nb_tasks = count($tasks);
+
+ if ($nb_tasks > 0) {
+ $output->writeln('Trigger task event: project_id='.$project_id.', nb_tasks='.$nb_tasks);
+ $this->sendEvent($tasks, $project_id);
+ }
+ }
+ }
+
+ private function getProjectIds()
+ {
+ $listeners = $this->dispatcher->getListeners(Task::EVENT_DAILY_CRONJOB);
+ $project_ids = array();
+
+ foreach ($listeners as $listener) {
+ $project_ids[] = $listener[0]->getProjectId();
+ }
+
+ return array_unique($project_ids);
+ }
+
+ private function sendEvent(array &$tasks, $project_id)
+ {
+ $event = new TaskListEvent(array('project_id' => $project_id));
+ $event->setTasks($tasks);
+
+ $this->dispatcher->dispatch(Task::EVENT_DAILY_CRONJOB, $event);
+ }
+}
diff --git a/app/Controller/Action.php b/app/Controller/Action.php
index 37d1c248..6c324324 100644
--- a/app/Controller/Action.php
+++ b/app/Controller/Action.php
@@ -18,17 +18,18 @@ class Action extends Base
public function index()
{
$project = $this->getProject();
+ $actions = $this->action->getAllByProject($project['id']);
- $this->response->html($this->projectLayout('action/index', array(
+ $this->response->html($this->helper->layout->project('action/index', array(
'values' => array('project_id' => $project['id']),
'project' => $project,
- 'actions' => $this->action->getAllByProject($project['id']),
- 'available_actions' => $this->action->getAvailableActions(),
- 'available_events' => $this->action->getAvailableEvents(),
- 'available_params' => $this->action->getAllActionParameters(),
- 'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getMemberList($project['id']),
- 'projects_list' => $this->project->getList(false),
+ 'actions' => $actions,
+ 'available_actions' => $this->actionManager->getAvailableActions(),
+ 'available_events' => $this->eventManager->getAll(),
+ 'available_params' => $this->actionManager->getAvailableParameters($actions),
+ 'columns_list' => $this->column->getList($project['id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
+ 'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'links_list' => $this->link->getList(0, false),
@@ -50,10 +51,10 @@ class Action extends Base
$this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id'])));
}
- $this->response->html($this->projectLayout('action/event', array(
+ $this->response->html($this->helper->layout->project('action/event', array(
'values' => $values,
'project' => $project,
- 'events' => $this->action->getCompatibleEvents($values['action_name']),
+ 'events' => $this->actionManager->getCompatibleEvents($values['action_name']),
'title' => t('Automatic actions')
)));
}
@@ -72,21 +73,21 @@ class Action extends Base
$this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id'])));
}
- $action = $this->action->load($values['action_name'], $values['project_id'], $values['event_name']);
+ $action = $this->actionManager->getAction($values['action_name']);
$action_params = $action->getActionRequiredParameters();
if (empty($action_params)) {
$this->doCreation($project, $values + array('params' => array()));
}
- $projects_list = $this->project->getList(false);
+ $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
unset($projects_list[$project['id']]);
- $this->response->html($this->projectLayout('action/params', array(
+ $this->response->html($this->helper->layout->project('action/params', array(
'values' => $values,
'action_params' => $action_params,
- 'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getMemberList($project['id']),
+ 'columns_list' => $this->column->getList($project['id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
'projects_list' => $projects_list,
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
@@ -115,13 +116,13 @@ class Action extends Base
*/
private function doCreation(array $project, array $values)
{
- list($valid, ) = $this->action->validateCreation($values);
+ list($valid, ) = $this->actionValidator->validateCreation($values);
if ($valid) {
if ($this->action->create($values) !== false) {
- $this->session->flash(t('Your automatic action have been created successfully.'));
+ $this->flash->success(t('Your automatic action have been created successfully.'));
} else {
- $this->session->flashError(t('Unable to create your automatic action.'));
+ $this->flash->failure(t('Unable to create your automatic action.'));
}
}
@@ -137,10 +138,10 @@ class Action extends Base
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('action/remove', array(
+ $this->response->html($this->helper->layout->project('action/remove', array(
'action' => $this->action->getById($this->request->getIntegerParam('action_id')),
- 'available_events' => $this->action->getAvailableEvents(),
- 'available_actions' => $this->action->getAvailableActions(),
+ 'available_events' => $this->eventManager->getAll(),
+ 'available_actions' => $this->actionManager->getAvailableActions(),
'project' => $project,
'title' => t('Remove an action')
)));
@@ -158,9 +159,9 @@ class Action extends Base
$action = $this->action->getById($this->request->getIntegerParam('action_id'));
if (! empty($action) && $this->action->remove($action['id'])) {
- $this->session->flash(t('Action removed successfully.'));
+ $this->flash->success(t('Action removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this action.'));
+ $this->flash->failure(t('Unable to remove this action.'));
}
$this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id'])));
diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php
index 24327c23..db520ebe 100644
--- a/app/Controller/Activity.php
+++ b/app/Controller/Activity.php
@@ -19,8 +19,7 @@ class Activity extends Base
{
$project = $this->getProject();
- $this->response->html($this->template->layout('activity/project', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ $this->response->html($this->helper->layout->app('activity/project', array(
'events' => $this->projectActivity->getProject($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
@@ -36,7 +35,7 @@ class Activity extends Base
{
$task = $this->getTask();
- $this->response->html($this->taskLayout('activity/task', array(
+ $this->response->html($this->helper->layout->task('activity/task', array(
'title' => $task['title'],
'task' => $task,
'events' => $this->projectActivity->getTask($task['id']),
diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php
index 1082b462..6ce062c4 100644
--- a/app/Controller/Analytic.php
+++ b/app/Controller/Analytic.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Model\Task as TaskModel;
+
/**
* Project Analytic controller
*
@@ -11,22 +13,6 @@ namespace Kanboard\Controller;
class Analytic extends Base
{
/**
- * Common layout for analytic views
- *
- * @access private
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- private function layout($template, array $params)
- {
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
- $params['content_for_sublayout'] = $this->template->render($template, $params);
-
- return $this->template->layout('analytic/layout', $params);
- }
-
- /**
* Show average Lead and Cycle time
*
* @access public
@@ -34,33 +20,49 @@ class Analytic extends Base
public function leadAndCycleTime()
{
$project = $this->getProject();
- $values = $this->request->getValues();
-
- $this->projectDailyStats->updateTotals($project['id'], date('Y-m-d'));
-
- $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
- $to = $this->request->getStringParam('to', date('Y-m-d'));
+ list($from, $to) = $this->getDates();
- if (! empty($values)) {
- $from = $values['from'];
- $to = $values['to'];
- }
-
- $this->response->html($this->layout('analytic/lead_cycle_time', array(
+ $this->response->html($this->helper->layout->analytic('analytic/lead_cycle_time', array(
'values' => array(
'from' => $from,
'to' => $to,
),
'project' => $project,
- 'average' => $this->projectAnalytic->getAverageLeadAndCycleTime($project['id']),
+ 'average' => $this->averageLeadCycleTimeAnalytic->build($project['id']),
'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to),
'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
'title' => t('Lead and Cycle time for "%s"', $project['name']),
)));
}
/**
+ * Show comparison between actual and estimated hours chart
+ *
+ * @access public
+ */
+ public function compareHours()
+ {
+ $project = $this->getProject();
+ $params = $this->getProjectFilters('analytic', 'compareHours');
+ $query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery();
+
+ $paginator = $this->paginator
+ ->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
+ ->setMax(30)
+ ->setOrder(TaskModel::TABLE.'.id')
+ ->setQuery($query)
+ ->calculate();
+
+ $this->response->html($this->helper->layout->analytic('analytic/compare_hours', array(
+ 'project' => $project,
+ 'paginator' => $paginator,
+ 'metrics' => $this->estimatedTimeComparisonAnalytic->build($project['id']),
+ 'title' => t('Compare hours for "%s"', $project['name']),
+ )));
+ }
+
+ /**
* Show average time spent by column
*
* @access public
@@ -69,9 +71,9 @@ class Analytic extends Base
{
$project = $this->getProject();
- $this->response->html($this->layout('analytic/avg_time_columns', array(
+ $this->response->html($this->helper->layout->analytic('analytic/avg_time_columns', array(
'project' => $project,
- 'metrics' => $this->projectAnalytic->getAverageTimeSpentByColumn($project['id']),
+ 'metrics' => $this->averageTimeSpentColumnAnalytic->build($project['id']),
'title' => t('Average time spent into each column for "%s"', $project['name']),
)));
}
@@ -85,9 +87,9 @@ class Analytic extends Base
{
$project = $this->getProject();
- $this->response->html($this->layout('analytic/tasks', array(
+ $this->response->html($this->helper->layout->analytic('analytic/tasks', array(
'project' => $project,
- 'metrics' => $this->projectAnalytic->getTaskRepartition($project['id']),
+ 'metrics' => $this->taskDistributionAnalytic->build($project['id']),
'title' => t('Task repartition for "%s"', $project['name']),
)));
}
@@ -101,9 +103,9 @@ class Analytic extends Base
{
$project = $this->getProject();
- $this->response->html($this->layout('analytic/users', array(
+ $this->response->html($this->helper->layout->analytic('analytic/users', array(
'project' => $project,
- 'metrics' => $this->projectAnalytic->getUserRepartition($project['id']),
+ 'metrics' => $this->userDistributionAnalytic->build($project['id']),
'title' => t('User repartition for "%s"', $project['name']),
)));
}
@@ -132,25 +134,18 @@ class Analytic extends Base
* Common method for CFD and Burdown chart
*
* @access private
+ * @param string $template
+ * @param string $column
+ * @param string $title
*/
private function commonAggregateMetrics($template, $column, $title)
{
$project = $this->getProject();
- $values = $this->request->getValues();
-
- $this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d'));
-
- $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
- $to = $this->request->getStringParam('to', date('Y-m-d'));
-
- if (! empty($values)) {
- $from = $values['from'];
- $to = $values['to'];
- }
+ list($from, $to) = $this->getDates();
$display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2;
- $this->response->html($this->layout($template, array(
+ $this->response->html($this->helper->layout->analytic($template, array(
'values' => array(
'from' => $from,
'to' => $to,
@@ -159,8 +154,23 @@ class Analytic extends Base
'metrics' => $display_graph ? $this->projectDailyColumnStats->getAggregatedMetrics($project['id'], $from, $to, $column) : array(),
'project' => $project,
'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
'title' => t($title, $project['name']),
)));
}
+
+ private function getDates()
+ {
+ $values = $this->request->getValues();
+
+ $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
+ $to = $this->request->getStringParam('to', date('Y-m-d'));
+
+ if (! empty($values)) {
+ $from = $values['from'];
+ $to = $values['to'];
+ }
+
+ return array($from, $to);
+ }
}
diff --git a/app/Controller/App.php b/app/Controller/App.php
index 2fae004c..1ce74506 100644
--- a/app/Controller/App.php
+++ b/app/Controller/App.php
@@ -13,22 +13,6 @@ use Kanboard\Model\Subtask as SubtaskModel;
class App extends Base
{
/**
- * Common layout for dashboard views
- *
- * @access private
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- private function layout($template, array $params)
- {
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
- $params['content_for_sublayout'] = $this->template->render($template, $params);
-
- return $this->template->layout('app/layout', $params);
- }
-
- /**
* Get project pagination
*
* @access private
@@ -42,7 +26,7 @@ class App extends Base
->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id))
->setMax($max)
->setOrder('name')
- ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id)))
+ ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id)))
->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects');
}
@@ -101,7 +85,7 @@ class App extends Base
{
$user = $this->getUser();
- $this->response->html($this->layout('app/overview', array(
+ $this->response->html($this->helper->layout->dashboard('app/overview', array(
'title' => t('Dashboard'),
'project_paginator' => $this->getProjectPaginator($user['id'], 'index', 10),
'task_paginator' => $this->getTaskPaginator($user['id'], 'index', 10),
@@ -119,7 +103,7 @@ class App extends Base
{
$user = $this->getUser();
- $this->response->html($this->layout('app/tasks', array(
+ $this->response->html($this->helper->layout->dashboard('app/tasks', array(
'title' => t('My tasks'),
'paginator' => $this->getTaskPaginator($user['id'], 'tasks', 50),
'user' => $user,
@@ -135,7 +119,7 @@ class App extends Base
{
$user = $this->getUser();
- $this->response->html($this->layout('app/subtasks', array(
+ $this->response->html($this->helper->layout->dashboard('app/subtasks', array(
'title' => t('My subtasks'),
'paginator' => $this->getSubtaskPaginator($user['id'], 'subtasks', 50),
'user' => $user,
@@ -151,7 +135,7 @@ class App extends Base
{
$user = $this->getUser();
- $this->response->html($this->layout('app/projects', array(
+ $this->response->html($this->helper->layout->dashboard('app/projects', array(
'title' => t('My projects'),
'paginator' => $this->getProjectPaginator($user['id'], 'projects', 25),
'user' => $user,
@@ -167,9 +151,9 @@ class App extends Base
{
$user = $this->getUser();
- $this->response->html($this->layout('app/activity', array(
+ $this->response->html($this->helper->layout->dashboard('app/activity', array(
'title' => t('My activity stream'),
- 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($user['id']), 100),
+ 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100),
'user' => $user,
)));
}
@@ -181,7 +165,7 @@ class App extends Base
*/
public function calendar()
{
- $this->response->html($this->layout('app/calendar', array(
+ $this->response->html($this->helper->layout->dashboard('app/calendar', array(
'title' => t('My calendar'),
'user' => $this->getUser(),
)));
@@ -196,55 +180,10 @@ class App extends Base
{
$user = $this->getUser();
- $this->response->html($this->layout('app/notifications', array(
+ $this->response->html($this->helper->layout->dashboard('app/notifications', array(
'title' => t('My notifications'),
'notifications' => $this->userUnreadNotification->getAll($user['id']),
'user' => $user,
)));
}
-
- /**
- * Render Markdown text and reply with the HTML Code
- *
- * @access public
- */
- public function preview()
- {
- $payload = $this->request->getJson();
-
- if (empty($payload['text'])) {
- $this->response->html('<p>'.t('Nothing to preview...').'</p>');
- }
-
- $this->response->html($this->helper->text->markdown($payload['text']));
- }
-
- /**
- * Task autocompletion (Ajax)
- *
- * @access public
- */
- public function autocomplete()
- {
- $search = $this->request->getStringParam('term');
- $projects = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
-
- if (empty($projects)) {
- $this->response->json(array());
- }
-
- $filter = $this->taskFilterAutoCompleteFormatter
- ->create()
- ->filterByProjects($projects)
- ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
-
- // Search by task id or by title
- if (ctype_digit($search)) {
- $filter->filterById($search);
- } else {
- $filter->filterByTitle($search);
- }
-
- $this->response->json($filter->format());
- }
}
diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php
index 95ad8d9e..46b5a546 100644
--- a/app/Controller/Auth.php
+++ b/app/Controller/Auth.php
@@ -2,8 +2,6 @@
namespace Kanboard\Controller;
-use Gregwar\Captcha\CaptchaBuilder;
-
/**
* Authentication controller
*
@@ -23,8 +21,8 @@ class Auth extends Base
$this->response->redirect($this->helper->url->to('app', 'index'));
}
- $this->response->html($this->template->layout('auth/index', array(
- 'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']),
+ $this->response->html($this->helper->layout->app('auth/index', array(
+ 'captcha' => ! empty($values['username']) && $this->userLocking->hasCaptcha($values['username']),
'errors' => $errors,
'values' => $values,
'no_layout' => true,
@@ -40,16 +38,11 @@ class Auth extends Base
public function check()
{
$values = $this->request->getValues();
- list($valid, $errors) = $this->authentication->validateForm($values);
+ $this->sessionStorage->hasRememberMe = ! empty($values['remember_me']);
+ list($valid, $errors) = $this->authValidator->validateForm($values);
if ($valid) {
- if (! empty($this->session['login_redirect']) && ! filter_var($this->session['login_redirect'], FILTER_VALIDATE_URL)) {
- $redirect = $this->session['login_redirect'];
- unset($this->session['login_redirect']);
- $this->response->redirect($redirect);
- }
-
- $this->response->redirect($this->helper->url->to('app', 'index'));
+ $this->redirectAfterLogin();
}
$this->login($values, $errors);
@@ -62,23 +55,27 @@ class Auth extends Base
*/
public function logout()
{
- $this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
- $this->session->close();
- $this->response->redirect($this->helper->url->to('auth', 'login'));
+ if (! DISABLE_LOGOUT) {
+ $this->sessionManager->close();
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ } else {
+ $this->response->redirect($this->helper->url->to('auth', 'index'));
+ }
}
/**
- * Display captcha image
+ * Redirect the user after the authentication
*
- * @access public
+ * @access private
*/
- public function captcha()
+ private function redirectAfterLogin()
{
- $this->response->contentType('image/jpeg');
+ if (isset($this->sessionStorage->redirectAfterLogin) && ! empty($this->sessionStorage->redirectAfterLogin) && ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
+ $redirect = $this->sessionStorage->redirectAfterLogin;
+ unset($this->sessionStorage->redirectAfterLogin);
+ $this->response->redirect($redirect);
+ }
- $builder = new CaptchaBuilder;
- $builder->build();
- $this->session['captcha'] = $builder->getPhrase();
- $builder->output();
+ $this->response->redirect($this->helper->url->to('app', 'index'));
}
}
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index a955b12c..884c439c 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -2,11 +2,7 @@
namespace Kanboard\Controller;
-use Pimple\Container;
-use Kanboard\Core\Security;
-use Kanboard\Core\Request;
-use Kanboard\Core\Response;
-use Symfony\Component\EventDispatcher\Event;
+use Kanboard\Core\Security\Role;
/**
* Base controller
@@ -17,54 +13,22 @@ use Symfony\Component\EventDispatcher\Event;
abstract class Base extends \Kanboard\Core\Base
{
/**
- * Request instance
- *
- * @accesss protected
- * @var \Kanboard\Core\Request
- */
- protected $request;
-
- /**
- * Response instance
- *
- * @accesss protected
- * @var \Kanboard\Core\Response
- */
- protected $response;
-
- /**
- * Constructor
- *
- * @access public
- * @param \Pimple\Container $container
- */
- public function __construct(Container $container)
- {
- $this->container = $container;
- $this->request = new Request;
- $this->response = new Response;
-
- if (DEBUG) {
- $this->container['logger']->debug('START_REQUEST='.$_SERVER['REQUEST_URI']);
- }
- }
-
- /**
- * Destructor
+ * Method executed before each action
*
* @access public
*/
- public function __destruct()
+ public function beforeAction()
{
- if (DEBUG) {
- foreach ($this->container['db']->getLogMessages() as $message) {
- $this->container['logger']->debug($message);
- }
+ $this->sessionManager->open();
+ $this->dispatcher->dispatch('app.bootstrap');
+ $this->sendHeaders();
+ $this->authenticationManager->checkCurrentSession();
- $this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries));
- $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT']));
- $this->container['logger']->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage()));
- $this->container['logger']->debug('END_REQUEST='.$_SERVER['REQUEST_URI']);
+ if (! $this->applicationAuthorization->isAllowed($this->router->getController(), $this->router->getAction(), Role::APP_PUBLIC)) {
+ $this->handleAuthentication();
+ $this->handlePostAuthentication();
+ $this->checkApplicationAuthorization();
+ $this->checkProjectAuthorization();
}
}
@@ -73,7 +37,7 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access private
*/
- private function sendHeaders($action)
+ private function sendHeaders()
{
// HTTP secure headers
$this->response->csp($this->container['cspRules']);
@@ -81,7 +45,7 @@ abstract class Base extends \Kanboard\Core\Base
$this->response->xss();
// Allow the public board iframe inclusion
- if (ENABLE_XFRAME && $action !== 'readonly') {
+ if (ENABLE_XFRAME && $this->router->getAction() !== 'readonly') {
$this->response->xframe();
}
@@ -91,53 +55,34 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
- * Method executed before each action
- *
- * @access public
- */
- public function beforeAction($controller, $action)
- {
- // Start the session
- $this->session->open($this->helper->url->dir());
- $this->sendHeaders($action);
- $this->container['dispatcher']->dispatch('session.bootstrap', new Event);
-
- if (! $this->acl->isPublicAction($controller, $action)) {
- $this->handleAuthentication();
- $this->handle2FA($controller, $action);
- $this->handleAuthorization($controller, $action);
-
- $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
- }
- }
-
- /**
* Check authentication
*
- * @access public
+ * @access private
*/
- public function handleAuthentication()
+ private function handleAuthentication()
{
- if (! $this->authentication->isAuthenticated()) {
+ if (! $this->userSession->isLogged() && ! $this->authenticationManager->preAuthentication()) {
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
- $this->session['login_redirect'] = $this->request->getUri();
+ $this->sessionStorage->redirectAfterLogin = $this->request->getUri();
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
}
/**
- * Check 2FA
+ * Handle Post-Authentication (2FA)
*
- * @access public
+ * @access private
*/
- public function handle2FA($controller, $action)
+ private function handlePostAuthentication()
{
+ $controller = strtolower($this->router->getController());
+ $action = strtolower($this->router->getAction());
$ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout');
- if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) {
+ if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) {
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
@@ -147,11 +92,23 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
- * Check page access and authorization
+ * Check application authorization
*
- * @access public
+ * @access private
*/
- public function handleAuthorization($controller, $action)
+ private function checkApplicationAuthorization()
+ {
+ if (! $this->helper->user->hasAccess($this->router->getController(), $this->router->getAction())) {
+ $this->forbidden();
+ }
+ }
+
+ /**
+ * Check project authorization
+ *
+ * @access private
+ */
+ private function checkProjectAuthorization()
{
$project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_id');
@@ -161,7 +118,7 @@ abstract class Base extends \Kanboard\Core\Base
$project_id = $this->taskFinder->getProjectId($task_id);
}
- if (! $this->acl->isAllowed($controller, $action, $project_id)) {
+ if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($this->router->getController(), $this->router->getAction(), $project_id)) {
$this->forbidden();
}
}
@@ -169,12 +126,12 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Application not found page (404 error)
*
- * @access public
+ * @access protected
* @param boolean $no_layout Display the layout or not
*/
- public function notfound($no_layout = false)
+ protected function notfound($no_layout = false)
{
- $this->response->html($this->template->layout('app/notfound', array(
+ $this->response->html($this->helper->layout->app('app/notfound', array(
'title' => t('Page not found'),
'no_layout' => $no_layout,
)));
@@ -183,12 +140,16 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Application forbidden page
*
- * @access public
+ * @access protected
* @param boolean $no_layout Display the layout or not
*/
- public function forbidden($no_layout = false)
+ protected function forbidden($no_layout = false)
{
- $this->response->html($this->template->layout('app/forbidden', array(
+ if ($this->request->isAjax()) {
+ $this->response->text('Access Forbidden', 403);
+ }
+
+ $this->response->html($this->helper->layout->app('app/forbidden', array(
'title' => t('Access Forbidden'),
'no_layout' => $no_layout,
)));
@@ -201,7 +162,7 @@ abstract class Base extends \Kanboard\Core\Base
*/
protected function checkCSRFParam()
{
- if (! Security::validateCSRFToken($this->request->getStringParam('csrf_token'))) {
+ if (! $this->token->validateCSRFToken($this->request->getStringParam('csrf_token'))) {
$this->forbidden();
}
}
@@ -219,43 +180,6 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
- * Common layout for task views
- *
- * @access protected
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- protected function taskLayout($template, array $params)
- {
- $content = $this->template->render($template, $params);
- $params['task_content_for_layout'] = $content;
- $params['title'] = $params['task']['project_name'].' &gt; '.$params['task']['title'];
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
-
- return $this->template->layout('task/layout', $params);
- }
-
- /**
- * Common layout for project views
- *
- * @access protected
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- protected function projectLayout($template, array $params, $sidebar_template = 'project/sidebar')
- {
- $content = $this->template->render($template, $params);
- $params['project_content_for_layout'] = $content;
- $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' &gt; '.$params['title'];
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
- $params['sidebar_template'] = $sidebar_template;
-
- return $this->template->layout('project/layout', $params);
- }
-
- /**
* Common method to get a task for task views
*
* @access protected
@@ -278,6 +202,36 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
+ * Get Task or Project file
+ *
+ * @access protected
+ */
+ protected function getFile()
+ {
+ $task_id = $this->request->getIntegerParam('task_id');
+ $file_id = $this->request->getIntegerParam('file_id');
+ $model = 'projectFile';
+
+ if ($task_id > 0) {
+ $model = 'taskFile';
+ $project_id = $this->taskFinder->getProjectId($task_id);
+
+ if ($project_id !== $this->request->getIntegerParam('project_id')) {
+ $this->forbidden();
+ }
+ }
+
+ $file = $this->$model->getById($file_id);
+
+ if (empty($file)) {
+ $this->notfound();
+ }
+
+ $file['model'] = $model;
+ return $file;
+ }
+
+ /**
* Common method to get a project
*
* @access protected
@@ -287,11 +241,10 @@ abstract class Base extends \Kanboard\Core\Base
protected function getProject($project_id = 0)
{
$project_id = $this->request->getIntegerParam('project_id', $project_id);
- $project = $this->project->getById($project_id);
+ $project = $this->project->getByIdWithOwner($project_id);
if (empty($project)) {
- $this->session->flashError(t('Project not found.'));
- $this->response->redirect($this->helper->url->to('project', 'index'));
+ $this->notfound();
}
return $project;
@@ -319,15 +272,35 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
+ * Get the current subtask
+ *
+ * @access protected
+ * @return array
+ */
+ protected function getSubtask()
+ {
+ $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id'));
+
+ if (empty($subtask)) {
+ $this->notfound();
+ }
+
+ 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->projectPermission->getAllowedProjects($this->userSession->getId());
+ $board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
unset($board_selector[$project['id']]);
$filters = array(
@@ -344,6 +317,30 @@ abstract class Base extends \Kanboard\Core\Base
'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->template->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 2d75db89..199f1703 100644
--- a/app/Controller/Board.php
+++ b/app/Controller/Board.php
@@ -27,7 +27,7 @@ class Board extends Base
}
// Display the board with a specific layout
- $this->response->html($this->template->layout('board/view_public', array(
+ $this->response->html($this->helper->layout->app('board/view_public', array(
'project' => $project,
'swimlanes' => $this->board->getBoard($project['id']),
'title' => $project['name'],
@@ -49,12 +49,11 @@ class Board extends Base
{
$params = $this->getProjectFilters('board', 'show');
- $this->response->html($this->template->layout('board/view_private', array(
+ $this->response->html($this->helper->layout->app('board/view_private', array(
'categories_list' => $this->category->getList($params['project']['id'], false),
- 'users_list' => $this->projectPermission->getMemberList($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']),
- 'description' => $params['project']['description'],
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
) + $params));
@@ -73,10 +72,6 @@ class Board extends Base
return $this->response->status(403);
}
- if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
- $this->response->text('Forbidden', 403);
- }
-
$values = $this->request->getJson();
$result =$this->taskPosition->movePosition(
@@ -101,22 +96,18 @@ class Board extends Base
*/
public function check()
{
- if (! $this->request->isAjax()) {
- return $this->response->status(403);
- }
-
$project_id = $this->request->getIntegerParam('project_id');
$timestamp = $this->request->getIntegerParam('timestamp');
- if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
- $this->response->text('Forbidden', 403);
+ if (! $project_id || ! $this->request->isAjax()) {
+ return $this->response->status(403);
}
if (! $this->project->isModifiedSince($project_id, $timestamp)) {
return $this->response->status(304);
}
- $this->response->html($this->renderBoard($project_id));
+ return $this->response->html($this->renderBoard($project_id));
}
/**
@@ -126,14 +117,10 @@ class Board extends Base
*/
public function reload()
{
- if (! $this->request->isAjax()) {
- return $this->response->status(403);
- }
-
$project_id = $this->request->getIntegerParam('project_id');
- if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
- $this->response->text('Forbidden', 403);
+ if (! $project_id || ! $this->request->isAjax()) {
+ return $this->response->status(403);
}
$values = $this->request->getJson();
@@ -143,195 +130,6 @@ class Board extends Base
}
/**
- * Get links on mouseover
- *
- * @access public
- */
- public function tasklinks()
- {
- $task = $this->getTask();
- $this->response->html($this->template->render('board/tooltip_tasklinks', array(
- 'links' => $this->taskLink->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- /**
- * Get subtasks on mouseover
- *
- * @access public
- */
- public function subtasks()
- {
- $task = $this->getTask();
- $this->response->html($this->template->render('board/tooltip_subtasks', array(
- 'subtasks' => $this->subtask->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- /**
- * Display all attachments during the task mouseover
- *
- * @access public
- */
- public function attachments()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('board/tooltip_files', array(
- 'files' => $this->file->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- /**
- * Display comments during a task mouseover
- *
- * @access public
- */
- public function comments()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('board/tooltip_comments', array(
- 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
- )));
- }
-
- /**
- * Display task description
- *
- * @access public
- */
- public function description()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('board/tooltip_description', array(
- 'task' => $task
- )));
- }
-
- /**
- * Change a task assignee directly from the board
- *
- * @access public
- */
- public function changeAssignee()
- {
- $task = $this->getTask();
- $project = $this->project->getById($task['project_id']);
-
- $this->response->html($this->template->render('board/popover_assignee', array(
- 'values' => $task,
- 'users_list' => $this->projectPermission->getMemberList($project['id']),
- 'project' => $project,
- )));
- }
-
- /**
- * Validate an assignee modification
- *
- * @access public
- */
- public function updateAssignee()
- {
- $values = $this->request->getValues();
-
- list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
-
- if ($valid && $this->taskModification->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update your task.'));
- }
-
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Change a task category directly from the board
- *
- * @access public
- */
- public function changeCategory()
- {
- $task = $this->getTask();
- $project = $this->project->getById($task['project_id']);
-
- $this->response->html($this->template->render('board/popover_category', array(
- 'values' => $task,
- 'categories_list' => $this->category->getList($project['id']),
- 'project' => $project,
- )));
- }
-
- /**
- * Validate a category modification
- *
- * @access public
- */
- public function updateCategory()
- {
- $values = $this->request->getValues();
-
- list($valid, ) = $this->taskValidator->validateCategoryModification($values);
-
- if ($valid && $this->taskModification->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update your task.'));
- }
-
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Screenshot popover
- *
- * @access public
- */
- public function screenshot()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('file/screenshot', array(
- 'task' => $task,
- 'redirect' => 'board',
- )));
- }
-
- /**
- * Get recurrence information on mouseover
- *
- * @access public
- */
- public function recurrence()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('task/recurring_info', array(
- 'task' => $task,
- 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
- 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
- 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
- )));
- }
-
- /**
- * Display swimlane description in tooltip
- *
- * @access public
- */
- public function swimlane()
- {
- $this->getProject();
- $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
- $this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
- }
-
- /**
* Enable collapsed mode
*
* @access public
@@ -355,6 +153,7 @@ class Board extends Base
* Change display mode
*
* @access private
+ * @param boolean $mode
*/
private function changeDisplayMode($mode)
{
@@ -372,6 +171,7 @@ class Board extends Base
* Render board
*
* @access private
+ * @param integer $project_id
*/
private function renderBoard($project_id)
{
diff --git a/app/Controller/BoardPopover.php b/app/Controller/BoardPopover.php
new file mode 100644
index 00000000..63dab302
--- /dev/null
+++ b/app/Controller/BoardPopover.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Board Popover
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class BoardPopover extends Base
+{
+ /**
+ * Change a task assignee directly from the board
+ *
+ * @access public
+ */
+ public function changeAssignee()
+ {
+ $task = $this->getTask();
+ $project = $this->project->getById($task['project_id']);
+
+ $this->response->html($this->template->render('board/popover_assignee', array(
+ 'values' => $task,
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
+ 'project' => $project,
+ )));
+ }
+
+ /**
+ * Validate an assignee modification
+ *
+ * @access public
+ */
+ public function updateAssignee()
+ {
+ $values = $this->request->getValues();
+
+ list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
+
+ if ($valid && $this->taskModification->update($values)) {
+ $this->flash->success(t('Task updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Change a task category directly from the board
+ *
+ * @access public
+ */
+ public function changeCategory()
+ {
+ $task = $this->getTask();
+ $project = $this->project->getById($task['project_id']);
+
+ $this->response->html($this->template->render('board/popover_category', array(
+ 'values' => $task,
+ 'categories_list' => $this->category->getList($project['id']),
+ 'project' => $project,
+ )));
+ }
+
+ /**
+ * Validate a category modification
+ *
+ * @access public
+ */
+ public function updateCategory()
+ {
+ $values = $this->request->getValues();
+
+ list($valid, ) = $this->taskValidator->validateCategoryModification($values);
+
+ if ($valid && $this->taskModification->update($values)) {
+ $this->flash->success(t('Task updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Screenshot popover
+ *
+ * @access public
+ */
+ public function screenshot()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('task_file/screenshot', array(
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Confirmation before to close all column tasks
+ *
+ * @access public
+ */
+ public function confirmCloseColumnTasks()
+ {
+ $project = $this->getProject();
+ $column_id = $this->request->getIntegerParam('column_id');
+ $swimlane_id = $this->request->getIntegerParam('swimlane_id');
+
+ $this->response->html($this->template->render('board/popover_close_all_tasks_column', array(
+ 'project' => $project,
+ 'nb_tasks' => $this->taskFinder->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id),
+ 'column' => $this->column->getColumnTitleById($column_id),
+ 'swimlane' => $this->swimlane->getNameById($swimlane_id) ?: t($project['default_swimlane']),
+ 'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id),
+ )));
+ }
+
+ /**
+ * Close all column tasks
+ *
+ * @access public
+ */
+ public function closeColumnTasks()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ $this->taskStatus->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']);
+ $this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->column->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane'])));
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id'])));
+ }
+}
diff --git a/app/Controller/BoardTooltip.php b/app/Controller/BoardTooltip.php
new file mode 100644
index 00000000..bc07ce09
--- /dev/null
+++ b/app/Controller/BoardTooltip.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Board Tooltip
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class BoardTooltip extends Base
+{
+ /**
+ * Get links on mouseover
+ *
+ * @access public
+ */
+ public function tasklinks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tooltip_tasklinks', array(
+ 'links' => $this->taskLink->getAllGroupedByLabel($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Get links on mouseover
+ *
+ * @access public
+ */
+ public function externallinks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tooltip_external_links', array(
+ 'links' => $this->taskExternalLink->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Get subtasks on mouseover
+ *
+ * @access public
+ */
+ public function subtasks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tooltip_subtasks', array(
+ 'subtasks' => $this->subtask->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Display all attachments during the task mouseover
+ *
+ * @access public
+ */
+ public function attachments()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('board/tooltip_files', array(
+ 'files' => $this->taskFile->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Display comments during a task mouseover
+ *
+ * @access public
+ */
+ public function comments()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('board/tooltip_comments', array(
+ 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
+ )));
+ }
+
+ /**
+ * Display task description
+ *
+ * @access public
+ */
+ public function description()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('board/tooltip_description', array(
+ 'task' => $task
+ )));
+ }
+
+ /**
+ * Get recurrence information on mouseover
+ *
+ * @access public
+ */
+ public function recurrence()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('task_recurrence/info', array(
+ 'task' => $task,
+ 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
+ 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
+ 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
+ )));
+ }
+
+ /**
+ * Display swimlane description in tooltip
+ *
+ * @access public
+ */
+ public function swimlane()
+ {
+ $this->getProject();
+ $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
+ $this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
+ }
+}
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
index 67a402d3..a0a25e41 100644
--- a/app/Controller/Calendar.php
+++ b/app/Controller/Calendar.php
@@ -20,7 +20,7 @@ class Calendar extends Base
*/
public function show()
{
- $this->response->html($this->template->layout('calendar/show', array(
+ $this->response->html($this->helper->layout->app('calendar/show', array(
'check_interval' => $this->config->get('board_private_refresh_interval'),
) + $this->getProjectFilters('calendar', 'show')));
}
diff --git a/app/Controller/Captcha.php b/app/Controller/Captcha.php
new file mode 100644
index 00000000..fcf081ea
--- /dev/null
+++ b/app/Controller/Captcha.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Gregwar\Captcha\CaptchaBuilder;
+
+/**
+ * Captcha Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Captcha extends Base
+{
+ /**
+ * Display captcha image
+ *
+ * @access public
+ */
+ public function image()
+ {
+ $this->response->contentType('image/jpeg');
+
+ $builder = new CaptchaBuilder;
+ $builder->build();
+ $this->sessionStorage->captcha = $builder->getPhrase();
+ $builder->output();
+ }
+}
diff --git a/app/Controller/Category.php b/app/Controller/Category.php
index 4aefd9fe..258a3b78 100644
--- a/app/Controller/Category.php
+++ b/app/Controller/Category.php
@@ -22,7 +22,7 @@ class Category extends Base
$category = $this->category->getById($this->request->getIntegerParam('category_id'));
if (empty($category)) {
- $this->session->flashError(t('Category not found.'));
+ $this->flash->failure(t('Category not found.'));
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project_id)));
}
@@ -38,7 +38,7 @@ class Category extends Base
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('category/index', array(
+ $this->response->html($this->helper->layout->project('category/index', array(
'categories' => $this->category->getList($project['id'], false),
'values' => $values + array('project_id' => $project['id']),
'errors' => $errors,
@@ -57,14 +57,14 @@ class Category extends Base
$project = $this->getProject();
$values = $this->request->getValues();
- list($valid, $errors) = $this->category->validateCreation($values);
+ list($valid, $errors) = $this->categoryValidator->validateCreation($values);
if ($valid) {
if ($this->category->create($values)) {
- $this->session->flash(t('Your category have been created successfully.'));
+ $this->flash->success(t('Your category have been created successfully.'));
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to create your category.'));
+ $this->flash->failure(t('Unable to create your category.'));
}
}
@@ -81,7 +81,7 @@ class Category extends Base
$project = $this->getProject();
$category = $this->getCategory($project['id']);
- $this->response->html($this->projectLayout('category/edit', array(
+ $this->response->html($this->helper->layout->project('category/edit', array(
'values' => empty($values) ? $category : $values,
'errors' => $errors,
'project' => $project,
@@ -99,14 +99,14 @@ class Category extends Base
$project = $this->getProject();
$values = $this->request->getValues();
- list($valid, $errors) = $this->category->validateModification($values);
+ list($valid, $errors) = $this->categoryValidator->validateModification($values);
if ($valid) {
if ($this->category->update($values)) {
- $this->session->flash(t('Your category have been updated successfully.'));
+ $this->flash->success(t('Your category have been updated successfully.'));
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to update your category.'));
+ $this->flash->failure(t('Unable to update your category.'));
}
}
@@ -123,7 +123,7 @@ class Category extends Base
$project = $this->getProject();
$category = $this->getCategory($project['id']);
- $this->response->html($this->projectLayout('category/remove', array(
+ $this->response->html($this->helper->layout->project('category/remove', array(
'project' => $project,
'category' => $category,
'title' => t('Remove a category')
@@ -142,9 +142,9 @@ class Category extends Base
$category = $this->getCategory($project['id']);
if ($this->category->remove($category['id'])) {
- $this->session->flash(t('Category removed successfully.'));
+ $this->flash->success(t('Category removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this category.'));
+ $this->flash->failure(t('Unable to remove this category.'));
}
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id'])));
diff --git a/app/Controller/Column.php b/app/Controller/Column.php
index d28fb293..66073b56 100644
--- a/app/Controller/Column.php
+++ b/app/Controller/Column.php
@@ -15,20 +15,12 @@ class Column extends Base
*
* @access public
*/
- public function index(array $values = array(), array $errors = array())
+ public function index()
{
$project = $this->getProject();
- $columns = $this->board->getColumns($project['id']);
+ $columns = $this->column->getAll($project['id']);
- foreach ($columns as $column) {
- $values['title['.$column['id'].']'] = $column['title'];
- $values['description['.$column['id'].']'] = $column['description'];
- $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
- }
-
- $this->response->html($this->projectLayout('column/index', array(
- 'errors' => $errors,
- 'values' => $values + array('project_id' => $project['id']),
+ $this->response->html($this->helper->layout->project('column/index', array(
'columns' => $columns,
'project' => $project,
'title' => t('Edit board')
@@ -36,33 +28,48 @@ class Column extends Base
}
/**
- * Validate and add a new column
+ * Show form to create a new column
*
* @access public
*/
- public function create()
+ public function create(array $values = array(), array $errors = array())
{
$project = $this->getProject();
- $columns = $this->board->getColumnsList($project['id']);
- $data = $this->request->getValues();
- $values = array();
- foreach ($columns as $column_id => $column_title) {
- $values['title['.$column_id.']'] = $column_title;
+ if (empty($values)) {
+ $values = array('project_id' => $project['id']);
}
- list($valid, $errors) = $this->board->validateCreation($data);
+ $this->response->html($this->template->render('column/create', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'project' => $project,
+ 'title' => t('Add a new column')
+ )));
+ }
+
+ /**
+ * Validate and add a new column
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->columnValidator->validateCreation($values);
if ($valid) {
- if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) {
- $this->session->flash(t('Board updated successfully.'));
- $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
+ if ($this->column->create($project['id'], $values['title'], $values['task_limit'], $values['description'])) {
+ $this->flash->success(t('Column created successfully.'));
+ return $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])), true);
} else {
- $this->session->flashError(t('Unable to update this board.'));
+ $errors['title'] = array(t('Another column with the same name exists in the project'));
}
}
- $this->index($values, $errors);
+ $this->create($values, $errors);
}
/**
@@ -73,9 +80,9 @@ class Column extends Base
public function edit(array $values = array(), array $errors = array())
{
$project = $this->getProject();
- $column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
+ $column = $this->column->getById($this->request->getIntegerParam('column_id'));
- $this->response->html($this->projectLayout('column/edit', array(
+ $this->response->html($this->helper->layout->project('column/edit', array(
'errors' => $errors,
'values' => $values ?: $column,
'project' => $project,
@@ -94,14 +101,14 @@ class Column extends Base
$project = $this->getProject();
$values = $this->request->getValues();
- list($valid, $errors) = $this->board->validateModification($values);
+ list($valid, $errors) = $this->columnValidator->validateModification($values);
if ($valid) {
- if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
- $this->session->flash(t('Board updated successfully.'));
+ if ($this->column->update($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
+ $this->flash->success(t('Board updated successfully.'));
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to update this board.'));
+ $this->flash->failure(t('Unable to update this board.'));
}
}
@@ -109,22 +116,21 @@ class Column extends Base
}
/**
- * Move a column up or down
+ * Move column position
*
* @access public
*/
public function move()
{
- $this->checkCSRFParam();
$project = $this->getProject();
- $column_id = $this->request->getIntegerParam('column_id');
- $direction = $this->request->getStringParam('direction');
+ $values = $this->request->getJson();
- if ($direction === 'up' || $direction === 'down') {
- $this->board->{'move'.$direction}($project['id'], $column_id);
+ if (! empty($values) && isset($values['column_id']) && isset($values['position'])) {
+ $result = $this->column->changePosition($project['id'], $values['column_id'], $values['position']);
+ return $this->response->json(array('result' => $result));
}
- $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
+ $this->forbidden();
}
/**
@@ -136,8 +142,8 @@ class Column extends Base
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('column/remove', array(
- 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
+ $this->response->html($this->helper->layout->project('column/remove', array(
+ 'column' => $this->column->getById($this->request->getIntegerParam('column_id')),
'project' => $project,
'title' => t('Remove a column from a board')
)));
@@ -152,12 +158,12 @@ class Column extends Base
{
$project = $this->getProject();
$this->checkCSRFParam();
- $column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
+ $column_id = $this->request->getIntegerParam('column_id');
- if (! empty($column) && $this->board->removeColumn($column['id'])) {
- $this->session->flash(t('Column removed successfully.'));
+ if ($this->column->remove($column_id)) {
+ $this->flash->success(t('Column removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this column.'));
+ $this->flash->failure(t('Unable to remove this column.'));
}
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
diff --git a/app/Controller/Comment.php b/app/Controller/Comment.php
index d6cbbf1e..da3213e0 100644
--- a/app/Controller/Comment.php
+++ b/app/Controller/Comment.php
@@ -21,13 +21,11 @@ class Comment extends Base
$comment = $this->comment->getById($this->request->getIntegerParam('comment_id'));
if (empty($comment)) {
- $this->notfound();
+ return $this->notfound();
}
if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) {
- $this->response->html($this->template->layout('comment/forbidden', array(
- 'title' => t('Access Forbidden')
- )));
+ return $this->forbidden();
}
return $comment;
@@ -41,7 +39,6 @@ class Comment extends Base
public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
- $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
if (empty($values)) {
$values = array(
@@ -50,16 +47,7 @@ class Comment extends Base
);
}
- if ($ajax) {
- $this->response->html($this->template->render('comment/create', array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'ajax' => $ajax,
- )));
- }
-
- $this->response->html($this->taskLayout('comment/create', array(
+ $this->response->html($this->helper->layout->task('comment/create', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
@@ -76,22 +64,17 @@ class Comment extends Base
{
$task = $this->getTask();
$values = $this->request->getValues();
- $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
- list($valid, $errors) = $this->comment->validateCreation($values);
+ list($valid, $errors) = $this->commentValidator->validateCreation($values);
if ($valid) {
if ($this->comment->create($values)) {
- $this->session->flash(t('Comment added successfully.'));
+ $this->flash->success(t('Comment added successfully.'));
} else {
- $this->session->flashError(t('Unable to create your comment.'));
+ $this->flash->failure(t('Unable to create your comment.'));
}
- if ($ajax) {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true);
}
$this->create($values, $errors);
@@ -107,7 +90,7 @@ class Comment extends Base
$task = $this->getTask();
$comment = $this->getComment();
- $this->response->html($this->taskLayout('comment/edit', array(
+ $this->response->html($this->helper->layout->task('comment/edit', array(
'values' => empty($values) ? $comment : $values,
'errors' => $errors,
'comment' => $comment,
@@ -124,19 +107,19 @@ class Comment extends Base
public function update()
{
$task = $this->getTask();
- $comment = $this->getComment();
+ $this->getComment();
$values = $this->request->getValues();
- list($valid, $errors) = $this->comment->validateModification($values);
+ list($valid, $errors) = $this->commentValidator->validateModification($values);
if ($valid) {
if ($this->comment->update($values)) {
- $this->session->flash(t('Comment updated successfully.'));
+ $this->flash->success(t('Comment updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update your comment.'));
+ $this->flash->failure(t('Unable to update your comment.'));
}
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comment-'.$comment['id']));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), false);
}
$this->edit($values, $errors);
@@ -152,7 +135,7 @@ class Comment extends Base
$task = $this->getTask();
$comment = $this->getComment();
- $this->response->html($this->taskLayout('comment/remove', array(
+ $this->response->html($this->helper->layout->task('comment/remove', array(
'comment' => $comment,
'task' => $task,
'title' => t('Remove a comment')
@@ -171,9 +154,9 @@ class Comment extends Base
$comment = $this->getComment();
if ($this->comment->remove($comment['id'])) {
- $this->session->flash(t('Comment removed successfully.'));
+ $this->flash->success(t('Comment removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this comment.'));
+ $this->flash->failure(t('Unable to remove this comment.'));
}
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'));
diff --git a/app/Controller/Config.php b/app/Controller/Config.php
index 47b844e4..e811f870 100644
--- a/app/Controller/Config.php
+++ b/app/Controller/Config.php
@@ -11,24 +11,6 @@ namespace Kanboard\Controller;
class Config extends Base
{
/**
- * Common layout for config views
- *
- * @access private
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- private function layout($template, array $params)
- {
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
- $params['values'] = $this->config->getAll();
- $params['errors'] = array();
- $params['config_content_for_layout'] = $this->template->render($template, $params);
-
- return $this->template->layout('config/layout', $params);
- }
-
- /**
* Common method between pages
*
* @access private
@@ -40,8 +22,16 @@ class Config extends Base
$values = $this->request->getValues();
switch ($redirect) {
+ case 'application':
+ $values += array('password_reset' => 0);
+ break;
case 'project':
- $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0);
+ $values += array(
+ 'subtask_restriction' => 0,
+ 'subtask_time_tracking' => 0,
+ 'cfd_include_closed_tasks' => 0,
+ 'disable_private_project' => 0,
+ );
break;
case 'integrations':
$values += array('integration_gravatar' => 0);
@@ -53,9 +43,9 @@ class Config extends Base
if ($this->config->save($values)) {
$this->config->reload();
- $this->session->flash(t('Settings saved successfully.'));
+ $this->flash->success(t('Settings saved successfully.'));
} else {
- $this->session->flashError(t('Unable to save your settings.'));
+ $this->flash->failure(t('Unable to save your settings.'));
}
$this->response->redirect($this->helper->url->to('config', $redirect));
@@ -69,7 +59,7 @@ class Config extends Base
*/
public function index()
{
- $this->response->html($this->layout('config/about', array(
+ $this->response->html($this->helper->layout->config('config/about', array(
'db_size' => $this->config->getDatabaseSize(),
'title' => t('Settings').' &gt; '.t('About'),
)));
@@ -82,7 +72,7 @@ class Config extends Base
*/
public function plugins()
{
- $this->response->html($this->layout('config/plugins', array(
+ $this->response->html($this->helper->layout->config('config/plugins', array(
'plugins' => $this->pluginLoader->plugins,
'title' => t('Settings').' &gt; '.t('Plugins'),
)));
@@ -97,10 +87,12 @@ class Config extends Base
{
$this->common('application');
- $this->response->html($this->layout('config/application', array(
+ $this->response->html($this->helper->layout->config('config/application', array(
'languages' => $this->config->getLanguages(),
'timezones' => $this->config->getTimezones(),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
+ 'datetime_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateTimeFormats()),
+ 'time_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getTimeFormats()),
'title' => t('Settings').' &gt; '.t('Application settings'),
)));
}
@@ -114,7 +106,7 @@ class Config extends Base
{
$this->common('project');
- $this->response->html($this->layout('config/project', array(
+ $this->response->html($this->helper->layout->config('config/project', array(
'colors' => $this->color->getList(),
'default_columns' => implode(', ', $this->board->getDefaultColumns()),
'title' => t('Settings').' &gt; '.t('Project settings'),
@@ -130,7 +122,7 @@ class Config extends Base
{
$this->common('board');
- $this->response->html($this->layout('config/board', array(
+ $this->response->html($this->helper->layout->config('config/board', array(
'title' => t('Settings').' &gt; '.t('Board settings'),
)));
}
@@ -144,7 +136,7 @@ class Config extends Base
{
$this->common('calendar');
- $this->response->html($this->layout('config/calendar', array(
+ $this->response->html($this->helper->layout->config('config/calendar', array(
'title' => t('Settings').' &gt; '.t('Calendar settings'),
)));
}
@@ -158,7 +150,7 @@ class Config extends Base
{
$this->common('integrations');
- $this->response->html($this->layout('config/integrations', array(
+ $this->response->html($this->helper->layout->config('config/integrations', array(
'title' => t('Settings').' &gt; '.t('Integrations'),
)));
}
@@ -172,7 +164,7 @@ class Config extends Base
{
$this->common('webhook');
- $this->response->html($this->layout('config/webhook', array(
+ $this->response->html($this->helper->layout->config('config/webhook', array(
'title' => t('Settings').' &gt; '.t('Webhook settings'),
)));
}
@@ -184,7 +176,7 @@ class Config extends Base
*/
public function api()
{
- $this->response->html($this->layout('config/api', array(
+ $this->response->html($this->helper->layout->config('config/api', array(
'title' => t('Settings').' &gt; '.t('API'),
)));
}
@@ -210,7 +202,7 @@ class Config extends Base
{
$this->checkCSRFParam();
$this->config->optimizeDatabase();
- $this->session->flash(t('Database optimization done.'));
+ $this->flash->success(t('Database optimization done.'));
$this->response->redirect($this->helper->url->to('config', 'index'));
}
@@ -226,7 +218,7 @@ class Config extends Base
$this->checkCSRFParam();
$this->config->regenerateToken($type.'_token');
- $this->session->flash(t('Token regenerated.'));
+ $this->flash->success(t('Token regenerated.'));
$this->response->redirect($this->helper->url->to('config', $type));
}
}
diff --git a/app/Controller/Currency.php b/app/Controller/Currency.php
index 9d6b0249..ecaa9834 100644
--- a/app/Controller/Currency.php
+++ b/app/Controller/Currency.php
@@ -11,34 +11,18 @@ namespace Kanboard\Controller;
class Currency extends Base
{
/**
- * Common layout for config views
- *
- * @access private
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- private function layout($template, array $params)
- {
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
- $params['config_content_for_layout'] = $this->template->render($template, $params);
-
- return $this->template->layout('config/layout', $params);
- }
-
- /**
* Display all currency rates and form
*
* @access public
*/
public function index(array $values = array(), array $errors = array())
{
- $this->response->html($this->layout('currency/index', array(
+ $this->response->html($this->helper->layout->config('currency/index', array(
'config_values' => array('application_currency' => $this->config->get('application_currency')),
'values' => $values,
'errors' => $errors,
'rates' => $this->currency->getAll(),
- 'currencies' => $this->config->getCurrencies(),
+ 'currencies' => $this->currency->getCurrencies(),
'title' => t('Settings').' &gt; '.t('Currency rates'),
)));
}
@@ -51,14 +35,14 @@ class Currency extends Base
public function create()
{
$values = $this->request->getValues();
- list($valid, $errors) = $this->currency->validate($values);
+ list($valid, $errors) = $this->currencyValidator->validateCreation($values);
if ($valid) {
if ($this->currency->create($values['currency'], $values['rate'])) {
- $this->session->flash(t('The currency rate have been added successfully.'));
+ $this->flash->success(t('The currency rate have been added successfully.'));
$this->response->redirect($this->helper->url->to('currency', 'index'));
} else {
- $this->session->flashError(t('Unable to add this currency rate.'));
+ $this->flash->failure(t('Unable to add this currency rate.'));
}
}
@@ -76,9 +60,9 @@ class Currency extends Base
if ($this->config->save($values)) {
$this->config->reload();
- $this->session->flash(t('Settings saved successfully.'));
+ $this->flash->success(t('Settings saved successfully.'));
} else {
- $this->session->flashError(t('Unable to save your settings.'));
+ $this->flash->failure(t('Unable to save your settings.'));
}
$this->response->redirect($this->helper->url->to('currency', 'index'));
diff --git a/app/Controller/Customfilter.php b/app/Controller/Customfilter.php
index a152c668..41da0b11 100644
--- a/app/Controller/Customfilter.php
+++ b/app/Controller/Customfilter.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Core\Security\Role;
+
/**
* Custom Filter management
*
@@ -19,7 +21,7 @@ class Customfilter extends Base
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('custom_filter/index', array(
+ $this->response->html($this->helper->layout->project('custom_filter/index', array(
'values' => $values + array('project_id' => $project['id']),
'errors' => $errors,
'project' => $project,
@@ -40,14 +42,14 @@ class Customfilter extends Base
$values = $this->request->getValues();
$values['user_id'] = $this->userSession->getId();
- list($valid, $errors) = $this->customFilter->validateCreation($values);
+ list($valid, $errors) = $this->customFilterValidator->validateCreation($values);
if ($valid) {
if ($this->customFilter->create($values)) {
- $this->session->flash(t('Your custom filter have been created successfully.'));
+ $this->flash->success(t('Your custom filter have been created successfully.'));
$this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to create your custom filter.'));
+ $this->flash->failure(t('Unable to create your custom filter.'));
}
}
@@ -55,6 +57,23 @@ class Customfilter extends Base
}
/**
+ * Confirmation dialog before removing a custom filter
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $project = $this->getProject();
+ $filter = $this->customFilter->getById($this->request->getIntegerParam('filter_id'));
+
+ $this->response->html($this->helper->layout->project('custom_filter/remove', array(
+ 'project' => $project,
+ 'filter' => $filter,
+ 'title' => t('Remove a custom filter')
+ )));
+ }
+
+ /**
* Remove a custom filter
*
* @access public
@@ -68,9 +87,9 @@ class Customfilter extends Base
$this->checkPermission($project, $filter);
if ($this->customFilter->remove($filter['id'])) {
- $this->session->flash(t('Custom filter removed successfully.'));
+ $this->flash->success(t('Custom filter removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this custom filter.'));
+ $this->flash->failure(t('Unable to remove this custom filter.'));
}
$this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id'])));
@@ -88,7 +107,7 @@ class Customfilter extends Base
$this->checkPermission($project, $filter);
- $this->response->html($this->projectLayout('custom_filter/edit', array(
+ $this->response->html($this->helper->layout->project('custom_filter/edit', array(
'values' => empty($values) ? $filter : $values,
'errors' => $errors,
'project' => $project,
@@ -119,14 +138,14 @@ class Customfilter extends Base
$values += array('append' => 0);
}
- list($valid, $errors) = $this->customFilter->validateModification($values);
+ list($valid, $errors) = $this->customFilterValidator->validateModification($values);
if ($valid) {
if ($this->customFilter->update($values)) {
- $this->session->flash(t('Your custom filter have been updated successfully.'));
+ $this->flash->success(t('Your custom filter have been updated successfully.'));
$this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to update custom filter.'));
+ $this->flash->failure(t('Unable to update custom filter.'));
}
}
@@ -137,7 +156,7 @@ class Customfilter extends Base
{
$user_id = $this->userSession->getId();
- if ($filter['user_id'] != $user_id && (! $this->projectPermission->isManager($project['id'], $user_id) || ! $this->userSession->isAdmin())) {
+ if ($filter['user_id'] != $user_id && ($this->projectUserRole->getUserRole($project['id'], $user_id) === Role::PROJECT_MANAGER || ! $this->userSession->isAdmin())) {
$this->forbidden();
}
}
diff --git a/app/Controller/Doc.php b/app/Controller/Doc.php
index 32413048..6f309d48 100644
--- a/app/Controller/Doc.php
+++ b/app/Controller/Doc.php
@@ -52,8 +52,6 @@ class Doc extends Base
}
}
- $this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
- )));
+ $this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename)));
}
}
diff --git a/app/Controller/Export.php b/app/Controller/Export.php
index cdedcb88..726edd42 100644
--- a/app/Controller/Export.php
+++ b/app/Controller/Export.php
@@ -27,7 +27,7 @@ class Export extends Base
$this->response->csv($data);
}
- $this->response->html($this->projectLayout('export/'.$action, array(
+ $this->response->html($this->helper->layout->project('export/'.$action, array(
'values' => array(
'controller' => 'export',
'action' => $action,
@@ -37,7 +37,7 @@ class Export extends Base
),
'errors' => array(),
'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
'project' => $project,
'title' => $page_title,
), 'export/sidebar'));
@@ -70,7 +70,7 @@ class Export extends Base
*/
public function summary()
{
- $this->common('ProjectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export'));
+ $this->common('projectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export'));
}
/**
diff --git a/app/Controller/Feed.php b/app/Controller/Feed.php
index 95b81fb8..8457c383 100644
--- a/app/Controller/Feed.php
+++ b/app/Controller/Feed.php
@@ -25,10 +25,8 @@ class Feed extends Base
$this->forbidden(true);
}
- $projects = $this->projectPermission->getActiveMemberProjects($user['id']);
-
$this->response->xml($this->template->render('feed/user', array(
- 'events' => $this->projectActivity->getProjects(array_keys($projects)),
+ 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])),
'user' => $user,
)));
}
diff --git a/app/Controller/File.php b/app/Controller/File.php
deleted file mode 100644
index 4d771e2f..00000000
--- a/app/Controller/File.php
+++ /dev/null
@@ -1,192 +0,0 @@
-<?php
-
-namespace Kanboard\Controller;
-
-use Kanboard\Core\ObjectStorage\ObjectStorageException;
-
-/**
- * File controller
- *
- * @package controller
- * @author Frederic Guillot
- */
-class File extends Base
-{
- /**
- * Screenshot
- *
- * @access public
- */
- public function screenshot()
- {
- $task = $this->getTask();
-
- if ($this->request->isPost() && $this->file->uploadScreenshot($task['project_id'], $task['id'], $this->request->getValue('screenshot')) !== false) {
- $this->session->flash(t('Screenshot uploaded successfully.'));
-
- if ($this->request->getStringParam('redirect') === 'board') {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
- }
-
- $this->response->html($this->taskLayout('file/screenshot', array(
- 'task' => $task,
- 'redirect' => 'task',
- )));
- }
-
- /**
- * File upload form
- *
- * @access public
- */
- public function create()
- {
- $task = $this->getTask();
-
- $this->response->html($this->taskLayout('file/new', array(
- 'task' => $task,
- 'max_size' => ini_get('upload_max_filesize'),
- )));
- }
-
- /**
- * File upload (save files)
- *
- * @access public
- */
- public function save()
- {
- $task = $this->getTask();
-
- if (! $this->file->uploadFiles($task['project_id'], $task['id'], 'files')) {
- $this->session->flashError(t('Unable to upload the file.'));
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
- }
-
- /**
- * File download
- *
- * @access public
- */
- public function download()
- {
- try {
- $task = $this->getTask();
- $file = $this->file->getById($this->request->getIntegerParam('file_id'));
-
- if ($file['task_id'] != $task['id']) {
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
- }
-
- $this->response->forceDownload($file['name']);
- $this->objectStorage->output($file['path']);
- } catch (ObjectStorageException $e) {
- $this->logger->error($e->getMessage());
- }
- }
-
- /**
- * Open a file (show the content in a popover)
- *
- * @access public
- */
- public function open()
- {
- $task = $this->getTask();
- $file = $this->file->getById($this->request->getIntegerParam('file_id'));
-
- if ($file['task_id'] == $task['id']) {
- $this->response->html($this->template->render('file/open', array(
- 'file' => $file,
- 'task' => $task,
- )));
- }
- }
-
- /**
- * Display image
- *
- * @access public
- */
- public function image()
- {
- try {
- $task = $this->getTask();
- $file = $this->file->getById($this->request->getIntegerParam('file_id'));
-
- if ($file['task_id'] == $task['id']) {
- $this->response->contentType($this->file->getImageMimeType($file['name']));
- $this->objectStorage->output($file['path']);
- }
- } catch (ObjectStorageException $e) {
- $this->logger->error($e->getMessage());
- }
- }
-
- /**
- * Display image thumbnails
- *
- * @access public
- */
- public function thumbnail()
- {
- $this->response->contentType('image/jpeg');
-
- try {
- $task = $this->getTask();
- $file = $this->file->getById($this->request->getIntegerParam('file_id'));
-
- if ($file['task_id'] == $task['id']) {
- $this->objectStorage->output($this->file->getThumbnailPath($file['path']));
- }
- } catch (ObjectStorageException $e) {
- $this->logger->error($e->getMessage());
-
- // Try to generate thumbnail on the fly for images uploaded before Kanboard < 1.0.19
- $data = $this->objectStorage->get($file['path']);
- $this->file->generateThumbnailFromData($file['path'], $data);
- $this->objectStorage->output($this->file->getThumbnailPath($file['path']));
- }
- }
-
- /**
- * Remove a file
- *
- * @access public
- */
- public function remove()
- {
- $this->checkCSRFParam();
- $task = $this->getTask();
- $file = $this->file->getById($this->request->getIntegerParam('file_id'));
-
- if ($file['task_id'] == $task['id'] && $this->file->remove($file['id'])) {
- $this->session->flash(t('File removed successfully.'));
- } else {
- $this->session->flashError(t('Unable to remove this file.'));
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
- }
-
- /**
- * Confirmation dialog before removing a file
- *
- * @access public
- */
- public function confirm()
- {
- $task = $this->getTask();
- $file = $this->file->getById($this->request->getIntegerParam('file_id'));
-
- $this->response->html($this->taskLayout('file/remove', array(
- 'task' => $task,
- 'file' => $file,
- )));
- }
-}
diff --git a/app/Controller/FileViewer.php b/app/Controller/FileViewer.php
new file mode 100644
index 00000000..bc91c3d8
--- /dev/null
+++ b/app/Controller/FileViewer.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\ObjectStorage\ObjectStorageException;
+
+/**
+ * File Viewer Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class FileViewer extends Base
+{
+ /**
+ * Get file content from object storage
+ *
+ * @access private
+ * @param array $file
+ * @return string
+ */
+ private function getFileContent(array $file)
+ {
+ $content = '';
+
+ try {
+
+ if ($file['is_image'] == 0) {
+ $content = $this->objectStorage->get($file['path']);
+ }
+
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+
+ return $content;
+ }
+
+ /**
+ * Show file content in a popover
+ *
+ * @access public
+ */
+ public function show()
+ {
+ $file = $this->getFile();
+ $type = $this->helper->file->getPreviewType($file['name']);
+ $params = array('file_id' => $file['id'], 'project_id' => $this->request->getIntegerParam('project_id'));
+
+ if ($file['model'] === 'taskFile') {
+ $params['task_id'] = $file['task_id'];
+ }
+
+ $this->response->html($this->template->render('file_viewer/show', array(
+ 'file' => $file,
+ 'params' => $params,
+ 'type' => $type,
+ 'content' => $this->getFileContent($file),
+ )));
+ }
+
+ /**
+ * Display image
+ *
+ * @access public
+ */
+ public function image()
+ {
+ 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());
+ }
+ }
+
+ /**
+ * Display image thumbnail
+ *
+ * @access public
+ */
+ public function thumbnail()
+ {
+ $this->response->contentType('image/jpeg');
+
+ try {
+ $file = $this->getFile();
+ $model = $file['model'];
+ $this->objectStorage->output($this->$model->getThumbnailPath($file['path']));
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+
+ // Try to generate thumbnail on the fly for images uploaded before Kanboard < 1.0.19
+ $data = $this->objectStorage->get($file['path']);
+ $this->$model->generateThumbnailFromData($file['path'], $data);
+ $this->objectStorage->output($this->$model->getThumbnailPath($file['path']));
+ }
+ }
+
+ /**
+ * File download
+ *
+ * @access public
+ */
+ public function download()
+ {
+ try {
+ $file = $this->getFile();
+ $this->response->forceDownload($file['name']);
+ $this->objectStorage->output($file['path']);
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+}
diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php
index 24d94f02..9ffa277f 100644
--- a/app/Controller/Gantt.php
+++ b/app/Controller/Gantt.php
@@ -20,13 +20,12 @@ class Gantt extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
- $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
- $this->response->html($this->template->layout('gantt/projects', array(
+ $this->response->html($this->helper->layout->app('gantt/projects', array(
'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
'title' => t('Gantt chart for all projects'),
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
)));
}
@@ -65,8 +64,8 @@ class Gantt extends Base
$filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position');
}
- $this->response->html($this->template->layout('gantt/project', $params + array(
- 'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
+ $this->response->html($this->helper->layout->app('gantt/project', $params + array(
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
'sorting' => $sorting,
'tasks' => $filter->format(),
)));
@@ -102,19 +101,23 @@ class Gantt extends Base
{
$project = $this->getProject();
+ $values = $values + array(
+ 'project_id' => $project['id'],
+ 'column_id' => $this->column->getFirstColumnId($project['id']),
+ 'position' => 1
+ );
+
+ $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values));
+ $values = $this->hook->merge('controller:gantt:task:form:default', $values, array('default_values' => $values));
+
$this->response->html($this->template->render('gantt/task_creation', array(
+ 'project' => $project,
'errors' => $errors,
- 'values' => $values + array(
- 'project_id' => $project['id'],
- 'column_id' => $this->board->getFirstColumn($project['id']),
- 'position' => 1
- ),
- 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
+ 'values' => $values,
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $this->swimlane->getList($project['id'], false, true),
- 'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $project['name'].' &gt; '.t('New task')
)));
}
@@ -135,10 +138,10 @@ class Gantt extends Base
$task_id = $this->taskCreation->create($values);
if ($task_id !== false) {
- $this->session->flash(t('Task created successfully.'));
+ $this->flash->success(t('Task created successfully.'));
$this->response->redirect($this->helper->url->to('gantt', 'project', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to create your task.'));
+ $this->flash->failure(t('Unable to create your task.'));
}
}
diff --git a/app/Controller/Group.php b/app/Controller/Group.php
new file mode 100644
index 00000000..fa47f428
--- /dev/null
+++ b/app/Controller/Group.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Group Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Group extends Base
+{
+ /**
+ * List all groups
+ *
+ * @access public
+ */
+ public function index()
+ {
+ $paginator = $this->paginator
+ ->setUrl('group', 'index')
+ ->setMax(30)
+ ->setOrder('name')
+ ->setQuery($this->group->getQuery())
+ ->calculate();
+
+ $this->response->html($this->helper->layout->app('group/index', array(
+ 'title' => t('Groups').' ('.$paginator->getTotal().')',
+ 'paginator' => $paginator,
+ )));
+ }
+
+ /**
+ * List all users
+ *
+ * @access public
+ */
+ public function users()
+ {
+ $group_id = $this->request->getIntegerParam('group_id');
+ $group = $this->group->getById($group_id);
+
+ $paginator = $this->paginator
+ ->setUrl('group', 'users', array('group_id' => $group_id))
+ ->setMax(30)
+ ->setOrder('username')
+ ->setQuery($this->groupMember->getQuery($group_id))
+ ->calculate();
+
+ $this->response->html($this->helper->layout->app('group/users', array(
+ 'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')',
+ 'paginator' => $paginator,
+ 'group' => $group,
+ )));
+ }
+
+ /**
+ * Display a form to create a new group
+ *
+ * @access public
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $this->response->html($this->helper->layout->app('group/create', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'title' => t('New group')
+ )));
+ }
+
+ /**
+ * Validate and save a new group
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->groupValidator->validateCreation($values);
+
+ if ($valid) {
+ if ($this->group->create($values['name']) !== false) {
+ $this->flash->success(t('Group created successfully.'));
+ $this->response->redirect($this->helper->url->to('group', 'index'));
+ } else {
+ $this->flash->failure(t('Unable to create your group.'));
+ }
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Display a form to update a group
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ if (empty($values)) {
+ $values = $this->group->getById($this->request->getIntegerParam('group_id'));
+ }
+
+ $this->response->html($this->helper->layout->app('group/edit', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'title' => t('Edit group')
+ )));
+ }
+
+ /**
+ * Validate and save a group
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->groupValidator->validateModification($values);
+
+ if ($valid) {
+ if ($this->group->update($values) !== false) {
+ $this->flash->success(t('Group updated successfully.'));
+ $this->response->redirect($this->helper->url->to('group', 'index'));
+ } else {
+ $this->flash->failure(t('Unable to update your group.'));
+ }
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Form to associate a user to a group
+ *
+ * @access public
+ */
+ public function associate(array $values = array(), array $errors = array())
+ {
+ $group_id = $this->request->getIntegerParam('group_id');
+ $group = $this->group->getbyId($group_id);
+
+ if (empty($values)) {
+ $values['group_id'] = $group_id;
+ }
+
+ $this->response->html($this->helper->layout->app('group/associate', array(
+ 'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)),
+ 'group' => $group,
+ 'errors' => $errors,
+ 'values' => $values,
+ 'title' => t('Add group member to "%s"', $group['name']),
+ )));
+ }
+
+ /**
+ * Add user to a group
+ *
+ * @access public
+ */
+ public function addUser()
+ {
+ $values = $this->request->getValues();
+
+ if (isset($values['group_id']) && isset($values['user_id'])) {
+ if ($this->groupMember->addUser($values['group_id'], $values['user_id'])) {
+ $this->flash->success(t('Group member added successfully.'));
+ $this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $values['group_id'])));
+ } else {
+ $this->flash->failure(t('Unable to add group member.'));
+ }
+ }
+
+ $this->associate($values);
+ }
+
+ /**
+ * Confirmation dialog to remove a user from a group
+ *
+ * @access public
+ */
+ public function dissociate()
+ {
+ $group_id = $this->request->getIntegerParam('group_id');
+ $user_id = $this->request->getIntegerParam('user_id');
+ $group = $this->group->getById($group_id);
+ $user = $this->user->getById($user_id);
+
+ $this->response->html($this->helper->layout->app('group/dissociate', array(
+ 'group' => $group,
+ 'user' => $user,
+ 'title' => t('Remove user from group "%s"', $group['name']),
+ )));
+ }
+
+ /**
+ * Remove a user from a group
+ *
+ * @access public
+ */
+ public function removeUser()
+ {
+ $this->checkCSRFParam();
+ $group_id = $this->request->getIntegerParam('group_id');
+ $user_id = $this->request->getIntegerParam('user_id');
+
+ if ($this->groupMember->removeUser($group_id, $user_id)) {
+ $this->flash->success(t('User removed successfully from this group.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this user from the group.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $group_id)));
+ }
+
+ /**
+ * Confirmation dialog to remove a group
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $group_id = $this->request->getIntegerParam('group_id');
+ $group = $this->group->getById($group_id);
+
+ $this->response->html($this->helper->layout->app('group/remove', array(
+ 'group' => $group,
+ 'title' => t('Remove group'),
+ )));
+ }
+
+ /**
+ * Remove a group
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $group_id = $this->request->getIntegerParam('group_id');
+
+ if ($this->group->remove($group_id)) {
+ $this->flash->success(t('Group removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this group.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('group', 'index'));
+ }
+}
diff --git a/app/Controller/GroupHelper.php b/app/Controller/GroupHelper.php
new file mode 100644
index 00000000..34f522a6
--- /dev/null
+++ b/app/Controller/GroupHelper.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Group Helper
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class GroupHelper extends Base
+{
+ /**
+ * Group autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $search = $this->request->getStringParam('term');
+ $groups = $this->groupManager->find($search);
+ $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format());
+ }
+}
diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php
index f8e9e25f..f1ea6d8f 100644
--- a/app/Controller/Ical.php
+++ b/app/Controller/Ical.php
@@ -3,6 +3,7 @@
namespace Kanboard\Controller;
use Kanboard\Model\TaskFilter;
+use Kanboard\Model\Task as TaskModel;
use Eluceo\iCal\Component\Calendar as iCalendar;
/**
@@ -31,6 +32,7 @@ class Ical extends Base
// Common filter
$filter = $this->taskFilterICalendarFormatter
->create()
+ ->filterByStatus(TaskModel::STATUS_OPEN)
->filterByOwner($user['id']);
// Calendar properties
@@ -60,6 +62,7 @@ class Ical extends Base
// Common filter
$filter = $this->taskFilterICalendarFormatter
->create()
+ ->filterByStatus(TaskModel::STATUS_OPEN)
->filterByProject($project['id']);
// Calendar properties
diff --git a/app/Controller/Link.php b/app/Controller/Link.php
index 0eb3d679..ec7ab1af 100644
--- a/app/Controller/Link.php
+++ b/app/Controller/Link.php
@@ -12,22 +12,6 @@ namespace Kanboard\Controller;
class Link extends Base
{
/**
- * Common layout for config views
- *
- * @access private
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- private function layout($template, array $params)
- {
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
- $params['config_content_for_layout'] = $this->template->render($template, $params);
-
- return $this->template->layout('config/layout', $params);
- }
-
- /**
* Get the current link
*
* @access private
@@ -51,7 +35,7 @@ class Link extends Base
*/
public function index(array $values = array(), array $errors = array())
{
- $this->response->html($this->layout('link/index', array(
+ $this->response->html($this->helper->layout->config('link/index', array(
'links' => $this->link->getMergedList(),
'values' => $values,
'errors' => $errors,
@@ -67,14 +51,14 @@ class Link extends Base
public function save()
{
$values = $this->request->getValues();
- list($valid, $errors) = $this->link->validateCreation($values);
+ list($valid, $errors) = $this->linkValidator->validateCreation($values);
if ($valid) {
if ($this->link->create($values['label'], $values['opposite_label']) !== false) {
- $this->session->flash(t('Link added successfully.'));
+ $this->flash->success(t('Link added successfully.'));
$this->response->redirect($this->helper->url->to('link', 'index'));
} else {
- $this->session->flashError(t('Unable to create your link.'));
+ $this->flash->failure(t('Unable to create your link.'));
}
}
@@ -91,7 +75,7 @@ class Link extends Base
$link = $this->getLink();
$link['label'] = t($link['label']);
- $this->response->html($this->layout('link/edit', array(
+ $this->response->html($this->helper->layout->config('link/edit', array(
'values' => $values ?: $link,
'errors' => $errors,
'labels' => $this->link->getList($link['id']),
@@ -108,14 +92,14 @@ class Link extends Base
public function update()
{
$values = $this->request->getValues();
- list($valid, $errors) = $this->link->validateModification($values);
+ list($valid, $errors) = $this->linkValidator->validateModification($values);
if ($valid) {
if ($this->link->update($values)) {
- $this->session->flash(t('Link updated successfully.'));
+ $this->flash->success(t('Link updated successfully.'));
$this->response->redirect($this->helper->url->to('link', 'index'));
} else {
- $this->session->flashError(t('Unable to update your link.'));
+ $this->flash->failure(t('Unable to update your link.'));
}
}
@@ -131,7 +115,7 @@ class Link extends Base
{
$link = $this->getLink();
- $this->response->html($this->layout('link/remove', array(
+ $this->response->html($this->helper->layout->config('link/remove', array(
'link' => $link,
'title' => t('Remove a link')
)));
@@ -148,9 +132,9 @@ class Link extends Base
$link = $this->getLink();
if ($this->link->remove($link['id'])) {
- $this->session->flash(t('Link removed successfully.'));
+ $this->flash->success(t('Link removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this link.'));
+ $this->flash->failure(t('Unable to remove this link.'));
}
$this->response->redirect($this->helper->url->to('link', 'index'));
diff --git a/app/Controller/Listing.php b/app/Controller/Listing.php
index b9c851f5..c784dd50 100644
--- a/app/Controller/Listing.php
+++ b/app/Controller/Listing.php
@@ -30,8 +30,11 @@ class Listing extends Base
->setQuery($query)
->calculate();
- $this->response->html($this->template->layout('listing/show', $params + array(
+ $this->response->html($this->helper->layout->app('listing/show', $params + array(
'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 8c701cf7..452faecd 100644
--- a/app/Controller/Oauth.php
+++ b/app/Controller/Oauth.php
@@ -11,49 +11,19 @@ namespace Kanboard\Controller;
class Oauth extends Base
{
/**
- * Link or authenticate a Google account
- *
- * @access public
- */
- public function google()
- {
- $this->step1('google');
- }
-
- /**
- * Link or authenticate a Github account
- *
- * @access public
- */
- public function github()
- {
- $this->step1('github');
- }
-
- /**
- * Link or authenticate a Gitlab account
- *
- * @access public
- */
- public function gitlab()
- {
- $this->step1('gitlab');
- }
-
- /**
* Unlink external account
*
* @access public
*/
- public function unlink($backend = '')
+ public function unlink()
{
- $backend = $this->request->getStringParam('backend', $backend);
+ $backend = $this->request->getStringParam('backend');
$this->checkCSRFParam();
- if ($this->authentication->backend($backend)->unlink($this->userSession->getId())) {
- $this->session->flash(t('Your external account is not linked anymore to your profile.'));
+ 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->session->flashError(t('Unable to unlink your external account.'));
+ $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())));
@@ -63,46 +33,52 @@ class Oauth extends Base
* Redirect to the provider if no code received
*
* @access private
+ * @param string $provider
*/
- private function step1($backend)
+ protected function step1($provider)
{
$code = $this->request->getStringParam('code');
if (! empty($code)) {
- $this->step2($backend, $code);
+ $this->step2($provider, $code);
} else {
- $this->response->redirect($this->authentication->backend($backend)->getService()->getAuthorizationUrl());
+ $this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl());
}
}
/**
* Link or authenticate the user
*
- * @access private
+ * @access protected
+ * @param string $provider
+ * @param string $code
*/
- private function step2($backend, $code)
+ protected function step2($provider, $code)
{
- $profile = $this->authentication->backend($backend)->getProfile($code);
+ $this->authenticationManager->getProvider($provider)->setCode($code);
if ($this->userSession->isLogged()) {
- $this->link($backend, $profile);
+ $this->link($provider);
}
- $this->authenticate($backend, $profile);
+ $this->authenticate($provider);
}
/**
* Link the account
*
- * @access private
+ * @access protected
+ * @param string $provider
*/
- private function link($backend, $profile)
+ protected function link($provider)
{
- if (empty($profile)) {
- $this->session->flashError(t('External authentication failed'));
+ $authProvider = $this->authenticationManager->getProvider($provider);
+
+ if (! $authProvider->authenticate()) {
+ $this->flash->failure(t('External authentication failed'));
} else {
- $this->session->flash(t('Your external account is linked to your profile successfully.'));
- $this->authentication->backend($backend)->updateUser($this->userSession->getId(), $profile);
+ $this->userProfile->assign($this->userSession->getId(), $authProvider->getUser());
+ $this->flash->success(t('Your external account is linked to your profile successfully.'));
}
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
@@ -111,14 +87,15 @@ class Oauth extends Base
/**
* Authenticate the account
*
- * @access private
+ * @access protected
+ * @param string $provider
*/
- private function authenticate($backend, $profile)
+ protected function authenticate($provider)
{
- if (! empty($profile) && $this->authentication->backend($backend)->authenticate($profile['id'])) {
+ if ($this->authenticationManager->oauthAuthentication($provider)) {
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
- $this->response->html($this->template->layout('auth/index', array(
+ $this->response->html($this->helper->layout->app('auth/index', array(
'errors' => array('login' => t('External authentication failed')),
'values' => array(),
'no_layout' => true,
diff --git a/app/Controller/PasswordReset.php b/app/Controller/PasswordReset.php
new file mode 100644
index 00000000..f6a0eb8e
--- /dev/null
+++ b/app/Controller/PasswordReset.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Password Reset Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class PasswordReset extends Base
+{
+ /**
+ * Show the form to reset the password
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $this->checkActivation();
+
+ $this->response->html($this->helper->layout->app('password_reset/create', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'no_layout' => true,
+ )));
+ }
+
+ /**
+ * Validate and send the email
+ */
+ public function save()
+ {
+ $this->checkActivation();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->passwordResetValidator->validateCreation($values);
+
+ if ($valid) {
+ $this->sendEmail($values['username']);
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Show the form to set a new password
+ */
+ public function change(array $values = array(), array $errors = array())
+ {
+ $this->checkActivation();
+
+ $token = $this->request->getStringParam('token');
+ $user_id = $this->passwordReset->getUserIdByToken($token);
+
+ if ($user_id !== false) {
+ $this->response->html($this->helper->layout->app('password_reset/change', array(
+ 'token' => $token,
+ 'errors' => $errors,
+ 'values' => $values,
+ 'no_layout' => true,
+ )));
+ }
+
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+
+ /**
+ * Set the new password
+ */
+ public function update()
+ {
+ $this->checkActivation();
+
+ $token = $this->request->getStringParam('token');
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->passwordResetValidator->validateModification($values);
+
+ if ($valid) {
+ $user_id = $this->passwordReset->getUserIdByToken($token);
+
+ if ($user_id !== false) {
+ $this->user->update(array('id' => $user_id, 'password' => $values['password']));
+ $this->passwordReset->disable($user_id);
+ }
+
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+
+ $this->change($values, $errors);
+ }
+
+ /**
+ * Send the email
+ */
+ private function sendEmail($username)
+ {
+ $token = $this->passwordReset->create($username);
+
+ if ($token !== false) {
+ $user = $this->user->getByUsername($username);
+
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ t('Password Reset for Kanboard'),
+ $this->template->render('password_reset/email', array('token' => $token))
+ );
+ }
+ }
+
+ /**
+ * Check feature availability
+ */
+ private function checkActivation()
+ {
+ if ($this->config->get('password_reset', 0) == 0) {
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+ }
+}
diff --git a/app/Controller/Project.php b/app/Controller/Project.php
index f30d70e2..cdfbd94a 100644
--- a/app/Controller/Project.php
+++ b/app/Controller/Project.php
@@ -20,7 +20,7 @@ class Project extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
- $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
$nb_projects = count($project_ids);
@@ -29,11 +29,10 @@ class Project extends Base
->setUrl('project', 'index')
->setMax(20)
->setOrder('name')
- ->setQuery($this->project->getQueryProjectDetails($project_ids))
+ ->setQuery($this->project->getQueryColumnStats($project_ids))
->calculate();
- $this->response->html($this->template->layout('project/index', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ $this->response->html($this->helper->layout->app('project/index', array(
'paginator' => $paginator,
'nb_projects' => $nb_projects,
'title' => t('Projects').' ('.$nb_projects.')'
@@ -49,7 +48,7 @@ class Project extends Base
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('project/show', array(
+ $this->response->html($this->helper->layout->project('project/show', array(
'project' => $project,
'stats' => $this->project->getTaskStats($project['id']),
'title' => $project['name'],
@@ -70,15 +69,15 @@ class Project extends Base
$this->checkCSRFParam();
if ($this->project->{$switch.'PublicAccess'}($project['id'])) {
- $this->session->flash(t('Project updated successfully.'));
+ $this->flash->success(t('Project updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update this project.'));
+ $this->flash->failure(t('Unable to update this project.'));
}
$this->response->redirect($this->helper->url->to('project', 'share', array('project_id' => $project['id'])));
}
- $this->response->html($this->projectLayout('project/share', array(
+ $this->response->html($this->helper->layout->project('project/share', array(
'project' => $project,
'title' => t('Public access'),
)));
@@ -95,11 +94,11 @@ class Project extends Base
if ($this->request->isPost()) {
$this->projectMetadata->save($project['id'], $this->request->getValues());
- $this->session->flash(t('Project updated successfully.'));
+ $this->flash->success(t('Project updated successfully.'));
$this->response->redirect($this->helper->url->to('project', 'integrations', array('project_id' => $project['id'])));
}
- $this->response->html($this->projectLayout('project/integrations', array(
+ $this->response->html($this->helper->layout->project('project/integrations', array(
'project' => $project,
'title' => t('Integrations'),
'webhook_token' => $this->config->get('webhook_token'),
@@ -120,11 +119,11 @@ class Project extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
$this->projectNotification->saveSettings($project['id'], $values);
- $this->session->flash(t('Project updated successfully.'));
+ $this->flash->success(t('Project updated successfully.'));
$this->response->redirect($this->helper->url->to('project', 'notifications', array('project_id' => $project['id'])));
}
- $this->response->html($this->projectLayout('project/notifications', array(
+ $this->response->html($this->helper->layout->project('project/notifications', array(
'notifications' => $this->projectNotification->readSettings($project['id']),
'types' => $this->projectNotificationType->getTypes(),
'project' => $project,
@@ -133,171 +132,6 @@ class Project extends Base
}
/**
- * Display a form to edit a project
- *
- * @access public
- */
- public function edit(array $values = array(), array $errors = array())
- {
- $project = $this->getProject();
-
- $this->response->html($this->projectLayout('project/edit', array(
- 'values' => empty($values) ? $project : $values,
- 'errors' => $errors,
- 'project' => $project,
- 'title' => t('Edit project')
- )));
- }
-
- /**
- * Validate and update a project
- *
- * @access public
- */
- public function update()
- {
- $project = $this->getProject();
- $values = $this->request->getValues();
-
- if (isset($values['is_private'])) {
- if (! $this->helper->user->isProjectAdministrationAllowed($project['id'])) {
- unset($values['is_private']);
- }
- } elseif ($project['is_private'] == 1 && ! isset($values['is_private'])) {
- if ($this->helper->user->isProjectAdministrationAllowed($project['id'])) {
- $values += array('is_private' => 0);
- }
- }
-
- list($valid, $errors) = $this->project->validateModification($values);
-
- if ($valid) {
- if ($this->project->update($values)) {
- $this->session->flash(t('Project updated successfully.'));
- $this->response->redirect($this->helper->url->to('project', 'edit', array('project_id' => $project['id'])));
- } else {
- $this->session->flashError(t('Unable to update this project.'));
- }
- }
-
- $this->edit($values, $errors);
- }
-
- /**
- * Users list for the selected project
- *
- * @access public
- */
- public function users()
- {
- $project = $this->getProject();
-
- $this->response->html($this->projectLayout('project/users', array(
- 'project' => $project,
- 'users' => $this->projectPermission->getAllUsers($project['id']),
- 'title' => t('Edit project access list')
- )));
- }
-
- /**
- * Allow everybody
- *
- * @access public
- */
- public function allowEverybody()
- {
- $project = $this->getProject();
- $values = $this->request->getValues() + array('is_everybody_allowed' => 0);
- list($valid, ) = $this->projectPermission->validateProjectModification($values);
-
- if ($valid) {
- if ($this->project->update($values)) {
- $this->session->flash(t('Project updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $project['id'])));
- }
-
- /**
- * Allow a specific user (admin only)
- *
- * @access public
- */
- public function allow()
- {
- $values = $this->request->getValues();
- list($valid, ) = $this->projectPermission->validateUserModification($values);
-
- if ($valid) {
- if ($this->projectPermission->addMember($values['project_id'], $values['user_id'])) {
- $this->session->flash(t('Project updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Change the role of a project member
- *
- * @access public
- */
- public function role()
- {
- $this->checkCSRFParam();
-
- $values = array(
- 'project_id' => $this->request->getIntegerParam('project_id'),
- 'user_id' => $this->request->getIntegerParam('user_id'),
- 'is_owner' => $this->request->getIntegerParam('is_owner'),
- );
-
- list($valid, ) = $this->projectPermission->validateUserModification($values);
-
- if ($valid) {
- if ($this->projectPermission->changeRole($values['project_id'], $values['user_id'], $values['is_owner'])) {
- $this->session->flash(t('Project updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Revoke user access (admin only)
- *
- * @access public
- */
- public function revoke()
- {
- $this->checkCSRFParam();
-
- $values = array(
- 'project_id' => $this->request->getIntegerParam('project_id'),
- 'user_id' => $this->request->getIntegerParam('user_id'),
- );
-
- list($valid, ) = $this->projectPermission->validateUserModification($values);
-
- if ($valid) {
- if ($this->projectPermission->revokeMember($values['project_id'], $values['user_id'])) {
- $this->session->flash(t('Project updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
- }
-
- /**
* Remove a project
*
* @access public
@@ -310,15 +144,15 @@ class Project extends Base
$this->checkCSRFParam();
if ($this->project->remove($project['id'])) {
- $this->session->flash(t('Project removed successfully.'));
+ $this->flash->success(t('Project removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this project.'));
+ $this->flash->failure(t('Unable to remove this project.'));
}
$this->response->redirect($this->helper->url->to('project', 'index'));
}
- $this->response->html($this->projectLayout('project/remove', array(
+ $this->response->html($this->helper->layout->project('project/remove', array(
'project' => $project,
'title' => t('Remove project')
)));
@@ -336,17 +170,18 @@ class Project extends Base
$project = $this->getProject();
if ($this->request->getStringParam('duplicate') === 'yes') {
- $values = array_keys($this->request->getValues());
- if ($this->projectDuplication->duplicate($project['id'], $values) !== false) {
- $this->session->flash(t('Project cloned successfully.'));
+ $project_id = $this->projectDuplication->duplicate($project['id'], array_keys($this->request->getValues()), $this->userSession->getId());
+
+ if ($project_id !== false) {
+ $this->flash->success(t('Project cloned successfully.'));
} else {
- $this->session->flashError(t('Unable to clone this project.'));
+ $this->flash->failure(t('Unable to clone this project.'));
}
- $this->response->redirect($this->helper->url->to('project', 'index'));
+ $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id)));
}
- $this->response->html($this->projectLayout('project/duplicate', array(
+ $this->response->html($this->helper->layout->project('project/duplicate', array(
'project' => $project,
'title' => t('Clone this project')
)));
@@ -365,15 +200,15 @@ class Project extends Base
$this->checkCSRFParam();
if ($this->project->disable($project['id'])) {
- $this->session->flash(t('Project disabled successfully.'));
+ $this->flash->success(t('Project disabled successfully.'));
} else {
- $this->session->flashError(t('Unable to disable this project.'));
+ $this->flash->failure(t('Unable to disable this project.'));
}
$this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project['id'])));
}
- $this->response->html($this->projectLayout('project/disable', array(
+ $this->response->html($this->helper->layout->project('project/disable', array(
'project' => $project,
'title' => t('Project activation')
)));
@@ -392,59 +227,17 @@ class Project extends Base
$this->checkCSRFParam();
if ($this->project->enable($project['id'])) {
- $this->session->flash(t('Project activated successfully.'));
+ $this->flash->success(t('Project activated successfully.'));
} else {
- $this->session->flashError(t('Unable to activate this project.'));
+ $this->flash->failure(t('Unable to activate this project.'));
}
$this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project['id'])));
}
- $this->response->html($this->projectLayout('project/enable', array(
+ $this->response->html($this->helper->layout->project('project/enable', array(
'project' => $project,
'title' => t('Project activation')
)));
}
-
- /**
- * Display a form to create a new project
- *
- * @access public
- */
- public function create(array $values = array(), array $errors = array())
- {
- $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() || $this->userSession->isProjectAdmin() ? 0 : 1);
-
- $this->response->html($this->template->layout('project/new', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
- 'values' => empty($values) ? array('is_private' => $is_private) : $values,
- 'errors' => $errors,
- 'is_private' => $is_private,
- 'title' => $is_private ? t('New private project') : t('New project'),
- )));
- }
-
- /**
- * Validate and save a new project
- *
- * @access public
- */
- public function save()
- {
- $values = $this->request->getValues();
- list($valid, $errors) = $this->project->validateCreation($values);
-
- if ($valid) {
- $project_id = $this->project->create($values, $this->userSession->getId(), true);
-
- if ($project_id > 0) {
- $this->session->flash(t('Your project have been created successfully.'));
- $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id)));
- }
-
- $this->session->flashError(t('Unable to create your project.'));
- }
-
- $this->create($values, $errors);
- }
}
diff --git a/app/Controller/ProjectCreation.php b/app/Controller/ProjectCreation.php
new file mode 100644
index 00000000..88f41fcd
--- /dev/null
+++ b/app/Controller/ProjectCreation.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Project Creation Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class ProjectCreation extends Base
+{
+ /**
+ * Display a form to create a new project
+ *
+ * @access public
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $is_private = isset($values['is_private']) && $values['is_private'] == 1;
+ $projects_list = array(0 => t('Do not duplicate anything')) + $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
+
+ $this->response->html($this->helper->layout->app('project_creation/create', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'is_private' => $is_private,
+ 'projects_list' => $projects_list,
+ 'title' => $is_private ? t('New private project') : t('New project'),
+ )));
+ }
+
+ /**
+ * Display a form to create a private project
+ *
+ * @access public
+ */
+ public function createPrivate(array $values = array(), array $errors = array())
+ {
+ $values['is_private'] = 1;
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Validate and save a new project
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->projectValidator->validateCreation($values);
+
+ if ($valid) {
+ $project_id = $this->createOrDuplicate($values);
+
+ if ($project_id > 0) {
+ $this->flash->success(t('Your project have been created successfully.'));
+ return $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id)));
+ }
+
+ $this->flash->failure(t('Unable to create your project.'));
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Create or duplicate a project
+ *
+ * @access private
+ * @param array $values
+ * @return boolean|integer
+ */
+ private function createOrDuplicate(array $values)
+ {
+ if (empty($values['src_project_id'])) {
+ return $this->createNewProject($values);
+ }
+
+ return $this->duplicateNewProject($values);
+ }
+
+ /**
+ * Save a new project
+ *
+ * @access private
+ * @param array $values
+ * @return boolean|integer
+ */
+ private function createNewProject(array $values)
+ {
+ $project = array(
+ 'name' => $values['name'],
+ 'is_private' => $values['is_private'],
+ );
+
+ return $this->project->create($project, $this->userSession->getId(), true);
+ }
+
+ /**
+ * Creatte from another project
+ *
+ * @access private
+ * @param array $values
+ * @return boolean|integer
+ */
+ private function duplicateNewProject(array $values)
+ {
+ $selection = array();
+
+ foreach ($this->projectDuplication->getOptionalSelection() as $item) {
+ if (isset($values[$item]) && $values[$item] == 1) {
+ $selection[] = $item;
+ }
+ }
+
+ return $this->projectDuplication->duplicate(
+ $values['src_project_id'],
+ $selection,
+ $this->userSession->getId(),
+ $values['name'],
+ $values['is_private'] == 1
+ );
+ }
+}
diff --git a/app/Controller/ProjectEdit.php b/app/Controller/ProjectEdit.php
new file mode 100644
index 00000000..f4a3a7cb
--- /dev/null
+++ b/app/Controller/ProjectEdit.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Project Edit Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class ProjectEdit extends Base
+{
+ /**
+ * General edition (most common operations)
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $this->renderView('project_edit/general', $values, $errors);
+ }
+
+ /**
+ * Change start and end dates
+ *
+ * @access public
+ */
+ public function dates(array $values = array(), array $errors = array())
+ {
+ $this->renderView('project_edit/dates', $values, $errors);
+ }
+
+ /**
+ * Change project description
+ *
+ * @access public
+ */
+ public function description(array $values = array(), array $errors = array())
+ {
+ $this->renderView('project_edit/description', $values, $errors);
+ }
+
+ /**
+ * Change task priority
+ *
+ * @access public
+ */
+ public function priority(array $values = array(), array $errors = array())
+ {
+ $this->renderView('project_edit/task_priority', $values, $errors);
+ }
+
+ /**
+ * Validate and update a project
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+ $redirect = $this->request->getStringParam('redirect', 'edit');
+
+ $values = $this->prepareValues($redirect, $project, $values);
+ list($valid, $errors) = $this->projectValidator->validateModification($values);
+
+ if ($valid) {
+ if ($this->project->update($values)) {
+ $this->flash->success(t('Project updated successfully.'));
+ $this->response->redirect($this->helper->url->to('ProjectEdit', $redirect, array('project_id' => $project['id'])));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+ }
+
+ $this->$redirect($values, $errors);
+ }
+
+ /**
+ * Prepare form values
+ *
+ * @access private
+ * @param string $redirect
+ * @param array $project
+ * @param array $values
+ * @return array
+ */
+ private function prepareValues($redirect, array $project, array $values)
+ {
+ if ($redirect === 'edit') {
+ if (isset($values['is_private'])) {
+ if (! $this->helper->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])) {
+ unset($values['is_private']);
+ }
+ } elseif ($project['is_private'] == 1 && ! isset($values['is_private'])) {
+ if ($this->helper->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])) {
+ $values += array('is_private' => 0);
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Common metthod to render different views
+ *
+ * @access private
+ * @param string $template
+ * @param array $values
+ * @param array $errors
+ */
+ private function renderView($template, array $values, array $errors)
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->helper->layout->project($template, array(
+ 'owners' => $this->projectUserRole->getAssignableUsersList($project['id'], true),
+ 'values' => empty($values) ? $project : $values,
+ 'errors' => $errors,
+ 'project' => $project,
+ 'title' => t('Edit project')
+ )));
+ }
+}
diff --git a/app/Controller/ProjectFile.php b/app/Controller/ProjectFile.php
new file mode 100644
index 00000000..96764a92
--- /dev/null
+++ b/app/Controller/ProjectFile.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Project File Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class ProjectFile extends Base
+{
+ /**
+ * File upload form
+ *
+ * @access public
+ */
+ public function create()
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->template->render('project_file/create', array(
+ 'project' => $project,
+ 'max_size' => $this->helper->text->phpToBytes(ini_get('upload_max_filesize')),
+ )));
+ }
+
+ /**
+ * Save uploaded files
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $project = $this->getProject();
+
+ if (! $this->projectFile->uploadFiles($project['id'], $this->request->getFileInfo('files'))) {
+ $this->flash->failure(t('Unable to upload the file.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectOverview', 'show', array('project_id' => $project['id'])), true);
+ }
+
+ /**
+ * Remove a file
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $file = $this->projectFile->getById($this->request->getIntegerParam('file_id'));
+
+ if ($this->projectFile->remove($file['id'])) {
+ $this->flash->success(t('File removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this file.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectOverview', 'show', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Confirmation dialog before removing a file
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $project = $this->getProject();
+ $file = $this->projectFile->getById($this->request->getIntegerParam('file_id'));
+
+ $this->response->html($this->template->render('project_file/remove', array(
+ 'project' => $project,
+ 'file' => $file,
+ )));
+ }
+}
diff --git a/app/Controller/ProjectOverview.php b/app/Controller/ProjectOverview.php
new file mode 100644
index 00000000..b0687ed3
--- /dev/null
+++ b/app/Controller/ProjectOverview.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Project Overview Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class ProjectOverview extends Base
+{
+ /**
+ * Show project overview
+ */
+ 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']);
+
+ $this->project->getColumnStats($params['project']);
+
+ $this->response->html($this->helper->layout->app('project_overview/show', $params));
+ }
+}
diff --git a/app/Controller/ProjectPermission.php b/app/Controller/ProjectPermission.php
new file mode 100644
index 00000000..800da02f
--- /dev/null
+++ b/app/Controller/ProjectPermission.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project Permission
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class ProjectPermission extends Base
+{
+ /**
+ * Permissions are only available for team projects
+ *
+ * @access protected
+ * @param integer $project_id Default project id
+ * @return array
+ */
+ protected function getProject($project_id = 0)
+ {
+ $project = parent::getProject($project_id);
+
+ if ($project['is_private'] == 1) {
+ $this->forbidden();
+ }
+
+ return $project;
+ }
+
+ /**
+ * Show all permissions
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+
+ if (empty($values)) {
+ $values['role'] = Role::PROJECT_MEMBER;
+ }
+
+ $this->response->html($this->helper->layout->project('project_permission/index', array(
+ 'project' => $project,
+ 'users' => $this->projectUserRole->getUsers($project['id']),
+ 'groups' => $this->projectGroupRole->getGroups($project['id']),
+ 'roles' => $this->role->getProjectRoles(),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'title' => t('Project Permissions'),
+ )));
+ }
+
+ /**
+ * Allow everybody
+ *
+ * @access public
+ */
+ public function allowEverybody()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues() + array('is_everybody_allowed' => 0);
+
+ if ($this->project->update($values)) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Add user to the project
+ *
+ * @access public
+ */
+ public function addUser()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ if ($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.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Revoke user access
+ *
+ * @access public
+ */
+ public function removeUser()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $user_id = $this->request->getIntegerParam('user_id');
+
+ if ($this->projectUserRole->removeUser($project['id'], $user_id)) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Change user role
+ *
+ * @access public
+ */
+ public function changeUserRole()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getJson();
+
+ if (! empty($project) && ! empty($values) && $this->projectUserRole->changeUserRole($project['id'], $values['id'], $values['role'])) {
+ $this->response->json(array('status' => 'ok'));
+ } else {
+ $this->response->json(array('status' => 'error'));
+ }
+ }
+
+ /**
+ * Add group to the project
+ *
+ * @access public
+ */
+ public function addGroup()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ if (empty($values['group_id']) && ! empty($values['external_id'])) {
+ $values['group_id'] = $this->group->create($values['name'], $values['external_id']);
+ }
+
+ if ($this->projectGroupRole->addGroup($project['id'], $values['group_id'], $values['role'])) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Revoke group access
+ *
+ * @access public
+ */
+ public function removeGroup()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $group_id = $this->request->getIntegerParam('group_id');
+
+ if ($this->projectGroupRole->removeGroup($project['id'], $group_id)) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Change group role
+ *
+ * @access public
+ */
+ public function changeGroupRole()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getJson();
+
+ if (! empty($project) && ! empty($values) && $this->projectGroupRole->changeGroupRole($project['id'], $values['id'], $values['role'])) {
+ $this->response->json(array('status' => 'ok'));
+ } else {
+ $this->response->json(array('status' => 'error'));
+ }
+ }
+}
diff --git a/app/Controller/Projectuser.php b/app/Controller/Projectuser.php
index 18829b3c..a6d4fe4e 100644
--- a/app/Controller/Projectuser.php
+++ b/app/Controller/Projectuser.php
@@ -4,6 +4,7 @@ namespace Kanboard\Controller;
use Kanboard\Model\User as UserModel;
use Kanboard\Model\Task as TaskModel;
+use Kanboard\Core\Security\Role;
/**
* Project User overview
@@ -13,23 +14,6 @@ use Kanboard\Model\Task as TaskModel;
*/
class Projectuser extends Base
{
- /**
- * Common layout for users overview views
- *
- * @access private
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- private function layout($template, array $params)
- {
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
- $params['content_for_sublayout'] = $this->template->render($template, $params);
- $params['filter'] = array('user_id' => $params['user_id']);
-
- return $this->template->layout('project_user/layout', $params);
- }
-
private function common()
{
$user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID);
@@ -37,19 +21,19 @@ class Projectuser extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
- $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
- return array($user_id, $project_ids, $this->user->getList(true));
+ return array($user_id, $project_ids, $this->user->getActiveUsersList(true));
}
- private function role($is_owner, $action, $title, $title_user)
+ private function role($role, $action, $title, $title_user)
{
list($user_id, $project_ids, $users) = $this->common();
- $query = $this->projectPermission->getQueryByRole($project_ids, $is_owner)->callback(array($this->project, 'applyColumnStats'));
+ $query = $this->projectPermission->getQueryByRole($project_ids, $role)->callback(array($this->project, 'applyColumnStats'));
- if ($user_id !== UserModel::EVERYBODY_ID) {
+ if ($user_id !== UserModel::EVERYBODY_ID && isset($users[$user_id])) {
$query->eq(UserModel::TABLE.'.id', $user_id);
$title = t($title_user, $users[$user_id]);
}
@@ -61,7 +45,7 @@ class Projectuser extends Base
->setQuery($query)
->calculate();
- $this->response->html($this->layout('project_user/roles', array(
+ $this->response->html($this->helper->layout->projectUser('project_user/roles', array(
'paginator' => $paginator,
'title' => $title,
'user_id' => $user_id,
@@ -75,7 +59,7 @@ class Projectuser extends Base
$query = $this->taskFinder->getProjectUserOverviewQuery($project_ids, $is_active);
- if ($user_id !== UserModel::EVERYBODY_ID) {
+ if ($user_id !== UserModel::EVERYBODY_ID && isset($users[$user_id])) {
$query->eq(TaskModel::TABLE.'.owner_id', $user_id);
$title = t($title_user, $users[$user_id]);
}
@@ -87,7 +71,7 @@ class Projectuser extends Base
->setQuery($query)
->calculate();
- $this->response->html($this->layout('project_user/tasks', array(
+ $this->response->html($this->helper->layout->projectUser('project_user/tasks', array(
'paginator' => $paginator,
'title' => $title,
'user_id' => $user_id,
@@ -101,7 +85,7 @@ class Projectuser extends Base
*/
public function managers()
{
- $this->role(1, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
+ $this->role(Role::PROJECT_MANAGER, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
}
/**
@@ -110,7 +94,7 @@ class Projectuser extends Base
*/
public function members()
{
- $this->role(0, 'members', t('People who are project members'), 'Projects where "%s" is member');
+ $this->role(ROLE::PROJECT_MEMBER, 'members', t('People who are project members'), 'Projects where "%s" is member');
}
/**
@@ -130,4 +114,17 @@ class Projectuser extends Base
{
$this->tasks(TaskModel::STATUS_CLOSED, 'closed', t('Closed tasks'), 'Closed tasks assigned to "%s"');
}
+
+ /**
+ * Users tooltip
+ */
+ public function users()
+ {
+ $project = $this->getProject();
+
+ return $this->response->html($this->template->render('project_user/tooltip_users', array(
+ 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']),
+ 'roles' => $this->role->getProjectRoles(),
+ )));
+ }
}
diff --git a/app/Controller/Search.php b/app/Controller/Search.php
index 0aff9073..9b9b9e65 100644
--- a/app/Controller/Search.php
+++ b/app/Controller/Search.php
@@ -12,7 +12,7 @@ class Search extends Base
{
public function index()
{
- $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$search = urldecode($this->request->getStringParam('search'));
$nb_tasks = 0;
@@ -36,8 +36,7 @@ class Search extends Base
$nb_tasks = $paginator->getTotal();
}
- $this->response->html($this->template->layout('search/index', array(
- 'board_selector' => $projects,
+ $this->response->html($this->helper->layout->app('search/index', array(
'values' => array(
'search' => $search,
'controller' => 'search',
diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php
index 4ef3e74e..8ca0ce92 100644
--- a/app/Controller/Subtask.php
+++ b/app/Controller/Subtask.php
@@ -2,8 +2,6 @@
namespace Kanboard\Controller;
-use Kanboard\Model\Subtask as SubtaskModel;
-
/**
* Subtask controller
*
@@ -13,20 +11,19 @@ use Kanboard\Model\Subtask as SubtaskModel;
class Subtask extends Base
{
/**
- * Get the current subtask
- *
- * @access private
- * @return array
+ * Show list of subtasks
*/
- private function getSubtask()
+ public function show()
{
- $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id'));
-
- if (empty($subtask)) {
- $this->notfound();
- }
+ $task = $this->getTask();
- return $subtask;
+ $this->response->html($this->helper->layout->task('subtask/show', array(
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
+ 'task' => $task,
+ 'project' => $this->getProject(),
+ 'subtasks' => $this->subtask->getAll($task['id']),
+ 'editable' => true,
+ )));
}
/**
@@ -45,10 +42,10 @@ class Subtask extends Base
);
}
- $this->response->html($this->taskLayout('subtask/create', array(
+ $this->response->html($this->helper->layout->task('subtask/create', array(
'values' => $values,
'errors' => $errors,
- 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'task' => $task,
)));
}
@@ -63,20 +60,20 @@ class Subtask extends Base
$task = $this->getTask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->subtask->validateCreation($values);
+ list($valid, $errors) = $this->subtaskValidator->validateCreation($values);
if ($valid) {
if ($this->subtask->create($values)) {
- $this->session->flash(t('Sub-task added successfully.'));
+ $this->flash->success(t('Sub-task added successfully.'));
} else {
- $this->session->flashError(t('Unable to create your sub-task.'));
+ $this->flash->failure(t('Unable to create your sub-task.'));
}
if (isset($values['another_subtask']) && $values['another_subtask'] == 1) {
- $this->response->redirect($this->helper->url->to('subtask', 'create', array('project_id' => $task['project_id'], 'task_id' => $task['id'], 'another_subtask' => 1)));
+ return $this->create(array('project_id' => $task['project_id'], 'task_id' => $task['id'], 'another_subtask' => 1));
}
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks'));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks'), true);
}
$this->create($values, $errors);
@@ -92,10 +89,10 @@ class Subtask extends Base
$task = $this->getTask();
$subtask = $this->getSubTask();
- $this->response->html($this->taskLayout('subtask/edit', array(
+ $this->response->html($this->helper->layout->task('subtask/edit', array(
'values' => empty($values) ? $subtask : $values,
'errors' => $errors,
- 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'status_list' => $this->subtask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
@@ -113,16 +110,16 @@ class Subtask extends Base
$this->getSubtask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->subtask->validateModification($values);
+ list($valid, $errors) = $this->subtaskValidator->validateModification($values);
if ($valid) {
if ($this->subtask->update($values)) {
- $this->session->flash(t('Sub-task updated successfully.'));
+ $this->flash->success(t('Sub-task updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update your sub-task.'));
+ $this->flash->failure(t('Unable to update your sub-task.'));
}
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks'));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
}
$this->edit($values, $errors);
@@ -138,7 +135,7 @@ class Subtask extends Base
$task = $this->getTask();
$subtask = $this->getSubtask();
- $this->response->html($this->taskLayout('subtask/remove', array(
+ $this->response->html($this->helper->layout->task('subtask/remove', array(
'subtask' => $subtask,
'task' => $task,
)));
@@ -156,102 +153,12 @@ class Subtask extends Base
$subtask = $this->getSubtask();
if ($this->subtask->remove($subtask['id'])) {
- $this->session->flash(t('Sub-task removed successfully.'));
+ $this->flash->success(t('Sub-task removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this sub-task.'));
+ $this->flash->failure(t('Unable to remove this sub-task.'));
}
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks'));
- }
-
- /**
- * Change status to the next status: Toto -> In Progress -> Done
- *
- * @access public
- */
- public function toggleStatus()
- {
- $task = $this->getTask();
- $subtask = $this->getSubtask();
- $redirect = $this->request->getStringParam('redirect', 'task');
-
- $this->subtask->toggleStatus($subtask['id']);
-
- if ($redirect === 'board') {
- $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
-
- $this->response->html($this->template->render('board/tooltip_subtasks', array(
- 'subtasks' => $this->subtask->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- $this->toggleRedirect($task, $redirect);
- }
-
- /**
- * Handle subtask restriction (popover)
- *
- * @access public
- */
- public function subtaskRestriction()
- {
- $task = $this->getTask();
- $subtask = $this->getSubtask();
-
- $this->response->html($this->template->render('subtask/restriction_change_status', array(
- 'status_list' => array(
- SubtaskModel::STATUS_TODO => t('Todo'),
- SubtaskModel::STATUS_DONE => t('Done'),
- ),
- 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()),
- 'subtask' => $subtask,
- 'task' => $task,
- 'redirect' => $this->request->getStringParam('redirect'),
- )));
- }
-
- /**
- * Change status of the in progress subtask and the other subtask
- *
- * @access public
- */
- public function changeRestrictionStatus()
- {
- $task = $this->getTask();
- $subtask = $this->getSubtask();
- $values = $this->request->getValues();
-
- // Change status of the previous in progress subtask
- $this->subtask->update(array(
- 'id' => $values['id'],
- 'status' => $values['status'],
- ));
-
- // Set the current subtask to in pogress
- $this->subtask->update(array(
- 'id' => $subtask['id'],
- 'status' => SubtaskModel::STATUS_INPROGRESS,
- ));
-
- $this->toggleRedirect($task, $values['redirect']);
- }
-
- /**
- * Redirect to the right page
- *
- * @access private
- */
- private function toggleRedirect(array $task, $redirect)
- {
- switch ($redirect) {
- case 'board':
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
- case 'dashboard':
- $this->response->redirect($this->helper->url->to('app', 'index'));
- default:
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'subtasks'));
- }
+ $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
}
/**
@@ -261,14 +168,15 @@ class Subtask extends Base
*/
public function movePosition()
{
- $this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_id');
- $subtask_id = $this->request->getIntegerParam('subtask_id');
- $direction = $this->request->getStringParam('direction');
- $method = $direction === 'up' ? 'moveUp' : 'moveDown';
+ $values = $this->request->getJson();
+
+ if (! empty($values) && $this->helper->user->hasProjectAccess('Subtask', 'movePosition', $project_id)) {
+ $result = $this->subtask->changePosition($task_id, $values['subtask_id'], $values['position']);
+ return $this->response->json(array('result' => $result));
+ }
- $this->subtask->$method($task_id, $subtask_id);
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id), 'subtasks'));
+ $this->forbidden();
}
}
diff --git a/app/Controller/SubtaskRestriction.php b/app/Controller/SubtaskRestriction.php
new file mode 100644
index 00000000..56024867
--- /dev/null
+++ b/app/Controller/SubtaskRestriction.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Model\Subtask as SubtaskModel;
+
+/**
+ * Subtask Restriction
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class SubtaskRestriction extends Base
+{
+ /**
+ * Show popup
+ *
+ * @access public
+ */
+ public function popover()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+
+ $this->response->html($this->template->render('subtask_restriction/popover', array(
+ 'status_list' => array(
+ SubtaskModel::STATUS_TODO => t('Todo'),
+ SubtaskModel::STATUS_DONE => t('Done'),
+ ),
+ 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()),
+ 'subtask' => $subtask,
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Change status of the in progress subtask and the other subtask
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+ $values = $this->request->getValues();
+
+ // Change status of the previous "in progress" subtask
+ $this->subtask->update(array(
+ 'id' => $values['id'],
+ 'status' => $values['status'],
+ ));
+
+ // Set the current subtask to "in progress"
+ $this->subtask->update(array(
+ 'id' => $subtask['id'],
+ 'status' => SubtaskModel::STATUS_INPROGRESS,
+ ));
+
+ $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
+ }
+}
diff --git a/app/Controller/SubtaskStatus.php b/app/Controller/SubtaskStatus.php
new file mode 100644
index 00000000..4fb82fc0
--- /dev/null
+++ b/app/Controller/SubtaskStatus.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Subtask Status
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class SubtaskStatus extends Base
+{
+ /**
+ * Change status to the next status: Toto -> In Progress -> Done
+ *
+ * @access public
+ */
+ public function change()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+
+ $status = $this->subtask->toggleStatus($subtask['id']);
+
+ if ($this->request->getIntegerParam('refresh-table') === 0) {
+ $subtask['status'] = $status;
+ $html = $this->helper->subtask->toggleStatus($subtask, $task['project_id']);
+ } else {
+ $html = $this->renderTable($task);
+ }
+
+ $this->response->html($html);
+ }
+
+ /**
+ * Start/stop timer for subtasks
+ *
+ * @access public
+ */
+ public function timer()
+ {
+ $task = $this->getTask();
+ $subtask_id = $this->request->getIntegerParam('subtask_id');
+ $timer = $this->request->getStringParam('timer');
+
+ if ($timer === 'start') {
+ $this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId());
+ } elseif ($timer === 'stop') {
+ $this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId());
+ $this->subtaskTimeTracking->updateTaskTimeTracking($task['id']);
+ }
+
+ $this->response->html($this->renderTable($task));
+ }
+
+ /**
+ * Render table
+ *
+ * @access private
+ * @param array $task
+ * @return string
+ */
+ private function renderTable(array $task)
+ {
+ return $this->template->render('subtask/table', array(
+ 'task' => $task,
+ 'subtasks' => $this->subtask->getAll($task['id']),
+ 'editable' => true,
+ 'redirect' => 'task',
+ ));
+ }
+}
diff --git a/app/Controller/Swimlane.php b/app/Controller/Swimlane.php
index 0b29f598..8270a16f 100644
--- a/app/Controller/Swimlane.php
+++ b/app/Controller/Swimlane.php
@@ -24,7 +24,7 @@ class Swimlane extends Base
$swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
if (empty($swimlane)) {
- $this->session->flashError(t('Swimlane not found.'));
+ $this->flash->failure(t('Swimlane not found.'));
$this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project_id)));
}
@@ -36,18 +36,32 @@ class Swimlane extends Base
*
* @access public
*/
- public function index(array $values = array(), array $errors = array())
+ public function index()
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('swimlane/index', array(
+ $this->response->html($this->helper->layout->project('swimlane/index', array(
'default_swimlane' => $this->swimlane->getDefault($project['id']),
'active_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::ACTIVE),
'inactive_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::INACTIVE),
+ 'project' => $project,
+ 'title' => t('Swimlanes')
+ )));
+ }
+
+ /**
+ * Create a new swimlane
+ *
+ * @access public
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->template->render('swimlane/create', array(
'values' => $values + array('project_id' => $project['id']),
'errors' => $errors,
'project' => $project,
- 'title' => t('Swimlanes')
)));
}
@@ -60,18 +74,35 @@ class Swimlane extends Base
{
$project = $this->getProject();
$values = $this->request->getValues();
- list($valid, $errors) = $this->swimlane->validateCreation($values);
+ list($valid, $errors) = $this->swimlaneValidator->validateCreation($values);
if ($valid) {
if ($this->swimlane->create($values)) {
- $this->session->flash(t('Your swimlane have been created successfully.'));
+ $this->flash->success(t('Your swimlane have been created successfully.'));
$this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to create your swimlane.'));
+ $errors = array('name' => array(t('Another swimlane with the same name exists in the project')));
}
}
- $this->index($values, $errors);
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Edit default swimlane (display the form)
+ *
+ * @access public
+ */
+ public function editDefault(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+ $swimlane = $this->swimlane->getDefault($project['id']);
+
+ $this->response->html($this->helper->layout->project('swimlane/edit_default', array(
+ 'values' => empty($values) ? $swimlane : $values,
+ 'errors' => $errors,
+ 'project' => $project,
+ )));
}
/**
@@ -79,23 +110,23 @@ class Swimlane extends Base
*
* @access public
*/
- public function change()
+ public function updateDefault()
{
$project = $this->getProject();
$values = $this->request->getValues() + array('show_default_swimlane' => 0);
- list($valid, ) = $this->swimlane->validateDefaultModification($values);
+ list($valid, $errors) = $this->swimlaneValidator->validateDefaultModification($values);
if ($valid) {
if ($this->swimlane->updateDefault($values)) {
- $this->session->flash(t('The default swimlane have been updated successfully.'));
- $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
+ $this->flash->success(t('The default swimlane have been updated successfully.'));
+ $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])), true);
} else {
- $this->session->flashError(t('Unable to update this swimlane.'));
+ $this->flash->failure(t('Unable to update this swimlane.'));
}
}
- $this->index();
+ $this->editDefault($values, $errors);
}
/**
@@ -108,11 +139,10 @@ class Swimlane extends Base
$project = $this->getProject();
$swimlane = $this->getSwimlane($project['id']);
- $this->response->html($this->projectLayout('swimlane/edit', array(
+ $this->response->html($this->helper->layout->project('swimlane/edit', array(
'values' => empty($values) ? $swimlane : $values,
'errors' => $errors,
'project' => $project,
- 'title' => t('Swimlanes')
)));
}
@@ -126,14 +156,14 @@ class Swimlane extends Base
$project = $this->getProject();
$values = $this->request->getValues();
- list($valid, $errors) = $this->swimlane->validateModification($values);
+ list($valid, $errors) = $this->swimlaneValidator->validateModification($values);
if ($valid) {
if ($this->swimlane->update($values)) {
- $this->session->flash(t('Swimlane updated successfully.'));
+ $this->flash->success(t('Swimlane updated successfully.'));
$this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
} else {
- $this->session->flashError(t('Unable to update this swimlane.'));
+ $errors = array('name' => array(t('Another swimlane with the same name exists in the project')));
}
}
@@ -150,10 +180,9 @@ class Swimlane extends Base
$project = $this->getProject();
$swimlane = $this->getSwimlane($project['id']);
- $this->response->html($this->projectLayout('swimlane/remove', array(
+ $this->response->html($this->helper->layout->project('swimlane/remove', array(
'project' => $project,
'swimlane' => $swimlane,
- 'title' => t('Remove a swimlane')
)));
}
@@ -169,9 +198,9 @@ class Swimlane extends Base
$swimlane_id = $this->request->getIntegerParam('swimlane_id');
if ($this->swimlane->remove($project['id'], $swimlane_id)) {
- $this->session->flash(t('Swimlane removed successfully.'));
+ $this->flash->success(t('Swimlane removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this swimlane.'));
+ $this->flash->failure(t('Unable to remove this swimlane.'));
}
$this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
@@ -189,9 +218,28 @@ class Swimlane extends Base
$swimlane_id = $this->request->getIntegerParam('swimlane_id');
if ($this->swimlane->disable($project['id'], $swimlane_id)) {
- $this->session->flash(t('Swimlane updated successfully.'));
+ $this->flash->success(t('Swimlane updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update this swimlane.'));
+ $this->flash->failure(t('Unable to update this swimlane.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Disable default swimlane
+ *
+ * @access public
+ */
+ public function disableDefault()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+
+ if ($this->swimlane->disableDefault($project['id'])) {
+ $this->flash->success(t('Swimlane updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this swimlane.'));
}
$this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
@@ -209,41 +257,48 @@ class Swimlane extends Base
$swimlane_id = $this->request->getIntegerParam('swimlane_id');
if ($this->swimlane->enable($project['id'], $swimlane_id)) {
- $this->session->flash(t('Swimlane updated successfully.'));
+ $this->flash->success(t('Swimlane updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update this swimlane.'));
+ $this->flash->failure(t('Unable to update this swimlane.'));
}
$this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
}
/**
- * Move up a swimlane
+ * Enable default swimlane
*
* @access public
*/
- public function moveup()
+ public function enableDefault()
{
$this->checkCSRFParam();
$project = $this->getProject();
- $swimlane_id = $this->request->getIntegerParam('swimlane_id');
- $this->swimlane->moveUp($project['id'], $swimlane_id);
+ if ($this->swimlane->enableDefault($project['id'])) {
+ $this->flash->success(t('Swimlane updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this swimlane.'));
+ }
+
$this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
}
/**
- * Move down a swimlane
+ * Move swimlane position
*
* @access public
*/
- public function movedown()
+ public function move()
{
- $this->checkCSRFParam();
$project = $this->getProject();
- $swimlane_id = $this->request->getIntegerParam('swimlane_id');
+ $values = $this->request->getJson();
- $this->swimlane->moveDown($project['id'], $swimlane_id);
- $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])));
+ if (! empty($values) && isset($values['swimlane_id']) && isset($values['position'])) {
+ $result = $this->swimlane->changePosition($project['id'], $values['swimlane_id'], $values['position']);
+ return $this->response->json(array('result' => $result));
+ }
+
+ $this->forbidden();
}
}
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index 894802d8..539d377b 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -30,13 +30,13 @@ class Task extends Base
$this->notfound(true);
}
- $this->response->html($this->template->layout('task/public', array(
+ $this->response->html($this->helper->layout->app('task/public', array(
'project' => $project,
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $this->subtask->getAll($task['id']),
'links' => $this->taskLink->getAllGroupedByLabel($task['id']),
'task' => $task,
- 'columns_list' => $this->board->getColumnsList($task['project_id']),
+ 'columns_list' => $this->column->getList($task['project_id']),
'colors_list' => $this->color->getList(),
'title' => $task['title'],
'no_layout' => true,
@@ -62,23 +62,21 @@ class Task extends Base
'time_spent' => $task['time_spent'] ?: '',
);
- $this->dateParser->format($values, array('date_started'), 'Y-m-d H:i');
+ $values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', 'm/d/Y H:i'));
- $this->response->html($this->taskLayout('task/show', array(
+ $this->response->html($this->helper->layout->task('task/show', array(
'project' => $this->project->getById($task['project_id']),
- 'files' => $this->file->getAllDocuments($task['id']),
- 'images' => $this->file->getAllImages($task['id']),
+ 'files' => $this->taskFile->getAllDocuments($task['id']),
+ 'images' => $this->taskFile->getAllImages($task['id']),
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()),
'subtasks' => $subtasks,
'links' => $this->taskLink->getAllGroupedByLabel($task['id']),
'task' => $task,
'values' => $values,
'link_label_list' => $this->link->getList(0, false),
- 'columns_list' => $this->board->getColumnsList($task['project_id']),
+ 'columns_list' => $this->column->getList($task['project_id']),
'colors_list' => $this->color->getList(),
- 'users_list' => $this->projectPermission->getMemberList($task['project_id'], true, false, false),
- 'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false),
'title' => $task['project_name'].' &gt; '.$task['title'],
'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
@@ -95,7 +93,7 @@ class Task extends Base
{
$task = $this->getTask();
- $this->response->html($this->taskLayout('task/analytics', array(
+ $this->response->html($this->helper->layout->task('task/analytics', array(
'title' => $task['title'],
'task' => $task,
'lead_time' => $this->taskAnalytic->getLeadTime($task),
@@ -114,14 +112,14 @@ class Task extends Base
$task = $this->getTask();
$subtask_paginator = $this->paginator
- ->setUrl('task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks'))
+ ->setUrl('task', 'timetracking', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks'))
->setMax(15)
->setOrder('start')
->setDirection('DESC')
->setQuery($this->subtaskTimeTracking->getTaskQuery($task['id']))
->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks');
- $this->response->html($this->taskLayout('task/time_tracking_details', array(
+ $this->response->html($this->helper->layout->task('task/time_tracking_details', array(
'task' => $task,
'subtask_paginator' => $subtask_paginator,
)));
@@ -136,7 +134,7 @@ class Task extends Base
{
$task = $this->getTask();
- $this->response->html($this->taskLayout('task/transitions', array(
+ $this->response->html($this->helper->layout->task('task/transitions', array(
'task' => $task,
'transitions' => $this->transition->getAllByTask($task['id']),
)));
@@ -159,15 +157,15 @@ class Task extends Base
$this->checkCSRFParam();
if ($this->task->remove($task['id'])) {
- $this->session->flash(t('Task removed successfully.'));
+ $this->flash->success(t('Task removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this task.'));
+ $this->flash->failure(t('Unable to remove this task.'));
}
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
}
- $this->response->html($this->taskLayout('task/remove', array(
+ $this->response->html($this->helper->layout->task('task/remove', array(
'task' => $task,
)));
}
diff --git a/app/Controller/TaskExternalLink.php b/app/Controller/TaskExternalLink.php
new file mode 100644
index 00000000..f26922dd
--- /dev/null
+++ b/app/Controller/TaskExternalLink.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound;
+
+/**
+ * Task External Link Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class TaskExternalLink extends Base
+{
+ /**
+ * Creation form
+ *
+ * @access public
+ */
+ public function show()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->helper->layout->task('task_external_link/show', array(
+ 'links' => $this->taskExternalLink->getAll($task['id']),
+ 'task' => $task,
+ 'title' => t('List of external links'),
+ )));
+ }
+
+ /**
+ * First creation form
+ *
+ * @access public
+ */
+ public function find(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->helper->layout->task('task_external_link/find', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'types' => $this->externalLinkManager->getTypes(),
+ )));
+ }
+
+ /**
+ * Second creation form
+ *
+ * @access public
+ */
+ public function create()
+ {
+ try {
+
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ $provider = $this->externalLinkManager->setUserInput($values)->find();
+ $link = $provider->getLink();
+
+ $this->response->html($this->helper->layout->task('task_external_link/create', array(
+ 'values' => array(
+ 'title' => $link->getTitle(),
+ 'url' => $link->getUrl(),
+ 'link_type' => $provider->getType(),
+ ),
+ 'dependencies' => $provider->getDependencies(),
+ 'errors' => array(),
+ 'task' => $task,
+ )));
+
+ } catch (ExternalLinkProviderNotFound $e) {
+ $errors = array('text' => array(t('Unable to fetch link information.')));
+ $this->find($values, $errors);
+ }
+ }
+
+ /**
+ * Save link
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->externalLinkValidator->validateCreation($values);
+
+ if ($valid && $this->taskExternalLink->create($values)) {
+ $this->flash->success(t('Link added successfully.'));
+ return $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true);
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Edit form
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+ $link_id = $this->request->getIntegerParam('link_id');
+
+ if ($link_id > 0) {
+ $values = $this->taskExternalLink->getById($link_id);
+ }
+
+ if (empty($values)) {
+ return $this->notfound();
+ }
+
+ $provider = $this->externalLinkManager->getProvider($values['link_type']);
+
+ $this->response->html($this->helper->layout->task('task_external_link/edit', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'dependencies' => $provider->getDependencies(),
+ )));
+ }
+
+ /**
+ * Update link
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->externalLinkValidator->validateModification($values);
+
+ if ($valid && $this->taskExternalLink->update($values)) {
+ $this->flash->success(t('Link updated successfully.'));
+ return $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true);
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $task = $this->getTask();
+ $link_id = $this->request->getIntegerParam('link_id');
+ $link = $this->taskExternalLink->getById($link_id);
+
+ if (empty($link)) {
+ return $this->notfound();
+ }
+
+ $this->response->html($this->helper->layout->task('task_external_link/remove', array(
+ 'link' => $link,
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $task = $this->getTask();
+
+ if ($this->taskExternalLink->remove($this->request->getIntegerParam('link_id'))) {
+ $this->flash->success(t('Link removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this link.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
+}
diff --git a/app/Controller/TaskFile.php b/app/Controller/TaskFile.php
new file mode 100644
index 00000000..2b0152a7
--- /dev/null
+++ b/app/Controller/TaskFile.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Task File Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class TaskFile extends Base
+{
+ /**
+ * Screenshot
+ *
+ * @access public
+ */
+ public function screenshot()
+ {
+ $task = $this->getTask();
+
+ if ($this->request->isPost() && $this->taskFile->uploadScreenshot($task['id'], $this->request->getValue('screenshot')) !== false) {
+ $this->flash->success(t('Screenshot uploaded successfully.'));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true);
+ }
+
+ $this->response->html($this->template->render('task_file/screenshot', array(
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * File upload form
+ *
+ * @access public
+ */
+ public function create()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('task_file/create', array(
+ 'task' => $task,
+ 'max_size' => $this->helper->text->phpToBytes(ini_get('upload_max_filesize')),
+ )));
+ }
+
+ /**
+ * File upload (save files)
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $task = $this->getTask();
+
+ if (! $this->taskFile->uploadFiles($task['id'], $this->request->getFileInfo('files'))) {
+ $this->flash->failure(t('Unable to upload the file.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true);
+ }
+
+ /**
+ * Remove a file
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $task = $this->getTask();
+ $file = $this->taskFile->getById($this->request->getIntegerParam('file_id'));
+
+ if ($file['task_id'] == $task['id'] && $this->taskFile->remove($file['id'])) {
+ $this->flash->success(t('File removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this file.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
+
+ /**
+ * Confirmation dialog before removing a file
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $task = $this->getTask();
+ $file = $this->taskFile->getById($this->request->getIntegerParam('file_id'));
+
+ $this->response->html($this->template->render('task_file/remove', array(
+ 'task' => $task,
+ 'file' => $file,
+ )));
+ }
+}
diff --git a/app/Controller/TaskHelper.php b/app/Controller/TaskHelper.php
new file mode 100644
index 00000000..236af33e
--- /dev/null
+++ b/app/Controller/TaskHelper.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Task Ajax Helper
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class TaskHelper extends Base
+{
+ /**
+ * Render Markdown text and reply with the HTML Code
+ *
+ * @access public
+ */
+ public function preview()
+ {
+ $payload = $this->request->getJson();
+
+ if (empty($payload['text'])) {
+ $this->response->html('<p>'.t('Nothing to preview...').'</p>');
+ }
+
+ $this->response->html($this->helper->text->markdown($payload['text']));
+ }
+
+ /**
+ * Task autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $search = $this->request->getStringParam('term');
+ $projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+
+ if (empty($projects)) {
+ $this->response->json(array());
+ }
+
+ $filter = $this->taskFilterAutoCompleteFormatter
+ ->create()
+ ->filterByProjects($projects)
+ ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
+
+ // Search by task id or by title
+ if (ctype_digit($search)) {
+ $filter->filterById($search);
+ } else {
+ $filter->filterByTitle($search);
+ }
+
+ $this->response->json($filter->format());
+ }
+}
diff --git a/app/Controller/TaskImport.php b/app/Controller/TaskImport.php
index 0e9d2169..460c608c 100644
--- a/app/Controller/TaskImport.php
+++ b/app/Controller/TaskImport.php
@@ -20,7 +20,7 @@ class TaskImport extends Base
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('task_import/step1', array(
+ $this->response->html($this->helper->layout->project('task_import/step1', array(
'project' => $project,
'values' => $values,
'errors' => $errors,
@@ -52,9 +52,9 @@ class TaskImport extends Base
$csv->read($filename, array($this->taskImport, 'import'));
if ($this->taskImport->counter > 0) {
- $this->session->flash(t('%d task(s) have been imported successfully.', $this->taskImport->counter));
+ $this->flash->success(t('%d task(s) have been imported successfully.', $this->taskImport->counter));
} else {
- $this->session->flashError(t('Nothing have been imported!'));
+ $this->flash->failure(t('Nothing have been imported!'));
}
$this->response->redirect($this->helper->url->to('taskImport', 'step1', array('project_id' => $project['id'])));
diff --git a/app/Controller/TaskRecurrence.php b/app/Controller/TaskRecurrence.php
new file mode 100644
index 00000000..f02f3cdc
--- /dev/null
+++ b/app/Controller/TaskRecurrence.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Task Recurrence controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class TaskRecurrence extends Base
+{
+ /**
+ * Edit recurrence form
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+
+ if (empty($values)) {
+ $values = $task;
+ }
+
+ $this->response->html($this->helper->layout->task('task_recurrence/edit', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'recurrence_status_list' => $this->task->getRecurrenceStatusList(),
+ 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
+ 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
+ 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
+ )));
+ }
+
+ /**
+ * Update recurrence form
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values);
+
+ if ($valid) {
+ if ($this->taskModification->update($values)) {
+ $this->flash->success(t('Task updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
+ }
+
+ $this->edit($values, $errors);
+ }
+}
diff --git a/app/Controller/Taskcreation.php b/app/Controller/Taskcreation.php
index e47cd1b7..1d8a0e29 100644
--- a/app/Controller/Taskcreation.php
+++ b/app/Controller/Taskcreation.php
@@ -18,30 +18,29 @@ class Taskcreation extends Base
public function create(array $values = array(), array $errors = array())
{
$project = $this->getProject();
- $method = $this->request->isAjax() ? 'render' : 'layout';
$swimlanes_list = $this->swimlane->getList($project['id'], false, true);
if (empty($values)) {
$values = array(
'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanes_list)),
'column_id' => $this->request->getIntegerParam('column_id'),
- 'color_id' => $this->request->getStringParam('color_id', $this->color->getDefaultColor()),
- 'owner_id' => $this->request->getIntegerParam('owner_id'),
- 'another_task' => $this->request->getIntegerParam('another_task'),
+ 'color_id' => $this->color->getDefaultColor(),
+ 'owner_id' => $this->userSession->getId(),
);
+
+ $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values));
+ $values = $this->hook->merge('controller:task-creation:form:default', $values, array('default_values' => $values));
}
- $this->response->html($this->template->$method('task_creation/form', array(
- 'ajax' => $this->request->isAjax(),
+ $this->response->html($this->template->render('task_creation/form', array(
+ 'project' => $project,
'errors' => $errors,
'values' => $values + array('project_id' => $project['id']),
- 'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
+ 'columns_list' => $this->column->getList($project['id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $swimlanes_list,
- 'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $project['name'].' &gt; '.t('New task')
)));
}
@@ -59,26 +58,27 @@ class Taskcreation extends Base
list($valid, $errors) = $this->taskValidator->validateCreation($values);
if ($valid && $this->taskCreation->create($values)) {
- $this->session->flash(t('Task created successfully.'));
- $this->afterSave($project, $values);
- } else {
- $this->session->flashError(t('Unable to create your task.'));
+ $this->flash->success(t('Task created successfully.'));
+ return $this->afterSave($project, $values);
}
+ $this->flash->failure(t('Unable to create your task.'));
$this->create($values, $errors);
}
private function afterSave(array $project, array &$values)
{
if (isset($values['another_task']) && $values['another_task'] == 1) {
- unset($values['title']);
- unset($values['description']);
-
- if (! $this->request->isAjax()) {
- $this->response->redirect($this->helper->url->to('taskcreation', 'create', $values));
- }
- } else {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id'])));
+ return $this->create(array(
+ 'owner_id' => $values['owner_id'],
+ 'color_id' => $values['color_id'],
+ 'category_id' => isset($values['category_id']) ? $values['category_id'] : 0,
+ 'column_id' => $values['column_id'],
+ 'swimlane_id' => isset($values['swimlane_id']) ? $values['swimlane_id'] : 0,
+ 'another_task' => 1,
+ ));
}
+
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id'])));
}
}
diff --git a/app/Controller/Taskduplication.php b/app/Controller/Taskduplication.php
index 79f498fc..7641a48d 100644
--- a/app/Controller/Taskduplication.php
+++ b/app/Controller/Taskduplication.php
@@ -24,15 +24,15 @@ class Taskduplication extends Base
$task_id = $this->taskDuplication->duplicate($task['id']);
if ($task_id > 0) {
- $this->session->flash(t('Task created successfully.'));
+ $this->flash->success(t('Task created successfully.'));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task_id)));
} else {
- $this->session->flashError(t('Unable to create this task.'));
- $this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
+ $this->flash->failure(t('Unable to create this task.'));
+ $this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
}
}
- $this->response->html($this->taskLayout('task_duplication/duplicate', array(
+ $this->response->html($this->helper->layout->task('task_duplication/duplicate', array(
'task' => $task,
)));
}
@@ -56,11 +56,11 @@ class Taskduplication extends Base
$values['column_id'],
$values['category_id'],
$values['owner_id'])) {
- $this->session->flash(t('Task updated successfully.'));
+ $this->flash->success(t('Task updated successfully.'));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task['id'])));
}
- $this->session->flashError(t('Unable to update your task.'));
+ $this->flash->failure(t('Unable to update your task.'));
}
$this->chooseDestination($task, 'task_duplication/move');
@@ -86,12 +86,12 @@ class Taskduplication extends Base
);
if ($task_id > 0) {
- $this->session->flash(t('Task created successfully.'));
+ $this->flash->success(t('Task created successfully.'));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task_id)));
}
}
- $this->session->flashError(t('Unable to create your task.'));
+ $this->flash->failure(t('Unable to create your task.'));
}
$this->chooseDestination($task, 'task_duplication/copy');
@@ -107,7 +107,7 @@ class Taskduplication extends Base
private function chooseDestination(array $task, $template)
{
$values = array();
- $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
+ $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
unset($projects_list[$task['project_id']]);
@@ -115,9 +115,9 @@ class Taskduplication extends Base
$dst_project_id = $this->request->getIntegerParam('dst_project_id', key($projects_list));
$swimlanes_list = $this->swimlane->getList($dst_project_id, false, true);
- $columns_list = $this->board->getColumnsList($dst_project_id);
+ $columns_list = $this->column->getList($dst_project_id);
$categories_list = $this->category->getList($dst_project_id);
- $users_list = $this->projectPermission->getMemberList($dst_project_id);
+ $users_list = $this->projectUserRole->getAssignableUsersList($dst_project_id);
$values = $this->taskDuplication->checkDestinationProjectValues($task);
$values['project_id'] = $dst_project_id;
@@ -128,7 +128,7 @@ class Taskduplication extends Base
$users_list = array();
}
- $this->response->html($this->taskLayout($template, array(
+ $this->response->html($this->helper->layout->task($template, array(
'values' => $values,
'task' => $task,
'projects_list' => $projects_list,
diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php
index 587769ee..fdb4fada 100644
--- a/app/Controller/Tasklink.php
+++ b/app/Controller/Tasklink.php
@@ -22,13 +22,32 @@ class Tasklink extends Base
$link = $this->taskLink->getById($this->request->getIntegerParam('link_id'));
if (empty($link)) {
- $this->notfound();
+ return $this->notfound();
}
return $link;
}
/**
+ * Show links
+ *
+ * @access public
+ */
+ public function show()
+ {
+ $task = $this->getTask();
+ $project = $this->project->getById($task['project_id']);
+
+ $this->response->html($this->helper->layout->task('tasklink/show', array(
+ 'links' => $this->taskLink->getAllGroupedByLabel($task['id']),
+ 'task' => $task,
+ 'project' => $project,
+ 'editable' => true,
+ 'is_public' => false,
+ )));
+ }
+
+ /**
* Creation form
*
* @access public
@@ -36,20 +55,8 @@ class Tasklink extends Base
public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
- $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
-
- if ($ajax && empty($errors)) {
- $this->response->html($this->template->render('tasklink/create', array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'labels' => $this->link->getList(0, false),
- 'title' => t('Add a new link'),
- 'ajax' => $ajax,
- )));
- }
- $this->response->html($this->taskLayout('tasklink/create', array(
+ $this->response->html($this->helper->layout->task('tasklink/create', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
@@ -67,23 +74,17 @@ class Tasklink extends Base
{
$task = $this->getTask();
$values = $this->request->getValues();
- $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
- list($valid, $errors) = $this->taskLink->validateCreation($values);
+ list($valid, $errors) = $this->taskLinkValidator->validateCreation($values);
if ($valid) {
if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) {
- $this->session->flash(t('Link added successfully.'));
-
- if ($ajax) {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
+ $this->flash->success(t('Link added successfully.'));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links', true);
}
$errors = array('title' => array(t('The exact same link already exists')));
- $this->session->flashError(t('Unable to create your link.'));
+ $this->flash->failure(t('Unable to create your link.'));
}
$this->create($values, $errors);
@@ -105,7 +106,7 @@ class Tasklink extends Base
$values['title'] = '#'.$opposite_task['id'].' - '.$opposite_task['title'];
}
- $this->response->html($this->taskLayout('tasklink/edit', array(
+ $this->response->html($this->helper->layout->task('tasklink/edit', array(
'values' => $values,
'errors' => $errors,
'task_link' => $task_link,
@@ -125,15 +126,15 @@ class Tasklink extends Base
$task = $this->getTask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->taskLink->validateModification($values);
+ list($valid, $errors) = $this->taskLinkValidator->validateModification($values);
if ($valid) {
if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) {
- $this->session->flash(t('Link updated successfully.'));
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
+ $this->flash->success(t('Link updated successfully.'));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
}
- $this->session->flashError(t('Unable to update your link.'));
+ $this->flash->failure(t('Unable to update your link.'));
}
$this->edit($values, $errors);
@@ -149,7 +150,7 @@ class Tasklink extends Base
$task = $this->getTask();
$link = $this->getTaskLink();
- $this->response->html($this->taskLayout('tasklink/remove', array(
+ $this->response->html($this->helper->layout->task('tasklink/remove', array(
'link' => $link,
'task' => $task,
)));
@@ -166,9 +167,9 @@ class Tasklink extends Base
$task = $this->getTask();
if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) {
- $this->session->flash(t('Link removed successfully.'));
+ $this->flash->success(t('Link removed successfully.'));
} else {
- $this->session->flashError(t('Unable to remove this link.'));
+ $this->flash->failure(t('Unable to remove this link.'));
}
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
diff --git a/app/Controller/Taskmodification.php b/app/Controller/Taskmodification.php
index b1105dcc..306d34c0 100644
--- a/app/Controller/Taskmodification.php
+++ b/app/Controller/Taskmodification.php
@@ -23,71 +23,48 @@ class Taskmodification extends Base
}
/**
- * Update time tracking information
+ * Edit description form
*
* @access public
*/
- public function time()
+ public function description(array $values = array(), array $errors = array())
{
$task = $this->getTask();
- $values = $this->request->getValues();
- list($valid, ) = $this->taskValidator->validateTimeModification($values);
-
- if ($valid && $this->taskModification->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update your task.'));
+ if (empty($values)) {
+ $values = array('id' => $task['id'], 'description' => $task['description']);
}
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
+ $this->response->html($this->helper->layout->task('task_modification/edit_description', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ )));
}
/**
- * Edit description form
+ * Update description
*
* @access public
*/
- public function description()
+ public function updateDescription()
{
$task = $this->getTask();
- $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
-
- if ($this->request->isPost()) {
- $values = $this->request->getValues();
-
- list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($values);
+ $values = $this->request->getValues();
- if ($valid) {
- if ($this->taskModification->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update your task.'));
- }
+ list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($values);
- if ($ajax) {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
- } else {
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
- }
+ if ($valid) {
+ if ($this->taskModification->update($values)) {
+ $this->flash->success(t('Task updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
}
- } else {
- $values = $task;
- $errors = array();
- }
- $params = array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'ajax' => $ajax,
- );
-
- if ($ajax) {
- $this->response->html($this->template->render('task_modification/edit_description', $params));
- } else {
- $this->response->html($this->taskLayout('task_modification/edit_description', $params));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
}
+
+ $this->description($values, $errors);
}
/**
@@ -98,33 +75,26 @@ class Taskmodification extends Base
public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
- $ajax = $this->request->isAjax();
+ $project = $this->project->getById($task['project_id']);
if (empty($values)) {
$values = $task;
+ $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values));
+ $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values));
}
- $this->dateParser->format($values, array('date_due'));
+ $values = $this->dateParser->format($values, array('date_due'), $this->config->get('application_date_format', 'm/d/Y'));
+ $values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', 'm/d/Y H:i'));
- $params = array(
+ $this->response->html($this->helper->layout->task('task_modification/edit_task', array(
+ 'project' => $project,
'values' => $values,
'errors' => $errors,
'task' => $task,
- 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($task['project_id']),
- 'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
- 'ajax' => $ajax,
- );
-
- if ($ajax) {
- $html = $this->template->render('task_modification/edit_task', $params);
- } else {
- $html = $this->taskLayout('task_modification/edit_task', $params);
- }
-
- $this->response->html($html);
+ )));
}
/**
@@ -140,57 +110,11 @@ class Taskmodification extends Base
list($valid, $errors) = $this->taskValidator->validateModification($values);
if ($valid && $this->taskModification->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
-
- if ($this->request->isAjax()) {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
- } else {
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
- }
+ $this->flash->success(t('Task updated successfully.'));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
} else {
- $this->session->flashError(t('Unable to update your task.'));
+ $this->flash->failure(t('Unable to update your task.'));
$this->edit($values, $errors);
}
}
-
- /**
- * Edit recurrence form
- *
- * @access public
- */
- public function recurrence()
- {
- $task = $this->getTask();
-
- if ($this->request->isPost()) {
- $values = $this->request->getValues();
-
- list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values);
-
- if ($valid) {
- if ($this->taskModification->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
- } else {
- $this->session->flashError(t('Unable to update your task.'));
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
- }
- } else {
- $values = $task;
- $errors = array();
- }
-
- $params = array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'recurrence_status_list' => $this->task->getRecurrenceStatusList(),
- 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
- 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
- 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
- );
-
- $this->response->html($this->taskLayout('task_modification/edit_recurrence', $params));
- }
}
diff --git a/app/Controller/Taskstatus.php b/app/Controller/Taskstatus.php
index c0421ea7..c07f2cc5 100644
--- a/app/Controller/Taskstatus.php
+++ b/app/Controller/Taskstatus.php
@@ -17,9 +17,7 @@ class Taskstatus extends Base
*/
public function close()
{
- $task = $this->getTask();
- $this->changeStatus($task, 'close', t('Task closed successfully.'), t('Unable to close this task.'));
- $this->renderTemplate($task, 'task_status/close');
+ $this->changeStatus('close', 'task_status/close', t('Task closed successfully.'), t('Unable to close this task.'));
}
/**
@@ -29,44 +27,36 @@ class Taskstatus extends Base
*/
public function open()
{
- $task = $this->getTask();
- $this->changeStatus($task, 'open', t('Task opened successfully.'), t('Unable to open this task.'));
- $this->renderTemplate($task, 'task_status/open');
+ $this->changeStatus('open', 'task_status/open', t('Task opened successfully.'), t('Unable to open this task.'));
}
- private function changeStatus(array $task, $method, $success_message, $failure_message)
+ /**
+ * Common method to change status
+ *
+ * @access private
+ * @param string $method
+ * @param string $template
+ * @param string $success_message
+ * @param string $failure_message
+ */
+ private function changeStatus($method, $template, $success_message, $failure_message)
{
+ $task = $this->getTask();
+
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
if ($this->taskStatus->$method($task['id'])) {
- $this->session->flash($success_message);
+ $this->flash->success($success_message);
} else {
- $this->session->flashError($failure_message);
+ $this->flash->failure($failure_message);
}
- if ($this->request->getStringParam('redirect') === 'board') {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
- }
- }
-
- private function renderTemplate(array $task, $template)
- {
- $redirect = $this->request->getStringParam('redirect');
-
- if ($this->request->isAjax()) {
- $this->response->html($this->template->render($template, array(
- 'task' => $task,
- 'redirect' => $redirect,
- )));
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true);
}
- $this->response->html($this->taskLayout($template, array(
+ $this->response->html($this->helper->layout->task($template, array(
'task' => $task,
- 'redirect' => $redirect,
)));
}
}
diff --git a/app/Controller/Timer.php b/app/Controller/Timer.php
deleted file mode 100644
index 0267fcdd..00000000
--- a/app/Controller/Timer.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-namespace Kanboard\Controller;
-
-/**
- * Time Tracking controller
- *
- * @package controller
- * @author Frederic Guillot
- */
-class Timer extends Base
-{
- /**
- * Start/stop timer for subtasks
- *
- * @access public
- */
- public function subtask()
- {
- $project_id = $this->request->getIntegerParam('project_id');
- $task_id = $this->request->getIntegerParam('task_id');
- $subtask_id = $this->request->getIntegerParam('subtask_id');
- $timer = $this->request->getStringParam('timer');
-
- if ($timer === 'start') {
- $this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId());
- } elseif ($timer === 'stop') {
- $this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId());
- $this->subtaskTimeTracking->updateTaskTimeTracking($task_id);
- }
-
- $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id)).'#subtasks');
- }
-}
diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php
index 179241f8..10292261 100644
--- a/app/Controller/Twofactor.php
+++ b/app/Controller/Twofactor.php
@@ -2,10 +2,6 @@
namespace Kanboard\Controller;
-use Otp\Otp;
-use Otp\GoogleAuthenticator;
-use Base32\Base32;
-
/**
* Two Factor Auth controller
*
@@ -27,7 +23,7 @@ class Twofactor extends User
}
/**
- * Index
+ * Show form to disable/enable 2FA
*
* @access public
*/
@@ -35,68 +31,98 @@ class Twofactor extends User
{
$user = $this->getUser();
$this->checkCurrentUser($user);
+ unset($this->sessionStorage->twoFactorSecret);
+
+ $this->response->html($this->helper->layout->user('twofactor/index', array(
+ 'user' => $user,
+ 'provider' => $this->authenticationManager->getPostAuthenticationProvider()->getName(),
+ )));
+ }
+
+ /**
+ * Show page with secret and test form
+ *
+ * @access public
+ */
+ public function show()
+ {
+ $user = $this->getUser();
+ $this->checkCurrentUser($user);
$label = $user['email'] ?: $user['username'];
+ $provider = $this->authenticationManager->getPostAuthenticationProvider();
- $this->response->html($this->layout('twofactor/index', array(
+ if (! isset($this->sessionStorage->twoFactorSecret)) {
+ $provider->generateSecret();
+ $provider->beforeCode();
+ $this->sessionStorage->twoFactorSecret = $provider->getSecret();
+ } else {
+ $provider->setSecret($this->sessionStorage->twoFactorSecret);
+ }
+
+ $this->response->html($this->helper->layout->user('twofactor/show', array(
'user' => $user,
- 'qrcode_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getQrCodeUrl('totp', $label, $user['twofactor_secret']) : '',
- 'key_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getKeyUri('totp', $label, $user['twofactor_secret']) : '',
+ 'secret' => $this->sessionStorage->twoFactorSecret,
+ 'qrcode_url' => $provider->getQrCodeUrl($label),
+ 'key_url' => $provider->getKeyUrl($label),
)));
}
/**
- * Enable/disable 2FA
+ * Test code and save secret
*
* @access public
*/
- public function save()
+ public function test()
{
$user = $this->getUser();
$this->checkCurrentUser($user);
$values = $this->request->getValues();
- if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) {
+ $provider = $this->authenticationManager->getPostAuthenticationProvider();
+ $provider->setCode(empty($values['code']) ? '' : $values['code']);
+ $provider->setSecret($this->sessionStorage->twoFactorSecret);
+
+ if ($provider->authenticate()) {
+ $this->flash->success(t('The two factor authentication code is valid.'));
+
$this->user->update(array(
'id' => $user['id'],
'twofactor_activated' => 1,
- 'twofactor_secret' => GoogleAuthenticator::generateRandom(),
+ 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(),
));
- } else {
- $this->user->update(array(
- 'id' => $user['id'],
- 'twofactor_activated' => 0,
- 'twofactor_secret' => '',
- ));
- }
- // Allow the user to test or disable the feature
- $_SESSION['user']['twofactor_activated'] = false;
+ unset($this->sessionStorage->twoFactorSecret);
+ $this->userSession->disablePostAuthentication();
- $this->session->flash(t('User updated successfully.'));
- $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
+ $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
+ } else {
+ $this->flash->failure(t('The two factor authentication code is not valid.'));
+ $this->response->redirect($this->helper->url->to('twofactor', 'show', array('user_id' => $user['id'])));
+ }
}
/**
- * Test 2FA
+ * Disable 2FA for the current user
*
* @access public
*/
- public function test()
+ public function deactivate()
{
$user = $this->getUser();
$this->checkCurrentUser($user);
- $otp = new Otp;
- $values = $this->request->getValues();
+ $this->user->update(array(
+ 'id' => $user['id'],
+ 'twofactor_activated' => 0,
+ 'twofactor_secret' => '',
+ ));
- if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
- $this->session->flash(t('The two factor authentication code is valid.'));
- } else {
- $this->session->flashError(t('The two factor authentication code is not valid.'));
- }
+ // Allow the user to test or disable the feature
+ $this->userSession->disablePostAuthentication();
+ $this->flash->success(t('User updated successfully.'));
$this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
}
@@ -110,15 +136,18 @@ class Twofactor extends User
$user = $this->getUser();
$this->checkCurrentUser($user);
- $otp = new Otp;
$values = $this->request->getValues();
- if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
- $this->session['2fa_validated'] = true;
- $this->session->flash(t('The two factor authentication code is valid.'));
+ $provider = $this->authenticationManager->getPostAuthenticationProvider();
+ $provider->setCode(empty($values['code']) ? '' : $values['code']);
+ $provider->setSecret($user['twofactor_secret']);
+
+ if ($provider->authenticate()) {
+ $this->userSession->validatePostAuthentication();
+ $this->flash->success(t('The two factor authentication code is valid.'));
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
- $this->session->flashError(t('The two factor authentication code is not valid.'));
+ $this->flash->failure(t('The two factor authentication code is not valid.'));
$this->response->redirect($this->helper->url->to('twofactor', 'code'));
}
}
@@ -130,7 +159,13 @@ class Twofactor extends User
*/
public function code()
{
- $this->response->html($this->template->layout('twofactor/check', array(
+ if (! isset($this->sessionStorage->twoFactorBeforeCodeCalled)) {
+ $provider = $this->authenticationManager->getPostAuthenticationProvider();
+ $provider->beforeCode();
+ $this->sessionStorage->twoFactorBeforeCodeCalled = true;
+ }
+
+ $this->response->html($this->helper->layout->app('twofactor/check', array(
'title' => t('Check two factor authentication code'),
)));
}
@@ -156,7 +191,7 @@ class Twofactor extends User
$this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id'])));
}
- $this->response->html($this->layout('twofactor/disable', array(
+ $this->response->html($this->helper->layout->user('twofactor/disable', array(
'user' => $user,
)));
}
diff --git a/app/Controller/User.php b/app/Controller/User.php
index b5f4edeb..f7d7d2e0 100644
--- a/app/Controller/User.php
+++ b/app/Controller/User.php
@@ -2,7 +2,9 @@
namespace Kanboard\Controller;
-use Kanboard\Model\NotificationType;
+use Kanboard\Notification\Mail as MailNotification;
+use Kanboard\Model\Project as ProjectModel;
+use Kanboard\Core\Security\Role;
/**
* User controller
@@ -13,27 +15,6 @@ use Kanboard\Model\NotificationType;
class User extends Base
{
/**
- * Common layout for user views
- *
- * @access protected
- * @param string $template Template name
- * @param array $params Template parameters
- * @return string
- */
- protected function layout($template, array $params)
- {
- $content = $this->template->render($template, $params);
- $params['user_content_for_layout'] = $content;
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
-
- if (isset($params['user'])) {
- $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')';
- }
-
- return $this->template->layout('user/layout', $params);
- }
-
- /**
* List all users
*
* @access public
@@ -48,11 +29,32 @@ class User extends Base
->calculate();
$this->response->html(
- $this->template->layout('user/index', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ $this->helper->layout->app('user/index', array(
'title' => t('Users').' ('.$paginator->getTotal().')',
'paginator' => $paginator,
- )));
+ )
+ ));
+ }
+
+ /**
+ * Public user profile
+ *
+ * @access public
+ */
+ public function profile()
+ {
+ $user = $this->user->getById($this->request->getIntegerParam('user_id'));
+
+ if (empty($user)) {
+ $this->notfound();
+ }
+
+ $this->response->html(
+ $this->helper->layout->app('user/profile', array(
+ 'title' => $user['name'] ?: $user['username'],
+ 'user' => $user,
+ )
+ ));
}
/**
@@ -64,13 +66,13 @@ class User extends Base
{
$is_remote = $this->request->getIntegerParam('remote') == 1 || (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1);
- $this->response->html($this->template->layout($is_remote ? 'user/create_remote' : 'user/create_local', array(
+ $this->response->html($this->helper->layout->app($is_remote ? 'user/create_remote' : 'user/create_local', array(
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'roles' => $this->role->getApplicationRoles(),
'projects' => $this->project->getList(),
'errors' => $errors,
- 'values' => $values,
+ 'values' => $values + array('role' => Role::APP_USER),
'title' => t('New user')
)));
}
@@ -83,7 +85,7 @@ class User extends Base
public function save()
{
$values = $this->request->getValues();
- list($valid, $errors) = $this->user->validateCreation($values);
+ list($valid, $errors) = $this->userValidator->validateCreation($values);
if ($valid) {
$project_id = empty($values['project_id']) ? 0 : $values['project_id'];
@@ -92,16 +94,16 @@ class User extends Base
$user_id = $this->user->create($values);
if ($user_id !== false) {
- $this->projectPermission->addMember($project_id, $user_id);
+ $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER);
if (! empty($values['notifications_enabled'])) {
- $this->userNotificationType->saveSelectedTypes($user_id, array(NotificationType::TYPE_EMAIL));
+ $this->userNotificationType->saveSelectedTypes($user_id, array(MailNotification::TYPE));
}
- $this->session->flash(t('User created successfully.'));
+ $this->flash->success(t('User created successfully.'));
$this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user_id)));
} else {
- $this->session->flashError(t('Unable to create your user.'));
+ $this->flash->failure(t('Unable to create your user.'));
$values['project_id'] = $project_id;
}
}
@@ -117,7 +119,7 @@ class User extends Base
public function show()
{
$user = $this->getUser();
- $this->response->html($this->layout('user/show', array(
+ $this->response->html($this->helper->layout->user('user/show', array(
'user' => $user,
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
@@ -141,13 +143,27 @@ class User extends Base
->setQuery($this->subtaskTimeTracking->getUserQuery($user['id']))
->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks');
- $this->response->html($this->layout('user/timesheet', array(
+ $this->response->html($this->helper->layout->user('user/timesheet', array(
'subtask_paginator' => $subtask_paginator,
'user' => $user,
)));
}
/**
+ * Display last password reset
+ *
+ * @access public
+ */
+ public function passwordReset()
+ {
+ $user = $this->getUser();
+ $this->response->html($this->helper->layout->user('user/password_reset', array(
+ 'tokens' => $this->passwordReset->getAll($user['id']),
+ 'user' => $user,
+ )));
+ }
+
+ /**
* Display last connections
*
* @access public
@@ -155,7 +171,7 @@ class User extends Base
public function last()
{
$user = $this->getUser();
- $this->response->html($this->layout('user/last', array(
+ $this->response->html($this->helper->layout->user('user/last', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
@@ -169,8 +185,8 @@ class User extends Base
public function sessions()
{
$user = $this->getUser();
- $this->response->html($this->layout('user/sessions', array(
- 'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']),
+ $this->response->html($this->helper->layout->user('user/sessions', array(
+ 'sessions' => $this->rememberMeSession->getAll($user['id']),
'user' => $user,
)));
}
@@ -184,8 +200,8 @@ class User extends Base
{
$this->checkCSRFParam();
$user = $this->getUser();
- $this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id'));
- $this->response->redirect($this->helper->url->to('user', 'session', array('user_id' => $user['id'])));
+ $this->rememberMeSession->remove($this->request->getIntegerParam('id'));
+ $this->response->redirect($this->helper->url->to('user', 'sessions', array('user_id' => $user['id'])));
}
/**
@@ -200,12 +216,12 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
$this->userNotification->saveSettings($user['id'], $values);
- $this->session->flash(t('User updated successfully.'));
+ $this->flash->success(t('User updated successfully.'));
$this->response->redirect($this->helper->url->to('user', 'notifications', array('user_id' => $user['id'])));
}
- $this->response->html($this->layout('user/notifications', array(
- 'projects' => $this->projectPermission->getMemberProjects($user['id']),
+ $this->response->html($this->helper->layout->user('user/notifications', array(
+ 'projects' => $this->projectUserRole->getProjectsByUser($user['id'], array(ProjectModel::ACTIVE)),
'notifications' => $this->userNotification->readSettings($user['id']),
'types' => $this->userNotificationType->getTypes(),
'filters' => $this->userNotificationFilter->getFilters(),
@@ -225,11 +241,11 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
$this->userMetadata->save($user['id'], $values);
- $this->session->flash(t('User updated successfully.'));
+ $this->flash->success(t('User updated successfully.'));
$this->response->redirect($this->helper->url->to('user', 'integrations', array('user_id' => $user['id'])));
}
- $this->response->html($this->layout('user/integrations', array(
+ $this->response->html($this->helper->layout->user('user/integrations', array(
'user' => $user,
'values' => $this->userMetadata->getall($user['id']),
)));
@@ -243,7 +259,7 @@ class User extends Base
public function external()
{
$user = $this->getUser();
- $this->response->html($this->layout('user/external', array(
+ $this->response->html($this->helper->layout->user('user/external', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
@@ -263,15 +279,15 @@ class User extends Base
$this->checkCSRFParam();
if ($this->user->{$switch.'PublicAccess'}($user['id'])) {
- $this->session->flash(t('User updated successfully.'));
+ $this->flash->success(t('User updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update this user.'));
+ $this->flash->failure(t('Unable to update this user.'));
}
$this->response->redirect($this->helper->url->to('user', 'share', array('user_id' => $user['id'])));
}
- $this->response->html($this->layout('user/share', array(
+ $this->response->html($this->helper->layout->user('user/share', array(
'user' => $user,
'title' => t('Public access'),
)));
@@ -290,20 +306,20 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
- list($valid, $errors) = $this->user->validatePasswordModification($values);
+ list($valid, $errors) = $this->userValidator->validatePasswordModification($values);
if ($valid) {
if ($this->user->update($values)) {
- $this->session->flash(t('Password modified successfully.'));
+ $this->flash->success(t('Password modified successfully.'));
} else {
- $this->session->flashError(t('Unable to change the password.'));
+ $this->flash->failure(t('Unable to change the password.'));
}
$this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id'])));
}
}
- $this->response->html($this->layout('user/password', array(
+ $this->response->html($this->helper->layout->user('user/password', array(
'values' => $values,
'errors' => $errors,
'user' => $user,
@@ -326,38 +342,32 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
- if ($this->userSession->isAdmin()) {
- $values += array('is_admin' => 0, 'is_project_admin' => 0);
- } else {
- // Regular users can't be admin
- if (isset($values['is_admin'])) {
- unset($values['is_admin']);
- }
-
- if (isset($values['is_project_admin'])) {
- unset($values['is_project_admin']);
+ if (! $this->userSession->isAdmin()) {
+ if (isset($values['role'])) {
+ unset($values['role']);
}
}
- list($valid, $errors) = $this->user->validateModification($values);
+ list($valid, $errors) = $this->userValidator->validateModification($values);
if ($valid) {
if ($this->user->update($values)) {
- $this->session->flash(t('User updated successfully.'));
+ $this->flash->success(t('User updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update your user.'));
+ $this->flash->failure(t('Unable to update your user.'));
}
$this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id'])));
}
}
- $this->response->html($this->layout('user/edit', array(
+ $this->response->html($this->helper->layout->user('user/edit', array(
'values' => $values,
'errors' => $errors,
'user' => $user,
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
+ 'roles' => $this->role->getApplicationRoles(),
)));
}
@@ -376,49 +386,23 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues() + array('disable_login_form' => 0, 'is_ldap_user' => 0);
- list($valid, $errors) = $this->user->validateModification($values);
+ list($valid, $errors) = $this->userValidator->validateModification($values);
if ($valid) {
if ($this->user->update($values)) {
- $this->session->flash(t('User updated successfully.'));
+ $this->flash->success(t('User updated successfully.'));
} else {
- $this->session->flashError(t('Unable to update your user.'));
+ $this->flash->failure(t('Unable to update your user.'));
}
$this->response->redirect($this->helper->url->to('user', 'authentication', array('user_id' => $user['id'])));
}
}
- $this->response->html($this->layout('user/authentication', array(
+ $this->response->html($this->helper->layout->user('user/authentication', array(
'values' => $values,
'errors' => $errors,
'user' => $user,
)));
}
-
- /**
- * Remove a user
- *
- * @access public
- */
- public function remove()
- {
- $user = $this->getUser();
-
- if ($this->request->getStringParam('confirmation') === 'yes') {
- $this->checkCSRFParam();
-
- if ($this->user->remove($user['id'])) {
- $this->session->flash(t('User removed successfully.'));
- } else {
- $this->session->flashError(t('Unable to remove this user.'));
- }
-
- $this->response->redirect($this->helper->url->to('user', 'index'));
- }
-
- $this->response->html($this->layout('user/remove', array(
- 'user' => $user,
- )));
- }
}
diff --git a/app/Controller/UserHelper.php b/app/Controller/UserHelper.php
new file mode 100644
index 00000000..041ed2c8
--- /dev/null
+++ b/app/Controller/UserHelper.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * User Helper
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class UserHelper extends Base
+{
+ /**
+ * User autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $search = $this->request->getStringParam('term');
+ $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
+ $this->response->json($users);
+ }
+
+ /**
+ * User mention autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function mention()
+ {
+ $project_id = $this->request->getStringParam('project_id');
+ $query = $this->request->getStringParam('q');
+ $users = $this->projectPermission->findUsernames($project_id, $query);
+ $this->response->json($users);
+ }
+}
diff --git a/app/Controller/UserImport.php b/app/Controller/UserImport.php
index 32b9a865..debd69e5 100644
--- a/app/Controller/UserImport.php
+++ b/app/Controller/UserImport.php
@@ -18,7 +18,7 @@ class UserImport extends Base
*/
public function step1(array $values = array(), array $errors = array())
{
- $this->response->html($this->template->layout('user_import/step1', array(
+ $this->response->html($this->helper->layout->app('user_import/step1', array(
'values' => $values,
'errors' => $errors,
'max_size' => ini_get('upload_max_filesize'),
@@ -46,9 +46,9 @@ class UserImport extends Base
$csv->read($filename, array($this->userImport, 'import'));
if ($this->userImport->counter > 0) {
- $this->session->flash(t('%d user(s) have been imported successfully.', $this->userImport->counter));
+ $this->flash->success(t('%d user(s) have been imported successfully.', $this->userImport->counter));
} else {
- $this->session->flashError(t('Nothing have been imported!'));
+ $this->flash->failure(t('Nothing have been imported!'));
}
$this->response->redirect($this->helper->url->to('userImport', 'step1'));
diff --git a/app/Controller/UserStatus.php b/app/Controller/UserStatus.php
new file mode 100644
index 00000000..b8ee5c91
--- /dev/null
+++ b/app/Controller/UserStatus.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * User Status Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class UserStatus extends Base
+{
+ /**
+ * Confirm remove a user
+ *
+ * @access public
+ */
+ public function confirmRemove()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->helper->layout->user('user_status/remove', array(
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Remove a user
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $user = $this->getUser();
+ $this->checkCSRFParam();
+
+ if ($this->user->remove($user['id'])) {
+ $this->flash->success(t('User removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this user.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('user', 'index'));
+ }
+
+ /**
+ * Confirm enable a user
+ *
+ * @access public
+ */
+ public function confirmEnable()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->helper->layout->user('user_status/enable', array(
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Enable a user
+ *
+ * @access public
+ */
+ public function enable()
+ {
+ $user = $this->getUser();
+ $this->checkCSRFParam();
+
+ if ($this->user->enable($user['id'])) {
+ $this->flash->success(t('User activated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to enable this user.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('user', 'index'));
+ }
+
+ /**
+ * Confirm disable a user
+ *
+ * @access public
+ */
+ public function confirmDisable()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->helper->layout->user('user_status/disable', array(
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Disable a user
+ *
+ * @access public
+ */
+ public function disable()
+ {
+ $user = $this->getUser();
+ $this->checkCSRFParam();
+
+ if ($this->user->disable($user['id'])) {
+ $this->flash->success(t('User disabled successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to disable this user.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('user', 'index'));
+ }
+}
diff --git a/app/Controller/Webhook.php b/app/Controller/Webhook.php
index a7e9bde4..0eafe3e5 100644
--- a/app/Controller/Webhook.php
+++ b/app/Controller/Webhook.php
@@ -39,57 +39,4 @@ class Webhook extends Base
$this->response->text('FAILED');
}
-
- /**
- * Handle Github webhooks
- *
- * @access public
- */
- public function github()
- {
- $this->checkWebhookToken();
-
- $this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id'));
-
- $result = $this->githubWebhook->parsePayload(
- $this->request->getHeader('X-Github-Event'),
- $this->request->getJson()
- );
-
- echo $result ? 'PARSED' : 'IGNORED';
- }
-
- /**
- * Handle Gitlab webhooks
- *
- * @access public
- */
- public function gitlab()
- {
- $this->checkWebhookToken();
-
- $this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id'));
- $result = $this->gitlabWebhook->parsePayload($this->request->getJson());
-
- echo $result ? 'PARSED' : 'IGNORED';
- }
-
- /**
- * Handle Bitbucket webhooks
- *
- * @access public
- */
- public function bitbucket()
- {
- $this->checkWebhookToken();
-
- $this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id'));
-
- $result = $this->bitbucketWebhook->parsePayload(
- $this->request->getHeader('X-Event-Key'),
- $this->request->getJson()
- );
-
- echo $result ? 'PARSED' : 'IGNORED';
- }
}
diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php
new file mode 100644
index 00000000..f1ea8abe
--- /dev/null
+++ b/app/Core/Action/ActionManager.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Kanboard\Core\Action;
+
+use RuntimeException;
+use Kanboard\Core\Base;
+use Kanboard\Action\Base as ActionBase;
+
+/**
+ * Action Manager
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class ActionManager extends Base
+{
+ /**
+ * List of automatic actions
+ *
+ * @access private
+ * @var array
+ */
+ private $actions = array();
+
+ /**
+ * Register a new automatic action
+ *
+ * @access public
+ * @param ActionBase $action
+ * @return ActionManager
+ */
+ public function register(ActionBase $action)
+ {
+ $this->actions[$action->getName()] = $action;
+ return $this;
+ }
+
+ /**
+ * Get automatic action instance
+ *
+ * @access public
+ * @param string $name Absolute class name with namespace
+ * @return ActionBase
+ */
+ public function getAction($name)
+ {
+ if (isset($this->actions[$name])) {
+ return $this->actions[$name];
+ }
+
+ throw new RuntimeException('Automatic Action Not Found: '.$name);
+ }
+
+ /**
+ * Get available automatic actions
+ *
+ * @access public
+ * @return array
+ */
+ public function getAvailableActions()
+ {
+ $actions = array();
+
+ foreach ($this->actions as $action) {
+ if (count($action->getEvents()) > 0) {
+ $actions[$action->getName()] = $action->getDescription();
+ }
+ }
+
+ asort($actions);
+
+ return $actions;
+ }
+
+ /**
+ * Get all available action parameters
+ *
+ * @access public
+ * @param array $actions
+ * @return array
+ */
+ public function getAvailableParameters(array $actions)
+ {
+ $params = array();
+
+ foreach ($actions as $action) {
+ $currentAction = $this->getAction($action['action_name']);
+ $params[$currentAction->getName()] = $currentAction->getActionRequiredParameters();
+ }
+
+ return $params;
+ }
+
+ /**
+ * Get list of compatible events for a given action
+ *
+ * @access public
+ * @param string $name
+ * @return array
+ */
+ public function getCompatibleEvents($name)
+ {
+ $events = array();
+ $actionEvents = $this->getAction($name)->getEvents();
+
+ foreach ($this->eventManager->getAll() as $event => $description) {
+ if (in_array($event, $actionEvents)) {
+ $events[$event] = $description;
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Bind automatic actions to events
+ *
+ * @access public
+ * @return ActionManager
+ */
+ public function attachEvents()
+ {
+ if ($this->userSession->isLogged()) {
+ $actions = $this->action->getAllByUser($this->userSession->getId());
+ } else {
+ $actions = $this->action->getAll();
+ }
+
+ foreach ($actions as $action) {
+ $listener = clone $this->getAction($action['action_name']);
+ $listener->setProjectId($action['project_id']);
+
+ foreach ($action['params'] as $param_name => $param_value) {
+ $listener->setParam($param_name, $param_value);
+ }
+
+ $this->dispatcher->addListener($action['event_name'], array($listener, 'execute'));
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Core/Base.php b/app/Core/Base.php
index d402fb37..f1053114 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -5,49 +5,78 @@ namespace Kanboard\Core;
use Pimple\Container;
/**
- * Base class
+ * Base Class
*
* @package core
* @author Frederic Guillot
*
- * @property \Kanboard\Core\Helper $helper
+ * @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic
+ * @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic
+ * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
+ * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
+ * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
+ * @property \Kanboard\Core\Action\ActionManager $actionManager
+ * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
+ * @property \Kanboard\Core\Cache\MemoryCache $memoryCache
+ * @property \Kanboard\Core\Event\EventManager $eventManager
+ * @property \Kanboard\Core\Group\GroupManager $groupManager
+ * @property \Kanboard\Core\Http\Client $httpClient
+ * @property \Kanboard\Core\Http\OAuth2 $oauth
+ * @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie
+ * @property \Kanboard\Core\Http\Request $request
+ * @property \Kanboard\Core\Http\Response $response
+ * @property \Kanboard\Core\Http\Router $router
+ * @property \Kanboard\Core\Http\Route $route
* @property \Kanboard\Core\Mail\Client $emailClient
- * @property \Kanboard\Core\HttpClient $httpClient
- * @property \Kanboard\Core\Paginator $paginator
- * @property \Kanboard\Core\Request $request
- * @property \Kanboard\Core\Session $session
- * @property \Kanboard\Core\Template $template
- * @property \Kanboard\Core\OAuth2 $oauth
- * @property \Kanboard\Core\Router $router
- * @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage
- * @property \Kanboard\Core\Cache\Cache $memoryCache
* @property \Kanboard\Core\Plugin\Hook $hook
* @property \Kanboard\Core\Plugin\Loader $pluginLoader
- * @property \Kanboard\Integration\BitbucketWebhook $bitbucketWebhook
- * @property \Kanboard\Integration\GithubWebhook $githubWebhook
- * @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook
+ * @property \Kanboard\Core\Security\AccessMap $projectAccessMap
+ * @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager
+ * @property \Kanboard\Core\Security\AccessMap $applicationAccessMap
+ * @property \Kanboard\Core\Security\AccessMap $projectAccessMap
+ * @property \Kanboard\Core\Security\Authorization $applicationAuthorization
+ * @property \Kanboard\Core\Security\Authorization $projectAuthorization
+ * @property \Kanboard\Core\Security\Role $role
+ * @property \Kanboard\Core\Security\Token $token
+ * @property \Kanboard\Core\Session\FlashMessage $flash
+ * @property \Kanboard\Core\Session\SessionManager $sessionManager
+ * @property \Kanboard\Core\Session\SessionStorage $sessionStorage
+ * @property \Kanboard\Core\User\GroupSync $groupSync
+ * @property \Kanboard\Core\User\UserProfile $userProfile
+ * @property \Kanboard\Core\User\UserSync $userSync
+ * @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\Model\Acl $acl
+ * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
+ * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
- * @property \Kanboard\Model\Authentication $authentication
+ * @property \Kanboard\Model\ActionParameter $actionParameter
* @property \Kanboard\Model\Board $board
* @property \Kanboard\Model\Category $category
* @property \Kanboard\Model\Color $color
+ * @property \Kanboard\Model\Column $column
* @property \Kanboard\Model\Comment $comment
* @property \Kanboard\Model\Config $config
* @property \Kanboard\Model\Currency $currency
* @property \Kanboard\Model\CustomFilter $customFilter
- * @property \Kanboard\Model\DateParser $dateParser
- * @property \Kanboard\Model\File $file
+ * @property \Kanboard\Model\TaskFile $taskFile
+ * @property \Kanboard\Model\ProjectFile $projectFile
+ * @property \Kanboard\Model\Group $group
+ * @property \Kanboard\Model\GroupMember $groupMember
* @property \Kanboard\Model\LastLogin $lastLogin
* @property \Kanboard\Model\Link $link
* @property \Kanboard\Model\Notification $notification
* @property \Kanboard\Model\OverdueNotification $overdueNotification
+ * @property \Kanboard\Model\PasswordReset $passwordReset
* @property \Kanboard\Model\Project $project
* @property \Kanboard\Model\ProjectActivity $projectActivity
* @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
@@ -56,8 +85,12 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
* @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
+ * @property \Kanboard\Model\RememberMeSession $rememberMeSession
* @property \Kanboard\Model\Subtask $subtask
* @property \Kanboard\Model\SubtaskExport $subtaskExport
* @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking
@@ -67,6 +100,7 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskCreation $taskCreation
* @property \Kanboard\Model\TaskDuplication $taskDuplication
* @property \Kanboard\Model\TaskExport $taskExport
+ * @property \Kanboard\Model\TaskExternalLink $taskExternalLink
* @property \Kanboard\Model\TaskImport $taskImport
* @property \Kanboard\Model\TaskFinder $taskFinder
* @property \Kanboard\Model\TaskFilter $taskFilter
@@ -75,21 +109,39 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskPermission $taskPermission
* @property \Kanboard\Model\TaskPosition $taskPosition
* @property \Kanboard\Model\TaskStatus $taskStatus
- * @property \Kanboard\Model\TaskValidator $taskValidator
* @property \Kanboard\Model\TaskMetadata $taskMetadata
* @property \Kanboard\Model\Transition $transition
* @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserImport $userImport
+ * @property \Kanboard\Model\UserLocking $userLocking
+ * @property \Kanboard\Model\UserMention $userMention
* @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationType $userNotificationType
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
- * @property \Kanboard\Model\UserSession $userSession
* @property \Kanboard\Model\UserMetadata $userMetadata
* @property \Kanboard\Model\Webhook $webhook
+ * @property \Kanboard\Validator\ActionValidator $actionValidator
+ * @property \Kanboard\Validator\AuthValidator $authValidator
+ * @property \Kanboard\Validator\ColumnValidator $columnValidator
+ * @property \Kanboard\Validator\CategoryValidator $categoryValidator
+ * @property \Kanboard\Validator\ColumnValidator $columnValidator
+ * @property \Kanboard\Validator\CommentValidator $commentValidator
+ * @property \Kanboard\Validator\CurrencyValidator $currencyValidator
+ * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator
+ * @property \Kanboard\Validator\GroupValidator $groupValidator
+ * @property \Kanboard\Validator\LinkValidator $linkValidator
+ * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
+ * @property \Kanboard\Validator\ProjectValidator $projectValidator
+ * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator
+ * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator
+ * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator
+ * @property \Kanboard\Validator\TaskExternalLinkValidator $taskExternalLinkValidator
+ * @property \Kanboard\Validator\TaskValidator $taskValidator
+ * @property \Kanboard\Validator\UserValidator $userValidator
* @property \Psr\Log\LoggerInterface $logger
- * @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter
* @property \PicoDb\Database $db
+ * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
abstract class Base
{
diff --git a/app/Core/Cache/MemoryCache.php b/app/Core/Cache/MemoryCache.php
index c4fb7ca4..39e3947b 100644
--- a/app/Core/Cache/MemoryCache.php
+++ b/app/Core/Cache/MemoryCache.php
@@ -23,7 +23,7 @@ class MemoryCache extends Base implements CacheInterface
*
* @access public
* @param string $key
- * @param string $value
+ * @param mixed $value
*/
public function set($key, $value)
{
diff --git a/app/Core/Csv.php b/app/Core/Csv.php
index bec400ed..e45af24c 100644
--- a/app/Core/Csv.php
+++ b/app/Core/Csv.php
@@ -93,8 +93,7 @@ class Csv
{
if (! empty($value)) {
$value = trim(strtolower($value));
- return $value === '1' || $value{0}
- === 't' ? 1 : 0;
+ return $value === '1' || $value{0} === 't' || $value{0} === 'y' ? 1 : 0;
}
return 0;
@@ -164,10 +163,14 @@ class Csv
*/
public function write($filename, array $rows)
{
- $file = new SplFileObject($filename, 'w');
+ $fp = fopen($filename, 'w');
- foreach ($rows as $row) {
- $file->fputcsv($row, $this->delimiter, $this->enclosure);
+ if (is_resource($fp)) {
+ foreach ($rows as $row) {
+ fputcsv($fp, $row, $this->delimiter, $this->enclosure);
+ }
+
+ fclose($fp);
}
return $this;
diff --git a/app/Core/DateParser.php b/app/Core/DateParser.php
index 6577af0f..20e79ff9 100644
--- a/app/Core/DateParser.php
+++ b/app/Core/DateParser.php
@@ -13,67 +13,91 @@ use DateTime;
class DateParser extends Base
{
/**
- * Return true if the date is within the date range
+ * List of time formats
*
* @access public
- * @param DateTime $date
- * @param DateTime $start
- * @param DateTime $end
- * @return boolean
+ * @return string[]
*/
- public function withinDateRange(DateTime $date, DateTime $start, DateTime $end)
+ public function getTimeFormats()
{
- return $date >= $start && $date <= $end;
+ return array(
+ 'H:i',
+ 'g:i a',
+ );
}
/**
- * Get the total number of hours between 2 datetime objects
- * Minutes are rounded to the nearest quarter
+ * List of date formats
*
* @access public
- * @param DateTime $d1
- * @param DateTime $d2
- * @return float
+ * @param boolean $iso
+ * @return string[]
*/
- public function getHours(DateTime $d1, DateTime $d2)
+ public function getDateFormats($iso = false)
{
- $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp()));
- return round($seconds / 3600, 2);
+ $iso_formats = array(
+ 'Y-m-d',
+ 'Y_m_d',
+ );
+
+ $user_formats = array(
+ 'm/d/Y',
+ 'd/m/Y',
+ 'Y/m/d',
+ 'd.m.Y',
+ );
+
+ return $iso ? array_merge($iso_formats, $user_formats) : $user_formats;
}
/**
- * Round the timestamp to the nearest quarter
+ * List of datetime formats
*
* @access public
- * @param integer $seconds Timestamp
- * @return integer
+ * @param boolean $iso
+ * @return string[]
*/
- public function getRoundedSeconds($seconds)
+ public function getDateTimeFormats($iso = false)
{
- return (int) round($seconds / (15 * 60)) * (15 * 60);
+ $formats = array();
+
+ foreach ($this->getDateFormats($iso) as $date) {
+ foreach ($this->getTimeFormats() as $time) {
+ $formats[] = $date.' '.$time;
+ }
+ }
+
+ return $formats;
}
/**
- * Return a timestamp if the given date format is correct otherwise return 0
+ * List of all date formats
*
* @access public
- * @param string $value Date to parse
- * @param string $format Date format
- * @return integer
+ * @param boolean $iso
+ * @return string[]
*/
- public function getValidDate($value, $format)
+ public function getAllDateFormats($iso = false)
{
- $date = DateTime::createFromFormat($format, $value);
+ return array_merge($this->getDateFormats($iso), $this->getDateTimeFormats($iso));
+ }
- if ($date !== false) {
- $errors = DateTime::getLastErrors();
- if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) {
- $timestamp = $date->getTimestamp();
- return $timestamp > 0 ? $timestamp : 0;
- }
+ /**
+ * Get available formats (visible in settings)
+ *
+ * @access public
+ * @param array $formats
+ * @return array
+ */
+ public function getAvailableFormats(array $formats)
+ {
+ $values = array();
+
+ foreach ($formats as $format) {
+ $values[$format] = date($format);
}
- return 0;
+ return $values;
}
/**
@@ -85,7 +109,11 @@ class DateParser extends Base
*/
public function getTimestamp($value)
{
- foreach ($this->getAllFormats() as $format) {
+ if (ctype_digit($value)) {
+ return (int) $value;
+ }
+
+ foreach ($this->getAllDateFormats(true) as $format) {
$timestamp = $this->getValidDate($value, $format);
if ($timestamp !== 0) {
@@ -97,104 +125,103 @@ class DateParser extends Base
}
/**
- * Get ISO8601 date from user input
+ * Return a timestamp if the given date format is correct otherwise return 0
*
- * @access public
- * @param string $value Date to parse
- * @return string
+ * @access private
+ * @param string $value Date to parse
+ * @param string $format Date format
+ * @return integer
*/
- public function getIsoDate($value)
+ private function getValidDate($value, $format)
{
- return date('Y-m-d', ctype_digit($value) ? $value : $this->getTimestamp($value));
+ $date = DateTime::createFromFormat($format, $value);
+
+ if ($date !== false) {
+ $errors = DateTime::getLastErrors();
+ if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) {
+ $timestamp = $date->getTimestamp();
+ return $timestamp > 0 ? $timestamp : 0;
+ }
+ }
+
+ return 0;
}
/**
- * Get all combinations of date/time formats
+ * Return true if the date is within the date range
*
* @access public
- * @return string[]
+ * @param DateTime $date
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return boolean
*/
- public function getAllFormats()
+ public function withinDateRange(DateTime $date, DateTime $start, DateTime $end)
{
- $formats = array();
-
- foreach ($this->getDateFormats() as $date) {
- foreach ($this->getTimeFormats() as $time) {
- $formats[] = $date.' '.$time;
- }
- }
-
- return array_merge($formats, $this->getDateFormats());
+ return $date >= $start && $date <= $end;
}
/**
- * Return the list of supported date formats (for the parser)
+ * Get the total number of hours between 2 datetime objects
+ * Minutes are rounded to the nearest quarter
*
* @access public
- * @return string[]
+ * @param DateTime $d1
+ * @param DateTime $d2
+ * @return float
*/
- public function getDateFormats()
+ public function getHours(DateTime $d1, DateTime $d2)
{
- return array(
- $this->config->get('application_date_format', 'm/d/Y'),
- 'Y-m-d',
- 'Y_m_d',
- );
+ $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp()));
+ return round($seconds / 3600, 2);
}
/**
- * Return the list of supported time formats (for the parser)
+ * Round the timestamp to the nearest quarter
*
* @access public
- * @return string[]
+ * @param integer $seconds Timestamp
+ * @return integer
*/
- public function getTimeFormats()
+ public function getRoundedSeconds($seconds)
{
- return array(
- 'H:i',
- 'g:i A',
- 'g:iA',
- );
+ return (int) round($seconds / (15 * 60)) * (15 * 60);
}
/**
- * Return the list of available date formats (for the config page)
+ * Get ISO-8601 date from user input
*
* @access public
- * @return array
+ * @param string $value Date to parse
+ * @return string
*/
- public function getAvailableFormats()
+ public function getIsoDate($value)
{
- return array(
- 'm/d/Y' => date('m/d/Y'),
- 'd/m/Y' => date('d/m/Y'),
- 'Y/m/d' => date('Y/m/d'),
- 'd.m.Y' => date('d.m.Y'),
- );
+ return date('Y-m-d', $this->getTimestamp($value));
}
/**
- * Remove the time from a timestamp
+ * Get a timetstamp from an ISO date format
*
* @access public
- * @param integer $timestamp Timestamp
+ * @param string $value
* @return integer
*/
- public function removeTimeFromTimestamp($timestamp)
+ public function getTimestampFromIsoFormat($value)
{
- return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
+ return $this->removeTimeFromTimestamp(ctype_digit($value) ? $value : strtotime($value));
}
/**
- * Get a timetstamp from an ISO date format
+ * Remove the time from a timestamp
*
* @access public
- * @param string $date
+ * @param integer $timestamp
* @return integer
*/
- public function getTimestampFromIsoFormat($date)
+ public function removeTimeFromTimestamp($timestamp)
{
- return $this->removeTimeFromTimestamp(ctype_digit($date) ? $date : strtotime($date));
+ return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
}
/**
@@ -204,13 +231,10 @@ class DateParser extends Base
* @param array $values Database values
* @param string[] $fields Date fields
* @param string $format Date format
+ * @return array
*/
- public function format(array &$values, array $fields, $format = '')
+ public function format(array $values, array $fields, $format)
{
- if ($format === '') {
- $format = $this->config->get('application_date_format');
- }
-
foreach ($fields as $field) {
if (! empty($values[$field])) {
$values[$field] = date($format, $values[$field]);
@@ -218,23 +242,28 @@ class DateParser extends Base
$values[$field] = '';
}
}
+
+ return $values;
}
/**
- * Convert date (form input data)
+ * Convert date to timestamp
*
* @access public
* @param array $values Database values
* @param string[] $fields Date fields
* @param boolean $keep_time Keep time or not
+ * @return array
*/
- public function convert(array &$values, array $fields, $keep_time = false)
+ public function convert(array $values, array $fields, $keep_time = false)
{
foreach ($fields as $field) {
- if (! empty($values[$field]) && ! is_numeric($values[$field])) {
+ if (! empty($values[$field])) {
$timestamp = $this->getTimestamp($values[$field]);
$values[$field] = $keep_time ? $timestamp : $this->removeTimeFromTimestamp($timestamp);
}
}
+
+ return $values;
}
}
diff --git a/app/Core/Event/EventManager.php b/app/Core/Event/EventManager.php
new file mode 100644
index 00000000..162d23e8
--- /dev/null
+++ b/app/Core/Event/EventManager.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Kanboard\Core\Event;
+
+use Kanboard\Model\Task;
+use Kanboard\Model\TaskLink;
+
+/**
+ * Event Manager
+ *
+ * @package event
+ * @author Frederic Guillot
+ */
+class EventManager
+{
+ /**
+ * Extended events
+ *
+ * @access private
+ * @var array
+ */
+ private $events = array();
+
+ /**
+ * Add new event
+ *
+ * @access public
+ * @param string $event
+ * @param string $description
+ * @return EventManager
+ */
+ public function register($event, $description)
+ {
+ $this->events[$event] = $description;
+ return $this;
+ }
+
+ /**
+ * Get the list of events and description that can be used from the user interface
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ $events = array(
+ TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'),
+ Task::EVENT_MOVE_COLUMN => t('Move a task to another column'),
+ Task::EVENT_UPDATE => t('Task modification'),
+ Task::EVENT_CREATE => t('Task creation'),
+ Task::EVENT_OPEN => t('Reopen a task'),
+ Task::EVENT_CLOSE => t('Closing a task'),
+ Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
+ Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
+ Task::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'),
+ );
+
+ $events = array_merge($events, $this->events);
+ asort($events);
+
+ return $events;
+ }
+}
diff --git a/app/Core/ExternalLink/ExternalLinkInterface.php b/app/Core/ExternalLink/ExternalLinkInterface.php
new file mode 100644
index 00000000..2dbc0a19
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+/**
+ * External Link Interface
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+interface ExternalLinkInterface
+{
+ /**
+ * Get link title
+ *
+ * @access public
+ * @return string
+ */
+ public function getTitle();
+
+ /**
+ * Get link URL
+ *
+ * @access public
+ * @return string
+ */
+ public function getUrl();
+
+ /**
+ * Set link URL
+ *
+ * @access public
+ * @param string $url
+ */
+ public function setUrl($url);
+}
diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php
new file mode 100644
index 00000000..1fa423c2
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkManager.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+use Kanboard\Core\Base;
+
+/**
+ * External Link Manager
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class ExternalLinkManager extends Base
+{
+ /**
+ * Automatic type value
+ *
+ * @var string
+ */
+ const TYPE_AUTO = 'auto';
+
+ /**
+ * Registered providers
+ *
+ * @access private
+ * @var array
+ */
+ private $providers = array();
+
+ /**
+ * Type chosen by the user
+ *
+ * @access private
+ * @var string
+ */
+ private $userInputType = '';
+
+ /**
+ * Text entered by the user
+ *
+ * @access private
+ * @var string
+ */
+ private $userInputText = '';
+
+ /**
+ * Register a new provider
+ *
+ * Providers are registered in a LIFO queue
+ *
+ * @access public
+ * @param ExternalLinkProviderInterface $provider
+ * @return ExternalLinkManager
+ */
+ public function register(ExternalLinkProviderInterface $provider)
+ {
+ array_unshift($this->providers, $provider);
+ return $this;
+ }
+
+ /**
+ * Get provider
+ *
+ * @access public
+ * @param string $type
+ * @throws ExternalLinkProviderNotFound
+ * @return ExternalLinkProviderInterface
+ */
+ public function getProvider($type)
+ {
+ foreach ($this->providers as $provider) {
+ if ($provider->getType() === $type) {
+ return $provider;
+ }
+ }
+
+ throw new ExternalLinkProviderNotFound('Unable to find link provider: '.$type);
+ }
+
+ /**
+ * Get link types
+ *
+ * @access public
+ * @return array
+ */
+ public function getTypes()
+ {
+ $types = array();
+
+ foreach ($this->providers as $provider) {
+ $types[$provider->getType()] = $provider->getName();
+ }
+
+ asort($types);
+
+ return array(self::TYPE_AUTO => t('Auto')) + $types;
+ }
+
+ /**
+ * Get dependency label from a provider
+ *
+ * @access public
+ * @param string $type
+ * @param string $dependency
+ * @return string
+ */
+ public function getDependencyLabel($type, $dependency)
+ {
+ $provider = $this->getProvider($type);
+ $dependencies = $provider->getDependencies();
+ return isset($dependencies[$dependency]) ? $dependencies[$dependency] : $dependency;
+ }
+
+ /**
+ * Find a provider that match
+ *
+ * @access public
+ * @throws ExternalLinkProviderNotFound
+ * @return ExternalLinkProviderInterface
+ */
+ public function find()
+ {
+ if ($this->userInputType === self::TYPE_AUTO) {
+ $provider = $this->findProvider();
+ } else {
+ $provider = $this->getProvider($this->userInputType);
+ $provider->setUserTextInput($this->userInputText);
+
+ if (! $provider->match()) {
+ throw new ExternalLinkProviderNotFound('Unable to parse URL with selected provider');
+ }
+ }
+
+ if ($provider === null) {
+ throw new ExternalLinkProviderNotFound('Unable to find link information from provided information');
+ }
+
+ return $provider;
+ }
+
+ /**
+ * Set form values
+ *
+ * @access public
+ * @param array $values
+ * @return ExternalLinkManager
+ */
+ public function setUserInput(array $values)
+ {
+ $this->userInputType = empty($values['type']) ? self::TYPE_AUTO : $values['type'];
+ $this->userInputText = empty($values['text']) ? '' : trim($values['text']);
+ return $this;
+ }
+
+ /**
+ * Find a provider that user input
+ *
+ * @access private
+ * @return ExternalLinkProviderInterface
+ */
+ private function findProvider()
+ {
+ foreach ($this->providers as $provider) {
+ $provider->setUserTextInput($this->userInputText);
+
+ if ($provider->match()) {
+ return $provider;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/Core/ExternalLink/ExternalLinkProviderInterface.php b/app/Core/ExternalLink/ExternalLinkProviderInterface.php
new file mode 100644
index 00000000..c908e1eb
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkProviderInterface.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+/**
+ * External Link Provider Interface
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+interface ExternalLinkProviderInterface
+{
+ /**
+ * Get provider name (label)
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Get link type (will be saved in the database)
+ *
+ * @access public
+ * @return string
+ */
+ public function getType();
+
+ /**
+ * Get a dictionary of supported dependency types by the provider
+ *
+ * Example:
+ *
+ * [
+ * 'related' => t('Related'),
+ * 'child' => t('Child'),
+ * 'parent' => t('Parent'),
+ * 'self' => t('Self'),
+ * ]
+ *
+ * The dictionary key is saved in the database.
+ *
+ * @access public
+ * @return array
+ */
+ public function getDependencies();
+
+ /**
+ * Set text entered by the user
+ *
+ * @access public
+ * @param string $input
+ */
+ public function setUserTextInput($input);
+
+ /**
+ * Return true if the provider can parse correctly the user input
+ *
+ * @access public
+ * @return boolean
+ */
+ public function match();
+
+ /**
+ * Get the link found with the properties
+ *
+ * @access public
+ * @return ExternalLinkInterface
+ */
+ public function getLink();
+}
diff --git a/app/Core/ExternalLink/ExternalLinkProviderNotFound.php b/app/Core/ExternalLink/ExternalLinkProviderNotFound.php
new file mode 100644
index 00000000..4fd05202
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkProviderNotFound.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+use Exception;
+
+/**
+ * External Link Provider Not Found Exception
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class ExternalLinkProviderNotFound extends Exception
+{
+}
diff --git a/app/Core/Group/GroupBackendProviderInterface.php b/app/Core/Group/GroupBackendProviderInterface.php
new file mode 100644
index 00000000..74c5cb03
--- /dev/null
+++ b/app/Core/Group/GroupBackendProviderInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Kanboard\Core\Group;
+
+/**
+ * Group Backend Provider Interface
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+interface GroupBackendProviderInterface
+{
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return GroupProviderInterface[]
+ */
+ public function find($input);
+}
diff --git a/app/Core/Group/GroupManager.php b/app/Core/Group/GroupManager.php
new file mode 100644
index 00000000..48b6c4f8
--- /dev/null
+++ b/app/Core/Group/GroupManager.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Core\Group;
+
+/**
+ * Group Manager
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class GroupManager
+{
+ /**
+ * List of backend providers
+ *
+ * @access private
+ * @var array
+ */
+ private $providers = array();
+
+ /**
+ * Register a new group backend provider
+ *
+ * @access public
+ * @param GroupBackendProviderInterface $provider
+ * @return GroupManager
+ */
+ public function register(GroupBackendProviderInterface $provider)
+ {
+ $this->providers[] = $provider;
+ return $this;
+ }
+
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return GroupProviderInterface[]
+ */
+ public function find($input)
+ {
+ $groups = array();
+
+ foreach ($this->providers as $provider) {
+ $groups = array_merge($groups, $provider->find($input));
+ }
+
+ return $this->removeDuplicates($groups);
+ }
+
+ /**
+ * Remove duplicated groups
+ *
+ * @access private
+ * @param array $groups
+ * @return GroupProviderInterface[]
+ */
+ private function removeDuplicates(array $groups)
+ {
+ $result = array();
+
+ foreach ($groups as $group) {
+ if (! isset($result[$group->getName()])) {
+ $result[$group->getName()] = $group;
+ }
+ }
+
+ return array_values($result);
+ }
+}
diff --git a/app/Core/Group/GroupProviderInterface.php b/app/Core/Group/GroupProviderInterface.php
new file mode 100644
index 00000000..4c7c16ec
--- /dev/null
+++ b/app/Core/Group/GroupProviderInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Core\Group;
+
+/**
+ * Group Provider Interface
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+interface GroupProviderInterface
+{
+ /**
+ * Get internal id
+ *
+ * You must return 0 if the group come from an external backend
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId();
+
+ /**
+ * Get external id
+ *
+ * You must return a unique id if the group come from an external provider
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId();
+
+ /**
+ * Get group name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
index 5edaa3f0..bf71769f 100644
--- a/app/Core/Helper.php
+++ b/app/Core/Helper.php
@@ -20,6 +20,7 @@ use Pimple\Container;
* @property \Helper\Text $text
* @property \Helper\Url $url
* @property \Helper\User $user
+ * @property \Helper\Layout $layout
*/
class Helper
{
diff --git a/app/Core/HttpClient.php b/app/Core/Http/Client.php
index 7f4ea47a..12b0a1cb 100644
--- a/app/Core/HttpClient.php
+++ b/app/Core/Http/Client.php
@@ -1,14 +1,16 @@
<?php
-namespace Kanboard\Core;
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
/**
* HTTP client
*
- * @package core
+ * @package http
* @author Frederic Guillot
*/
-class HttpClient extends Base
+class Client extends Base
{
/**
* HTTP connection timeout in seconds
@@ -32,6 +34,19 @@ class HttpClient extends Base
const HTTP_USER_AGENT = 'Kanboard';
/**
+ * Send a GET HTTP request
+ *
+ * @access public
+ * @param string $url
+ * @param string[] $headers
+ * @return string
+ */
+ public function get($url, array $headers = array())
+ {
+ return $this->doRequest('GET', $url, '', $headers);
+ }
+
+ /**
* Send a GET HTTP request and parse JSON response
*
* @access public
@@ -99,6 +114,36 @@ class HttpClient extends Base
return '';
}
+ $stream = @fopen(trim($url), 'r', false, stream_context_create($this->getContext($method, $content, $headers)));
+ $response = '';
+
+ if (is_resource($stream)) {
+ $response = stream_get_contents($stream);
+ } else {
+ $this->logger->error('HttpClient: request failed');
+ }
+
+ if (DEBUG) {
+ $this->logger->debug('HttpClient: url='.$url);
+ $this->logger->debug('HttpClient: payload='.$content);
+ $this->logger->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true));
+ $this->logger->debug('HttpClient: response='.$response);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Get stream context
+ *
+ * @access private
+ * @param string $method
+ * @param string $content
+ * @param string[] $headers
+ * @return array
+ */
+ private function getContext($method, $content, array $headers)
+ {
$default_headers = array(
'User-Agent: '.self::HTTP_USER_AGENT,
'Connection: close',
@@ -126,22 +171,6 @@ class HttpClient extends Base
$context['http']['request_fulluri'] = true;
}
- $stream = @fopen(trim($url), 'r', false, stream_context_create($context));
- $response = '';
-
- if (is_resource($stream)) {
- $response = stream_get_contents($stream);
- } else {
- $this->container['logger']->error('HttpClient: request failed');
- }
-
- if (DEBUG) {
- $this->container['logger']->debug('HttpClient: url='.$url);
- $this->container['logger']->debug('HttpClient: payload='.$content);
- $this->container['logger']->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true));
- $this->container['logger']->debug('HttpClient: response='.$response);
- }
-
- return $response;
+ return $context;
}
}
diff --git a/app/Core/OAuth2.php b/app/Core/Http/OAuth2.php
index a5bbba1a..6fa1fb0a 100644
--- a/app/Core/OAuth2.php
+++ b/app/Core/Http/OAuth2.php
@@ -1,11 +1,13 @@
<?php
-namespace Kanboard\Core;
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
/**
- * OAuth2 client
+ * OAuth2 Client
*
- * @package core
+ * @package http
* @author Frederic Guillot
*/
class OAuth2 extends Base
diff --git a/app/Core/Http/RememberMeCookie.php b/app/Core/Http/RememberMeCookie.php
new file mode 100644
index 00000000..a32b35f3
--- /dev/null
+++ b/app/Core/Http/RememberMeCookie.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
+
+/**
+ * Remember Me Cookie
+ *
+ * @package http
+ * @author Frederic Guillot
+ */
+class RememberMeCookie extends Base
+{
+ /**
+ * Cookie name
+ *
+ * @var string
+ */
+ const COOKIE_NAME = 'KB_RM';
+
+ /**
+ * Encode the cookie
+ *
+ * @access public
+ * @param string $token Session token
+ * @param string $sequence Sequence token
+ * @return string
+ */
+ public function encode($token, $sequence)
+ {
+ return implode('|', array($token, $sequence));
+ }
+
+ /**
+ * Decode the value of a cookie
+ *
+ * @access public
+ * @param string $value Raw cookie data
+ * @return array
+ */
+ public function decode($value)
+ {
+ list($token, $sequence) = explode('|', $value);
+
+ return array(
+ 'token' => $token,
+ 'sequence' => $sequence,
+ );
+ }
+
+ /**
+ * Return true if the current user has a RememberMe cookie
+ *
+ * @access public
+ * @return bool
+ */
+ public function hasCookie()
+ {
+ return $this->request->getCookie(self::COOKIE_NAME) !== '';
+ }
+
+ /**
+ * Write and encode the cookie
+ *
+ * @access public
+ * @param string $token Session token
+ * @param string $sequence Sequence token
+ * @param string $expiration Cookie expiration
+ * @return boolean
+ */
+ public function write($token, $sequence, $expiration)
+ {
+ return setcookie(
+ self::COOKIE_NAME,
+ $this->encode($token, $sequence),
+ $expiration,
+ $this->helper->url->dir(),
+ null,
+ $this->request->isHTTPS(),
+ true
+ );
+ }
+
+ /**
+ * Read and decode the cookie
+ *
+ * @access public
+ * @return mixed
+ */
+ public function read()
+ {
+ $cookie = $this->request->getCookie(self::COOKIE_NAME);
+
+ if (empty($cookie)) {
+ return false;
+ }
+
+ return $this->decode($cookie);
+ }
+
+ /**
+ * Remove the cookie
+ *
+ * @access public
+ * @return boolean
+ */
+ public function remove()
+ {
+ return setcookie(
+ self::COOKIE_NAME,
+ '',
+ time() - 3600,
+ $this->helper->url->dir(),
+ null,
+ $this->request->isHTTPS(),
+ true
+ );
+ }
+}
diff --git a/app/Core/Http/Request.php b/app/Core/Http/Request.php
new file mode 100644
index 00000000..1b3036d5
--- /dev/null
+++ b/app/Core/Http/Request.php
@@ -0,0 +1,337 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use Pimple\Container;
+use Kanboard\Core\Base;
+
+/**
+ * Request class
+ *
+ * @package http
+ * @author Frederic Guillot
+ */
+class Request extends Base
+{
+ /**
+ * Pointer to PHP environment variables
+ *
+ * @access private
+ * @var array
+ */
+ private $server;
+ private $get;
+ private $post;
+ private $files;
+ private $cookies;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ */
+ public function __construct(Container $container, array $server = array(), array $get = array(), array $post = array(), array $files = array(), array $cookies = array())
+ {
+ parent::__construct($container);
+ $this->server = empty($server) ? $_SERVER : $server;
+ $this->get = empty($get) ? $_GET : $get;
+ $this->post = empty($post) ? $_POST : $post;
+ $this->files = empty($files) ? $_FILES : $files;
+ $this->cookies = empty($cookies) ? $_COOKIE : $cookies;
+ }
+
+ /**
+ * Set GET parameters
+ *
+ * @param array $params
+ */
+ public function setParams(array $params)
+ {
+ $this->get = array_merge($this->get, $params);
+ }
+
+ /**
+ * Get query string string parameter
+ *
+ * @access public
+ * @param string $name Parameter name
+ * @param string $default_value Default value
+ * @return string
+ */
+ public function getStringParam($name, $default_value = '')
+ {
+ return isset($this->get[$name]) ? $this->get[$name] : $default_value;
+ }
+
+ /**
+ * Get query string integer parameter
+ *
+ * @access public
+ * @param string $name Parameter name
+ * @param integer $default_value Default value
+ * @return integer
+ */
+ public function getIntegerParam($name, $default_value = 0)
+ {
+ return isset($this->get[$name]) && ctype_digit($this->get[$name]) ? (int) $this->get[$name] : $default_value;
+ }
+
+ /**
+ * Get a form value
+ *
+ * @access public
+ * @param string $name Form field name
+ * @return string|null
+ */
+ public function getValue($name)
+ {
+ $values = $this->getValues();
+ return isset($values[$name]) ? $values[$name] : null;
+ }
+
+ /**
+ * Get form values and check for CSRF token
+ *
+ * @access public
+ * @return array
+ */
+ public function getValues()
+ {
+ if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) {
+ unset($this->post['csrf_token']);
+ return $this->post;
+ }
+
+ return array();
+ }
+
+ /**
+ * Get the raw body of the HTTP request
+ *
+ * @access public
+ * @return string
+ */
+ public function getBody()
+ {
+ return file_get_contents('php://input');
+ }
+
+ /**
+ * Get the Json request body
+ *
+ * @access public
+ * @return array
+ */
+ public function getJson()
+ {
+ return json_decode($this->getBody(), true) ?: array();
+ }
+
+ /**
+ * Get the content of an uploaded file
+ *
+ * @access public
+ * @param string $name Form file name
+ * @return string
+ */
+ public function getFileContent($name)
+ {
+ if (isset($this->files[$name]['tmp_name'])) {
+ return file_get_contents($this->files[$name]['tmp_name']);
+ }
+
+ return '';
+ }
+
+ /**
+ * Get the path of an uploaded file
+ *
+ * @access public
+ * @param string $name Form file name
+ * @return string
+ */
+ public function getFilePath($name)
+ {
+ return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : '';
+ }
+
+ /**
+ * Get info of an uploaded file
+ *
+ * @access public
+ * @param string $name Form file name
+ * @return array
+ */
+ public function getFileInfo($name)
+ {
+ return isset($this->files[$name]) ? $this->files[$name] : array();
+ }
+
+ /**
+ * Return HTTP method
+ *
+ * @access public
+ * @return bool
+ */
+ public function getMethod()
+ {
+ return $this->getServerVariable('REQUEST_METHOD');
+ }
+
+ /**
+ * Return true if the HTTP request is sent with the POST method
+ *
+ * @access public
+ * @return bool
+ */
+ public function isPost()
+ {
+ return $this->getServerVariable('REQUEST_METHOD') === 'POST';
+ }
+
+ /**
+ * Return true if the HTTP request is an Ajax request
+ *
+ * @access public
+ * @return bool
+ */
+ public function isAjax()
+ {
+ return $this->getHeader('X-Requested-With') === 'XMLHttpRequest';
+ }
+
+ /**
+ * Check if the page is requested through HTTPS
+ *
+ * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isHTTPS()
+ {
+ return isset($this->server['HTTPS']) && $this->server['HTTPS'] !== '' && $this->server['HTTPS'] !== 'off';
+ }
+
+ /**
+ * Get cookie value
+ *
+ * @access public
+ * @param string $name
+ * @return string
+ */
+ public function getCookie($name)
+ {
+ return isset($this->cookies[$name]) ? $this->cookies[$name] : '';
+ }
+
+ /**
+ * Return a HTTP header value
+ *
+ * @access public
+ * @param string $name Header name
+ * @return string
+ */
+ public function getHeader($name)
+ {
+ $name = 'HTTP_'.str_replace('-', '_', strtoupper($name));
+ return $this->getServerVariable($name);
+ }
+
+ /**
+ * Get remote user
+ *
+ * @access public
+ * @return string
+ */
+ public function getRemoteUser()
+ {
+ return $this->getServerVariable(REVERSE_PROXY_USER_HEADER);
+ }
+
+ /**
+ * Returns query string
+ *
+ * @access public
+ * @return string
+ */
+ public function getQueryString()
+ {
+ return $this->getServerVariable('QUERY_STRING');
+ }
+
+ /**
+ * Return URI
+ *
+ * @access public
+ * @return string
+ */
+ public function getUri()
+ {
+ return $this->getServerVariable('REQUEST_URI');
+ }
+
+ /**
+ * Get the user agent
+ *
+ * @access public
+ * @return string
+ */
+ public function getUserAgent()
+ {
+ return empty($this->server['HTTP_USER_AGENT']) ? t('Unknown') : $this->server['HTTP_USER_AGENT'];
+ }
+
+ /**
+ * Get the IP address of the user
+ *
+ * @access public
+ * @return string
+ */
+ public function getIpAddress()
+ {
+ $keys = array(
+ 'HTTP_CLIENT_IP',
+ 'HTTP_X_FORWARDED_FOR',
+ 'HTTP_X_FORWARDED',
+ 'HTTP_X_CLUSTER_CLIENT_IP',
+ 'HTTP_FORWARDED_FOR',
+ 'HTTP_FORWARDED',
+ 'REMOTE_ADDR'
+ );
+
+ foreach ($keys as $key) {
+ if ($this->getServerVariable($key) !== '') {
+ foreach (explode(',', $this->server[$key]) as $ipAddress) {
+ return trim($ipAddress);
+ }
+ }
+ }
+
+ return t('Unknown');
+ }
+
+ /**
+ * Get start time
+ *
+ * @access public
+ * @return float
+ */
+ public function getStartTime()
+ {
+ return $this->getServerVariable('REQUEST_TIME_FLOAT') ?: 0;
+ }
+
+ /**
+ * Get server variable
+ *
+ * @access public
+ * @param string $variable
+ * @return string
+ */
+ public function getServerVariable($variable)
+ {
+ return isset($this->server[$variable]) ? $this->server[$variable] : '';
+ }
+}
diff --git a/app/Core/Response.php b/app/Core/Http/Response.php
index 528a6302..d098f519 100644
--- a/app/Core/Response.php
+++ b/app/Core/Http/Response.php
@@ -1,14 +1,17 @@
<?php
-namespace Kanboard\Core;
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Csv;
/**
* Response class
*
- * @package core
+ * @package http
* @author Frederic Guillot
*/
-class Response
+class Response extends Base
{
/**
* Send no cache headers
@@ -44,6 +47,8 @@ class Response
public function forceDownload($filename)
{
header('Content-Disposition: attachment; filename="'.$filename.'"');
+ header('Content-Transfer-Encoding: binary');
+ header('Content-Type: application/octet-stream');
}
/**
@@ -55,7 +60,7 @@ class Response
public function status($status_code)
{
header('Status: '.$status_code);
- header($_SERVER['SERVER_PROTOCOL'].' '.$status_code);
+ header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code);
}
/**
@@ -63,11 +68,12 @@ class Response
*
* @access public
* @param string $url Redirection URL
+ * @param boolean $self If Ajax request and true: refresh the current page
*/
- public function redirect($url)
+ public function redirect($url, $self = false)
{
- if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
- header('X-Ajax-Redirect: '.$url);
+ if ($this->request->isAjax()) {
+ header('X-Ajax-Redirect: '.($self ? 'self' : $url));
} else {
header('Location: '.$url);
}
@@ -215,7 +221,6 @@ class Response
*/
public function csp(array $policies = array())
{
- $policies['default-src'] = "'self'";
$values = '';
foreach ($policies as $policy => $acl) {
@@ -252,7 +257,7 @@ class Response
*/
public function hsts()
{
- if (Request::isHTTPS()) {
+ if ($this->request->isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000');
}
}
diff --git a/app/Core/Http/Route.php b/app/Core/Http/Route.php
new file mode 100644
index 00000000..7836146d
--- /dev/null
+++ b/app/Core/Http/Route.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
+
+/**
+ * Route Handler
+ *
+ * @package http
+ * @author Frederic Guillot
+ */
+class Route extends Base
+{
+ /**
+ * Flag that enable the routing table
+ *
+ * @access private
+ * @var boolean
+ */
+ private $activated = false;
+
+ /**
+ * Store routes for path lookup
+ *
+ * @access private
+ * @var array
+ */
+ private $paths = array();
+
+ /**
+ * Store routes for url lookup
+ *
+ * @access private
+ * @var array
+ */
+ private $urls = array();
+
+ /**
+ * Enable routing table
+ *
+ * @access public
+ * @return Route
+ */
+ public function enable()
+ {
+ $this->activated = true;
+ return $this;
+ }
+
+ /**
+ * Add route
+ *
+ * @access public
+ * @param string $path
+ * @param string $controller
+ * @param string $action
+ * @param string $plugin
+ * @return Route
+ */
+ public function addRoute($path, $controller, $action, $plugin = '')
+ {
+ if ($this->activated) {
+ $path = ltrim($path, '/');
+ $items = explode('/', $path);
+ $params = $this->findParams($items);
+
+ $this->paths[] = array(
+ 'items' => $items,
+ 'count' => count($items),
+ 'controller' => $controller,
+ 'action' => $action,
+ 'plugin' => $plugin,
+ );
+
+ $this->urls[$plugin][$controller][$action][] = array(
+ 'path' => $path,
+ 'params' => $params,
+ 'count' => count($params),
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Find a route according to the given path
+ *
+ * @access public
+ * @param string $path
+ * @return array
+ */
+ public function findRoute($path)
+ {
+ $items = explode('/', ltrim($path, '/'));
+ $count = count($items);
+
+ foreach ($this->paths as $route) {
+ if ($count === $route['count']) {
+ $params = array();
+
+ for ($i = 0; $i < $count; $i++) {
+ if ($route['items'][$i]{0} === ':') {
+ $params[substr($route['items'][$i], 1)] = $items[$i];
+ } elseif ($route['items'][$i] !== $items[$i]) {
+ break;
+ }
+ }
+
+ if ($i === $count) {
+ $this->request->setParams($params);
+ return array(
+ 'controller' => $route['controller'],
+ 'action' => $route['action'],
+ 'plugin' => $route['plugin'],
+ );
+ }
+ }
+ }
+
+ return array(
+ 'controller' => 'app',
+ 'action' => 'index',
+ 'plugin' => '',
+ );
+ }
+
+ /**
+ * Find route url
+ *
+ * @access public
+ * @param string $controller
+ * @param string $action
+ * @param array $params
+ * @param string $plugin
+ * @return string
+ */
+ public function findUrl($controller, $action, array $params = array(), $plugin = '')
+ {
+ if ($plugin === '' && isset($params['plugin'])) {
+ $plugin = $params['plugin'];
+ unset($params['plugin']);
+ }
+
+ if (! isset($this->urls[$plugin][$controller][$action])) {
+ return '';
+ }
+
+ foreach ($this->urls[$plugin][$controller][$action] as $route) {
+ if (array_diff_key($params, $route['params']) === array()) {
+ $url = $route['path'];
+ $i = 0;
+
+ foreach ($params as $variable => $value) {
+ $url = str_replace(':'.$variable, $value, $url);
+ $i++;
+ }
+
+ if ($i === $route['count']) {
+ return $url;
+ }
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Find url params
+ *
+ * @access public
+ * @param array $items
+ * @return array
+ */
+ public function findParams(array $items)
+ {
+ $params = array();
+
+ foreach ($items as $item) {
+ if ($item !== '' && $item{0} === ':') {
+ $params[substr($item, 1)] = true;
+ }
+ }
+
+ return $params;
+ }
+}
diff --git a/app/Core/Http/Router.php b/app/Core/Http/Router.php
new file mode 100644
index 00000000..0fe80ecc
--- /dev/null
+++ b/app/Core/Http/Router.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use RuntimeException;
+use Kanboard\Core\Base;
+
+/**
+ * Route Dispatcher
+ *
+ * @package http
+ * @author Frederic Guillot
+ */
+class Router extends Base
+{
+ /**
+ * Plugin name
+ *
+ * @access private
+ * @var string
+ */
+ private $plugin = '';
+
+ /**
+ * Controller
+ *
+ * @access private
+ * @var string
+ */
+ private $controller = '';
+
+ /**
+ * Action
+ *
+ * @access private
+ * @var string
+ */
+ private $action = '';
+
+ /**
+ * Get plugin name
+ *
+ * @access public
+ * @return string
+ */
+ public function getPlugin()
+ {
+ return $this->plugin;
+ }
+
+ /**
+ * Get controller
+ *
+ * @access public
+ * @return string
+ */
+ public function getController()
+ {
+ return $this->controller;
+ }
+
+ /**
+ * Get action
+ *
+ * @access public
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ /**
+ * Get the path to compare patterns
+ *
+ * @access public
+ * @return string
+ */
+ public function getPath()
+ {
+ $path = substr($this->request->getUri(), strlen($this->helper->url->dir()));
+
+ if ($this->request->getQueryString() !== '') {
+ $path = substr($path, 0, - strlen($this->request->getQueryString()) - 1);
+ }
+
+ if ($path !== '' && $path{0} === '/') {
+ $path = substr($path, 1);
+ }
+
+ return $path;
+ }
+
+ /**
+ * Find controller/action from the route table or from get arguments
+ *
+ * @access public
+ */
+ public function dispatch()
+ {
+ $controller = $this->request->getStringParam('controller');
+ $action = $this->request->getStringParam('action');
+ $plugin = $this->request->getStringParam('plugin');
+
+ if ($controller === '') {
+ $route = $this->route->findRoute($this->getPath());
+ $controller = $route['controller'];
+ $action = $route['action'];
+ $plugin = $route['plugin'];
+ }
+
+ $this->controller = ucfirst($this->sanitize($controller, 'app'));
+ $this->action = $this->sanitize($action, 'index');
+ $this->plugin = ucfirst($this->sanitize($plugin));
+
+ return $this->executeAction();
+ }
+
+ /**
+ * Check controller and action parameter
+ *
+ * @access public
+ * @param string $value
+ * @param string $default
+ * @return string
+ */
+ public function sanitize($value, $default = '')
+ {
+ return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default;
+ }
+
+ /**
+ * Execute controller action
+ *
+ * @access private
+ */
+ private function executeAction()
+ {
+ $class = $this->getControllerClassName();
+
+ if (! class_exists($class)) {
+ throw new RuntimeException('Controller not found');
+ }
+
+ if (! method_exists($class, $this->action)) {
+ throw new RuntimeException('Action not implemented');
+ }
+
+ $instance = new $class($this->container);
+ $instance->beforeAction();
+ $instance->{$this->action}();
+ return $instance;
+ }
+
+ /**
+ * Get controller class name
+ *
+ * @access private
+ * @return string
+ */
+ private function getControllerClassName()
+ {
+ if ($this->plugin !== '') {
+ return '\Kanboard\Plugin\\'.$this->plugin.'\Controller\\'.$this->controller;
+ }
+
+ return '\Kanboard\Controller\\'.$this->controller;
+ }
+}
diff --git a/app/Core/Ldap/Client.php b/app/Core/Ldap/Client.php
new file mode 100644
index 00000000..63149ae3
--- /dev/null
+++ b/app/Core/Ldap/Client.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+use LogicException;
+
+/**
+ * LDAP Client
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Client
+{
+ /**
+ * LDAP resource
+ *
+ * @access protected
+ * @var resource
+ */
+ protected $ldap;
+
+ /**
+ * Establish LDAP connection
+ *
+ * @static
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @return Client
+ */
+ public static function connect($username = null, $password = null)
+ {
+ $client = new self;
+ $client->open($client->getLdapServer());
+ $username = $username ?: $client->getLdapUsername();
+ $password = $password ?: $client->getLdapPassword();
+
+ if (empty($username) && empty($password)) {
+ $client->useAnonymousAuthentication();
+ } else {
+ $client->authenticate($username, $password);
+ }
+
+ return $client;
+ }
+
+ /**
+ * Get server connection
+ *
+ * @access public
+ * @return resource
+ */
+ public function getConnection()
+ {
+ return $this->ldap;
+ }
+
+ /**
+ * Establish server connection
+ *
+ * @access public
+ * @param string $server LDAP server hostname or IP
+ * @param integer $port LDAP port
+ * @param boolean $tls Start TLS
+ * @param boolean $verify Skip SSL certificate verification
+ * @return Client
+ */
+ public function open($server, $port = LDAP_PORT, $tls = LDAP_START_TLS, $verify = LDAP_SSL_VERIFY)
+ {
+ if (! function_exists('ldap_connect')) {
+ throw new ClientException('LDAP: The PHP LDAP extension is required');
+ }
+
+ if (! $verify) {
+ putenv('LDAPTLS_REQCERT=never');
+ }
+
+ $this->ldap = ldap_connect($server, $port);
+
+ if ($this->ldap === false) {
+ throw new ClientException('LDAP: Unable to connect to the LDAP server');
+ }
+
+ ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
+ ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, 0);
+ ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
+ ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, 1);
+
+ if ($tls && ! @ldap_start_tls($this->ldap)) {
+ throw new ClientException('LDAP: Unable to start TLS');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Anonymous authentication
+ *
+ * @access public
+ * @return boolean
+ */
+ public function useAnonymousAuthentication()
+ {
+ if (! @ldap_bind($this->ldap)) {
+ throw new ClientException('Unable to perform anonymous binding');
+ }
+
+ return true;
+ }
+
+ /**
+ * Authentication with username/password
+ *
+ * @access public
+ * @param string $bind_rdn
+ * @param string $bind_password
+ * @return boolean
+ */
+ public function authenticate($bind_rdn, $bind_password)
+ {
+ if (! @ldap_bind($this->ldap, $bind_rdn, $bind_password)) {
+ throw new ClientException('LDAP authentication failure for "'.$bind_rdn.'"');
+ }
+
+ return true;
+ }
+
+ /**
+ * Get LDAP server name
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapServer()
+ {
+ if (! LDAP_SERVER) {
+ throw new LogicException('LDAP server not configured, check the parameter LDAP_SERVER');
+ }
+
+ return LDAP_SERVER;
+ }
+
+ /**
+ * Get LDAP username (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapUsername()
+ {
+ return LDAP_USERNAME;
+ }
+
+ /**
+ * Get LDAP password (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapPassword()
+ {
+ return LDAP_PASSWORD;
+ }
+}
diff --git a/app/Core/Ldap/ClientException.php b/app/Core/Ldap/ClientException.php
new file mode 100644
index 00000000..a0f9f842
--- /dev/null
+++ b/app/Core/Ldap/ClientException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+use Exception;
+
+/**
+ * LDAP Client Exception
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class ClientException extends Exception
+{
+}
diff --git a/app/Core/Ldap/Entries.php b/app/Core/Ldap/Entries.php
new file mode 100644
index 00000000..0e779342
--- /dev/null
+++ b/app/Core/Ldap/Entries.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP Entries
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Entries
+{
+ /**
+ * LDAP entries
+ *
+ * @access protected
+ * @var array
+ */
+ protected $entries = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $entries
+ */
+ public function __construct(array $entries)
+ {
+ $this->entries = $entries;
+ }
+
+ /**
+ * Get all entries
+ *
+ * @access public
+ * @return Entry[]
+ */
+ public function getAll()
+ {
+ $entities = array();
+
+ if (! isset($this->entries['count'])) {
+ return $entities;
+ }
+
+ for ($i = 0; $i < $this->entries['count']; $i++) {
+ $entities[] = new Entry($this->entries[$i]);
+ }
+
+ return $entities;
+ }
+
+ /**
+ * Get first entry
+ *
+ * @access public
+ * @return Entry
+ */
+ public function getFirstEntry()
+ {
+ return new Entry(isset($this->entries[0]) ? $this->entries[0] : array());
+ }
+}
diff --git a/app/Core/Ldap/Entry.php b/app/Core/Ldap/Entry.php
new file mode 100644
index 00000000..0b99a58b
--- /dev/null
+++ b/app/Core/Ldap/Entry.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP Entry
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Entry
+{
+ /**
+ * LDAP entry
+ *
+ * @access protected
+ * @var array
+ */
+ protected $entry = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $entry
+ */
+ public function __construct(array $entry)
+ {
+ $this->entry = $entry;
+ }
+
+ /**
+ * Get all attribute values
+ *
+ * @access public
+ * @param string $attribute
+ * @return string[]
+ */
+ public function getAll($attribute)
+ {
+ $attributes = array();
+
+ if (! isset($this->entry[$attribute]['count'])) {
+ return $attributes;
+ }
+
+ for ($i = 0; $i < $this->entry[$attribute]['count']; $i++) {
+ $attributes[] = $this->entry[$attribute][$i];
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Get first attribute value
+ *
+ * @access public
+ * @param string $attribute
+ * @param string $default
+ * @return string
+ */
+ public function getFirstValue($attribute, $default = '')
+ {
+ return isset($this->entry[$attribute][0]) ? $this->entry[$attribute][0] : $default;
+ }
+
+ /**
+ * Get entry distinguished name
+ *
+ * @access public
+ * @return string
+ */
+ public function getDn()
+ {
+ return isset($this->entry['dn']) ? $this->entry['dn'] : '';
+ }
+
+ /**
+ * Return true if the given value exists in attribute list
+ *
+ * @access public
+ * @param string $attribute
+ * @param string $value
+ * @return boolean
+ */
+ public function hasValue($attribute, $value)
+ {
+ $attributes = $this->getAll($attribute);
+ return in_array($value, $attributes);
+ }
+}
diff --git a/app/Core/Ldap/Group.php b/app/Core/Ldap/Group.php
new file mode 100644
index 00000000..634d47ee
--- /dev/null
+++ b/app/Core/Ldap/Group.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+use LogicException;
+use Kanboard\Group\LdapGroupProvider;
+
+/**
+ * LDAP Group Finder
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Group
+{
+ /**
+ * Query
+ *
+ * @access protected
+ * @var Query
+ */
+ protected $query;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param Query $query
+ */
+ public function __construct(Query $query)
+ {
+ $this->query = $query;
+ }
+
+ /**
+ * Get groups
+ *
+ * @static
+ * @access public
+ * @param Client $client
+ * @param string $query
+ * @return array
+ */
+ public static function getGroups(Client $client, $query)
+ {
+ $className = get_called_class();
+ $self = new $className(new Query($client));
+ return $self->find($query);
+ }
+
+ /**
+ * Find groups
+ *
+ * @access public
+ * @param string $query
+ * @return array
+ */
+ public function find($query)
+ {
+ $this->query->execute($this->getBasDn(), $query, $this->getAttributes());
+ $groups = array();
+
+ if ($this->query->hasResult()) {
+ $groups = $this->build();
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Build groups list
+ *
+ * @access protected
+ * @return array
+ */
+ protected function build()
+ {
+ $groups = array();
+
+ foreach ($this->query->getEntries()->getAll() as $entry) {
+ $groups[] = new LdapGroupProvider($entry->getDn(), $entry->getFirstValue($this->getAttributeName()));
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Ge the list of attributes to fetch when reading the LDAP group entry
+ *
+ * Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
+ *
+ * @access public
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return array_values(array_filter(array(
+ $this->getAttributeName(),
+ )));
+ }
+
+ /**
+ * Get LDAP group name attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeName()
+ {
+ if (! LDAP_GROUP_ATTRIBUTE_NAME) {
+ throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME');
+ }
+
+ return LDAP_GROUP_ATTRIBUTE_NAME;
+ }
+
+ /**
+ * Get LDAP group base DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getBasDn()
+ {
+ if (! LDAP_GROUP_BASE_DN) {
+ throw new LogicException('LDAP group base DN empty, check the parameter LDAP_GROUP_BASE_DN');
+ }
+
+ return LDAP_GROUP_BASE_DN;
+ }
+}
diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php
new file mode 100644
index 00000000..e03495ec
--- /dev/null
+++ b/app/Core/Ldap/Query.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP Query
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Query
+{
+ /**
+ * LDAP client
+ *
+ * @access protected
+ * @var Client
+ */
+ protected $client = null;
+
+ /**
+ * Query result
+ *
+ * @access protected
+ * @var array
+ */
+ protected $entries = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param Client $client
+ */
+ public function __construct(Client $client)
+ {
+ $this->client = $client;
+ }
+
+ /**
+ * Execute query
+ *
+ * @access public
+ * @param string $baseDn
+ * @param string $filter
+ * @param array $attributes
+ * @return Query
+ */
+ public function execute($baseDn, $filter, array $attributes)
+ {
+ $sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes);
+ if ($sr === false) {
+ return $this;
+ }
+
+ $entries = ldap_get_entries($this->client->getConnection(), $sr);
+ if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
+ return $this;
+ }
+
+ $this->entries = $entries;
+
+ return $this;
+ }
+
+ /**
+ * Return true if the query returned a result
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasResult()
+ {
+ return ! empty($this->entries);
+ }
+
+ /**
+ * Get LDAP Entries
+ *
+ * @access public
+ * @return Entities
+ */
+ public function getEntries()
+ {
+ return new Entries($this->entries);
+ }
+}
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
new file mode 100644
index 00000000..d36d6f34
--- /dev/null
+++ b/app/Core/Ldap/User.php
@@ -0,0 +1,224 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+use LogicException;
+use Kanboard\Core\Security\Role;
+use Kanboard\User\LdapUserProvider;
+
+/**
+ * LDAP User Finder
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class User
+{
+ /**
+ * Query
+ *
+ * @access protected
+ * @var Query
+ */
+ protected $query;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param Query $query
+ */
+ public function __construct(Query $query)
+ {
+ $this->query = $query;
+ }
+
+ /**
+ * Get user profile
+ *
+ * @static
+ * @access public
+ * @param Client $client
+ * @param string $username
+ * @return LdapUserProvider
+ */
+ public static function getUser(Client $client, $username)
+ {
+ $className = get_called_class();
+ $self = new $className(new Query($client));
+ return $self->find($self->getLdapUserPattern($username));
+ }
+
+ /**
+ * Find user
+ *
+ * @access public
+ * @param string $query
+ * @return null|LdapUserProvider
+ */
+ public function find($query)
+ {
+ $this->query->execute($this->getBasDn(), $query, $this->getAttributes());
+ $user = null;
+
+ if ($this->query->hasResult()) {
+ $user = $this->build();
+ }
+
+ return $user;
+ }
+
+ /**
+ * Build user profile
+ *
+ * @access protected
+ * @return LdapUserProvider
+ */
+ protected function build()
+ {
+ $entry = $this->query->getEntries()->getFirstEntry();
+ $role = Role::APP_USER;
+
+ if ($entry->hasValue($this->getAttributeGroup(), $this->getGroupAdminDn())) {
+ $role = Role::APP_ADMIN;
+ } elseif ($entry->hasValue($this->getAttributeGroup(), $this->getGroupManagerDn())) {
+ $role = Role::APP_MANAGER;
+ }
+
+ return new LdapUserProvider(
+ $entry->getDn(),
+ $entry->getFirstValue($this->getAttributeUsername()),
+ $entry->getFirstValue($this->getAttributeName()),
+ $entry->getFirstValue($this->getAttributeEmail()),
+ $role,
+ $entry->getAll($this->getAttributeGroup())
+ );
+ }
+
+ /**
+ * Ge the list of attributes to fetch when reading the LDAP user entry
+ *
+ * Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
+ *
+ * @access public
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return array_values(array_filter(array(
+ $this->getAttributeUsername(),
+ $this->getAttributeName(),
+ $this->getAttributeEmail(),
+ $this->getAttributeGroup(),
+ )));
+ }
+
+ /**
+ * Get LDAP account id attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeUsername()
+ {
+ if (! LDAP_USER_ATTRIBUTE_USERNAME) {
+ throw new LogicException('LDAP username attribute empty, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
+ }
+
+ return LDAP_USER_ATTRIBUTE_USERNAME;
+ }
+
+ /**
+ * Get LDAP user name attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeName()
+ {
+ if (! LDAP_USER_ATTRIBUTE_FULLNAME) {
+ throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_USER_ATTRIBUTE_FULLNAME');
+ }
+
+ return LDAP_USER_ATTRIBUTE_FULLNAME;
+ }
+
+ /**
+ * Get LDAP account email attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeEmail()
+ {
+ if (! LDAP_USER_ATTRIBUTE_EMAIL) {
+ throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL');
+ }
+
+ return LDAP_USER_ATTRIBUTE_EMAIL;
+ }
+
+ /**
+ * Get LDAP account memberof attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeGroup()
+ {
+ return LDAP_USER_ATTRIBUTE_GROUPS;
+ }
+
+ /**
+ * Get LDAP admin group DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getGroupAdminDn()
+ {
+ return LDAP_GROUP_ADMIN_DN;
+ }
+
+ /**
+ * Get LDAP application manager group DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getGroupManagerDn()
+ {
+ return LDAP_GROUP_MANAGER_DN;
+ }
+
+ /**
+ * Get LDAP user base DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getBasDn()
+ {
+ if (! LDAP_USER_BASE_DN) {
+ throw new LogicException('LDAP user base DN empty, check the parameter LDAP_USER_BASE_DN');
+ }
+
+ return LDAP_USER_BASE_DN;
+ }
+
+ /**
+ * Get LDAP user pattern
+ *
+ * @access public
+ * @param string $username
+ * @return string
+ */
+ public function getLdapUserPattern($username)
+ {
+ if (! LDAP_USER_FILTER) {
+ throw new LogicException('LDAP user filter empty, check the parameter LDAP_USER_FILTER');
+ }
+
+ return sprintf(LDAP_USER_FILTER, $username);
+ }
+}
diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php
index ca2ef895..df2d90ae 100644
--- a/app/Core/Lexer.php
+++ b/app/Core/Lexer.php
@@ -39,6 +39,7 @@ class Lexer
"/^(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',
@@ -118,6 +119,7 @@ class Lexer
case 'T_COLUMN':
case 'T_PROJECT':
case 'T_SWIMLANE':
+ case 'T_LINK':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') {
diff --git a/app/Core/Mail/Client.php b/app/Core/Mail/Client.php
index 52caef73..e1f31696 100644
--- a/app/Core/Mail/Client.php
+++ b/app/Core/Mail/Client.php
@@ -45,19 +45,21 @@ class Client extends Base
*/
public function send($email, $name, $subject, $html)
{
- $this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
+ if (! empty($email)) {
+ $this->logger->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
- $start_time = microtime(true);
- $author = 'Kanboard';
+ $start_time = microtime(true);
+ $author = 'Kanboard';
- if ($this->userSession->isLogged()) {
- $author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
- }
+ if ($this->userSession->isLogged()) {
+ $author = e('%s via Kanboard', $this->helper->user->getFullname());
+ }
- $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author);
+ $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author);
- if (DEBUG) {
- $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
+ if (DEBUG) {
+ $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
+ }
}
return $this;
diff --git a/app/Core/Mail/Transport/Mail.php b/app/Core/Mail/Transport/Mail.php
index 4d833f8f..aff3ee20 100644
--- a/app/Core/Mail/Transport/Mail.php
+++ b/app/Core/Mail/Transport/Mail.php
@@ -46,7 +46,7 @@ class Mail extends Base implements ClientInterface
* Get SwiftMailer transport
*
* @access protected
- * @return \Swift_Transport
+ * @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport
*/
protected function getTransport()
{
diff --git a/app/Core/Mail/Transport/Sendmail.php b/app/Core/Mail/Transport/Sendmail.php
index 849e3385..039be705 100644
--- a/app/Core/Mail/Transport/Sendmail.php
+++ b/app/Core/Mail/Transport/Sendmail.php
@@ -16,7 +16,7 @@ class Sendmail extends Mail
* Get SwiftMailer transport
*
* @access protected
- * @return \Swift_Transport
+ * @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport
*/
protected function getTransport()
{
diff --git a/app/Core/Mail/Transport/Smtp.php b/app/Core/Mail/Transport/Smtp.php
index 757408ea..66f0a3aa 100644
--- a/app/Core/Mail/Transport/Smtp.php
+++ b/app/Core/Mail/Transport/Smtp.php
@@ -16,7 +16,7 @@ class Smtp extends Mail
* Get SwiftMailer transport
*
* @access protected
- * @return \Swift_Transport
+ * @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport
*/
protected function getTransport()
{
diff --git a/app/Core/Markdown.php b/app/Core/Markdown.php
index f08c486a..827fd0df 100644
--- a/app/Core/Markdown.php
+++ b/app/Core/Markdown.php
@@ -3,7 +3,7 @@
namespace Kanboard\Core;
use Parsedown;
-use Kanboard\Helper\Url;
+use Pimple\Container;
/**
* Specific Markdown rules for Kanboard
@@ -14,22 +14,51 @@ use Kanboard\Helper\Url;
*/
class Markdown extends Parsedown
{
- private $link;
- private $helper;
+ /**
+ * Link params for tasks
+ *
+ * @access private
+ * @var array
+ */
+ private $link = array();
- public function __construct($link, Url $helper)
+ /**
+ * Container
+ *
+ * @access private
+ * @var Container
+ */
+ private $container;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param Container $container
+ * @param array $link
+ */
+ public function __construct(Container $container, array $link)
{
$this->link = $link;
- $this->helper = $helper;
+ $this->container = $container;
$this->InlineTypes['#'][] = 'TaskLink';
- $this->inlineMarkerList .= '#';
+ $this->InlineTypes['@'][] = 'UserLink';
+ $this->inlineMarkerList .= '#@';
}
- protected function inlineTaskLink($Excerpt)
+ /**
+ * Handle Task Links
+ *
+ * Replace "#123" by a link to the task
+ *
+ * @access public
+ * @param array $Excerpt
+ * @return array
+ */
+ protected function inlineTaskLink(array $Excerpt)
{
- // Replace task #123 by a link to the task
if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) {
- $url = $this->helper->href(
+ $url = $this->container['helper']->url->href(
$this->link['controller'],
$this->link['action'],
$this->link['params'] + array('task_id' => $matches[1])
@@ -40,7 +69,38 @@ class Markdown extends Parsedown
'element' => array(
'name' => 'a',
'text' => $matches[0],
- 'attributes' => array('href' => $url)));
+ 'attributes' => array('href' => $url)
+ ),
+ );
+ }
+ }
+
+ /**
+ * Handle User Mentions
+ *
+ * Replace "@username" by a link to the user
+ *
+ * @access public
+ * @param array $Excerpt
+ * @return array
+ */
+ protected function inlineUserLink(array $Excerpt)
+ {
+ if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) {
+ $user_id = $this->container['user']->getIdByUsername($matches[1]);
+
+ if (! empty($user_id)) {
+ $url = $this->container['helper']->url->href('user', 'profile', array('user_id' => $user_id));
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[0],
+ 'attributes' => array('href' => $url, 'class' => 'user-mention-link'),
+ ),
+ );
+ }
}
}
}
diff --git a/app/Core/Plugin/Loader.php b/app/Core/Plugin/Loader.php
index 4769e5cb..530d9b40 100644
--- a/app/Core/Plugin/Loader.php
+++ b/app/Core/Plugin/Loader.php
@@ -4,6 +4,7 @@ namespace Kanboard\Core\Plugin;
use DirectoryIterator;
use PDOException;
+use LogicException;
use RuntimeException;
use Kanboard\Core\Tool;
@@ -59,6 +60,11 @@ class Loader extends \Kanboard\Core\Base
public function load($plugin)
{
$class = '\Kanboard\Plugin\\'.$plugin.'\\Plugin';
+
+ if (! class_exists($class)) {
+ throw new LogicException('Unable to load this plugin class '.$class);
+ }
+
$instance = new $class($this->container);
Tool::buildDic($this->container, $instance->getClasses());
diff --git a/app/Core/Request.php b/app/Core/Request.php
deleted file mode 100644
index 5eda2d02..00000000
--- a/app/Core/Request.php
+++ /dev/null
@@ -1,240 +0,0 @@
-<?php
-
-namespace Kanboard\Core;
-
-/**
- * Request class
- *
- * @package core
- * @author Frederic Guillot
- */
-class Request
-{
- /**
- * Get URL string parameter
- *
- * @access public
- * @param string $name Parameter name
- * @param string $default_value Default value
- * @return string
- */
- public function getStringParam($name, $default_value = '')
- {
- return isset($_GET[$name]) ? $_GET[$name] : $default_value;
- }
-
- /**
- * Get URL integer parameter
- *
- * @access public
- * @param string $name Parameter name
- * @param integer $default_value Default value
- * @return integer
- */
- public function getIntegerParam($name, $default_value = 0)
- {
- return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value;
- }
-
- /**
- * Get a form value
- *
- * @access public
- * @param string $name Form field name
- * @return string|null
- */
- public function getValue($name)
- {
- $values = $this->getValues();
- return isset($values[$name]) ? $values[$name] : null;
- }
-
- /**
- * Get form values and check for CSRF token
- *
- * @access public
- * @return array
- */
- public function getValues()
- {
- if (! empty($_POST) && Security::validateCSRFFormToken($_POST)) {
- return $_POST;
- }
-
- return array();
- }
-
- /**
- * Get the raw body of the HTTP request
- *
- * @access public
- * @return string
- */
- public function getBody()
- {
- return file_get_contents('php://input');
- }
-
- /**
- * Get the Json request body
- *
- * @access public
- * @return array
- */
- public function getJson()
- {
- return json_decode($this->getBody(), true) ?: array();
- }
-
- /**
- * Get the content of an uploaded file
- *
- * @access public
- * @param string $name Form file name
- * @return string
- */
- public function getFileContent($name)
- {
- if (isset($_FILES[$name])) {
- return file_get_contents($_FILES[$name]['tmp_name']);
- }
-
- return '';
- }
-
- /**
- * Get the path of an uploaded file
- *
- * @access public
- * @param string $name Form file name
- * @return string
- */
- public function getFilePath($name)
- {
- return isset($_FILES[$name]['tmp_name']) ? $_FILES[$name]['tmp_name'] : '';
- }
-
- /**
- * Return true if the HTTP request is sent with the POST method
- *
- * @access public
- * @return bool
- */
- public function isPost()
- {
- return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST';
- }
-
- /**
- * Return true if the HTTP request is an Ajax request
- *
- * @access public
- * @return bool
- */
- public function isAjax()
- {
- return $this->getHeader('X-Requested-With') === 'XMLHttpRequest';
- }
-
- /**
- * Check if the page is requested through HTTPS
- *
- * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS
- *
- * @static
- * @access public
- * @return boolean
- */
- public static function isHTTPS()
- {
- return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off';
- }
-
- /**
- * Return a HTTP header value
- *
- * @access public
- * @param string $name Header name
- * @return string
- */
- public function getHeader($name)
- {
- $name = 'HTTP_'.str_replace('-', '_', strtoupper($name));
- return isset($_SERVER[$name]) ? $_SERVER[$name] : '';
- }
-
- /**
- * Returns current request's query string, useful for redirecting
- *
- * @access public
- * @return string
- */
- public function getQueryString()
- {
- return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
- }
-
- /**
- * Returns uri
- *
- * @access public
- * @return string
- */
- public function getUri()
- {
- return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
- }
-
- /**
- * Get the user agent
- *
- * @static
- * @access public
- * @return string
- */
- public static function getUserAgent()
- {
- return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT'];
- }
-
- /**
- * Get the real IP address of the user
- *
- * @static
- * @access public
- * @param bool $only_public Return only public IP address
- * @return string
- */
- public static function getIpAddress($only_public = false)
- {
- $keys = array(
- 'HTTP_CLIENT_IP',
- 'HTTP_X_FORWARDED_FOR',
- 'HTTP_X_FORWARDED',
- 'HTTP_X_CLUSTER_CLIENT_IP',
- 'HTTP_FORWARDED_FOR',
- 'HTTP_FORWARDED',
- 'REMOTE_ADDR'
- );
-
- foreach ($keys as $key) {
- if (isset($_SERVER[$key])) {
- foreach (explode(',', $_SERVER[$key]) as $ip_address) {
- $ip_address = trim($ip_address);
-
- if ($only_public) {
-
- // Return only public IP address
- if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
- return $ip_address;
- }
- } else {
- return $ip_address;
- }
- }
- }
- }
-
- return t('Unknown');
- }
-}
diff --git a/app/Core/Router.php b/app/Core/Router.php
deleted file mode 100644
index 843f5139..00000000
--- a/app/Core/Router.php
+++ /dev/null
@@ -1,229 +0,0 @@
-<?php
-
-namespace Kanboard\Core;
-
-use RuntimeException;
-
-/**
- * Router class
- *
- * @package core
- * @author Frederic Guillot
- */
-class Router extends Base
-{
- /**
- * Controller
- *
- * @access private
- * @var string
- */
- private $controller = '';
-
- /**
- * Action
- *
- * @access private
- * @var string
- */
- private $action = '';
-
- /**
- * Store routes for path lookup
- *
- * @access private
- * @var array
- */
- private $paths = array();
-
- /**
- * Store routes for url lookup
- *
- * @access private
- * @var array
- */
- private $urls = array();
-
- /**
- * Get action
- *
- * @access public
- * @return string
- */
- public function getAction()
- {
- return $this->action;
- }
-
- /**
- * Get controller
- *
- * @access public
- * @return string
- */
- public function getController()
- {
- return $this->controller;
- }
-
- /**
- * Get the path to compare patterns
- *
- * @access public
- * @param string $uri
- * @param string $query_string
- * @return string
- */
- public function getPath($uri, $query_string = '')
- {
- $path = substr($uri, strlen($this->helper->url->dir()));
-
- if (! empty($query_string)) {
- $path = substr($path, 0, - strlen($query_string) - 1);
- }
-
- if (! empty($path) && $path{0} === '/') {
- $path = substr($path, 1);
- }
-
- return $path;
- }
-
- /**
- * Add route
- *
- * @access public
- * @param string $path
- * @param string $controller
- * @param string $action
- * @param array $params
- */
- public function addRoute($path, $controller, $action, array $params = array())
- {
- $pattern = explode('/', $path);
-
- $this->paths[] = array(
- 'pattern' => $pattern,
- 'count' => count($pattern),
- 'controller' => $controller,
- 'action' => $action,
- );
-
- $this->urls[$controller][$action][] = array(
- 'path' => $path,
- 'params' => array_flip($params),
- 'count' => count($params),
- );
- }
-
- /**
- * Find a route according to the given path
- *
- * @access public
- * @param string $path
- * @return array
- */
- public function findRoute($path)
- {
- $parts = explode('/', $path);
- $count = count($parts);
-
- foreach ($this->paths as $route) {
- if ($count === $route['count']) {
- $params = array();
-
- for ($i = 0; $i < $count; $i++) {
- if ($route['pattern'][$i]{0} === ':') {
- $params[substr($route['pattern'][$i], 1)] = $parts[$i];
- } elseif ($route['pattern'][$i] !== $parts[$i]) {
- break;
- }
- }
-
- if ($i === $count) {
- $_GET = array_merge($_GET, $params);
- return array($route['controller'], $route['action']);
- }
- }
- }
-
- return array('app', 'index');
- }
-
- /**
- * Find route url
- *
- * @access public
- * @param string $controller
- * @param string $action
- * @param array $params
- * @return string
- */
- public function findUrl($controller, $action, array $params = array())
- {
- if (! isset($this->urls[$controller][$action])) {
- return '';
- }
-
- foreach ($this->urls[$controller][$action] as $pattern) {
- if (array_diff_key($params, $pattern['params']) === array()) {
- $url = $pattern['path'];
- $i = 0;
-
- foreach ($params as $variable => $value) {
- $url = str_replace(':'.$variable, $value, $url);
- $i++;
- }
-
- if ($i === $pattern['count']) {
- return $url;
- }
- }
- }
-
- return '';
- }
-
- /**
- * Check controller and action parameter
- *
- * @access public
- * @param string $value Controller or action name
- * @param string $default_value Default value if validation fail
- * @return string
- */
- public function sanitize($value, $default_value)
- {
- return ! preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $default_value : $value;
- }
-
- /**
- * Find controller/action from the route table or from get arguments
- *
- * @access public
- * @param string $uri
- * @param string $query_string
- */
- public function dispatch($uri, $query_string = '')
- {
- if (! empty($_GET['controller']) && ! empty($_GET['action'])) {
- $this->controller = $this->sanitize($_GET['controller'], 'app');
- $this->action = $this->sanitize($_GET['action'], 'index');
- $plugin = ! empty($_GET['plugin']) ? $this->sanitize($_GET['plugin'], '') : '';
- } else {
- list($this->controller, $this->action) = $this->findRoute($this->getPath($uri, $query_string)); // TODO: add plugin for routes
- $plugin = '';
- }
-
- $class = '\Kanboard\\';
- $class .= empty($plugin) ? 'Controller\\'.ucfirst($this->controller) : 'Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($this->controller);
-
- if (! class_exists($class) || ! method_exists($class, $this->action)) {
- throw new RuntimeException('Controller or method not found for the given url!');
- }
-
- $instance = new $class($this->container);
- $instance->beforeAction($this->controller, $this->action);
- $instance->{$this->action}();
- }
-}
diff --git a/app/Core/Security.php b/app/Core/Security.php
deleted file mode 100644
index 54207ee1..00000000
--- a/app/Core/Security.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-namespace Kanboard\Core;
-
-/**
- * Security class
- *
- * @package core
- * @author Frederic Guillot
- */
-class Security
-{
- /**
- * Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid()
- *
- * @static
- * @access public
- * @return string Random token
- */
- public static function generateToken()
- {
- if (function_exists('openssl_random_pseudo_bytes')) {
- return bin2hex(\openssl_random_pseudo_bytes(30));
- } elseif (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
- return hash('sha256', file_get_contents('/dev/urandom', false, null, 0, 30));
- }
-
- return hash('sha256', uniqid(mt_rand(), true));
- }
-
- /**
- * Generate and store a CSRF token in the current session
- *
- * @static
- * @access public
- * @return string Random token
- */
- public static function getCSRFToken()
- {
- $nonce = self::generateToken();
-
- if (empty($_SESSION['csrf_tokens'])) {
- $_SESSION['csrf_tokens'] = array();
- }
-
- $_SESSION['csrf_tokens'][$nonce] = true;
-
- return $nonce;
- }
-
- /**
- * Check if the token exists for the current session (a token can be used only one time)
- *
- * @static
- * @access public
- * @param string $token CSRF token
- * @return bool
- */
- public static function validateCSRFToken($token)
- {
- if (isset($_SESSION['csrf_tokens'][$token])) {
- unset($_SESSION['csrf_tokens'][$token]);
- return true;
- }
-
- return false;
- }
-
- /**
- * Check if the token used in a form is correct and then remove the value
- *
- * @static
- * @access public
- * @param array $values Form values
- * @return bool
- */
- public static function validateCSRFFormToken(array &$values)
- {
- if (! empty($values['csrf_token']) && self::validateCSRFToken($values['csrf_token'])) {
- unset($values['csrf_token']);
- return true;
- }
-
- return false;
- }
-}
diff --git a/app/Core/Security/AccessMap.php b/app/Core/Security/AccessMap.php
new file mode 100644
index 00000000..f34c4b00
--- /dev/null
+++ b/app/Core/Security/AccessMap.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Access Map Definition
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+class AccessMap
+{
+ /**
+ * Default role
+ *
+ * @access private
+ * @var string
+ */
+ private $defaultRole = '';
+
+ /**
+ * Role hierarchy
+ *
+ * @access private
+ * @var array
+ */
+ private $hierarchy = array();
+
+ /**
+ * Access map
+ *
+ * @access private
+ * @var array
+ */
+ private $map = array();
+
+ /**
+ * Define the default role when nothing match
+ *
+ * @access public
+ * @param string $role
+ * @return Acl
+ */
+ public function setDefaultRole($role)
+ {
+ $this->defaultRole = $role;
+ return $this;
+ }
+
+ /**
+ * Define role hierarchy
+ *
+ * @access public
+ * @param string $role
+ * @param array $subroles
+ * @return Acl
+ */
+ public function setRoleHierarchy($role, array $subroles)
+ {
+ foreach ($subroles as $subrole) {
+ if (isset($this->hierarchy[$subrole])) {
+ $this->hierarchy[$subrole][] = $role;
+ } else {
+ $this->hierarchy[$subrole] = array($role);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get computed role hierarchy
+ *
+ * @access public
+ * @param string $role
+ * @return array
+ */
+ public function getRoleHierarchy($role)
+ {
+ $roles = array($role);
+
+ if (isset($this->hierarchy[$role])) {
+ $roles = array_merge($roles, $this->hierarchy[$role]);
+ }
+
+ return $roles;
+ }
+
+ /**
+ * Get the highest role from a list
+ *
+ * @access public
+ * @param array $roles
+ * @return string
+ */
+ public function getHighestRole(array $roles)
+ {
+ $rank = array();
+
+ foreach ($roles as $role) {
+ $rank[$role] = count($this->getRoleHierarchy($role));
+ }
+
+ asort($rank);
+
+ return key($rank);
+ }
+
+ /**
+ * Add new access rules
+ *
+ * @access public
+ * @param string $controller Controller class name
+ * @param mixed $methods List of method name or just one method
+ * @param string $role Lowest role required
+ * @return Acl
+ */
+ public function add($controller, $methods, $role)
+ {
+ if (is_array($methods)) {
+ foreach ($methods as $method) {
+ $this->addRule($controller, $method, $role);
+ }
+ } else {
+ $this->addRule($controller, $methods, $role);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add new access rule
+ *
+ * @access private
+ * @param string $controller
+ * @param string $method
+ * @param string $role
+ * @return Acl
+ */
+ private function addRule($controller, $method, $role)
+ {
+ $controller = strtolower($controller);
+ $method = strtolower($method);
+
+ if (! isset($this->map[$controller])) {
+ $this->map[$controller] = array();
+ }
+
+ $this->map[$controller][$method] = $role;
+
+ return $this;
+ }
+
+ /**
+ * Get roles that match the given controller/method
+ *
+ * @access public
+ * @param string $controller
+ * @param string $method
+ * @return boolean
+ */
+ public function getRoles($controller, $method)
+ {
+ $controller = strtolower($controller);
+ $method = strtolower($method);
+
+ foreach (array($method, '*') as $key) {
+ if (isset($this->map[$controller][$key])) {
+ return $this->getRoleHierarchy($this->map[$controller][$key]);
+ }
+ }
+
+ return $this->getRoleHierarchy($this->defaultRole);
+ }
+}
diff --git a/app/Core/Security/AuthenticationManager.php b/app/Core/Security/AuthenticationManager.php
new file mode 100644
index 00000000..b1ba76cf
--- /dev/null
+++ b/app/Core/Security/AuthenticationManager.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+use LogicException;
+use Kanboard\Core\Base;
+use Kanboard\Event\AuthFailureEvent;
+use Kanboard\Event\AuthSuccessEvent;
+
+/**
+ * Authentication Manager
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+class AuthenticationManager extends Base
+{
+ /**
+ * Event names
+ *
+ * @var string
+ */
+ const EVENT_SUCCESS = 'auth.success';
+ const EVENT_FAILURE = 'auth.failure';
+
+ /**
+ * List of authentication providers
+ *
+ * @access private
+ * @var array
+ */
+ private $providers = array();
+
+ /**
+ * Register a new authentication provider
+ *
+ * @access public
+ * @param AuthenticationProviderInterface $provider
+ * @return AuthenticationManager
+ */
+ public function register(AuthenticationProviderInterface $provider)
+ {
+ $this->providers[$provider->getName()] = $provider;
+ return $this;
+ }
+
+ /**
+ * Register a new authentication provider
+ *
+ * @access public
+ * @param string $name
+ * @return AuthenticationProviderInterface|OAuthAuthenticationProviderInterface|PasswordAuthenticationProviderInterface|PreAuthenticationProviderInterface|OAuthAuthenticationProviderInterface
+ */
+ public function getProvider($name)
+ {
+ if (! isset($this->providers[$name])) {
+ throw new LogicException('Authentication provider not found: '.$name);
+ }
+
+ return $this->providers[$name];
+ }
+
+ /**
+ * Execute providers that are able to validate the current session
+ *
+ * @access public
+ * @return boolean
+ */
+ public function checkCurrentSession()
+ {
+ if ($this->userSession->isLogged()) {
+ foreach ($this->filterProviders('SessionCheckProviderInterface') as $provider) {
+ if (! $provider->isValidSession()) {
+ $this->logger->debug('Invalidate session for '.$this->userSession->getUsername());
+ $this->sessionStorage->flush();
+ $this->preAuthentication();
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Execute pre-authentication providers
+ *
+ * @access public
+ * @return boolean
+ */
+ public function preAuthentication()
+ {
+ foreach ($this->filterProviders('PreAuthenticationProviderInterface') as $provider) {
+ if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
+ $this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Execute username/password authentication providers
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @param boolean $fireEvent
+ * @return boolean
+ */
+ public function passwordAuthentication($username, $password, $fireEvent = true)
+ {
+ foreach ($this->filterProviders('PasswordAuthenticationProviderInterface') as $provider) {
+ $provider->setUsername($username);
+ $provider->setPassword($password);
+
+ if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
+ if ($fireEvent) {
+ $this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
+ }
+
+ return true;
+ }
+ }
+
+ if ($fireEvent) {
+ $this->dispatcher->dispatch(self::EVENT_FAILURE, new AuthFailureEvent($username));
+ }
+
+ return false;
+ }
+
+ /**
+ * Perform OAuth2 authentication
+ *
+ * @access public
+ * @param string $name
+ * @return boolean
+ */
+ public function oauthAuthentication($name)
+ {
+ $provider = $this->getProvider($name);
+
+ if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
+ $this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
+ return true;
+ }
+
+ $this->dispatcher->dispatch(self::EVENT_FAILURE, new AuthFailureEvent);
+
+ return false;
+ }
+
+ /**
+ * Get the last Post-Authentication provider
+ *
+ * @access public
+ * @return PostAuthenticationProviderInterface
+ */
+ public function getPostAuthenticationProvider()
+ {
+ $providers = $this->filterProviders('PostAuthenticationProviderInterface');
+
+ if (empty($providers)) {
+ throw new LogicException('You must have at least one Post-Authentication Provider configured');
+ }
+
+ return array_pop($providers);
+ }
+
+ /**
+ * Filter registered providers by interface type
+ *
+ * @access private
+ * @param string $interface
+ * @return array
+ */
+ private function filterProviders($interface)
+ {
+ $interface = '\Kanboard\Core\Security\\'.$interface;
+
+ return array_filter($this->providers, function(AuthenticationProviderInterface $provider) use ($interface) {
+ return is_a($provider, $interface);
+ });
+ }
+}
diff --git a/app/Core/Security/AuthenticationProviderInterface.php b/app/Core/Security/AuthenticationProviderInterface.php
new file mode 100644
index 00000000..828e272c
--- /dev/null
+++ b/app/Core/Security/AuthenticationProviderInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface AuthenticationProviderInterface
+{
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate();
+}
diff --git a/app/Core/Security/Authorization.php b/app/Core/Security/Authorization.php
new file mode 100644
index 00000000..980db048
--- /dev/null
+++ b/app/Core/Security/Authorization.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Authorization Handler
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+class Authorization
+{
+ /**
+ * Access Map
+ *
+ * @access private
+ * @var AccessMap
+ */
+ private $accessMap;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param AccessMap $accessMap
+ */
+ public function __construct(AccessMap $accessMap)
+ {
+ $this->accessMap = $accessMap;
+ }
+
+ /**
+ * Check if the given role is allowed to access to the specified resource
+ *
+ * @access public
+ * @param string $controller
+ * @param string $method
+ * @param string $role
+ * @return boolean
+ */
+ public function isAllowed($controller, $method, $role)
+ {
+ $roles = $this->accessMap->getRoles($controller, $method);
+ return in_array($role, $roles);
+ }
+}
diff --git a/app/Core/Security/OAuthAuthenticationProviderInterface.php b/app/Core/Security/OAuthAuthenticationProviderInterface.php
new file mode 100644
index 00000000..c32339e0
--- /dev/null
+++ b/app/Core/Security/OAuthAuthenticationProviderInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * OAuth2 Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface OAuthAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return UserProviderInterface
+ */
+ public function getUser();
+
+ /**
+ * Unlink user
+ *
+ * @access public
+ * @param integer $userId
+ * @return bool
+ */
+ public function unlink($userId);
+
+ /**
+ * Get configured OAuth2 service
+ *
+ * @access public
+ * @return Kanboard\Core\Http\OAuth2
+ */
+ public function getService();
+
+ /**
+ * Set OAuth2 code
+ *
+ * @access public
+ * @param string $code
+ * @return OAuthAuthenticationProviderInterface
+ */
+ public function setCode($code);
+}
diff --git a/app/Core/Security/PasswordAuthenticationProviderInterface.php b/app/Core/Security/PasswordAuthenticationProviderInterface.php
new file mode 100644
index 00000000..918a4aec
--- /dev/null
+++ b/app/Core/Security/PasswordAuthenticationProviderInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Password Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface PasswordAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return UserProviderInterface
+ */
+ public function getUser();
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ */
+ public function setUsername($username);
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ */
+ public function setPassword($password);
+}
diff --git a/app/Core/Security/PostAuthenticationProviderInterface.php b/app/Core/Security/PostAuthenticationProviderInterface.php
new file mode 100644
index 00000000..3f628bb0
--- /dev/null
+++ b/app/Core/Security/PostAuthenticationProviderInterface.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Post Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Called only one time before to prompt the user for pin code
+ *
+ * @access public
+ */
+ public function beforeCode();
+
+ /**
+ * Set user pin-code
+ *
+ * @access public
+ * @param string $code
+ */
+ public function setCode($code);
+
+ /**
+ * Generate secret if necessary
+ *
+ * @access public
+ * @return string
+ */
+ public function generateSecret();
+
+ /**
+ * Set secret token (fetched from user profile)
+ *
+ * @access public
+ * @param string $secret
+ */
+ public function setSecret($secret);
+
+ /**
+ * Get secret token (will be saved in user profile)
+ *
+ * @access public
+ * @return string
+ */
+ public function getSecret();
+
+ /**
+ * Get QR code url (empty if no QR can be provided)
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getQrCodeUrl($label);
+
+ /**
+ * Get key url (empty if no url can be provided)
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getKeyUrl($label);
+}
diff --git a/app/Core/Security/PreAuthenticationProviderInterface.php b/app/Core/Security/PreAuthenticationProviderInterface.php
new file mode 100644
index 00000000..391e8d0f
--- /dev/null
+++ b/app/Core/Security/PreAuthenticationProviderInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Pre-Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface PreAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return UserProviderInterface
+ */
+ public function getUser();
+}
diff --git a/app/Core/Security/Role.php b/app/Core/Security/Role.php
new file mode 100644
index 00000000..cb45a8af
--- /dev/null
+++ b/app/Core/Security/Role.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Role Definitions
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+class Role
+{
+ const APP_ADMIN = 'app-admin';
+ const APP_MANAGER = 'app-manager';
+ const APP_USER = 'app-user';
+ const APP_PUBLIC = 'app-public';
+
+ const PROJECT_MANAGER = 'project-manager';
+ const PROJECT_MEMBER = 'project-member';
+ const PROJECT_VIEWER = 'project-viewer';
+
+ /**
+ * Get application roles
+ *
+ * @access public
+ * @return array
+ */
+ public function getApplicationRoles()
+ {
+ return array(
+ self::APP_ADMIN => t('Administrator'),
+ self::APP_MANAGER => t('Manager'),
+ self::APP_USER => t('User'),
+ );
+ }
+
+ /**
+ * Get project roles
+ *
+ * @access public
+ * @return array
+ */
+ public function getProjectRoles()
+ {
+ return array(
+ self::PROJECT_MANAGER => t('Project Manager'),
+ self::PROJECT_MEMBER => t('Project Member'),
+ self::PROJECT_VIEWER => t('Project Viewer'),
+ );
+ }
+
+ /**
+ * Get role name
+ *
+ * @access public
+ * @param string $role
+ * @return string
+ */
+ public function getRoleName($role)
+ {
+ $roles = $this->getApplicationRoles() + $this->getProjectRoles();
+ return isset($roles[$role]) ? $roles[$role] : t('Unknown');
+ }
+}
diff --git a/app/Core/Security/SessionCheckProviderInterface.php b/app/Core/Security/SessionCheckProviderInterface.php
new file mode 100644
index 00000000..232fe1db
--- /dev/null
+++ b/app/Core/Security/SessionCheckProviderInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Session Check Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface SessionCheckProviderInterface
+{
+ /**
+ * Check if the user session is valid
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isValidSession();
+}
diff --git a/app/Core/Security/Token.php b/app/Core/Security/Token.php
new file mode 100644
index 00000000..cbd784a8
--- /dev/null
+++ b/app/Core/Security/Token.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+use Kanboard\Core\Base;
+
+/**
+ * Token Handler
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+class Token extends Base
+{
+ /**
+ * Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid()
+ *
+ * @static
+ * @access public
+ * @return string Random token
+ */
+ public static function getToken()
+ {
+ return bin2hex(random_bytes(30));
+ }
+
+ /**
+ * Generate and store a CSRF token in the current session
+ *
+ * @access public
+ * @return string Random token
+ */
+ public function getCSRFToken()
+ {
+ if (! isset($this->sessionStorage->csrf)) {
+ $this->sessionStorage->csrf = array();
+ }
+
+ $nonce = self::getToken();
+ $this->sessionStorage->csrf[$nonce] = true;
+
+ return $nonce;
+ }
+
+ /**
+ * Check if the token exists for the current session (a token can be used only one time)
+ *
+ * @access public
+ * @param string $token CSRF token
+ * @return bool
+ */
+ public function validateCSRFToken($token)
+ {
+ if (isset($this->sessionStorage->csrf[$token])) {
+ unset($this->sessionStorage->csrf[$token]);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Core/Session.php b/app/Core/Session.php
deleted file mode 100644
index a93131c7..00000000
--- a/app/Core/Session.php
+++ /dev/null
@@ -1,143 +0,0 @@
-<?php
-
-namespace Kanboard\Core;
-
-use ArrayAccess;
-
-/**
- * Session class
- *
- * @package core
- * @author Frederic Guillot
- */
-class Session implements ArrayAccess
-{
- /**
- * Return true if the session is open
- *
- * @static
- * @access public
- * @return boolean
- */
- public static function isOpen()
- {
- return session_id() !== '';
- }
-
- /**
- * Open a session
- *
- * @access public
- * @param string $base_path Cookie path
- */
- public function open($base_path = '/')
- {
- // HttpOnly and secure flags for session cookie
- session_set_cookie_params(
- SESSION_DURATION,
- $base_path ?: '/',
- null,
- Request::isHTTPS(),
- true
- );
-
- // Avoid session id in the URL
- ini_set('session.use_only_cookies', '1');
-
- // Enable strict mode
- if (version_compare(PHP_VERSION, '7.0.0') < 0) {
- ini_set('session.use_strict_mode', '1');
- }
-
- // Ensure session ID integrity
- ini_set('session.entropy_file', '/dev/urandom');
- ini_set('session.entropy_length', '32');
- ini_set('session.hash_bits_per_character', 6);
-
- // If the session was autostarted with session.auto_start = 1 in php.ini destroy it
- if (isset($_SESSION)) {
- session_destroy();
- }
-
- // Custom session name
- session_name('__S');
-
- // Start the session
- session_start();
-
- // Regenerate the session id to avoid session fixation issue
- if (empty($_SESSION['__validated'])) {
- session_regenerate_id(true);
- $_SESSION['__validated'] = 1;
- }
- }
-
- /**
- * Destroy the session
- *
- * @access public
- */
- public function close()
- {
- // Flush all sessions variables
- $_SESSION = array();
-
- // Destroy the session cookie
- $params = session_get_cookie_params();
-
- setcookie(
- session_name(),
- '',
- time() - 42000,
- $params['path'],
- $params['domain'],
- $params['secure'],
- $params['httponly']
- );
-
- // Destroy session data
- session_destroy();
- }
-
- /**
- * Register a flash message (success notification)
- *
- * @access public
- * @param string $message Message
- */
- public function flash($message)
- {
- $_SESSION['flash_message'] = $message;
- }
-
- /**
- * Register a flash error message (error notification)
- *
- * @access public
- * @param string $message Message
- */
- public function flashError($message)
- {
- $_SESSION['flash_error_message'] = $message;
- }
-
- public function offsetSet($offset, $value)
- {
- $_SESSION[$offset] = $value;
- }
-
- public function offsetExists($offset)
- {
- return isset($_SESSION[$offset]);
- }
-
- public function offsetUnset($offset)
- {
- unset($_SESSION[$offset]);
- }
-
- public function offsetGet($offset)
- {
- return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null;
- }
-}
diff --git a/app/Core/Session/FlashMessage.php b/app/Core/Session/FlashMessage.php
new file mode 100644
index 00000000..e02d056d
--- /dev/null
+++ b/app/Core/Session/FlashMessage.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Core\Session;
+
+use Kanboard\Core\Base;
+
+/**
+ * Session Flash Message
+ *
+ * @package session
+ * @author Frederic Guillot
+ */
+class FlashMessage extends Base
+{
+ /**
+ * Add success message
+ *
+ * @access public
+ * @param string $message
+ */
+ public function success($message)
+ {
+ $this->setMessage('success', $message);
+ }
+
+ /**
+ * Add failure message
+ *
+ * @access public
+ * @param string $message
+ */
+ public function failure($message)
+ {
+ $this->setMessage('failure', $message);
+ }
+
+ /**
+ * Add new flash message
+ *
+ * @access public
+ * @param string $key
+ * @param string $message
+ */
+ public function setMessage($key, $message)
+ {
+ if (! isset($this->sessionStorage->flash)) {
+ $this->sessionStorage->flash = array();
+ }
+
+ $this->sessionStorage->flash[$key] = $message;
+ }
+
+ /**
+ * Get flash message
+ *
+ * @access public
+ * @param string $key
+ * @return string
+ */
+ public function getMessage($key)
+ {
+ $message = '';
+
+ if (isset($this->sessionStorage->flash[$key])) {
+ $message = $this->sessionStorage->flash[$key];
+ unset($this->sessionStorage->flash[$key]);
+ }
+
+ return $message;
+ }
+}
diff --git a/app/Core/Session/SessionManager.php b/app/Core/Session/SessionManager.php
new file mode 100644
index 00000000..4f9f2c0a
--- /dev/null
+++ b/app/Core/Session/SessionManager.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Kanboard\Core\Session;
+
+use Kanboard\Core\Base;
+
+/**
+ * Session Manager
+ *
+ * @package session
+ * @author Frederic Guillot
+ */
+class SessionManager extends Base
+{
+ /**
+ * Event names
+ *
+ * @var string
+ */
+ const EVENT_DESTROY = 'session.destroy';
+
+ /**
+ * Return true if the session is open
+ *
+ * @static
+ * @access public
+ * @return boolean
+ */
+ public static function isOpen()
+ {
+ return session_id() !== '';
+ }
+
+ /**
+ * Create a new session
+ *
+ * @access public
+ */
+ public function open()
+ {
+ $this->configure();
+
+ if (ini_get('session.auto_start') == 1) {
+ session_destroy();
+ }
+
+ session_name('KB_SID');
+ session_start();
+
+ $this->sessionStorage->setStorage($_SESSION);
+ }
+
+ /**
+ * Destroy the session
+ *
+ * @access public
+ */
+ public function close()
+ {
+ $this->dispatcher->dispatch(self::EVENT_DESTROY);
+
+ // Destroy the session cookie
+ $params = session_get_cookie_params();
+
+ setcookie(
+ session_name(),
+ '',
+ time() - 42000,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
+
+ session_unset();
+ session_destroy();
+ }
+
+ /**
+ * Define session settings
+ *
+ * @access private
+ */
+ private function configure()
+ {
+ // Session cookie: HttpOnly and secure flags
+ session_set_cookie_params(
+ SESSION_DURATION,
+ $this->helper->url->dir() ?: '/',
+ null,
+ $this->request->isHTTPS(),
+ true
+ );
+
+ // Avoid session id in the URL
+ ini_set('session.use_only_cookies', '1');
+ ini_set('session.use_trans_sid', '0');
+
+ // Enable strict mode
+ ini_set('session.use_strict_mode', '1');
+
+ // Better session hash
+ ini_set('session.hash_function', '1'); // 'sha512' is not compatible with FreeBSD, only MD5 '0' and SHA-1 '1' seems to work
+ ini_set('session.hash_bits_per_character', 6);
+
+ // Set an additional entropy
+ ini_set('session.entropy_file', '/dev/urandom');
+ ini_set('session.entropy_length', '256');
+ }
+}
diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php
new file mode 100644
index 00000000..667d9253
--- /dev/null
+++ b/app/Core/Session/SessionStorage.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Kanboard\Core\Session;
+
+/**
+ * Session Storage
+ *
+ * @package session
+ * @author Frederic Guillot
+ *
+ * @property array $user
+ * @property array $flash
+ * @property array $csrf
+ * @property array $postAuthenticationValidated
+ * @property array $filters
+ * @property string $redirectAfterLogin
+ * @property string $captcha
+ * @property string $commentSorting
+ * @property bool $hasSubtaskInProgress
+ * @property bool $hasRememberMe
+ * @property bool $boardCollapsed
+ * @property bool $twoFactorBeforeCodeCalled
+ * @property string $twoFactorSecret
+ */
+class SessionStorage
+{
+ /**
+ * Pointer to external storage
+ *
+ * @access private
+ * @var array
+ */
+ private $storage = array();
+
+ /**
+ * Set external storage
+ *
+ * @access public
+ * @param array $storage External session storage (example: $_SESSION)
+ */
+ public function setStorage(array &$storage)
+ {
+ $this->storage =& $storage;
+
+ // Load dynamically existing session variables into object properties
+ foreach ($storage as $key => $value) {
+ $this->$key = $value;
+ }
+ }
+
+ /**
+ * Get all session variables
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ $session = get_object_vars($this);
+ unset($session['storage']);
+
+ return $session;
+ }
+
+ /**
+ * Flush session data
+ *
+ * @access public
+ */
+ public function flush()
+ {
+ $session = get_object_vars($this);
+ unset($session['storage']);
+
+ foreach (array_keys($session) as $property) {
+ unset($this->$property);
+ }
+ }
+
+ /**
+ * Copy class properties to external storage
+ *
+ * @access public
+ */
+ public function __destruct()
+ {
+ $this->storage = $this->getAll();
+ }
+}
diff --git a/app/Core/Template.php b/app/Core/Template.php
index ce2884a7..8ded6f7c 100644
--- a/app/Core/Template.php
+++ b/app/Core/Template.php
@@ -19,6 +19,50 @@ class Template extends Helper
private $overrides = array();
/**
+ * Rendering start time
+ *
+ * @access private
+ * @var float
+ */
+ private $startTime = 0;
+
+ /**
+ * Total rendering time
+ *
+ * @access private
+ * @var float
+ */
+ private $renderingTime = 0;
+
+ /**
+ * Method executed before the rendering
+ *
+ * @access protected
+ * @param string $template
+ */
+ protected function beforeRender($template)
+ {
+ if (DEBUG) {
+ $this->startTime = microtime(true);
+ }
+ }
+
+ /**
+ * Method executed after the rendering
+ *
+ * @access protected
+ * @param string $template
+ */
+ protected function afterRender($template)
+ {
+ if (DEBUG) {
+ $duration = microtime(true) - $this->startTime;
+ $this->renderingTime += $duration;
+ $this->container['logger']->debug('Rendering '.$template.' in '.$duration.'s, total='.$this->renderingTime);
+ }
+ }
+
+ /**
* Render a template
*
* Example:
@@ -32,11 +76,16 @@ class Template extends Helper
*/
public function render($__template_name, array $__template_args = array())
{
- extract($__template_args);
+ $this->beforeRender($__template_name);
+ extract($__template_args);
ob_start();
include $this->getTemplateFile($__template_name);
- return ob_get_clean();
+ $html = ob_get_clean();
+
+ $this->afterRender($__template_name);
+
+ return $html;
}
/**
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index 247fda1a..edd2e609 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -39,6 +39,7 @@ class Tool
* @access public
* @param Container $container
* @param array $namespaces
+ * @return Container
*/
public static function buildDIC(Container $container, array $namespaces)
{
@@ -50,6 +51,8 @@ class Tool
};
}
}
+
+ return $container;
}
/**
diff --git a/app/Core/Translator.php b/app/Core/Translator.php
index 96a481f6..113c0dc6 100644
--- a/app/Core/Translator.php
+++ b/app/Core/Translator.php
@@ -147,32 +147,6 @@ class Translator
}
/**
- * Get a formatted datetime
- *
- * $translator->datetime('%Y-%m-%d', time());
- *
- * @access public
- * @param string $format Format defined by the strftime function
- * @param integer $timestamp Unix timestamp
- * @return string
- */
- public function datetime($format, $timestamp)
- {
- if (! $timestamp) {
- return '';
- }
-
- $format = $this->get($format, $format);
-
- if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
- $format = str_replace('%e', '%d', $format);
- $format = str_replace('%k', '%H', $format);
- }
-
- return strftime($format, (int) $timestamp);
- }
-
- /**
* Get an identifier from the translations or return the default
*
* @access public
@@ -199,8 +173,6 @@ class Translator
*/
public static function load($language, $path = self::PATH)
{
- setlocale(LC_TIME, $language.'.UTF-8', $language);
-
$filename = $path.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.'translations.php';
if (file_exists($filename)) {
diff --git a/app/Core/User/GroupSync.php b/app/Core/User/GroupSync.php
new file mode 100644
index 00000000..573acd47
--- /dev/null
+++ b/app/Core/User/GroupSync.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+
+/**
+ * Group Synchronization
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class GroupSync extends Base
+{
+ /**
+ * Synchronize group membership
+ *
+ * @access public
+ * @param integer $userId
+ * @param array $groupIds
+ */
+ public function synchronize($userId, array $groupIds)
+ {
+ foreach ($groupIds as $groupId) {
+ $group = $this->group->getByExternalId($groupId);
+
+ if (! empty($group) && ! $this->groupMember->isMember($group['id'], $userId)) {
+ $this->groupMember->addUser($group['id'], $userId);
+ }
+ }
+ }
+}
diff --git a/app/Core/User/UserProfile.php b/app/Core/User/UserProfile.php
new file mode 100644
index 00000000..ef325801
--- /dev/null
+++ b/app/Core/User/UserProfile.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+
+/**
+ * User Profile
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class UserProfile extends Base
+{
+ /**
+ * Assign provider data to the local user
+ *
+ * @access public
+ * @param integer $userId
+ * @param UserProviderInterface $user
+ * @return boolean
+ */
+ public function assign($userId, UserProviderInterface $user)
+ {
+ $profile = $this->user->getById($userId);
+
+ $values = UserProperty::filterProperties($profile, UserProperty::getProperties($user));
+ $values['id'] = $userId;
+
+ if ($this->user->update($values)) {
+ $profile = array_merge($profile, $values);
+ $this->userSession->initialize($profile);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Synchronize user properties with the local database and create the user session
+ *
+ * @access public
+ * @param UserProviderInterface $user
+ * @return boolean
+ */
+ public function initialize(UserProviderInterface $user)
+ {
+ if ($user->getInternalId()) {
+ $profile = $this->user->getById($user->getInternalId());
+ } elseif ($user->getExternalIdColumn() && $user->getExternalId()) {
+ $profile = $this->userSync->synchronize($user);
+ $this->groupSync->synchronize($profile['id'], $user->getExternalGroupIds());
+ }
+
+ if (! empty($profile) && $profile['is_active'] == 1) {
+ $this->userSession->initialize($profile);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Core/User/UserProperty.php b/app/Core/User/UserProperty.php
new file mode 100644
index 00000000..f8b08a3d
--- /dev/null
+++ b/app/Core/User/UserProperty.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+/**
+ * User Property
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class UserProperty
+{
+ /**
+ * Get filtered user properties from user provider
+ *
+ * @static
+ * @access public
+ * @param UserProviderInterface $user
+ * @return array
+ */
+ public static function getProperties(UserProviderInterface $user)
+ {
+ $properties = array(
+ 'username' => $user->getUsername(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail(),
+ 'role' => $user->getRole(),
+ $user->getExternalIdColumn() => $user->getExternalId(),
+ );
+
+ $properties = array_merge($properties, $user->getExtraAttributes());
+
+ return array_filter($properties, array(__NAMESPACE__.'\UserProperty', 'isNotEmptyValue'));
+ }
+
+ /**
+ * Filter user properties compared to existing user profile
+ *
+ * @static
+ * @access public
+ * @param array $profile
+ * @param array $properties
+ * @return array
+ */
+ public static function filterProperties(array $profile, array $properties)
+ {
+ $values = array();
+
+ foreach ($properties as $property => $value) {
+ if (array_key_exists($property, $profile) && ! self::isNotEmptyValue($profile[$property])) {
+ $values[$property] = $value;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Check if a value is not empty
+ *
+ * @static
+ * @access public
+ * @param string $value
+ * @return boolean
+ */
+ public static function isNotEmptyValue($value)
+ {
+ return $value !== null && $value !== '';
+ }
+}
diff --git a/app/Core/User/UserProviderInterface.php b/app/Core/User/UserProviderInterface.php
new file mode 100644
index 00000000..07e01f42
--- /dev/null
+++ b/app/Core/User/UserProviderInterface.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+/**
+ * User Provider Interface
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+interface UserProviderInterface
+{
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed();
+
+ /**
+ * Get external id column name
+ *
+ * Example: google_id, github_id, gitlab_id...
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn();
+
+ /**
+ * Get internal id
+ *
+ * If a value is returned the user properties won't be updated in the local database
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId();
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId();
+
+ /**
+ * Get user role
+ *
+ * Return an empty string to not override role stored in the database
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole();
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername();
+
+ /**
+ * Get user full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail();
+
+ /**
+ * Get external group ids
+ *
+ * A synchronization is done at login time,
+ * the user will be member of those groups if they exists in the database
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getExternalGroupIds();
+
+ /**
+ * Get extra user attributes
+ *
+ * Example: is_ldap_user, disable_login_form, notifications_enabled...
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes();
+}
diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php
new file mode 100644
index 00000000..534e5192
--- /dev/null
+++ b/app/Core/User/UserSession.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\Role;
+
+/**
+ * User Session
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class UserSession extends Base
+{
+ /**
+ * Update user session
+ *
+ * @access public
+ * @param array $user
+ */
+ public function initialize(array $user)
+ {
+ foreach (array('password', 'is_admin', 'is_project_admin', 'twofactor_secret') as $column) {
+ if (isset($user[$column])) {
+ unset($user[$column]);
+ }
+ }
+
+ $user['id'] = (int) $user['id'];
+ $user['is_ldap_user'] = isset($user['is_ldap_user']) ? (bool) $user['is_ldap_user'] : false;
+ $user['twofactor_activated'] = isset($user['twofactor_activated']) ? (bool) $user['twofactor_activated'] : false;
+
+ $this->sessionStorage->user = $user;
+ $this->sessionStorage->postAuthenticationValidated = false;
+ }
+
+ /**
+ * Get user application role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return $this->sessionStorage->user['role'];
+ }
+
+ /**
+ * Return true if the user has validated the 2FA key
+ *
+ * @access public
+ * @return bool
+ */
+ public function isPostAuthenticationValidated()
+ {
+ return isset($this->sessionStorage->postAuthenticationValidated) && $this->sessionStorage->postAuthenticationValidated === true;
+ }
+
+ /**
+ * Validate 2FA for the current session
+ *
+ * @access public
+ */
+ public function validatePostAuthentication()
+ {
+ $this->sessionStorage->postAuthenticationValidated = true;
+ }
+
+ /**
+ * Return true if the user has 2FA enabled
+ *
+ * @access public
+ * @return bool
+ */
+ public function hasPostAuthentication()
+ {
+ return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true;
+ }
+
+ /**
+ * Disable 2FA for the current session
+ *
+ * @access public
+ */
+ public function disablePostAuthentication()
+ {
+ $this->sessionStorage->user['twofactor_activated'] = false;
+ }
+
+ /**
+ * Return true if the logged user is admin
+ *
+ * @access public
+ * @return bool
+ */
+ public function isAdmin()
+ {
+ return isset($this->sessionStorage->user['role']) && $this->sessionStorage->user['role'] === Role::APP_ADMIN;
+ }
+
+ /**
+ * Get the connected user id
+ *
+ * @access public
+ * @return integer
+ */
+ public function getId()
+ {
+ return isset($this->sessionStorage->user['id']) ? (int) $this->sessionStorage->user['id'] : 0;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return isset($this->sessionStorage->user['username']) ? $this->sessionStorage->user['username'] : '';
+ }
+
+ /**
+ * Check is the user is connected
+ *
+ * @access public
+ * @return bool
+ */
+ public function isLogged()
+ {
+ return isset($this->sessionStorage->user) && ! empty($this->sessionStorage->user);
+ }
+
+ /**
+ * Get project filters from the session
+ *
+ * @access public
+ * @param integer $project_id
+ * @return string
+ */
+ public function getFilters($project_id)
+ {
+ return ! empty($this->sessionStorage->filters[$project_id]) ? $this->sessionStorage->filters[$project_id] : 'status:open';
+ }
+
+ /**
+ * Save project filters in the session
+ *
+ * @access public
+ * @param integer $project_id
+ * @param string $filters
+ */
+ public function setFilters($project_id, $filters)
+ {
+ $this->sessionStorage->filters[$project_id] = $filters;
+ }
+
+ /**
+ * Is board collapsed or expanded
+ *
+ * @access public
+ * @param integer $project_id
+ * @return boolean
+ */
+ public function isBoardCollapsed($project_id)
+ {
+ return ! empty($this->sessionStorage->boardCollapsed[$project_id]) ? $this->sessionStorage->boardCollapsed[$project_id] : false;
+ }
+
+ /**
+ * Set board display mode
+ *
+ * @access public
+ * @param integer $project_id
+ * @param boolean $is_collapsed
+ */
+ public function setBoardDisplayMode($project_id, $is_collapsed)
+ {
+ $this->sessionStorage->boardCollapsed[$project_id] = $is_collapsed;
+ }
+
+ /**
+ * Set comments sorting
+ *
+ * @access public
+ * @param string $order
+ */
+ public function setCommentSorting($order)
+ {
+ $this->sessionStorage->commentSorting = $order;
+ }
+
+ /**
+ * Get comments sorting direction
+ *
+ * @access public
+ * @return string
+ */
+ public function getCommentSorting()
+ {
+ return empty($this->sessionStorage->commentSorting) ? 'ASC' : $this->sessionStorage->commentSorting;
+ }
+}
diff --git a/app/Core/User/UserSync.php b/app/Core/User/UserSync.php
new file mode 100644
index 00000000..d450a0bd
--- /dev/null
+++ b/app/Core/User/UserSync.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+
+/**
+ * User Synchronization
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class UserSync extends Base
+{
+ /**
+ * Synchronize user profile
+ *
+ * @access public
+ * @param UserProviderInterface $user
+ * @return array
+ */
+ public function synchronize(UserProviderInterface $user)
+ {
+ $profile = $this->user->getByExternalId($user->getExternalIdColumn(), $user->getExternalId());
+ $properties = UserProperty::getProperties($user);
+
+ if (! empty($profile)) {
+ $profile = $this->updateUser($profile, $properties);
+ } elseif ($user->isUserCreationAllowed()) {
+ $profile = $this->createUser($user, $properties);
+ }
+
+ return $profile;
+ }
+
+ /**
+ * Update user profile
+ *
+ * @access public
+ * @param array $profile
+ * @param array $properties
+ * @return array
+ */
+ private function updateUser(array $profile, array $properties)
+ {
+ $values = UserProperty::filterProperties($profile, $properties);
+
+ if (! empty($values)) {
+ $values['id'] = $profile['id'];
+ $result = $this->user->update($values);
+ return $result ? array_merge($profile, $properties) : $profile;
+ }
+
+ return $profile;
+ }
+
+ /**
+ * Create user
+ *
+ * @access public
+ * @param UserProviderInterface $user
+ * @param array $properties
+ * @return array
+ */
+ private function createUser(UserProviderInterface $user, array $properties)
+ {
+ $id = $this->user->create($properties);
+
+ if ($id === false) {
+ $this->logger->error('Unable to create user profile: '.$user->getExternalId());
+ return array();
+ }
+
+ return $this->user->getById($id);
+ }
+}
diff --git a/app/Event/AuthEvent.php b/app/Event/AuthEvent.php
deleted file mode 100644
index 7cbced83..00000000
--- a/app/Event/AuthEvent.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-namespace Kanboard\Event;
-
-use Symfony\Component\EventDispatcher\Event as BaseEvent;
-
-class AuthEvent extends BaseEvent
-{
- private $auth_name;
- private $user_id;
-
- public function __construct($auth_name, $user_id)
- {
- $this->auth_name = $auth_name;
- $this->user_id = $user_id;
- }
-
- public function getUserId()
- {
- return $this->user_id;
- }
-
- public function getAuthType()
- {
- return $this->auth_name;
- }
-}
diff --git a/app/Event/AuthFailureEvent.php b/app/Event/AuthFailureEvent.php
new file mode 100644
index 00000000..225ac04a
--- /dev/null
+++ b/app/Event/AuthFailureEvent.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Event;
+
+use Symfony\Component\EventDispatcher\Event as BaseEvent;
+
+/**
+ * Authentication Failure Event
+ *
+ * @package event
+ * @author Frederic Guillot
+ */
+class AuthFailureEvent extends BaseEvent
+{
+ /**
+ * Username
+ *
+ * @access private
+ * @var string
+ */
+ private $username = '';
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $username
+ */
+ public function __construct($username = '')
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+}
diff --git a/app/Event/AuthSuccessEvent.php b/app/Event/AuthSuccessEvent.php
new file mode 100644
index 00000000..38323e82
--- /dev/null
+++ b/app/Event/AuthSuccessEvent.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Event;
+
+use Symfony\Component\EventDispatcher\Event as BaseEvent;
+
+/**
+ * Authentication Success Event
+ *
+ * @package event
+ * @author Frederic Guillot
+ */
+class AuthSuccessEvent extends BaseEvent
+{
+ /**
+ * Authentication provider name
+ *
+ * @access private
+ * @var string
+ */
+ private $authType;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $authType
+ */
+ public function __construct($authType)
+ {
+ $this->authType = $authType;
+ }
+
+ /**
+ * Get authentication type
+ *
+ * @return string
+ */
+ public function getAuthType()
+ {
+ return $this->authType;
+ }
+}
diff --git a/app/Event/GenericEvent.php b/app/Event/GenericEvent.php
index 1129fd16..94a51479 100644
--- a/app/Event/GenericEvent.php
+++ b/app/Event/GenericEvent.php
@@ -7,7 +7,7 @@ use Symfony\Component\EventDispatcher\Event as BaseEvent;
class GenericEvent extends BaseEvent implements ArrayAccess
{
- private $container = array();
+ protected $container = array();
public function __construct(array $values = array())
{
diff --git a/app/Event/TaskListEvent.php b/app/Event/TaskListEvent.php
new file mode 100644
index 00000000..9be1a7d9
--- /dev/null
+++ b/app/Event/TaskListEvent.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Kanboard\Event;
+
+class TaskListEvent extends GenericEvent
+{
+ public function setTasks(array &$tasks)
+ {
+ $this->container['tasks'] =& $tasks;
+ }
+}
diff --git a/app/ExternalLink/AttachmentLink.php b/app/ExternalLink/AttachmentLink.php
new file mode 100644
index 00000000..5a0d1344
--- /dev/null
+++ b/app/ExternalLink/AttachmentLink.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkInterface;
+
+/**
+ * Attachment Link
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class AttachmentLink extends BaseLink implements ExternalLinkInterface
+{
+ /**
+ * Get link title
+ *
+ * @access public
+ * @return string
+ */
+ public function getTitle()
+ {
+ $path = parse_url($this->url, PHP_URL_PATH);
+ return basename($path);
+ }
+}
diff --git a/app/ExternalLink/AttachmentLinkProvider.php b/app/ExternalLink/AttachmentLinkProvider.php
new file mode 100644
index 00000000..df27284f
--- /dev/null
+++ b/app/ExternalLink/AttachmentLinkProvider.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkProviderInterface;
+
+/**
+ * Attachment Link Provider
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class AttachmentLinkProvider extends BaseLinkProvider implements ExternalLinkProviderInterface
+{
+ /**
+ * File extensions that are not attachments
+ *
+ * @access protected
+ * @var array
+ */
+ protected $extensions = array(
+ 'html',
+ 'htm',
+ 'xhtml',
+ 'php',
+ 'jsp',
+ 'do',
+ 'action',
+ 'asp',
+ 'aspx',
+ 'cgi',
+ );
+
+ /**
+ * Get provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return t('Attachment');
+ }
+
+ /**
+ * Get link type
+ *
+ * @access public
+ * @return string
+ */
+ public function getType()
+ {
+ return 'attachment';
+ }
+
+ /**
+ * Get a dictionary of supported dependency types by the provider
+ *
+ * @access public
+ * @return array
+ */
+ public function getDependencies()
+ {
+ return array(
+ 'related' => t('Related'),
+ );
+ }
+
+ /**
+ * Return true if the provider can parse correctly the user input
+ *
+ * @access public
+ * @return boolean
+ */
+ public function match()
+ {
+ if (preg_match('/^https?:\/\/.*\.([^\/]+)$/', $this->userInput, $matches)) {
+ return $this->isValidExtension($matches[1]);
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the link found with the properties
+ *
+ * @access public
+ * @return ExternalLinkInterface
+ */
+ public function getLink()
+ {
+ $link = new AttachmentLink($this->container);
+ $link->setUrl($this->userInput);
+
+ return $link;
+ }
+
+ /**
+ * Check file extension
+ *
+ * @access protected
+ * @param string $extension
+ * @return boolean
+ */
+ protected function isValidExtension($extension)
+ {
+ $extension = strtolower($extension);
+
+ foreach ($this->extensions as $ext) {
+ if ($extension === $ext) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/ExternalLink/BaseLink.php b/app/ExternalLink/BaseLink.php
new file mode 100644
index 00000000..08693ae7
--- /dev/null
+++ b/app/ExternalLink/BaseLink.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\Base;
+
+/**
+ * Base Link
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+abstract class BaseLink extends Base
+{
+ /**
+ * URL
+ *
+ * @access protected
+ * @var string
+ */
+ protected $url = '';
+
+ /**
+ * Get link URL
+ *
+ * @access public
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set link URL
+ *
+ * @access public
+ * @param string $url
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ }
+}
diff --git a/app/ExternalLink/BaseLinkProvider.php b/app/ExternalLink/BaseLinkProvider.php
new file mode 100644
index 00000000..749cda94
--- /dev/null
+++ b/app/ExternalLink/BaseLinkProvider.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\Base;
+
+/**
+ * Base Link Provider
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+abstract class BaseLinkProvider extends Base
+{
+ /**
+ * User input
+ *
+ * @access protected
+ * @var string
+ */
+ protected $userInput = '';
+
+ /**
+ * Set text entered by the user
+ *
+ * @access public
+ * @param string $input
+ */
+ public function setUserTextInput($input)
+ {
+ $this->userInput = trim($input);
+ }
+}
diff --git a/app/ExternalLink/WebLink.php b/app/ExternalLink/WebLink.php
new file mode 100644
index 00000000..9338ca42
--- /dev/null
+++ b/app/ExternalLink/WebLink.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkInterface;
+
+/**
+ * Web Link
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class WebLink extends BaseLink implements ExternalLinkInterface
+{
+ /**
+ * Get link title
+ *
+ * @access public
+ * @return string
+ */
+ public function getTitle()
+ {
+ $html = $this->httpClient->get($this->url);
+
+ if (preg_match('/<title>(.*)<\/title>/siU', $html, $matches)) {
+ return trim($matches[1]);
+ }
+
+ $components = parse_url($this->url);
+
+ if (! empty($components['host']) && ! empty($components['path'])) {
+ return $components['host'].$components['path'];
+ }
+
+ return t('Title not found');
+ }
+}
diff --git a/app/ExternalLink/WebLinkProvider.php b/app/ExternalLink/WebLinkProvider.php
new file mode 100644
index 00000000..ea6dc132
--- /dev/null
+++ b/app/ExternalLink/WebLinkProvider.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkProviderInterface;
+
+/**
+ * Web Link Provider
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class WebLinkProvider extends BaseLinkProvider implements ExternalLinkProviderInterface
+{
+ /**
+ * Get provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return t('Web Link');
+ }
+
+ /**
+ * Get link type
+ *
+ * @access public
+ * @return string
+ */
+ public function getType()
+ {
+ return 'weblink';
+ }
+
+ /**
+ * Get a dictionary of supported dependency types by the provider
+ *
+ * @access public
+ * @return array
+ */
+ public function getDependencies()
+ {
+ return array(
+ 'related' => t('Related'),
+ );
+ }
+
+ /**
+ * Return true if the provider can parse correctly the user input
+ *
+ * @access public
+ * @return boolean
+ */
+ public function match()
+ {
+ $startWithHttp = strpos($this->userInput, 'http://') === 0 || strpos($this->userInput, 'https://') === 0;
+ $validUrl = filter_var($this->userInput, FILTER_VALIDATE_URL);
+
+ return $startWithHttp && $validUrl;
+ }
+
+ /**
+ * Get the link found with the properties
+ *
+ * @access public
+ * @return ExternalLinkInterface
+ */
+ public function getLink()
+ {
+ $link = new WebLink($this->container);
+ $link->setUrl($this->userInput);
+
+ return $link;
+ }
+}
diff --git a/app/Formatter/GroupAutoCompleteFormatter.php b/app/Formatter/GroupAutoCompleteFormatter.php
new file mode 100644
index 00000000..7023e367
--- /dev/null
+++ b/app/Formatter/GroupAutoCompleteFormatter.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+/**
+ * Autocomplete formatter for groups
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+class GroupAutoCompleteFormatter implements FormatterInterface
+{
+ /**
+ * Groups found
+ *
+ * @access private
+ * @var array
+ */
+ private $groups;
+
+ /**
+ * Format groups for the ajax autocompletion
+ *
+ * @access public
+ * @param array $groups
+ * @return GroupAutoCompleteFormatter
+ */
+ public function setGroups(array $groups)
+ {
+ $this->groups = $groups;
+ return $this;
+ }
+
+ /**
+ * Format groups for the ajax autocompletion
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $result = array();
+
+ foreach ($this->groups as $group) {
+ $result[] = array(
+ 'id' => $group->getInternalId(),
+ 'external_id' => $group->getExternalId(),
+ 'value' => $group->getName(),
+ 'label' => $group->getName(),
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Formatter/ProjectGanttFormatter.php b/app/Formatter/ProjectGanttFormatter.php
index 17496088..4f73e217 100644
--- a/app/Formatter/ProjectGanttFormatter.php
+++ b/app/Formatter/ProjectGanttFormatter.php
@@ -79,7 +79,7 @@ class ProjectGanttFormatter extends Project implements FormatterInterface
'gantt_link' => $this->helper->url->href('gantt', 'project', array('project_id' => $project['id'])),
'color' => $color,
'not_defined' => empty($project['start_date']) || empty($project['end_date']),
- 'users' => $this->projectPermission->getProjectUsers($project['id']),
+ 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']),
);
}
diff --git a/app/Formatter/TaskFilterGanttFormatter.php b/app/Formatter/TaskFilterGanttFormatter.php
index 08059d4c..a4eef1ee 100644
--- a/app/Formatter/TaskFilterGanttFormatter.php
+++ b/app/Formatter/TaskFilterGanttFormatter.php
@@ -47,7 +47,7 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface
private function formatTask(array $task)
{
if (! isset($this->columns[$task['project_id']])) {
- $this->columns[$task['project_id']] = $this->board->getColumnsList($task['project_id']);
+ $this->columns[$task['project_id']] = $this->column->getList($task['project_id']);
}
$start = $task['date_started'] ?: time();
diff --git a/app/Formatter/UserFilterAutoCompleteFormatter.php b/app/Formatter/UserFilterAutoCompleteFormatter.php
new file mode 100644
index 00000000..b98e0d69
--- /dev/null
+++ b/app/Formatter/UserFilterAutoCompleteFormatter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Model\User;
+use Kanboard\Model\UserFilter;
+
+/**
+ * Autocomplete formatter for user filter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface
+{
+ /**
+ * Format the tasks for the ajax autocompletion
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $users = $this->query->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')->findAll();
+
+ foreach ($users as &$user) {
+ $user['value'] = $user['username'].' (#'.$user['id'].')';
+
+ if (empty($user['name'])) {
+ $user['label'] = $user['username'];
+ } else {
+ $user['label'] = $user['name'].' ('.$user['username'].')';
+ }
+ }
+
+ return $users;
+ }
+}
diff --git a/app/Group/DatabaseBackendGroupProvider.php b/app/Group/DatabaseBackendGroupProvider.php
new file mode 100644
index 00000000..6dbaa43c
--- /dev/null
+++ b/app/Group/DatabaseBackendGroupProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Group;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Group\GroupBackendProviderInterface;
+
+/**
+ * Database Backend Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class DatabaseBackendGroupProvider extends Base implements GroupBackendProviderInterface
+{
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return DatabaseGroupProvider[]
+ */
+ public function find($input)
+ {
+ $result = array();
+ $groups = $this->group->search($input);
+
+ foreach ($groups as $group) {
+ $result[] = new DatabaseGroupProvider($group);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Group/DatabaseGroupProvider.php b/app/Group/DatabaseGroupProvider.php
new file mode 100644
index 00000000..430121a3
--- /dev/null
+++ b/app/Group/DatabaseGroupProvider.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Kanboard\Group;
+
+use Kanboard\Core\Group\GroupProviderInterface;
+
+/**
+ * Database Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class DatabaseGroupProvider implements GroupProviderInterface
+{
+ /**
+ * Group properties
+ *
+ * @access private
+ * @var array
+ */
+ private $group = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $group
+ */
+ public function __construct(array $group)
+ {
+ $this->group = $group;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId()
+ {
+ return (int) $this->group['id'];
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get group name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->group['name'];
+ }
+}
diff --git a/app/Group/LdapBackendGroupProvider.php b/app/Group/LdapBackendGroupProvider.php
new file mode 100644
index 00000000..cad732c4
--- /dev/null
+++ b/app/Group/LdapBackendGroupProvider.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Kanboard\Group;
+
+use LogicException;
+use Kanboard\Core\Base;
+use Kanboard\Core\Group\GroupBackendProviderInterface;
+use Kanboard\Core\Ldap\Client as LdapClient;
+use Kanboard\Core\Ldap\ClientException as LdapException;
+use Kanboard\Core\Ldap\Group as LdapGroup;
+
+/**
+ * LDAP Backend Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class LdapBackendGroupProvider extends Base implements GroupBackendProviderInterface
+{
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return LdapGroupProvider[]
+ */
+ public function find($input)
+ {
+ try {
+ $ldap = LdapClient::connect();
+ return LdapGroup::getGroups($ldap, $this->getLdapGroupPattern($input));
+
+ } catch (LdapException $e) {
+ $this->logger->error($e->getMessage());
+ return array();
+ }
+ }
+
+ /**
+ * Get LDAP group pattern
+ *
+ * @access public
+ * @param string $input
+ * @return string
+ */
+ public function getLdapGroupPattern($input)
+ {
+ if (LDAP_GROUP_FILTER === '') {
+ throw new LogicException('LDAP group filter empty, check the parameter LDAP_GROUP_FILTER');
+ }
+
+ return sprintf(LDAP_GROUP_FILTER, $input);
+ }
+}
diff --git a/app/Group/LdapGroupProvider.php b/app/Group/LdapGroupProvider.php
new file mode 100644
index 00000000..b497d485
--- /dev/null
+++ b/app/Group/LdapGroupProvider.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Group;
+
+use Kanboard\Core\Group\GroupProviderInterface;
+
+/**
+ * LDAP Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class LdapGroupProvider implements GroupProviderInterface
+{
+ /**
+ * Group DN
+ *
+ * @access private
+ * @var string
+ */
+ private $dn = '';
+
+ /**
+ * Group Name
+ *
+ * @access private
+ * @var string
+ */
+ private $name = '';
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $dn
+ * @param string $name
+ */
+ public function __construct($dn, $name)
+ {
+ $this->dn = $dn;
+ $this->name = $name;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->dn;
+ }
+
+ /**
+ * Get group name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+}
diff --git a/app/Helper/App.php b/app/Helper/App.php
index 19801fa8..79afa5b9 100644
--- a/app/Helper/App.php
+++ b/app/Helper/App.php
@@ -2,15 +2,65 @@
namespace Kanboard\Helper;
+use Kanboard\Core\Base;
+
/**
* Application helpers
*
* @package helper
* @author Frederic Guillot
*/
-class App extends \Kanboard\Core\Base
+class App extends Base
{
/**
+ * Get config variable
+ *
+ * @access public
+ * @param string $param
+ * @param mixed $default_value
+ * @return mixed
+ */
+ public function config($param, $default_value = '')
+ {
+ return $this->config->get($param, $default_value);
+ }
+
+ /**
+ * Make sidebar menu active
+ *
+ * @access public
+ * @param string $controller
+ * @param string $action
+ * @param string $plugin
+ * @return string
+ */
+ public function checkMenuSelection($controller, $action = '', $plugin = '')
+ {
+ $result = strtolower($this->getRouterController()) === strtolower($controller);
+
+ if ($result && $action !== '') {
+ $result = strtolower($this->getRouterAction()) === strtolower($action);
+ }
+
+ if ($result && $plugin !== '') {
+ $result = strtolower($this->getPluginName()) === strtolower($plugin);
+ }
+
+ return $result ? 'class="active"' : '';
+ }
+
+ /**
+ * Get plugin name from route
+ *
+ * @access public
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return $this->router->getPlugin();
+ }
+
+ /**
* Get router controller
*
* @access public
@@ -62,18 +112,17 @@ class App extends \Kanboard\Core\Base
*/
public function flashMessage()
{
- $html = '';
-
- if (isset($this->session['flash_message'])) {
- $html = '<div class="alert alert-success alert-fade-out">'.$this->helper->e($this->session['flash_message']).'</div>';
- unset($this->session['flash_message']);
- unset($this->session['flash_error_message']);
- } elseif (isset($this->session['flash_error_message'])) {
- $html = '<div class="alert alert-error">'.$this->helper->e($this->session['flash_error_message']).'</div>';
- unset($this->session['flash_message']);
- unset($this->session['flash_error_message']);
+ $success_message = $this->flash->getMessage('success');
+ $failure_message = $this->flash->getMessage('failure');
+
+ if (! empty($success_message)) {
+ return '<div class="alert alert-success alert-fade-out">'.$this->helper->e($success_message).'</div>';
+ }
+
+ if (! empty($failure_message)) {
+ return '<div class="alert alert-error">'.$this->helper->e($failure_message).'</div>';
}
- return $html;
+ return '';
}
}
diff --git a/app/Helper/Dt.php b/app/Helper/Dt.php
index 78002b1b..eb3f93b3 100644
--- a/app/Helper/Dt.php
+++ b/app/Helper/Dt.php
@@ -13,6 +13,50 @@ use DateTime;
class Dt extends \Kanboard\Core\Base
{
/**
+ * Get formatted time
+ *
+ * @access public
+ * @param integer $value
+ * @return string
+ */
+ public function time($value)
+ {
+ return date($this->config->get('application_time_format', 'H:i'), $value);
+ }
+
+ /**
+ * Get formatted date
+ *
+ * @access public
+ * @param integer $value
+ * @return string
+ */
+ public function date($value)
+ {
+ if (empty($value)) {
+ return '';
+ }
+
+ if (! ctype_digit($value)) {
+ $value = strtotime($value);
+ }
+
+ return date($this->config->get('application_date_format', 'm/d/Y'), $value);
+ }
+
+ /**
+ * Get formatted datetime
+ *
+ * @access public
+ * @param integer $value
+ * @return string
+ */
+ public function datetime($value)
+ {
+ return date($this->config->get('application_datetime_format', 'm/d/Y H:i'), $value);
+ }
+
+ /**
* Get duration in seconds into human format
*
* @access public
@@ -107,6 +151,6 @@ class Dt extends \Kanboard\Core\Base
*/
public function getWeekDay($day)
{
- return dt('%A', strtotime('next Monday +'.($day - 1).' days'));
+ return date('l', strtotime('next Monday +'.($day - 1).' days'));
}
}
diff --git a/app/Helper/File.php b/app/Helper/File.php
index d2cdfc6a..b493e64f 100644
--- a/app/Helper/File.php
+++ b/app/Helper/File.php
@@ -38,19 +38,70 @@ class File extends \Kanboard\Core\Base
return 'fa-file-powerpoint-o';
case 'zip':
case 'rar':
+ case 'tar':
+ case 'bz2':
+ case 'xz':
+ case 'gz':
return 'fa-file-archive-o';
case 'mp3':
- return 'fa-audio-o';
+ return 'fa-file-audio-o';
case 'avi':
- return 'fa-video-o';
+ case 'mov':
+ return 'fa-file-video-o';
case 'php':
case 'html':
case 'css':
- return 'fa-code-o';
+ return 'fa-file-code-o';
case 'pdf':
return 'fa-file-pdf-o';
}
return 'fa-file-o';
}
+
+ /**
+ * Return the image mimetype based on the file extension
+ *
+ * @access public
+ * @param $filename
+ * @return string
+ */
+ public function getImageMimeType($filename)
+ {
+ $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+
+ switch ($extension) {
+ case 'jpeg':
+ case 'jpg':
+ return 'image/jpeg';
+ case 'png':
+ return 'image/png';
+ case 'gif':
+ return 'image/gif';
+ default:
+ return 'image/jpeg';
+ }
+ }
+
+ /**
+ * Get the preview type
+ *
+ * @access public
+ * @param string $filename
+ * @return string
+ */
+ public function getPreviewType($filename)
+ {
+ $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+
+ switch ($extension) {
+ case 'md':
+ case 'markdown':
+ return 'markdown';
+ case 'txt':
+ return 'text';
+ }
+
+ return null;
+ }
}
diff --git a/app/Helper/Form.php b/app/Helper/Form.php
index 5f19f2a8..bfd75ee3 100644
--- a/app/Helper/Form.php
+++ b/app/Helper/Form.php
@@ -2,7 +2,7 @@
namespace Kanboard\Helper;
-use Kanboard\Core\Security;
+use Kanboard\Core\Base;
/**
* Form helpers
@@ -10,7 +10,7 @@ use Kanboard\Core\Security;
* @package helper
* @author Frederic Guillot
*/
-class Form extends \Kanboard\Core\Base
+class Form extends Base
{
/**
* Hidden CSRF token field
@@ -20,7 +20,7 @@ class Form extends \Kanboard\Core\Base
*/
public function csrf()
{
- return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>';
+ return '<input type="hidden" name="csrf_token" value="'.$this->token->getCSRFToken().'"/>';
}
/**
diff --git a/app/Helper/Layout.php b/app/Helper/Layout.php
new file mode 100644
index 00000000..3db23920
--- /dev/null
+++ b/app/Helper/Layout.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+
+/**
+ * Layout helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Layout extends Base
+{
+ /**
+ * Render a template without the layout if Ajax request
+ *
+ * @access public
+ * @param string $template Template name
+ * @param array $params Template parameters
+ * @return string
+ */
+ public function app($template, array $params = array())
+ {
+ if ($this->request->isAjax()) {
+ return $this->template->render($template, $params);
+ }
+
+ if (! isset($params['no_layout']) && ! isset($params['board_selector'])) {
+ $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
+ }
+
+ return $this->template->layout($template, $params);
+ }
+
+ /**
+ * Common layout for user views
+ *
+ * @access public
+ * @param string $template Template name
+ * @param array $params Template parameters
+ * @return string
+ */
+ public function user($template, array $params)
+ {
+ if (isset($params['user'])) {
+ $params['title'] = '#'.$params['user']['id'].' '.($params['user']['name'] ?: $params['user']['username']);
+ }
+
+ return $this->subLayout('user/layout', 'user/sidebar', $template, $params);
+ }
+
+ /**
+ * Common layout for task views
+ *
+ * @access public
+ * @param string $template Template name
+ * @param array $params Template parameters
+ * @return string
+ */
+ public function task($template, array $params)
+ {
+ $params['title'] = $params['task']['title'];
+ return $this->subLayout('task/layout', 'task/sidebar', $template, $params);
+ }
+
+ /**
+ * Common layout for project views
+ *
+ * @access public
+ * @param string $template
+ * @param array $params
+ * @param string $sidebar
+ * @return string
+ */
+ public function project($template, array $params, $sidebar = 'project/sidebar')
+ {
+ if (empty($params['title'])) {
+ $params['title'] = $params['project']['name'];
+ } elseif ($params['project']['name'] !== $params['title']) {
+ $params['title'] = $params['project']['name'].' &gt; '.$params['title'];
+ }
+
+ return $this->subLayout('project/layout', $sidebar, $template, $params);
+ }
+
+ /**
+ * Common layout for project user views
+ *
+ * @access public
+ * @param string $template
+ * @param array $params
+ * @return string
+ */
+ public function projectUser($template, array $params)
+ {
+ $params['filter'] = array('user_id' => $params['user_id']);
+ return $this->subLayout('project_user/layout', 'project_user/sidebar', $template, $params);
+ }
+
+ /**
+ * Common layout for config views
+ *
+ * @access public
+ * @param string $template
+ * @param array $params
+ * @return string
+ */
+ public function config($template, array $params)
+ {
+ if (! isset($params['values'])) {
+ $params['values'] = $this->config->getAll();
+ }
+
+ if (! isset($params['errors'])) {
+ $params['errors'] = array();
+ }
+
+ return $this->subLayout('config/layout', 'config/sidebar', $template, $params);
+ }
+
+ /**
+ * Common layout for dashboard views
+ *
+ * @access public
+ * @param string $template
+ * @param array $params
+ * @return string
+ */
+ public function dashboard($template, array $params)
+ {
+ return $this->subLayout('app/layout', 'app/sidebar', $template, $params);
+ }
+
+ /**
+ * Common layout for analytic views
+ *
+ * @access public
+ * @param string $template
+ * @param array $params
+ * @return string
+ */
+ public function analytic($template, array $params)
+ {
+ return $this->subLayout('analytic/layout', 'analytic/sidebar', $template, $params);
+ }
+
+ /**
+ * Common method to generate a sublayout
+ *
+ * @access public
+ * @param string $sublayout
+ * @param string $sidebar
+ * @param string $template
+ * @param array $params
+ * @return string
+ */
+ public function subLayout($sublayout, $sidebar, $template, array $params = array())
+ {
+ $content = $this->template->render($template, $params);
+
+ if ($this->request->isAjax()) {
+ return $content;
+ }
+
+ $params['content_for_sublayout'] = $content;
+ $params['sidebar_template'] = $sidebar;
+
+ return $this->app($sublayout, $params);
+ }
+}
diff --git a/app/Helper/Subtask.php b/app/Helper/Subtask.php
index 1f367b27..1784a2bf 100644
--- a/app/Helper/Subtask.php
+++ b/app/Helper/Subtask.php
@@ -10,32 +10,84 @@ namespace Kanboard\Helper;
*/
class Subtask extends \Kanboard\Core\Base
{
+ public function getTitle(array $subtask)
+ {
+ if ($subtask['status'] == 0) {
+ $html = '<i class="fa fa-square-o fa-fw"></i>';
+ } elseif ($subtask['status'] == 1) {
+ $html = '<i class="fa fa-gears fa-fw"></i>';
+ } else {
+ $html = '<i class="fa fa-check-square-o fa-fw"></i>';
+ }
+
+ return $html.$this->helper->e($subtask['title']);
+ }
+
/**
* Get the link to toggle subtask status
*
* @access public
- * @param array $subtask
- * @param string $redirect
+ * @param array $subtask
+ * @param integer $project_id
+ * @param boolean $refresh_table
* @return string
*/
- public function toggleStatus(array $subtask, $redirect)
+ public function toggleStatus(array $subtask, $project_id, $refresh_table = false)
{
- if ($subtask['status'] == 0 && isset($this->session['has_subtask_inprogress']) && $this->session['has_subtask_inprogress'] === true) {
- return $this->helper->url->link(
- trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
- 'subtask',
- 'subtaskRestriction',
- array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect),
- false,
- 'popover task-board-popover'
- );
+ if (! $this->helper->user->hasProjectAccess('subtask', 'edit', $project_id)) {
+ return $this->getTitle($subtask);
+ }
+
+ $params = array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'refresh-table' => (int) $refresh_table);
+
+ if ($subtask['status'] == 0 && isset($this->sessionStorage->hasSubtaskInProgress) && $this->sessionStorage->hasSubtaskInProgress) {
+ return $this->helper->url->link($this->getTitle($subtask), 'SubtaskRestriction', 'popover', $params, false, 'popover');
}
- return $this->helper->url->link(
- trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
- 'subtask',
- 'toggleStatus',
- array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect)
- );
+ $class = 'subtask-toggle-status '.($refresh_table ? 'subtask-refresh-table' : '');
+ return $this->helper->url->link($this->getTitle($subtask), 'SubtaskStatus', 'change', $params, false, $class);
+ }
+
+ public function selectTitle(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="1"', 'required', 'maxlength="255"'), $attributes);
+
+ $html = $this->helper->form->label(t('Title'), 'title');
+ $html .= $this->helper->form->text('title', $values, $errors, $attributes);
+
+ return $html;
+ }
+
+ public function selectAssignee(array $users, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="2"'), $attributes);
+
+ $html = $this->helper->form->label(t('Assignee'), 'user_id');
+ $html .= $this->helper->form->select('user_id', $users, $values, $errors, $attributes);
+ $html .= '&nbsp;<a href="#" class="assign-me" data-target-id="form-user_id" data-current-id="'.$this->userSession->getId().'" title="'.t('Assign to me').'">'.t('Me').'</a>';
+
+ return $html;
+ }
+
+ public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="3"'), $attributes);
+
+ $html = $this->helper->form->label(t('Original estimate'), 'time_estimated');
+ $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
+ }
+
+ public function selectTimeSpent(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="4"'), $attributes);
+
+ $html = $this->helper->form->label(t('Time spent'), 'time_spent');
+ $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
}
}
diff --git a/app/Helper/Task.php b/app/Helper/Task.php
index 1405a167..6058c099 100644
--- a/app/Helper/Task.php
+++ b/app/Helper/Task.php
@@ -2,14 +2,24 @@
namespace Kanboard\Helper;
+use Kanboard\Core\Base;
+
/**
* Task helpers
*
* @package helper
* @author Frederic Guillot
*/
-class Task extends \Kanboard\Core\Base
+class Task extends Base
{
+ /**
+ * Local cache for project columns
+ *
+ * @access private
+ * @var array
+ */
+ private $columns = array();
+
public function getColors()
{
return $this->color->getList();
@@ -34,4 +44,143 @@ class Task extends \Kanboard\Core\Base
{
return $this->taskPermission->canRemoveTask($task);
}
+
+ public function selectAssignee(array $users, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="3"'), $attributes);
+
+ $html = $this->helper->form->label(t('Assignee'), 'owner_id');
+ $html .= $this->helper->form->select('owner_id', $users, $values, $errors, $attributes);
+ $html .= '&nbsp;<a href="#" class="assign-me" data-target-id="form-owner_id" data-current-id="'.$this->userSession->getId().'" title="'.t('Assign to me').'">'.t('Me').'</a>';
+
+ return $html;
+ }
+
+ public function selectCategory(array $categories, array $values, array $errors = array(), array $attributes = array(), $allow_one_item = false)
+ {
+ $attributes = array_merge(array('tabindex="4"'), $attributes);
+ $html = '';
+
+ if (! (! $allow_one_item && count($categories) === 1 && key($categories) == 0)) {
+ $html .= $this->helper->form->label(t('Category'), 'category_id');
+ $html .= $this->helper->form->select('category_id', $categories, $values, $errors, $attributes);
+ }
+
+ return $html;
+ }
+
+ public function selectSwimlane(array $swimlanes, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="5"'), $attributes);
+ $html = '';
+
+ if (! (count($swimlanes) === 1 && key($swimlanes) == 0)) {
+ $html .= $this->helper->form->label(t('Swimlane'), 'swimlane_id');
+ $html .= $this->helper->form->select('swimlane_id', $swimlanes, $values, $errors, $attributes);
+ }
+
+ return $html;
+ }
+
+ public function selectColumn(array $columns, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="6"'), $attributes);
+
+ $html = $this->helper->form->label(t('Column'), 'column_id');
+ $html .= $this->helper->form->select('column_id', $columns, $values, $errors, $attributes);
+
+ return $html;
+ }
+
+ public function selectPriority(array $project, array $values)
+ {
+ $html = '';
+
+ if ($project['priority_end'] > $project['priority_start']) {
+ $range = range($project['priority_start'], $project['priority_end']);
+ $options = array_combine($range, $range);
+ $values += array('priority' => $project['priority_default']);
+
+ $html .= $this->helper->form->label(t('Priority'), 'priority');
+ $html .= $this->helper->form->select('priority', $options, $values, array(), array('tabindex="7"'));
+ }
+
+ return $html;
+ }
+
+ public function selectScore(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="8"'), $attributes);
+
+ $html = $this->helper->form->label(t('Complexity'), 'score');
+ $html .= $this->helper->form->number('score', $values, $errors, $attributes);
+
+ return $html;
+ }
+
+ public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="9"'), $attributes);
+
+ $html = $this->helper->form->label(t('Original estimate'), 'time_estimated');
+ $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
+ }
+
+ public function selectTimeSpent(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="10"'), $attributes);
+
+ $html = $this->helper->form->label(t('Time spent'), 'time_spent');
+ $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
+ }
+
+ public function selectStartDate(array $values, array $errors = array(), array $attributes = array())
+ {
+ $placeholder = date($this->config->get('application_date_format', 'm/d/Y H:i'));
+ $attributes = array_merge(array('tabindex="11"', 'placeholder="'.$placeholder.'"'), $attributes);
+
+ $html = $this->helper->form->label(t('Start Date'), 'date_started');
+ $html .= $this->helper->form->text('date_started', $values, $errors, $attributes, 'form-datetime');
+
+ return $html;
+ }
+
+ public function selectDueDate(array $values, array $errors = array(), array $attributes = array())
+ {
+ $placeholder = date($this->config->get('application_date_format', 'm/d/Y'));
+ $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes);
+
+ $html = $this->helper->form->label(t('Due Date'), 'date_due');
+ $html .= $this->helper->form->text('date_due', $values, $errors, $attributes, 'form-date');
+
+ return $html;
+ }
+
+ public function formatPriority(array $project, array $task)
+ {
+ $html = '';
+
+ if ($project['priority_end'] > $project['priority_start']) {
+ $html .= '<span class="task-board-priority" title="'.t('Task priority').'">';
+ $html .= $task['priority'] >= 0 ? 'P'.$task['priority'] : '-P'.abs($task['priority']);
+ $html .= '</span>';
+ }
+
+ return $html;
+ }
+
+ public function getProgress($task)
+ {
+ if (! isset($this->columns[$task['project_id']])) {
+ $this->columns[$task['project_id']] = $this->column->getList($task['project_id']);
+ }
+
+ return $this->task->getProgress($task, $this->columns[$task['project_id']]);
+ }
}
diff --git a/app/Helper/Text.php b/app/Helper/Text.php
index d2075fe4..83f1e3f9 100644
--- a/app/Helper/Text.php
+++ b/app/Helper/Text.php
@@ -3,14 +3,15 @@
namespace Kanboard\Helper;
use Kanboard\Core\Markdown;
+use Kanboard\Core\Base;
/**
- * Text helpers
+ * Text Helpers
*
* @package helper
* @author Frederic Guillot
*/
-class Text extends \Kanboard\Core\Base
+class Text extends Base
{
/**
* Markdown transformation
@@ -21,7 +22,7 @@ class Text extends \Kanboard\Core\Base
*/
public function markdown($text, array $link = array())
{
- $parser = new Markdown($link, $this->helper->url);
+ $parser = new Markdown($this->container, $link);
$parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML);
return $parser->text($text);
}
@@ -42,6 +43,29 @@ class Text extends \Kanboard\Core\Base
}
/**
+ * Get the number of bytes from PHP size
+ *
+ * @param integer $val PHP size (example: 2M)
+ * @return integer
+ */
+ public function phpToBytes($val)
+ {
+ $val = trim($val);
+ $last = strtolower($val[strlen($val)-1]);
+
+ switch ($last) {
+ case 'g':
+ $val *= 1024;
+ case 'm':
+ $val *= 1024;
+ case 'k':
+ $val *= 1024;
+ }
+
+ return $val;
+ }
+
+ /**
* Return true if needle is contained in the haystack
*
* @param string $haystack Haystack
diff --git a/app/Helper/Url.php b/app/Helper/Url.php
index f120252d..7de8a571 100644
--- a/app/Helper/Url.php
+++ b/app/Helper/Url.php
@@ -2,8 +2,7 @@
namespace Kanboard\Helper;
-use Kanboard\Core\Request;
-use Kanboard\Core\Security;
+use Kanboard\Core\Base;
/**
* Url helpers
@@ -11,7 +10,7 @@ use Kanboard\Core\Security;
* @package helper
* @author Frederic Guillot
*/
-class Url extends \Kanboard\Core\Base
+class Url extends Base
{
private $base = '';
private $directory = '';
@@ -45,7 +44,7 @@ class Url extends \Kanboard\Core\Base
*/
public function link($label, $controller, $action, array $params = array(), $csrf = false, $class = '', $title = '', $new_tab = false, $anchor = '')
{
- return '<a href="'.$this->href($controller, $action, $params, $csrf, $anchor).'" class="'.$class.'" title="'.$title.'" '.($new_tab ? 'target="_blank"' : '').'>'.$label.'</a>';
+ return '<a href="'.$this->href($controller, $action, $params, $csrf, $anchor).'" class="'.$class.'" title=\''.$title.'\' '.($new_tab ? 'target="_blank"' : '').'>'.$label.'</a>';
}
/**
@@ -104,8 +103,8 @@ class Url extends \Kanboard\Core\Base
*/
public function dir()
{
- if (empty($this->directory) && isset($_SERVER['REQUEST_METHOD'])) {
- $this->directory = str_replace('\\', '/', dirname($_SERVER['PHP_SELF']));
+ if ($this->directory === '' && $this->request->getMethod() !== '') {
+ $this->directory = str_replace('\\', '/', dirname($this->request->getServerVariable('PHP_SELF')));
$this->directory = $this->directory !== '/' ? $this->directory.'/' : '/';
$this->directory = str_replace('//', '/', $this->directory);
}
@@ -121,13 +120,13 @@ class Url extends \Kanboard\Core\Base
*/
public function server()
{
- if (empty($_SERVER['SERVER_NAME'])) {
+ if ($this->request->getServerVariable('SERVER_NAME') === '') {
return 'http://localhost/';
}
- $url = Request::isHTTPS() ? 'https://' : 'http://';
- $url .= $_SERVER['SERVER_NAME'];
- $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
+ $url = $this->request->isHTTPS() ? 'https://' : 'http://';
+ $url .= $this->request->getServerVariable('SERVER_NAME');
+ $url .= $this->request->getServerVariable('SERVER_PORT') == 80 || $this->request->getServerVariable('SERVER_PORT') == 443 ? '' : ':'.$this->request->getServerVariable('SERVER_PORT');
$url .= $this->dir() ?: '/';
return $url;
@@ -148,17 +147,19 @@ class Url extends \Kanboard\Core\Base
*/
private function build($separator, $controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false)
{
- $path = $this->router->findUrl($controller, $action, $params);
+ $path = $this->route->findUrl($controller, $action, $params);
$qs = array();
if (empty($path)) {
$qs['controller'] = $controller;
$qs['action'] = $action;
$qs += $params;
+ } else {
+ unset($params['plugin']);
}
if ($csrf) {
- $qs['csrf_token'] = Security::getCSRFToken();
+ $qs['csrf_token'] = $this->token->getCSRFToken();
}
if (! empty($qs)) {
diff --git a/app/Helper/User.php b/app/Helper/User.php
index 9cd39bd9..29844dfb 100644
--- a/app/Helper/User.php
+++ b/app/Helper/User.php
@@ -51,21 +51,6 @@ class User extends \Kanboard\Core\Base
}
/**
- * Get user profile
- *
- * @access public
- * @return string
- */
- public function getProfileLink()
- {
- return $this->helper->url->link(
- $this->helper->e($this->getFullname()),
- 'user',
- 'show',
- array('user_id' => $this->userSession->getId())
- );
- }
- /**
* Check if the given user_id is the connected user
*
* @param integer $user_id User id
@@ -88,44 +73,77 @@ class User extends \Kanboard\Core\Base
}
/**
- * Return if the logged user is project admin
+ * Get role name
*
* @access public
- * @return boolean
+ * @param string $role
+ * @return string
*/
- public function isProjectAdmin()
+ public function getRoleName($role = '')
{
- return $this->userSession->isProjectAdmin();
+ return $this->role->getRoleName($role ?: $this->userSession->getRole());
}
/**
- * Check for project administration actions access (Project Admin group)
+ * Check application access
*
- * @access public
- * @return boolean
+ * @param string $controller
+ * @param string $action
+ * @return bool
*/
- public function isProjectAdministrationAllowed($project_id)
+ public function hasAccess($controller, $action)
{
- if ($this->userSession->isAdmin()) {
- return true;
+ $key = 'app_access:'.$controller.$action;
+ $result = $this->memoryCache->get($key);
+
+ if ($result === null) {
+ $result = $this->applicationAuthorization->isAllowed($controller, $action, $this->userSession->getRole());
+ $this->memoryCache->set($key, $result);
}
- return $this->memoryCache->proxy($this->container['acl'], 'handleProjectAdminPermissions', $project_id);
+ return $result;
}
/**
- * Check for project management actions access (Regular users who are Project Managers)
+ * Check project access
*
- * @access public
- * @return boolean
+ * @param string $controller
+ * @param string $action
+ * @param integer $project_id
+ * @return bool
*/
- public function isProjectManagementAllowed($project_id)
+ public function hasProjectAccess($controller, $action, $project_id)
{
if ($this->userSession->isAdmin()) {
return true;
}
- return $this->memoryCache->proxy($this->container['acl'], 'handleProjectManagerPermissions', $project_id);
+ if (! $this->hasAccess($controller, $action)) {
+ return false;
+ }
+
+ $key = 'project_access:'.$controller.$action.$project_id;
+ $result = $this->memoryCache->get($key);
+
+ if ($result === null) {
+ $role = $this->getProjectUserRole($project_id);
+ $result = $this->projectAuthorization->isAllowed($controller, $action, $role);
+ $this->memoryCache->set($key, $result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get project role for the current user
+ *
+ * @access public
+ * @param integer $project_id
+ * @return string
+ */
+ public function getProjectUserRole($project_id)
+ {
+ return $this->memoryCache->proxy($this->projectUserRole, 'getUserRole', $project_id, $this->userSession->getId());
}
/**
@@ -136,7 +154,7 @@ class User extends \Kanboard\Core\Base
*/
public function getFullname(array $user = array())
{
- return $this->user->getFullname(empty($user) ? $_SESSION['user'] : $user);
+ return $this->user->getFullname(empty($user) ? $this->sessionStorage->user : $user);
}
/**
diff --git a/app/Integration/BitbucketWebhook.php b/app/Integration/BitbucketWebhook.php
deleted file mode 100644
index 97a39437..00000000
--- a/app/Integration/BitbucketWebhook.php
+++ /dev/null
@@ -1,312 +0,0 @@
-<?php
-
-namespace Kanboard\Integration;
-
-use Kanboard\Event\GenericEvent;
-
-/**
- * Bitbucket Webhook
- *
- * @package integration
- * @author Frederic Guillot
- */
-class BitbucketWebhook extends \Kanboard\Core\Base
-{
- /**
- * Events
- *
- * @var string
- */
- const EVENT_COMMIT = 'bitbucket.webhook.commit';
- const EVENT_ISSUE_OPENED = 'bitbucket.webhook.issue.opened';
- const EVENT_ISSUE_CLOSED = 'bitbucket.webhook.issue.closed';
- const EVENT_ISSUE_REOPENED = 'bitbucket.webhook.issue.reopened';
- const EVENT_ISSUE_ASSIGNEE_CHANGE = 'bitbucket.webhook.issue.assignee';
- const EVENT_ISSUE_COMMENT = 'bitbucket.webhook.issue.commented';
-
- /**
- * Project id
- *
- * @access private
- * @var integer
- */
- private $project_id = 0;
-
- /**
- * Set the project id
- *
- * @access public
- * @param integer $project_id Project id
- */
- public function setProjectId($project_id)
- {
- $this->project_id = $project_id;
- }
-
- /**
- * Parse incoming events
- *
- * @access public
- * @param string $type Bitbucket event type
- * @param array $payload Bitbucket event
- * @return boolean
- */
- public function parsePayload($type, array $payload)
- {
- switch ($type) {
- case 'issue:comment_created':
- return $this->handleCommentCreated($payload);
- case 'issue:created':
- return $this->handleIssueOpened($payload);
- case 'issue:updated':
- return $this->handleIssueUpdated($payload);
- case 'repo:push':
- return $this->handlePush($payload);
- }
-
- return false;
- }
-
- /**
- * Parse comment issue events
- *
- * @access public
- * @param array $payload
- * @return boolean
- */
- public function handleCommentCreated(array $payload)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']);
-
- if (! empty($task)) {
- $user = $this->user->getByUsername($payload['actor']['username']);
-
- if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) {
- $user = array();
- }
-
- $event = array(
- 'project_id' => $this->project_id,
- 'reference' => $payload['comment']['id'],
- 'comment' => $payload['comment']['content']['raw']."\n\n[".t('By @%s on Bitbucket', $payload['actor']['display_name']).']('.$payload['comment']['links']['html']['href'].')',
- 'user_id' => ! empty($user) ? $user['id'] : 0,
- 'task_id' => $task['id'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_COMMENT,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle new issues
- *
- * @access public
- * @param array $payload
- * @return boolean
- */
- public function handleIssueOpened(array $payload)
- {
- $event = array(
- 'project_id' => $this->project_id,
- 'reference' => $payload['issue']['id'],
- 'title' => $payload['issue']['title'],
- 'description' => $payload['issue']['content']['raw']."\n\n[".t('Bitbucket Issue').']('.$payload['issue']['links']['html']['href'].')',
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_OPENED,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- /**
- * Handle issue updates
- *
- * @access public
- * @param array $payload
- * @return boolean
- */
- public function handleIssueUpdated(array $payload)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']);
-
- if (empty($task)) {
- return false;
- }
-
- if (isset($payload['changes']['status'])) {
- return $this->handleStatusChange($task, $payload);
- } elseif (isset($payload['changes']['assignee'])) {
- return $this->handleAssigneeChange($task, $payload);
- }
-
- return false;
- }
-
- /**
- * Handle issue status change
- *
- * @access public
- * @param array $task
- * @param array $payload
- * @return boolean
- */
- public function handleStatusChange(array $task, array $payload)
- {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'reference' => $payload['issue']['id'],
- );
-
- switch ($payload['issue']['state']) {
- case 'closed':
- $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_CLOSED, new GenericEvent($event));
- return true;
- case 'open':
- $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_REOPENED, new GenericEvent($event));
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle issue assignee change
- *
- * @access public
- * @param array $task
- * @param array $payload
- * @return boolean
- */
- public function handleAssigneeChange(array $task, array $payload)
- {
- if (empty($payload['issue']['assignee'])) {
- return $this->handleIssueUnassigned($task, $payload);
- }
-
- return $this->handleIssueAssigned($task, $payload);
- }
-
- /**
- * Handle issue assigned
- *
- * @access public
- * @param array $task
- * @param array $payload
- * @return boolean
- */
- public function handleIssueAssigned(array $task, array $payload)
- {
- $user = $this->user->getByUsername($payload['issue']['assignee']['username']);
-
- if (empty($user)) {
- return false;
- }
-
- if (! $this->projectPermission->isMember($this->project_id, $user['id'])) {
- return false;
- }
-
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'owner_id' => $user['id'],
- 'reference' => $payload['issue']['id'],
- );
-
- $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_ASSIGNEE_CHANGE, new GenericEvent($event));
-
- return true;
- }
-
- /**
- * Handle issue unassigned
- *
- * @access public
- * @param array $task
- * @param array $payload
- * @return boolean
- */
- public function handleIssueUnassigned(array $task, array $payload)
- {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'owner_id' => 0,
- 'reference' => $payload['issue']['id'],
- );
-
- $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_ASSIGNEE_CHANGE, new GenericEvent($event));
-
- return true;
- }
-
- /**
- * Parse push events
- *
- * @access public
- * @param array $payload
- * @return boolean
- */
- public function handlePush(array $payload)
- {
- if (isset($payload['push']['changes'])) {
- foreach ($payload['push']['changes'] as $change) {
- if (isset($change['new']['target']) && $this->handleCommit($change['new']['target'], $payload['actor'])) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Parse commit
- *
- * @access public
- * @param array $commit Bitbucket commit
- * @param array $actor Bitbucket actor
- * @return boolean
- */
- public function handleCommit(array $commit, array $actor)
- {
- $task_id = $this->task->getTaskIdFromText($commit['message']);
-
- if (empty($task_id)) {
- return false;
- }
-
- $task = $this->taskFinder->getById($task_id);
-
- if (empty($task)) {
- return false;
- }
-
- if ($task['project_id'] != $this->project_id) {
- return false;
- }
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_COMMIT,
- new GenericEvent(array(
- 'task_id' => $task_id,
- 'commit_message' => $commit['message'],
- 'commit_url' => $commit['links']['html']['href'],
- 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Bitbucket', $actor['display_name']).']('.$commit['links']['html']['href'].')',
- ) + $task)
- );
-
- return true;
- }
-}
diff --git a/app/Integration/GithubWebhook.php b/app/Integration/GithubWebhook.php
deleted file mode 100644
index c8b53e37..00000000
--- a/app/Integration/GithubWebhook.php
+++ /dev/null
@@ -1,380 +0,0 @@
-<?php
-
-namespace Kanboard\Integration;
-
-use Kanboard\Event\GenericEvent;
-
-/**
- * Github Webhook
- *
- * @package integration
- * @author Frederic Guillot
- */
-class GithubWebhook extends \Kanboard\Core\Base
-{
- /**
- * Events
- *
- * @var string
- */
- const EVENT_ISSUE_OPENED = 'github.webhook.issue.opened';
- const EVENT_ISSUE_CLOSED = 'github.webhook.issue.closed';
- const EVENT_ISSUE_REOPENED = 'github.webhook.issue.reopened';
- const EVENT_ISSUE_ASSIGNEE_CHANGE = 'github.webhook.issue.assignee';
- const EVENT_ISSUE_LABEL_CHANGE = 'github.webhook.issue.label';
- const EVENT_ISSUE_COMMENT = 'github.webhook.issue.commented';
- const EVENT_COMMIT = 'github.webhook.commit';
-
- /**
- * Project id
- *
- * @access private
- * @var integer
- */
- private $project_id = 0;
-
- /**
- * Set the project id
- *
- * @access public
- * @param integer $project_id Project id
- */
- public function setProjectId($project_id)
- {
- $this->project_id = $project_id;
- }
-
- /**
- * Parse Github events
- *
- * @access public
- * @param string $type Github event type
- * @param array $payload Github event
- * @return boolean
- */
- public function parsePayload($type, array $payload)
- {
- switch ($type) {
- case 'push':
- return $this->parsePushEvent($payload);
- case 'issues':
- return $this->parseIssueEvent($payload);
- case 'issue_comment':
- return $this->parseCommentIssueEvent($payload);
- }
-
- return false;
- }
-
- /**
- * Parse Push events (list of commits)
- *
- * @access public
- * @param array $payload Event data
- * @return boolean
- */
- public function parsePushEvent(array $payload)
- {
- foreach ($payload['commits'] as $commit) {
- $task_id = $this->task->getTaskIdFromText($commit['message']);
-
- if (empty($task_id)) {
- continue;
- }
-
- $task = $this->taskFinder->getById($task_id);
-
- if (empty($task)) {
- continue;
- }
-
- if ($task['project_id'] != $this->project_id) {
- continue;
- }
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_COMMIT,
- new GenericEvent(array(
- 'task_id' => $task_id,
- 'commit_message' => $commit['message'],
- 'commit_url' => $commit['url'],
- 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Github', $commit['author']['username']).']('.$commit['url'].')'
- ) + $task)
- );
- }
-
- return true;
- }
-
- /**
- * Parse issue events
- *
- * @access public
- * @param array $payload Event data
- * @return boolean
- */
- public function parseIssueEvent(array $payload)
- {
- switch ($payload['action']) {
- case 'opened':
- return $this->handleIssueOpened($payload['issue']);
- case 'closed':
- return $this->handleIssueClosed($payload['issue']);
- case 'reopened':
- return $this->handleIssueReopened($payload['issue']);
- case 'assigned':
- return $this->handleIssueAssigned($payload['issue']);
- case 'unassigned':
- return $this->handleIssueUnassigned($payload['issue']);
- case 'labeled':
- return $this->handleIssueLabeled($payload['issue'], $payload['label']);
- case 'unlabeled':
- return $this->handleIssueUnlabeled($payload['issue'], $payload['label']);
- }
-
- return false;
- }
-
- /**
- * Parse comment issue events
- *
- * @access public
- * @param array $payload Event data
- * @return boolean
- */
- public function parseCommentIssueEvent(array $payload)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['number']);
-
- if (! empty($task)) {
- $user = $this->user->getByUsername($payload['comment']['user']['login']);
-
- if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) {
- $user = array();
- }
-
- $event = array(
- 'project_id' => $this->project_id,
- 'reference' => $payload['comment']['id'],
- 'comment' => $payload['comment']['body']."\n\n[".t('By @%s on Github', $payload['comment']['user']['login']).']('.$payload['comment']['html_url'].')',
- 'user_id' => ! empty($user) ? $user['id'] : 0,
- 'task_id' => $task['id'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_COMMENT,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle new issues
- *
- * @access public
- * @param array $issue Issue data
- * @return boolean
- */
- public function handleIssueOpened(array $issue)
- {
- $event = array(
- 'project_id' => $this->project_id,
- 'reference' => $issue['number'],
- 'title' => $issue['title'],
- 'description' => $issue['body']."\n\n[".t('Github Issue').']('.$issue['html_url'].')',
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_OPENED,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- /**
- * Handle issue closing
- *
- * @access public
- * @param array $issue Issue data
- * @return boolean
- */
- public function handleIssueClosed(array $issue)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
-
- if (! empty($task)) {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'reference' => $issue['number'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_CLOSED,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle issue reopened
- *
- * @access public
- * @param array $issue Issue data
- * @return boolean
- */
- public function handleIssueReopened(array $issue)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
-
- if (! empty($task)) {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'reference' => $issue['number'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_REOPENED,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle issue assignee change
- *
- * @access public
- * @param array $issue Issue data
- * @return boolean
- */
- public function handleIssueAssigned(array $issue)
- {
- $user = $this->user->getByUsername($issue['assignee']['login']);
- $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
-
- if (! empty($user) && ! empty($task) && $this->projectPermission->isMember($this->project_id, $user['id'])) {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'owner_id' => $user['id'],
- 'reference' => $issue['number'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_ASSIGNEE_CHANGE,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle unassigned issue
- *
- * @access public
- * @param array $issue Issue data
- * @return boolean
- */
- public function handleIssueUnassigned(array $issue)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
-
- if (! empty($task)) {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'owner_id' => 0,
- 'reference' => $issue['number'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_ASSIGNEE_CHANGE,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle labeled issue
- *
- * @access public
- * @param array $issue Issue data
- * @param array $label Label data
- * @return boolean
- */
- public function handleIssueLabeled(array $issue, array $label)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
-
- if (! empty($task)) {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'reference' => $issue['number'],
- 'label' => $label['name'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_LABEL_CHANGE,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Handle unlabeled issue
- *
- * @access public
- * @param array $issue Issue data
- * @param array $label Label data
- * @return boolean
- */
- public function handleIssueUnlabeled(array $issue, array $label)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
-
- if (! empty($task)) {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'reference' => $issue['number'],
- 'label' => $label['name'],
- 'category_id' => 0,
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_LABEL_CHANGE,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-}
diff --git a/app/Integration/GitlabWebhook.php b/app/Integration/GitlabWebhook.php
deleted file mode 100644
index b3f9b0b5..00000000
--- a/app/Integration/GitlabWebhook.php
+++ /dev/null
@@ -1,265 +0,0 @@
-<?php
-
-namespace Kanboard\Integration;
-
-use Kanboard\Event\GenericEvent;
-
-/**
- * Gitlab Webhook
- *
- * @package integration
- * @author Frederic Guillot
- */
-class GitlabWebhook extends \Kanboard\Core\Base
-{
- /**
- * Events
- *
- * @var string
- */
- const EVENT_ISSUE_OPENED = 'gitlab.webhook.issue.opened';
- const EVENT_ISSUE_CLOSED = 'gitlab.webhook.issue.closed';
- const EVENT_COMMIT = 'gitlab.webhook.commit';
- const EVENT_ISSUE_COMMENT = 'gitlab.webhook.issue.commented';
-
- /**
- * Supported webhook events
- *
- * @var string
- */
- const TYPE_PUSH = 'push';
- const TYPE_ISSUE = 'issue';
- const TYPE_COMMENT = 'comment';
-
- /**
- * Project id
- *
- * @access private
- * @var integer
- */
- private $project_id = 0;
-
- /**
- * Set the project id
- *
- * @access public
- * @param integer $project_id Project id
- */
- public function setProjectId($project_id)
- {
- $this->project_id = $project_id;
- }
-
- /**
- * Parse events
- *
- * @access public
- * @param array $payload Gitlab event
- * @return boolean
- */
- public function parsePayload(array $payload)
- {
- switch ($this->getType($payload)) {
- case self::TYPE_PUSH:
- return $this->handlePushEvent($payload);
- case self::TYPE_ISSUE;
- return $this->handleIssueEvent($payload);
- case self::TYPE_COMMENT;
- return $this->handleCommentEvent($payload);
- }
-
- return false;
- }
-
- /**
- * Get event type
- *
- * @access public
- * @param array $payload Gitlab event
- * @return string
- */
- public function getType(array $payload)
- {
- if (empty($payload['object_kind'])) {
- return '';
- }
-
- switch ($payload['object_kind']) {
- case 'issue':
- return self::TYPE_ISSUE;
- case 'note':
- return self::TYPE_COMMENT;
- case 'push':
- return self::TYPE_PUSH;
- default:
- return '';
- }
- }
-
- /**
- * Parse push event
- *
- * @access public
- * @param array $payload Gitlab event
- * @return boolean
- */
- public function handlePushEvent(array $payload)
- {
- foreach ($payload['commits'] as $commit) {
- $this->handleCommit($commit);
- }
-
- return true;
- }
-
- /**
- * Parse commit
- *
- * @access public
- * @param array $commit Gitlab commit
- * @return boolean
- */
- public function handleCommit(array $commit)
- {
- $task_id = $this->task->getTaskIdFromText($commit['message']);
-
- if (empty($task_id)) {
- return false;
- }
-
- $task = $this->taskFinder->getById($task_id);
-
- if (empty($task)) {
- return false;
- }
-
- if ($task['project_id'] != $this->project_id) {
- return false;
- }
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_COMMIT,
- new GenericEvent(array(
- 'task_id' => $task_id,
- 'commit_message' => $commit['message'],
- 'commit_url' => $commit['url'],
- 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Gitlab', $commit['author']['name']).']('.$commit['url'].')'
- ) + $task)
- );
-
- return true;
- }
-
- /**
- * Parse issue event
- *
- * @access public
- * @param array $payload Gitlab event
- * @return boolean
- */
- public function handleIssueEvent(array $payload)
- {
- switch ($payload['object_attributes']['action']) {
- case 'open':
- return $this->handleIssueOpened($payload['object_attributes']);
- case 'close':
- return $this->handleIssueClosed($payload['object_attributes']);
- }
-
- return false;
- }
-
- /**
- * Handle new issues
- *
- * @access public
- * @param array $issue Issue data
- * @return boolean
- */
- public function handleIssueOpened(array $issue)
- {
- $event = array(
- 'project_id' => $this->project_id,
- 'reference' => $issue['id'],
- 'title' => $issue['title'],
- 'description' => $issue['description']."\n\n[".t('Gitlab Issue').']('.$issue['url'].')',
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_OPENED,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- /**
- * Handle issue closing
- *
- * @access public
- * @param array $issue Issue data
- * @return boolean
- */
- public function handleIssueClosed(array $issue)
- {
- $task = $this->taskFinder->getByReference($this->project_id, $issue['id']);
-
- if (! empty($task)) {
- $event = array(
- 'project_id' => $this->project_id,
- 'task_id' => $task['id'],
- 'reference' => $issue['id'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_CLOSED,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Parse comment issue events
- *
- * @access public
- * @param array $payload Event data
- * @return boolean
- */
- public function handleCommentEvent(array $payload)
- {
- if (! isset($payload['issue'])) {
- return false;
- }
-
- $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']);
-
- if (! empty($task)) {
- $user = $this->user->getByUsername($payload['user']['username']);
-
- if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) {
- $user = array();
- }
-
- $event = array(
- 'project_id' => $this->project_id,
- 'reference' => $payload['object_attributes']['id'],
- 'comment' => $payload['object_attributes']['note']."\n\n[".t('By @%s on Gitlab', $payload['user']['username']).']('.$payload['object_attributes']['url'].')',
- 'user_id' => ! empty($user) ? $user['id'] : 0,
- 'task_id' => $task['id'],
- );
-
- $this->container['dispatcher']->dispatch(
- self::EVENT_ISSUE_COMMENT,
- new GenericEvent($event)
- );
-
- return true;
- }
-
- return false;
- }
-}
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
new file mode 100644
index 00000000..3abb095f
--- /dev/null
+++ b/app/Locale/bs_BA/translations.php
@@ -0,0 +1,1151 @@
+<?php
+
+return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => '.',
+ 'None' => 'None',
+ 'edit' => 'uredi',
+ 'Edit' => 'Uredi',
+ 'remove' => 'ukloni',
+ 'Remove' => 'Ukloni',
+ 'Update' => 'Ažuriraj',
+ 'Yes' => 'Da',
+ 'No' => 'Ne',
+ 'cancel' => 'odustani',
+ 'or' => 'ili',
+ 'Yellow' => 'Žuta',
+ 'Blue' => 'Plava',
+ 'Green' => 'Zelena',
+ 'Purple' => 'Ljubičasta',
+ 'Red' => 'Crvena',
+ 'Orange' => 'Narandžasta',
+ 'Grey' => 'Siva',
+ 'Brown' => 'Smeđa',
+ 'Deep Orange' => 'Tamno narandžasta',
+ 'Dark Grey' => 'Tamno siva',
+ 'Pink' => 'Roze',
+ 'Teal' => 'Tirkizna',
+ 'Cyan' => 'Zelenkasto plava',
+ 'Lime' => 'Žućkasto zelena',
+ 'Light Green' => 'Svijetlo zelena',
+ 'Amber' => 'Ćilibarska',
+ 'Save' => 'Sačuvaj',
+ 'Login' => 'Prijava',
+ 'Official website:' => 'Zvanična stranica:',
+ 'Unassigned' => 'Nedodijeljen',
+ 'View this task' => 'Pregledaj zadatak',
+ 'Remove user' => 'Ukloni korisnika',
+ 'Do you really want to remove this user: "%s"?' => 'Da li zaista želiš da ukloniš korisnika: "%s"?',
+ 'New user' => 'Novi korisnik',
+ 'All users' => 'Svi korisnici',
+ 'Username' => 'Korisničko ime',
+ 'Password' => 'Šifra',
+ 'Administrator' => 'Administrator',
+ 'Sign in' => 'Prijava',
+ 'Users' => 'Korisnici',
+ 'No user' => 'Nema korisnika',
+ 'Forbidden' => 'Zabranjeno',
+ 'Access Forbidden' => 'Pristup zabranjen',
+ 'Edit user' => 'Uredi korisnika',
+ 'Logout' => 'Odjava',
+ 'Bad username or password' => 'Pogrešno korisničko ime ili šifra',
+ 'Edit project' => 'Uredi projekat',
+ 'Name' => 'Ime',
+ 'Projects' => 'Projekti',
+ 'No project' => 'Bez projekta',
+ 'Project' => 'Projekat',
+ 'Status' => 'Status',
+ 'Tasks' => 'Zadatak',
+ 'Board' => 'Tabla',
+ 'Actions' => 'Akcije',
+ 'Inactive' => 'Neaktivan',
+ 'Active' => 'Aktivan',
+ 'Add this column' => 'Dodaj kolonu',
+ '%d tasks on the board' => '%d zadataka na tabli',
+ '%d tasks in total' => '%d zadataka ukupno',
+ 'Unable to update this board.' => 'Nemogu da ažuriram ovu tablu.',
+ 'Edit board' => 'Izmijeni tablu',
+ 'Disable' => 'Onemogući',
+ 'Enable' => 'Omogući',
+ 'New project' => 'Novi projekat',
+ 'Do you really want to remove this project: "%s"?' => 'Da li želiš da ukloniš projekat: "%s"?',
+ 'Remove project' => 'Ukloni projekat',
+ 'Edit the board for "%s"' => 'Uredi tablu za "%s"',
+ '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',
+ 'Unable to remove this column.' => 'Nemoguće uklanjanje kolone.',
+ 'Do you really want to remove this column: "%s"?' => 'Da li zaista želiš da ukoniš ovu kolonu: "%s"?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Ova akcija BRIŠE SVE ZADATKE vezane za ovu kolonu!',
+ 'Settings' => 'Podešavanja',
+ 'Application settings' => 'Podešavanja aplikacije',
+ 'Language' => 'Jezik',
+ 'Webhook token:' => 'Token:',
+ 'API token:' => 'Token za API',
+ 'Database size:' => 'Veličina baze:',
+ 'Download the database' => 'Preuzmi bazu',
+ 'Optimize the database' => 'Optimizuj bazu',
+ '(VACUUM command)' => '(Naredba VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(Sqlite baza spakovana Gzip-om)',
+ 'Close a task' => 'Zatvori zadatak',
+ 'Edit a task' => 'Uredi zadatak',
+ 'Column' => 'Kolona',
+ 'Color' => 'Boja',
+ 'Assignee' => 'Izvršilac',
+ 'Create another task' => 'Dodaj zadatak',
+ 'New task' => 'Novi zadatak',
+ 'Open a task' => 'Otvori zadatak',
+ 'Do you really want to open this task: "%s"?' => 'Da li zaista želiš da otvoriš zadatak: "%s"?',
+ 'Back to the board' => 'Nazad na tablu',
+ 'There is nobody assigned' => 'Niko nije dodijeljen!',
+ 'Column on the board:' => 'Kolona na tabli:',
+ 'Close this task' => 'Zatvori ovaj zadatak',
+ 'Open this task' => 'Otvori ovaj zadatak',
+ 'There is no description.' => 'Bez opisa.',
+ 'Add a new task' => 'Dodaj zadatak',
+ 'The username is required' => 'Korisničko ime je obavezno',
+ 'The maximum length is %d characters' => 'Maksimalna dužina je %d znakova',
+ 'The minimum length is %d characters' => 'Minimalna dužina je %d znakova',
+ 'The password is required' => 'Šifra je obavezna',
+ 'This value must be an integer' => 'Mora biti cio broj',
+ 'The username must be unique' => 'Korisničko ime mora biti jedinstveno',
+ 'The user id is required' => 'ID korisnika je obavezan',
+ 'Passwords don\'t match' => 'Šifre se ne podudaraju',
+ 'The confirmation is required' => 'Potvrda je obavezna',
+ 'The project is required' => 'Projekat je obavezan',
+ 'The id is required' => 'ID je obavezan',
+ 'The project id is required' => 'ID projekta je obavezan',
+ 'The project name is required' => 'Naziv projekta je obavezan',
+ 'The title is required' => 'Naslov je obavezan',
+ 'Settings saved successfully.' => 'Podešavanja uspješno sačuvana.',
+ 'Unable to save your settings.' => 'Nemoguće sačuvati podešavanja.',
+ 'Database optimization done.' => 'Optimizacija baze je završena.',
+ 'Your project have been created successfully.' => 'Projekat je uspješno napravljen.',
+ 'Unable to create your project.' => 'Nemoguće kreiranje projekta.',
+ 'Project updated successfully.' => 'Projekat je uspješno ažuriran.',
+ 'Unable to update this project.' => 'Nemoguće ažuriranje projekta.',
+ 'Unable to remove this project.' => 'Nemoguće uklanjanje projekta.',
+ 'Project removed successfully.' => 'Projekat uspješno uklonjen.',
+ 'Project activated successfully.' => 'Projekt uspješno aktiviran.',
+ 'Unable to activate this project.' => 'Nemoguće aktiviranje projekta.',
+ 'Project disabled successfully.' => 'Projekat uspješno deaktiviran.',
+ 'Unable to disable this project.' => 'nemoguće deaktiviranje projekta.',
+ 'Unable to open this task.' => 'Nemoguće otvaranje zadatka.',
+ 'Task opened successfully.' => 'Zadatak uspješno otvoren.',
+ 'Unable to close this task.' => 'Nije moguće zatvaranje ovog zadatka.',
+ 'Task closed successfully.' => 'Zadatak uspješno zatvoren.',
+ 'Unable to update your task.' => 'Nije moguće ažuriranje zadatka.',
+ 'Task updated successfully.' => 'Zadatak uspješno ažuriran.',
+ 'Unable to create your task.' => 'Nije moguće kreiranje zadatka.',
+ 'Task created successfully.' => 'Zadatak uspješno kreiran.',
+ 'User created successfully.' => 'Korisnik uspješno kreiran',
+ 'Unable to create your user.' => 'Nije uspjelo kreiranje korisnika.',
+ 'User updated successfully.' => 'Korisnik uspješno ažuriran.',
+ 'Unable to update your user.' => 'Nije moguće ažuriranje korisnika.',
+ 'User removed successfully.' => 'Korisnik uspješno uklonjen.',
+ 'Unable to remove this user.' => 'Nije moguće uklanjanje korisnika.',
+ 'Board updated successfully.' => 'Tabla uspješno ažurirana.',
+ 'Ready' => 'Spreman',
+ 'Backlog' => 'Zaliha',
+ 'Work in progress' => 'U izradi',
+ 'Done' => 'Gotovo',
+ 'Application version:' => 'Verzija aplikacije:',
+ 'Id' => 'Id',
+ '%d closed tasks' => '%d zatvorenih zadataka',
+ 'No task for this project' => 'Nema dodijeljenih zadataka ovom projektu',
+ 'Public link' => 'Javni link',
+ 'Change assignee' => 'Promjena izvršioca',
+ 'Change assignee for the task "%s"' => 'Promjena izvršioca za zadatak "%s"',
+ 'Timezone' => 'Vremenska zona',
+ 'Sorry, I didn\'t find this information in my database!' => 'Na žalost, nije pronađena informacija u bazi',
+ 'Page not found' => 'Strana nije pronađena',
+ 'Complexity' => 'Složenost',
+ 'Task limit' => 'Najviše zadataka',
+ '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',
+ 'Comment added successfully.' => 'Komentar uspješno dodan',
+ 'Unable to create your comment.' => 'Nemoguće kreiranje komentara',
+ 'Edit this task' => 'Uredi ovaj zadatak',
+ 'Due Date' => 'Treba biti gotovo do dana',
+ 'Invalid date' => 'Pogrešan datum',
+ 'Automatic actions' => 'Automatske akcije',
+ 'Your automatic action have been created successfully.' => 'Uspješno kreirana automatska akcija',
+ 'Unable to create your automatic action.' => 'Nemoguće kreiranje automatske akcije',
+ 'Remove an action' => 'Obriši akciju',
+ 'Unable to remove this action.' => 'Nije moguće obrisati akciju',
+ 'Action removed successfully.' => 'Akcija obrisana',
+ 'Automatic actions for the project "%s"' => 'Akcije za automatizaciju projekta "%s"',
+ 'Defined actions' => 'Definisane akcje',
+ 'Add an action' => 'dodaj akcju',
+ 'Event name' => 'Naziv događaja',
+ 'Action name' => 'Naziv akcije',
+ 'Action parameters' => 'Parametri akcije',
+ 'Action' => 'Akcija',
+ 'Event' => 'Događaj',
+ 'When the selected event occurs execute the corresponding action.' => 'Na izabrani događaj izvrši odgovarajuću akciju',
+ 'Next step' => 'Slijedeći korak',
+ 'Define action parameters' => 'Definiši parametre akcije',
+ 'Save this action' => 'Snimi akciju',
+ 'Do you really want to remove this action: "%s"?' => 'Da li da obrišem akciju "%s"?',
+ 'Remove an automatic action' => 'Obriši automatsku akciju',
+ 'Assign the task to a specific user' => 'Dodijeli zadatak određenom korisniku',
+ 'Assign the task to the person who does the action' => 'Dodeli zadatak korisniku koji je izvršio akciju',
+ 'Duplicate the task to another project' => 'Kopiraj akciju u drugi projekat',
+ 'Move a task to another column' => 'Premjesti zadatak u drugu kolonu',
+ 'Task modification' => 'Izmjene zadatka',
+ 'Task creation' => 'Kreiranje zadatka',
+ 'Closing a task' => 'Zatvaranja zadatka',
+ 'Assign a color to a specific user' => 'Dodeli boju korisniku',
+ 'Column title' => 'Naslov kolone',
+ 'Position' => 'Pozicija',
+ 'Duplicate to another project' => 'Dupliciraj u drugi projekat',
+ 'Duplicate' => 'Dupliciraj',
+ 'link' => 'link',
+ 'Comment updated successfully.' => 'Komentar uspješno ažuriran.',
+ 'Unable to update your comment.' => 'Neuspješno ažuriranje komentara.',
+ 'Remove a comment' => 'Obriši komentar',
+ 'Comment removed successfully.' => 'Komentar je uspješno obrisan.',
+ 'Unable to remove this comment.' => 'Neuspješno brisanje komentara.',
+ 'Do you really want to remove this comment?' => 'Da li zaista želiš obrisati ovaj komentar?',
+ 'Current password for the user "%s"' => 'Trenutna šifra korisnika "%s"',
+ 'The current password is required' => 'Trenutna šifra je obavezna',
+ 'Wrong password' => 'Pogrešna šifra',
+ 'Unknown' => 'Nepoznat',
+ 'Last logins' => 'Posljednje prijave',
+ 'Login date' => 'Datum prijave',
+ 'Authentication method' => 'Metod autentikacije',
+ 'IP address' => 'IP adresa',
+ 'User agent' => 'Browser',
+ 'Persistent connections' => 'Stalna konekcija',
+ 'No session.' => 'Bez sesije',
+ 'Expiration date' => 'Ističe',
+ 'Remember Me' => 'Zapamti me',
+ 'Creation date' => 'Datum kreiranja',
+ 'Everybody' => 'Svi',
+ 'Open' => 'Otvoreni',
+ 'Closed' => 'Zatvoreni',
+ 'Search' => 'Pretraga',
+ 'Nothing found.' => 'Ništa nije pronađeno',
+ 'Due date' => 'Treba biti gotovo do dana',
+ 'Others formats accepted: %s and %s' => 'Ostali podržani formati: %s i %s',
+ 'Description' => 'Opis',
+ '%d comments' => '%d Komentara',
+ '%d comment' => '%d Komentar',
+ 'Email address invalid' => 'Pogrešan e-mail',
+ 'Your external account is not linked anymore to your profile.' => 'Vaš vanjski korisnički profil nije više povezan.',
+ 'Unable to unlink your external account.' => 'Nemoguće ukloniti vezu s vanjskim korisničkim profilom',
+ 'External authentication failed' => 'Vanjska autentikacija nije uspostavljena',
+ 'Your external account is linked to your profile successfully.' => 'Uspješno uspostavljena vanjska autentikacija',
+ 'Email' => 'E-mail',
+ 'Task removed successfully.' => 'Zadatak uspješno uklonjen.',
+ 'Unable to remove this task.' => 'Nemoguće uklanjanje zadatka.',
+ 'Remove a task' => 'Ukloni zadatak',
+ 'Do you really want to remove this task: "%s"?' => 'Da li zaista želiš ukloniti zadatak "%s"?',
+ 'Assign automatically a color based on a category' => 'Automatski dodijeli boju po kategoriji',
+ 'Assign automatically a category based on a color' => 'Automatski dodijeli kategoriju po boji',
+ 'Task creation or modification' => 'Kreiranje ili izmjena zadatka',
+ 'Category' => 'Kategorija',
+ 'Category:' => 'Kategorija:',
+ 'Categories' => 'Kategorije',
+ 'Category not found.' => 'Kategorija nije pronađena',
+ 'Your category have been created successfully.' => 'Uspješno kreirana kategorija.',
+ 'Unable to create your category.' => 'Nije moguće kreirati kategoriju.',
+ 'Your category have been updated successfully.' => 'Kategorija je uspješno ažurirana',
+ 'Unable to update your category.' => 'Nemoguće izmijeniti kategoriju',
+ 'Remove a category' => 'Ukloni kategoriju',
+ 'Category removed successfully.' => 'Kategorija uspešno uklonjena.',
+ 'Unable to remove this category.' => 'Nije moguće ukloniti kategoriju.',
+ 'Category modification for the project "%s"' => 'Izmjena kategorije za projekat "%s"',
+ 'Category Name' => 'Naziv kategorije',
+ 'Add a new category' => 'Dodaj novu kategoriju',
+ 'Do you really want to remove this category: "%s"?' => 'Da li zaista želiš ukloniti kategoriju: "%s"?',
+ 'All categories' => 'Sve kategorije',
+ 'No category' => 'Bez kategorije',
+ 'The name is required' => 'Naziv je obavezan',
+ 'Remove a file' => 'Ukloni fajl',
+ 'Unable to remove this file.' => 'Fajl nije moguće ukloniti.',
+ 'File removed successfully.' => 'Uspješno uklonjen fajl.',
+ 'Attach a document' => 'Prikači dokument',
+ 'Do you really want to remove this file: "%s"?' => 'Da li da uklonim fajl: "%s"?',
+ 'Attachments' => 'Prilozi',
+ 'Edit the task' => 'Uredi zadatak',
+ 'Edit the description' => 'Uredi opis zadatka',
+ 'Add a comment' => 'Dodaj komentar',
+ 'Edit a comment' => 'Izmijeni komentar',
+ 'Summary' => 'Pregled',
+ 'Time tracking' => 'Praćenje vremena',
+ 'Estimate:' => 'Procjena:',
+ 'Spent:' => 'Potrošeno:',
+ 'Do you really want to remove this sub-task?' => 'Da li da zaista želiš ukloniti pod-zdadatak?',
+ 'Remaining:' => 'Preostalo:',
+ 'hours' => 'sati',
+ 'spent' => 'potrošeno',
+ 'estimated' => 'procijenjeno',
+ 'Sub-Tasks' => 'Pod-zadaci',
+ 'Add a sub-task' => 'Dodaj pod-zadatak',
+ 'Original estimate' => 'Originalna procjena',
+ 'Create another sub-task' => 'Dodaj novi pod-zadatak',
+ 'Time spent' => 'Utrošeno vrijeme',
+ 'Edit a sub-task' => 'Izmijeni pod-zadatak',
+ 'Remove a sub-task' => 'Ukloni pod-zadatak',
+ 'The time must be a numeric value' => 'Vrijeme mora biti broj',
+ 'Todo' => 'Za uraditi',
+ 'In progress' => 'U radu',
+ 'Sub-task removed successfully.' => 'Pod-zadatak uspješno uklonjen.',
+ 'Unable to remove this sub-task.' => 'Nemoguće ukloniti pod-zadatak.',
+ 'Sub-task updated successfully.' => 'Pod-zadatak uspješno ažuriran.',
+ 'Unable to update your sub-task.' => 'Nemoguće ažurirati pod-zadatak.',
+ 'Unable to create your sub-task.' => 'Nemoguće dodati pod-zadatak.',
+ 'Sub-task added successfully.' => 'Pod-zadatak uspješno dodan.',
+ 'Maximum size: ' => 'Maksimalna veličina: ',
+ 'Unable to upload the file.' => 'Nije moguće snimiti fajl.',
+ 'Display another project' => 'Prikaži drugi projekat',
+ 'Created by %s' => 'Kreirao %s',
+ 'Tasks Export' => 'Izvoz zadataka',
+ 'Tasks exportation for "%s"' => 'Izvoz zadataka za "%s"',
+ 'Start Date' => 'Početni datum',
+ 'End Date' => 'Datum završetka',
+ 'Execute' => 'Izvrši',
+ 'Task Id' => 'Identifikator zadatka',
+ 'Creator' => 'Autor',
+ 'Modification date' => 'Datum izmjene',
+ 'Completion date' => 'Datum završetka',
+ 'Clone' => 'Kloniraj',
+ 'Project cloned successfully.' => 'Projekat uspješno kloniran.',
+ 'Unable to clone this project.' => 'Nije moguće klonirati projekat.',
+ 'Enable email notifications' => 'Omogući obavještenja e-mailom',
+ 'Task position:' => 'Pozicija zadatka:',
+ 'The task #%d have been opened.' => 'Zadatak #%d je otvoren.',
+ 'The task #%d have been closed.' => 'Zadatak #%d je zatvoren.',
+ 'Sub-task updated' => 'Pod-zadatak izmijenjen',
+ 'Title:' => 'Naslov:',
+ 'Status:' => 'Status:',
+ 'Assignee:' => 'Izvršilac:',
+ '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',
+ 'New subtask' => 'Novi pod-zadatak',
+ 'Subtask updated' => 'Pod-zadatak ažuriran',
+ 'Task updated' => 'Zadatak ažuriran',
+ 'Task closed' => 'Zadatak je zatvoren',
+ 'Task opened' => 'Zadatak je otvoren',
+ 'I want to receive notifications only for those projects:' => 'Želim obavještenja samo za ove projekte:',
+ 'view the task on Kanboard' => 'Pregledaj zadatke',
+ 'Public access' => 'Javni pristup',
+ 'Active tasks' => 'Aktivni zadaci',
+ 'Disable public access' => 'Zabrani javni pristup',
+ 'Enable public access' => 'Dozvoli javni pristup',
+ 'Public access disabled' => 'Javni pristup onemogućen!',
+ 'Do you really want to disable this project: "%s"?' => 'Da li zaista želiš da deaktiviraš projekat: "%s"?',
+ 'Do you really want to enable this project: "%s"?' => 'Da li zaista želiš da aktiviraš projekat: "%s"?',
+ 'Project activation' => 'Aktivacija projekta',
+ 'Move the task to another project' => 'Premjesti zadatak u drugi projekat',
+ 'Move to another project' => 'Premjesti u drugi projekat',
+ 'Do you really want to duplicate this task?' => 'Da li zaista želiš duplicirati ovaj zadatak?',
+ 'Duplicate a task' => 'Dupliciraj zadatak',
+ 'External accounts' => 'Vanjski korisnički računi',
+ 'Account type' => 'Tip korisničkog računa',
+ 'Local' => 'Lokalno',
+ 'Remote' => 'Udaljeno',
+ 'Enabled' => 'Omogućeno',
+ 'Disabled' => 'Onemogućeno',
+ 'Username:' => 'Korisničko ime:',
+ 'Name:' => 'Ime i Prezime',
+ 'Email:' => 'Email: ',
+ 'Notifications:' => 'Obavještenja: ',
+ 'Notifications' => 'Obavještenja',
+ 'Account type:' => 'Vrsta korisničkog računa:',
+ 'Edit profile' => 'Uredi profil',
+ 'Change password' => 'Promijeni šifru',
+ 'Password modification' => 'Izmjena šifre',
+ 'External authentications' => 'Vanjske autentikacije',
+ 'Never connected.' => 'Bez konekcija.',
+ 'No external authentication enabled.' => 'Bez omogućenih vanjskih autentikacija.',
+ 'Password modified successfully.' => 'Uspješna izmjena šifre.',
+ 'Unable to change the password.' => 'Nije moguće izmijeniti šifru.',
+ 'Change category for the task "%s"' => 'Izijmeni kategoriju zadatka "%s"',
+ 'Change category' => 'Izmijeni kategoriju',
+ '%s updated the task %s' => '%s izmijenio zadatak %s',
+ '%s opened the task %s' => '%s otvorio zadatak %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s premjestio zadatak %s na poziciju #%d u koloni "%s"',
+ '%s moved the task %s to the column "%s"' => '%s premjestio zadatak %s u kolonu "%s"',
+ '%s created the task %s' => '%s kreirao zadatak %s',
+ '%s closed the task %s' => '%s zatvorio zadatak %s',
+ '%s created a subtask for the task %s' => '%s kreirao pod-zadatak zadatka %s',
+ '%s updated a subtask for the task %s' => '%s izmijenio pod-zadatak zadatka %s',
+ 'Assigned to %s with an estimate of %s/%sh' => 'Dodijeljen korisniku %s uz procjenu vremena %s/%sh',
+ 'Not assigned, estimate of %sh' => 'Ne dodijeljen, procijenjeno vrijeme %sh',
+ '%s updated a comment on the task %s' => '%s izmijenio komentar zadatka %s',
+ '%s commented the task %s' => '%s komentarisao zadatak %s',
+ '%s\'s activity' => 'Aktivnosti %s',
+ 'RSS feed' => 'RSS kanal',
+ '%s updated a comment on the task #%d' => '%s izmijenio komentar zadatka #%d',
+ '%s commented on the task #%d' => '%s komentarisao zadatak #%d',
+ '%s updated a subtask for the task #%d' => '%s izmijenio pod-zadatak zadatka #%d',
+ '%s created a subtask for the task #%d' => '%s kreirao pod-zadatak zadatka #%d',
+ '%s updated the task #%d' => '%s ažurirao zadatak #%d',
+ '%s created the task #%d' => '%s kreirao zadatak #%d',
+ '%s closed the task #%d' => '%s zatvorio zadatak #%d',
+ '%s open the task #%d' => '%s otvorio zadatak #%d',
+ '%s moved the task #%d to the column "%s"' => '%s premjestio zadatak #%d u kolonu "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s premjestio zadatak #%d na poziciju %d u koloni "%s"',
+ 'Activity' => 'Aktivnosti',
+ 'Default values are "%s"' => 'Podrazumijevane vrijednosti su: "%s"',
+ 'Default columns for new projects (Comma-separated)' => 'Podrazumijevane kolone za novi projekat (Odvojeni zarezom)',
+ 'Task assignee change' => 'Promijena izvršioca zadatka',
+ '%s change the assignee of the task #%d to %s' => '%s zamijeni izvršioca za zadatak #%d u %s',
+ '%s changed the assignee of the task %s to %s' => '%s promijenio izvršioca za zadatak %s u %s',
+ 'New password for the user "%s"' => 'Nova šifra korisnika "%s"',
+ 'Choose an event' => 'Izaberi događaj',
+ 'Create a task from an external provider' => 'Kreiraj zadatak preko posrednika',
+ 'Change the assignee based on an external username' => 'Izmijene izvršioca bazirano na vanjskom korisničkom imenu',
+ 'Change the category based on an external label' => 'Izmijene kategorije bazirano na vanjskoj etiketi',
+ 'Reference' => 'Referenca',
+ 'Label' => 'Etiketa',
+ 'Database' => 'Baza',
+ 'About' => 'O',
+ 'Database driver:' => 'Database driver:',
+ 'Board settings' => 'Postavke table',
+ 'URL and token' => 'URL i token',
+ 'Webhook settings' => 'Postavke za webhook',
+ 'URL for task creation:' => 'URL za kreiranje zadataka',
+ 'Reset token' => 'Resetuj token',
+ 'API endpoint:' => 'API endpoint',
+ 'Refresh interval for private board' => 'Interval osvježavanja privatnih tabli',
+ 'Refresh interval for public board' => 'Interval osvježavanja javnih tabli',
+ 'Task highlight period' => 'Period naznačavanja zadatka',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Period (u sekundama) u kom su se događale promjene na zadatku (0 je onemogućeno, 2 dana je uobičajeno)',
+ 'Frequency in second (60 seconds by default)' => 'Frekvencija u sekundama (60 sekundi je uobičajeno)',
+ 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvencija u sekundama (0 je onemogućeno u budućnosti, 10 sekundi je uobičajeno)',
+ 'Application URL' => 'URL aplikacje',
+ 'Token regenerated.' => 'Token regenerisan.',
+ 'Date format' => 'Format datuma',
+ 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek 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',
+ 'There is nothing assigned to you.' => 'Ništa vam nije dodijeljeno',
+ 'My tasks' => 'Moji zadaci',
+ 'Activity stream' => 'Spisak aktivnosti',
+ 'Dashboard' => 'Panel',
+ 'Confirmation' => 'Potvrda',
+ 'Allow everybody to access to this project' => 'Dozvoli svima pristup ovom projektu',
+ 'Everybody have access to this project.' => 'Svima je dozvoljen pristup ovom projektu.',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Create a comment from an external provider' => 'Napravi komentar preko vanjskog posrednika',
+ 'Project management' => 'Upravljanje projektima',
+ 'My projects' => 'Moji projekti',
+ 'Columns' => 'Kolone',
+ 'Task' => 'Zadatak',
+ 'Your are not member of any project.' => 'Nisi član ni jednog projekta',
+ 'Percentage' => 'Procenat',
+ 'Number of tasks' => 'Broj zadataka',
+ 'Task distribution' => 'Podjela zadataka',
+ 'Reportings' => 'Izveštaji',
+ 'Task repartition for "%s"' => 'Zaduženja zadataka za "%s"',
+ 'Analytics' => 'Analiza',
+ 'Subtask' => 'Pod-zadatak',
+ 'My subtasks' => 'Moji pod-zadaci',
+ 'User repartition' => 'Zaduženja korisnika',
+ 'User repartition for "%s"' => 'Zaduženja korisnika za "%s"',
+ 'Clone this project' => 'Kloniraj ovaj projekat',
+ 'Column removed successfully.' => 'Kolona uspješno uklonjena.',
+ 'Not enough data to show the graph.' => 'Nedovoljno podataka za prikaz na grafikonu.',
+ 'Previous' => 'Prethodni',
+ 'The id must be an integer' => 'ID mora biti cjeloviti broj',
+ 'The project id must be an integer' => 'ID projekta mora biti cjeloviti broj',
+ 'The status must be an integer' => 'Status mora biti cjeloviti broj',
+ 'The subtask id is required' => 'ID pod-zadataka je obavezan',
+ 'The subtask id must be an integer' => 'ID pod-zadatka mora biti cjeloviti broj',
+ 'The task id is required' => 'ID zadatka je obavezan',
+ 'The task id must be an integer' => 'ID zadatka mora biti cjeloviti broj',
+ 'The user id must be an integer' => 'ID korisnika mora biti cjeloviti broj',
+ 'This value is required' => 'Vrijednost je obavezna',
+ 'This value must be numeric' => 'Vrijednost mora biti broj',
+ 'Unable to create this task.' => 'Nije moguće kreirati zadatak.',
+ 'Cumulative flow diagram' => 'Zbirni dijagram toka',
+ 'Cumulative flow diagram for "%s"' => 'Zbirni dijagram toka za "%s"',
+ 'Daily project summary' => 'Zbirni pregled po danima',
+ 'Daily project summary export' => 'Izvoz zbirnog pregleda po danima',
+ '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',
+ 'Default swimlane' => 'Podrazumijevana swimline traka',
+ 'Do you really want to remove this swimlane: "%s"?' => 'Da li zaista želiš ukloniti ovu swimline traku: "%s"?',
+ 'Inactive swimlanes' => 'Neaktivne swimline trake',
+ 'Remove a swimlane' => 'Ukloni swimline traku',
+ 'Show default swimlane' => 'Prikaži podrazumijevanu swimline traku',
+ 'Swimlane modification for the project "%s"' => 'Izmjene swimline trake za projekat "%s"',
+ 'Swimlane not found.' => 'Swimline traka nije pronađena.',
+ 'Swimlane removed successfully.' => 'Swimline traka uspješno uklonjena.',
+ 'Swimlanes' => 'Swimline trake',
+ 'Swimlane updated successfully.' => 'Swimline traka uspjeno ažurirana.',
+ 'The default swimlane have been updated successfully.' => 'Podrazumijevana swimline traka uspješno ažurirana.',
+ 'Unable to remove this swimlane.' => 'Nemoguće ukloniti swimline traku.',
+ 'Unable to update this swimlane.' => 'Nemoguće ažurirati swimline traku.',
+ 'Your swimlane have been created successfully.' => 'Swimline traka je uspješno kreirana.',
+ 'Example: "Bug, Feature Request, Improvement"' => 'Npr: "Greška, Zahtjev za izmjenama, Poboljšanje"',
+ 'Default categories for new projects (Comma-separated)' => 'Podrazumijevane kategorije za novi projekat',
+ 'Integrations' => 'Integracije',
+ 'Integration with third-party services' => 'Integracija sa uslugama vanjskih servisa',
+ 'Subtask Id' => 'ID pod-zadatka',
+ 'Subtasks' => 'Pod-zadaci',
+ 'Subtasks Export' => 'Izvoz pod-zadataka',
+ 'Subtasks exportation for "%s"' => 'Izvoz pod-zadataka za "%s"',
+ 'Task Title' => 'Naslov zadatka',
+ 'Untitled' => 'Bez naslova',
+ 'Application default' => 'Podrazumijevano od aplikacije',
+ 'Language:' => 'Jezik:',
+ 'Timezone:' => 'Vremenska zona:',
+ 'All columns' => 'Sve kolone',
+ 'Calendar' => 'Kalendar',
+ 'Next' => 'Slijedeći',
+ '#%d' => '#%d',
+ 'All swimlanes' => 'Sve swimline trake',
+ 'All colors' => 'Sve boje',
+ 'Moved to column %s' => 'Premješten u kolonu %s',
+ 'Change description' => 'Promijeni opis',
+ 'User dashboard' => 'Korisnički panel',
+ 'Allow only one subtask in progress at the same time for a user' => 'Dozvoli samo jedan pod-zadatak "u radu" po korisniku',
+ 'Edit column "%s"' => 'Uredi kolonu "%s"',
+ 'Select the new status of the subtask: "%s"' => 'Izaberi novi status za pod-zadatak: "%s"',
+ 'Subtask timesheet' => 'Vremenska tabela za pod-zadatak',
+ 'There is nothing to show.' => 'Nema ništa za pokazati',
+ 'Time Tracking' => 'Praćenje vremena',
+ 'You already have one subtask in progress' => 'Već imaš jedan pod-zadatak "u radu"',
+ 'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želiš duplicirati?',
+ 'Disallow login form' => 'Zabrani prijavnu formu',
+ 'Start' => 'Početak',
+ 'End' => 'Kraj',
+ '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?',
+ 'Field required' => 'Polje je obavezno',
+ 'Link added successfully.' => 'Veza je uspješno dodana.',
+ 'Link updated successfully.' => 'Veza je uspješno ažurirana.',
+ 'Link removed successfully.' => 'Veza je uspješno uklonjena.',
+ 'Link labels' => 'Veza s etiketama',
+ 'Link modification' => 'Veza modifikacija',
+ 'Links' => 'Veze',
+ 'Link settings' => 'Postavke veza',
+ 'Opposite label' => 'Suprotna etiketa',
+ 'Remove a link' => 'Ukloni vezu',
+ 'Task\'s links' => 'Veze zadatka',
+ 'The labels must be different' => 'Etikete moraju biti različite',
+ 'There is no link.' => 'Ovdje nema veza',
+ 'This label must be unique' => 'Ova etiketa mora biti jedinstvena',
+ 'Unable to create your link.' => 'Nemoguće napraviti vezu.',
+ 'Unable to update your link.' => 'Nemoguće ažurirati vezu.',
+ 'Unable to remove this link.' => 'Nemoguće ukloniti vezu.',
+ 'relates to' => 'relacija sa',
+ 'blocks' => 'blokira',
+ 'is blocked by' => 'je blokiran od',
+ 'duplicates' => 'duplicira',
+ 'is duplicated by' => 'je dupliciran od',
+ 'is a child of' => 'je dijete od',
+ 'is a parent of' => 'je roditelj od',
+ 'targets milestone' => 'cilj prekretnice',
+ 'is a milestone of' => 'je od prekretnice',
+ 'fixes' => 'popravlja',
+ 'is fixed by' => 'je popravljen od',
+ 'This task' => 'Ovaj zadatak',
+ '<1h' => '<1h',
+ '%dh' => '%dh',
+ 'Expand tasks' => 'Proširi zadatke',
+ 'Collapse tasks' => 'Skupi zadatke',
+ 'Expand/collapse tasks' => 'Proširi/skupi zadatke',
+ 'Close dialog box' => 'Skupi dialog',
+ 'Submit a form' => 'Pošalji obrazac',
+ 'Board view' => 'Pregled ploče',
+ 'Keyboard shortcuts' => 'Prečice tastature',
+ 'Open board switcher' => 'Otvori prekidače ploče',
+ 'Application' => 'Aplikacija',
+ 'Compact view' => 'Kompaktan pregled',
+ 'Horizontal scrolling' => 'Horizontalno listanje',
+ 'Compact/wide view' => 'Skupi/raširi pregled',
+ 'No results match:' => 'Nema rezultata:',
+ 'Currency' => 'Valuta',
+ 'Private project' => 'Privatni projekat',
+ 'AUD - Australian Dollar' => 'AUD - Australijski dolar',
+ 'CAD - Canadian Dollar' => 'CAD - Kanadski dolar',
+ 'CHF - Swiss Francs' => 'CHF - Švicarski franak',
+ 'Custom Stylesheet' => 'Prilagođeni stil',
+ 'download' => 'preuzmi',
+ 'EUR - Euro' => 'EUR - Evro',
+ 'GBP - British Pound' => 'GBP - Britanska funta',
+ 'INR - Indian Rupee' => 'INR - Indijski rupi',
+ 'JPY - Japanese Yen' => 'JPY - Japanski jen',
+ 'NZD - New Zealand Dollar' => 'NZD - Novozelandski dolar',
+ 'RSD - Serbian dinar' => 'RSD - Srpski dinar',
+ 'USD - US Dollar' => 'USD - Američki dolar',
+ 'Destination column' => 'Odredišna kolona',
+ 'Move the task to another column when assigned to a user' => 'Premjesti zadatak u neku drugu kolonu kada se dodijeli izvršiocu',
+ 'Move the task to another column when assignee is cleared' => 'Premjesti zadatak u neku drugu kolonu kada se ukloni izvršilac',
+ 'Source column' => 'Izvorna kolona',
+ 'Transitions' => 'Prelaz',
+ 'Executer' => 'Izvršilac',
+ 'Time spent in the column' => 'Vrijeme provedeno u koloni',
+ 'Task transitions' => 'Prelazi zadatka',
+ 'Task transitions export' => 'Izvezi prelaze zadatka',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Ovaj izvještaj sadržava sve kolone premještanja za svaki zadatak s datumom, te korisnikom i utrošenim vremenom za svaki premještaj.',
+ 'Currency rates' => 'Stopa valute',
+ 'Rate' => 'Stopa',
+ 'Change reference currency' => 'Promijeni referencu valute',
+ 'Add a new currency rate' => 'Dodaj novu stopu valute',
+ 'Reference currency' => 'Referenca valute',
+ 'The currency rate have been added successfully.' => 'Stopa valute je uspješno dodana.',
+ 'Unable to add this currency rate.' => 'Nemoguće dodati stopu valute.',
+ 'Webhook URL' => 'Webhook URL',
+ '%s remove the assignee of the task %s' => '%s je uklonio izvršioca zadatka %s',
+ 'Enable Gravatar images' => 'Omogući Gravatar slike',
+ 'Information' => 'Informacije',
+ 'Check two factor authentication code' => 'Provjera faktor-dva autentifikacionog koda',
+ 'The two factor authentication code is not valid.' => 'Faktor-dva autentifikacionog koda nije validan.',
+ 'The two factor authentication code is valid.' => 'Faktor-dva autentifikacionog koda je validan.',
+ 'Code' => 'Kod',
+ 'Two factor authentication' => 'Faktor-dva autentifikacija',
+ 'This QR code contains the key URI: ' => 'Ovaj QR kod sadržava ključni URL: ',
+ 'Check my code' => 'Provjeri moj kod',
+ 'Secret key: ' => 'Tajni ključ: ',
+ 'Test your device' => 'Testiraj svoj uređaj',
+ 'Assign a color when the task is moved to a specific column' => 'Dodijeli boju kada je zadatak pomjeren u odabranu kolonu',
+ '%s via Kanboard' => '%s uz pomoć Kanboard-a',
+ 'Burndown chart for "%s"' => 'Grafikon izgaranja za "%s"',
+ 'Burndown chart' => 'Grafikon izgaranja',
+ 'This chart show the task complexity over the time (Work Remaining).' => 'Ovaj grafikon pokazuje kompleksnost zadatka u vremenu (Preostalo vremena)',
+ 'Screenshot taken %s' => 'Slika ekrana uzeta %s',
+ 'Add a screenshot' => 'Dodaj sliku ekrana',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Uzmi sliku ekrana i pritisni CTRL+V ili ⌘+V da zalijepiš ovdje.',
+ 'Screenshot uploaded successfully.' => 'Slika ekrana uspješno dodana.',
+ 'SEK - Swedish Krona' => 'SEK - Švedska kruna',
+ 'Identifier' => 'Identifikator',
+ 'Disable two factor authentication' => 'Onemogući faktor-dva autentifikaciju',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Da li zaista želiš onemogućiti faktor-dva autentifikaciju: "%s"?',
+ 'Edit link' => 'Uredi vezu',
+ 'Start to type task title...' => 'Počni pisati naslov zadatka...',
+ 'A task cannot be linked to itself' => 'Zadatak ne može biti povezan sa samim sobom',
+ 'The exact same link already exists' => 'Ista veza već postoji',
+ 'Recurrent task is scheduled to be generated' => 'Ponavljajući zadatak je pripremljen da bude kreiran',
+ 'Score' => 'Uspjeh',
+ 'The identifier must be unique' => 'Identifikator mora biti jedinstven',
+ 'This linked task id doesn\'t exists' => 'Povezani ID zadatka ne postoji',
+ 'This value must be alphanumeric' => 'Ova vrijednost mora biti alfanumerička',
+ 'Edit recurrence' => 'Uredi ponavljanje',
+ 'Generate recurrent task' => 'Napravi ponavljajući zadatak',
+ 'Trigger to generate recurrent task' => 'Okidač koji pravi ponavljajući zadatak',
+ 'Factor to calculate new due date' => 'Faktor za računanje novog datuma završetka',
+ 'Timeframe to calculate new due date' => 'Vremenski okvir za računanje novog datuma završetka',
+ 'Base date to calculate new due date' => 'Početni datum za računanje novog datuma završetka',
+ 'Action date' => 'Datum akcije',
+ 'Base date to calculate new due date: ' => 'Početni datum za računanje novog datuma završetka: ',
+ 'This task has created this child task: ' => 'Ovaj zadatak će napraviti zadatak-dijete: ',
+ 'Day(s)' => 'Dan(i)',
+ 'Existing due date' => 'Postojeći datum završetka',
+ 'Factor to calculate new due date: ' => 'Faktor za računanje novog datuma završetka: ',
+ 'Month(s)' => 'Mjesec(i)',
+ 'Recurrence' => 'Referenca',
+ 'This task has been created by: ' => 'Ovaj zadatak je napravio: ',
+ 'Recurrent task has been generated:' => 'Ponavljajući zadatak je napravio:',
+ 'Timeframe to calculate new due date: ' => 'Vremenski okvir za računanje novog datuma završetka:',
+ 'Trigger to generate recurrent task: ' => 'Okidač za pravljenje ponavljajućeg zadatka',
+ 'When task is closed' => 'Kada je zadatak zatvoren',
+ 'When task is moved from first column' => 'Kada je zadatak premješten iz prve kolone',
+ 'When task is moved to last column' => 'Kada je zadatak premješten u posljednju kolonu',
+ 'Year(s)' => 'Godina/e',
+ 'Calendar settings' => 'Postavke kalendara',
+ 'Project calendar view' => 'Pregled kalendara projekta',
+ 'Project settings' => 'Postavke projekta',
+ 'Show subtasks based on the time tracking' => 'Prikaži pod-zadatke bazirano na vremenskom praćenju',
+ 'Show tasks based on the creation date' => 'Prikaži zadatke bazirano na vremenu otvaranja',
+ 'Show tasks based on the start date' => 'Prikaži zadatke bazirano na vremenu početka rada',
+ 'Subtasks time tracking' => 'Vremensko praćenje pod-zadataka',
+ 'User calendar view' => 'Pregled korisničkog kalendara',
+ 'Automatically update the start date' => 'Automatski ažuriraj početni datum',
+ 'iCal feed' => 'iCal kanal',
+ 'Preferences' => 'Postavke',
+ 'Security' => 'Sigurnost',
+ 'Two factor authentication disabled' => 'Faktor-dva autentifikacija onemogućena',
+ 'Two factor authentication enabled' => 'Faktor-dva autentifikacija omogućena',
+ 'Unable to update this user.' => 'Nemoguće ažurirati ovog korisnika',
+ 'There is no user management for private projects.' => 'Nema mehanizma za upravljanje korisnicima kod privatnih projekata.',
+ 'User that will receive the email' => 'Korisnik će dobiti email',
+ 'Email subject' => 'Predmet email-a',
+ 'Date' => 'Datum',
+ 'Add a comment log when moving the task between columns' => 'Dodaj komentar u dnevnik kada se pomjeri zadatak između kolona',
+ 'Move the task to another column when the category is changed' => 'Pomjeri zadatak u drugu kolonu kada je kategorija promijenjena',
+ 'Send a task by email to someone' => 'Pošalji zadatak nekome emailom',
+ 'Reopen a task' => 'Ponovo otvori zadatak',
+ 'Column change' => 'Promijena kolone',
+ 'Position change' => 'Promjena pozicije',
+ 'Swimlane change' => 'Promjena swimline trake',
+ 'Assignee change' => 'Promijenjen izvršilac',
+ '[%s] Overdue tasks' => '[%s] Zaostali zadaci',
+ 'Notification' => 'Obavještenja',
+ '%s moved the task #%d to the first swimlane' => '%s je premjestio zadatak #%d u prvu swimline traku',
+ '%s moved the task #%d to the swimlane "%s"' => '%s je premjestio zadatak #%d u swimline traku "%s"',
+ 'Swimlane' => 'Swimline traka',
+ 'Gravatar' => 'Gravatar',
+ '%s moved the task %s to the first swimlane' => '%s je premjestio zadatak %s u prvi swimline traku',
+ '%s moved the task %s to the swimlane "%s"' => '%s je premjestio zadatak %s u swimline traku "%s"',
+ 'This report contains all subtasks information for the given date range.' => 'Ovaj izvještaj sadržava sve informacije o pod-zadacima za dati period',
+ 'This report contains all tasks information for the given date range.' => 'Ovaj izvještaj sadržava sve informacije o zadacima u datom periodu',
+ 'Project activities for %s' => 'Aktivnosti projekta za %s',
+ 'view the board on Kanboard' => 'pregled ploče na Kanboard-u',
+ 'The task have been moved to the first swimlane' => 'Zadatak je premješten u prvu swimline traku',
+ 'The task have been moved to another swimlane:' => 'Zadatak je premješten u drugu swimline traku',
+ 'Overdue tasks for the project "%s"' => 'Zadaci u kašnjenju za projekat "%s"',
+ 'New title: %s' => 'Novi naslov: %s',
+ 'The task is not assigned anymore' => 'Zadatak nema više izvršioca',
+ 'New assignee: %s' => 'Novi izvršilac: %s',
+ 'There is no category now' => 'Sada nema kategorije',
+ 'New category: %s' => 'Nova kategorija: %s',
+ 'New color: %s' => 'Nova boja: %s',
+ 'New complexity: %d' => 'Nova složenost: %d',
+ 'The due date have been removed' => 'Datum završetka je ukloljen',
+ 'There is no description anymore' => 'Nema više opisa',
+ 'Recurrence settings have been modified' => 'Promijenjene postavke za ponavljajuće zadatke',
+ 'Time spent changed: %sh' => 'Utrošeno vrijeme je promijenjeno: %sh',
+ 'Time estimated changed: %sh' => 'Očekivano vrijeme je promijenjeno: %sh',
+ 'The field "%s" have been updated' => 'Polje "%s" je ažurirano',
+ 'The description have been modified' => 'Promijenjen opis',
+ 'Do you really want to close the task "%s" as well as all subtasks?' => 'Da li zaista želiš zatvoriti zadatak "%s" kao i sve pod-zadatke?',
+ 'I want to receive notifications for:' => 'Želim dobijati obavještenja za:',
+ 'All tasks' => 'Sve zadatke',
+ 'Only for tasks assigned to me' => 'Samo za zadatke na kojima sam izvršilac',
+ 'Only for tasks created by me' => 'Samo za zadatke koje sam ja napravio',
+ 'Only for tasks created by me and assigned to me' => 'Samo za zadatke koje sam ja napravio i na kojima sam izvršilac',
+ '%%Y-%%m-%%d' => '%%Y-%%m-%%d',
+ 'Total for all columns' => 'Ukupno za sve kolone',
+ 'You need at least 2 days of data to show the chart.' => 'Da bi se prikazao ovaj grafik potrebni su podaci iz najmanje posljednja dva dana.',
+ '<15m' => '<15m',
+ '<30m' => '<30m',
+ 'Stop timer' => 'Zaustavi tajmer',
+ 'Start timer' => 'Pokreni tajmer',
+ 'Add project member' => 'Dodaj člana projekta',
+ 'Enable notifications' => 'Omogući obavještenja',
+ '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',
+ 'Tasks due tomorrow' => 'Zadaci koje treba završiti sutra',
+ 'Tasks due yesterday' => 'Zadaci koje je trebalo završiti jučer',
+ 'Closed tasks' => 'Zatvoreni zadaci',
+ 'Open tasks' => 'Otvoreni zadaci',
+ 'Not assigned' => 'Bez izvršioca',
+ 'View advanced search syntax' => 'Vidi naprednu sintaksu pretrage',
+ 'Overview' => 'Opšti pregled',
+ 'Board/Calendar/List view' => 'Pregle Table/Kalendara/Liste',
+ 'Switch to the board view' => 'Promijeni da vidim tablu',
+ 'Switch to the calendar view' => 'Promijeni da vidim kalendar',
+ 'Switch to the list view' => 'Promijeni da vidim listu',
+ 'Go to the search/filter box' => 'Idi na kutiju s pretragom/filterima',
+ 'There is no activity yet.' => 'Još uvijek nema aktivnosti.',
+ 'No tasks found.' => 'Zadaci nisu pronađeni.',
+ 'Keyboard shortcut: "%s"' => 'Prečica tastature: "%s"',
+ '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',
+ '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',
+ 'Average time spent' => 'Prosjek utrošenog vremena',
+ 'This chart show the average time spent into each column for the last %d tasks.' => 'Ovaj grafik pokazuje prosjek utrošenog vremena u svakoj koloni za posljednjih %d zadataka.',
+ 'Average Lead and Cycle time' => 'Prosjek vremena upravljanja i vremenskog ciklusa',
+ 'Average lead time: ' => 'Prosjek vremena upravljanja',
+ 'Average cycle time: ' => 'Prosjek vremenskog ciklusa',
+ 'Cycle Time' => 'Vremenski ciklus',
+ 'Lead Time' => 'Vrijeme upravljanja',
+ 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Ovaj grafik pokazuje prosjek vremena vođenja i vremenskog ciklusa za posljednjih %d zadataka tokom vremena.',
+ 'Average time into each column' => 'Prosječno vrijeme u svakoj koloni',
+ 'Lead and cycle time' => 'Vrijeme vođenja i vremenski ciklus',
+ 'Lead time: ' => 'Vrijeme vođenja: ',
+ 'Cycle time: ' => 'Vremenski ciklus: ',
+ 'Time spent into each column' => 'Utrošeno vrijeme u svakoj koloni',
+ 'The lead time is the duration between the task creation and the completion.' => 'Vrijeme vođenja je vrijeme koje je proteklo između otvaranja i zatvaranja zadatka.',
+ 'The cycle time is the duration between the start date and the completion.' => 'Vremenski ciklus je vrijeme koje je proteklo između početka i završetka rada na zadatku.',
+ 'If the task is not closed the current time is used instead of the completion date.' => 'Ako zadatak nije zatvoren trenutno vrijeme je iskorišteno umjesto datuma završetka.',
+ 'Set automatically the start date' => 'Automatski postavi početno vrijeme',
+ 'Edit Authentication' => 'Uredi autentifikaciju',
+ 'Remote user' => 'Vanjski korisnik',
+ 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Vanjski korisnik ne čuva šifru u Kanboard bazi, npr: LDAP, Google i Github korisnički računi.',
+ 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Ako ste označili kvadratić "Zabrani prijavnu formu", unos pristupnih podataka u prijavnoj formi će biti ignorisan.',
+ 'New remote user' => 'Novi vanjski korisnik',
+ 'New local user' => 'Novi lokalni korisnik',
+ 'Default task color' => 'Podrazumijevana boja zadatka',
+ 'This feature does not work with all browsers.' => 'Ovaj funkcionalnost ne radi na svim internet pretraživačima.',
+ 'There is no destination project available.' => 'Nema definisanog odredišta za projekat.',
+ 'Trigger automatically subtask time tracking' => 'Okidač za automatsko vremensko praćenje za pod-zadatke',
+ 'Include closed tasks in the cumulative flow diagram' => 'Obuhvati zatvorene zadatke u kumulativnom dijagramu toka',
+ 'Current swimlane: %s' => 'Trenutna swimline traka: %s',
+ 'Current column: %s' => 'Trenutna kolona: %s',
+ 'Current category: %s' => 'Trenutna kategorija: %s',
+ 'no category' => 'bez kategorije',
+ 'Current assignee: %s' => 'Trenutni izvršilac: %s',
+ 'not assigned' => 'bez ivršioca',
+ 'Author:' => 'Autor:',
+ 'contributors' => 'saradnici',
+ 'License:' => 'Licenca:',
+ 'License' => 'Licenca',
+ 'Enter the text below' => 'Unesi tekst ispod',
+ 'Gantt chart for %s' => 'Gantogram za %s',
+ 'Sort by position' => 'Sortiraj po poziciji',
+ 'Sort by date' => 'Sortiraj po datumu',
+ 'Add task' => 'Dodaj zadatak',
+ 'Start date:' => 'Početno vrijeme:',
+ 'Due date:' => 'Vrijeme do kada treba završiti:',
+ 'There is no start date or due date for this task.' => 'Nema početnog datuma ili datuma do kada treba završiti ovaj zadatak.',
+ 'Moving or resizing a task will change the start and due date of the task.' => 'Premještanje ili promjena veličine zadatka će promijeniti datum početka i datum do kada treba završiti zadatak.',
+ 'There is no task in your project.' => 'Nema zadataka u tvom projektu.',
+ 'Gantt chart' => 'Gantogram',
+ 'People who are project managers' => 'Osobe koji su menadžeri projekta',
+ 'People who are project members' => 'Osobe koje su članovi projekta',
+ 'NOK - Norwegian Krone' => 'NOK - Norveška kruna',
+ 'Show this column' => 'Prikaži ovu kolonu',
+ 'Hide this column' => 'Sakrij ovu kolonu',
+ 'open file' => 'otvori fajl',
+ 'End date' => 'Datum završetka',
+ 'Users overview' => 'Opšti pregled korisnika',
+ 'Members' => 'Članovi',
+ 'Shared project' => 'Dijeljeni projekti',
+ 'Project managers' => 'Menadžeri projekta',
+ 'Gantt chart for all projects' => 'Gantogram za sve projekte',
+ 'Projects list' => 'Lista projekata',
+ 'Gantt chart for this project' => 'Gantogram za ovaj projekat',
+ 'Project board' => 'Tabla projekta',
+ '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',
+ 'Documentation: %s' => 'Dokumentacija: %s',
+ 'Switch to the Gantt chart view' => 'Promijeni u gantogram pregled',
+ 'Reset the search/filter box' => 'Vrati na početno pretragu/filtere',
+ 'Documentation' => 'Dokumentacija',
+ 'Table of contents' => 'Sadržaj',
+ 'Gantt' => 'Gantogram',
+ 'Author' => 'Autor',
+ 'Version' => 'Verzija',
+ 'Plugins' => 'Dodaci',
+ 'There is no plugin loaded.' => 'Nema učitanih dodataka.',
+ 'Set maximum column height' => 'Postavi maksimalnu visinu kolone',
+ 'Remove maximum column height' => 'Ukloni maksimalnu visinu kolone',
+ 'My notifications' => 'Moja obavještenja',
+ 'Custom filters' => 'Prilagođeni filteri',
+ 'Your custom filter have been created successfully.' => 'Tvoj prilagođeni filter je uspješno napravljen.',
+ 'Unable to create your custom filter.' => 'Nemoguće napraviti prilagođeni filter.',
+ 'Custom filter removed successfully.' => 'Prilagođeni filter uspješno uklonjen.',
+ 'Unable to remove this custom filter.' => 'Nemoguće ukloniti prilagođeni filter.',
+ 'Edit custom filter' => 'Uredi prilagođeni filter',
+ 'Your custom filter have been updated successfully.' => 'Prilagođeni filter uspješno ažuriran.',
+ 'Unable to update custom filter.' => 'Nemoguće ažurirati prilagođeni filter',
+ 'Web' => 'Web',
+ 'New attachment on task #%d: %s' => 'Novi priložak na zadatku #%d: %s',
+ 'New comment on task #%d' => 'Novi komentar na zadatku #%d',
+ 'Comment updated on task #%d' => 'Ažuriran komentar na zadatku #%d',
+ 'New subtask on task #%d' => 'Novi pod-zadatak na zadatku #%d',
+ 'Subtask updated on task #%d' => 'Pod-zadatak ažuriran na zadatku #%d',
+ 'New task #%d: %s' => 'Novi zadatak #%d: %s',
+ 'Task updated #%d' => 'Zadatak ažuriran #%d',
+ 'Task #%d closed' => 'Zadatak #%d zatvoren',
+ 'Task #%d opened' => 'Zadatak #%d otvoren',
+ 'Column changed for task #%d' => 'Promijenjena kolona za zadatak #%d',
+ 'New position for task #%d' => 'Nova pozicija za zadatak #%d',
+ 'Swimlane changed for task #%d' => 'Swimline traka promijenjena za zadatak #%d',
+ 'Assignee changed on task #%d' => 'Promijenjen izvršilac na zadatku #%d',
+ '%d overdue tasks' => '%d zadataka kasni',
+ 'Task #%d is overdue' => 'Zadatak #%d kasni',
+ 'No new notifications.' => 'Nema novi obavještenja.',
+ 'Mark all as read' => 'Označi sve kao pročitano',
+ 'Mark as read' => 'Označi kao pročitano',
+ 'Total number of tasks in this column across all swimlanes' => 'Ukupan broj zadataka u ovoj koloni u svim swimline trakama',
+ 'Collapse swimlane' => 'Skupi swimline trake',
+ 'Expand swimlane' => 'Proširi swimline trake',
+ 'Add a new filter' => 'Dodaj novi filter',
+ 'Share with all project members' => 'Podijeli s svim članovima projekta',
+ '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',
+ '%d task(s) have been imported successfully.' => '%d zadataka uspješno uvezeno.',
+ 'Nothing have been imported!' => 'Ništa nije uvezeno!',
+ 'Import users from CSV file' => 'Uvezi korisnike putem CSV fajla',
+ '%d user(s) have been imported successfully.' => '%d korisnika uspješno uvezeno.',
+ 'Comma' => 'Zarez',
+ 'Semi-colon' => 'Tačka-zarez',
+ 'Tab' => 'Tab',
+ 'Vertical bar' => 'Vertikalna traka',
+ 'Double Quote' => 'Dvostruki navodnici',
+ 'Single Quote' => 'Jednostruki navodnici',
+ '%s attached a file to the task #%d' => '%s je dodao novi fajl u zadatak %d',
+ 'There is no column or swimlane activated in your project!' => 'Nema kolone ili swimline trake aktivirane za ovaj projekat!',
+ 'Append filter (instead of replacement)' => 'Dodaj filter (umjesto zamjene postojećeg)',
+ 'Append/Replace' => 'Dodaj/Zamijeni',
+ 'Append' => 'Dodaj',
+ 'Replace' => 'Zamijeni',
+ 'Import' => 'Uvoz',
+ 'change sorting' => 'Promijeni sortiranje',
+ 'Tasks Importation' => 'Uvoz zadataka',
+ 'Delimiter' => 'Djelilac',
+ 'Enclosure' => 'Prilog',
+ 'CSV File' => 'CSV File',
+ 'Instructions' => 'Uputstva',
+ 'Your file must use the predefined CSV format' => 'File mora biti predefinisani CSV format',
+ 'Your file must be encoded in UTF-8' => 'File mora biti u UTF-8 kodu',
+ 'The first row must be the header' => 'Prvi red mora biti zaglavlje',
+ 'Duplicates are not verified for you' => 'Dipliciranje nisu potvrđena',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'Datum do kog se treba izvršiti mora biti u ISO formatu: GGGG-MM-DD',
+ 'Download CSV template' => 'Preuzmi CSV šablon',
+ 'No external integration registered.' => 'Nema registrovanih vanjskih integracija.',
+ 'Duplicates are not imported' => 'Duplikati nisu uvezeni',
+ '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' => '',
+ 'Assign automatically a category based on a link' => 'Automatsko pridruživanje kategorije bazirano na vezi',
+ 'BAM - Konvertible Mark' => 'BAM - Konvertibilna marka',
+ // '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' => 'Članovi projekta',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
+);
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index 2631ab2a..f03d70c5 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Vyjmout projekt',
'Edit the board for "%s"' => 'Editace nástěnky pro "%s" ',
'All projects' => 'Všechny projekty',
- 'Change columns' => 'Změna sloupců',
'Add a new column' => 'Přidat nový sloupec',
'Title' => 'Název',
'Nobody assigned' => 'Nepřiřazena žádná osoba',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Otevřít úkol',
'Do you really want to open this task: "%s"?' => 'Opravdu chcete znovuotevřít tento úkol: "%s"?',
'Back to the board' => 'Zpět na nástěnku',
- 'Created on %B %e, %Y at %k:%M %p' => 'Vytvořeno dne %d.%m.%Y v čase %H:%M',
'There is nobody assigned' => 'Není přiřazeno žádnému uživateli',
'Column on the board:' => 'Sloupec:',
- 'Status is open' => 'Status je otevřený',
- 'Status is closed' => 'Status je uzavřený',
'Close this task' => 'Uzavřít úkol',
'Open this task' => 'Aufgabe wieder öffnen',
'There is no description.' => 'Bez popisu',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'ID je vyžadováno',
'The project id is required' => 'ID projektu je vyžadováno',
'The project name is required' => 'Jméno projektu je vyžadováno',
- 'This project must be unique' => 'Jméno projektu musí být jedinečné',
'The title is required' => 'Nadpis je vyžadován',
'Settings saved successfully.' => 'Nastavení bylo úspěšně uloženo',
'Unable to save your settings.' => 'Vaše nastavení nelze uložit.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'V řešení',
'Done' => 'Dokončeno',
'Application version:' => 'Verze:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Dokončeno %d.%m.%Y v %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d.%m.%Y v %H:%M',
- 'Date created' => 'Datum vytvoření',
- 'Date completed' => 'Datum dokončení',
'Id' => 'ID',
'%d closed tasks' => '%d dokončených úkolů',
'No task for this project' => 'Tento projekt nemá žádné úkoly',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Složitost',
'Task limit' => 'Maximální počet úkolů',
'Task count' => 'Počet úkolů',
- 'Edit project access list' => 'Upravit přístupový seznam projektu',
- 'Allow this user' => 'Povolit tomuto uživateli',
- 'Don\'t forget that administrators have access to everything.' => 'Nezapomeňte, že administrátoři mají přistup ke všem údajům.',
- 'Revoke' => 'Odebrat',
- 'List of authorized users' => 'Seznam autorizovaných uživatelů',
'User' => 'Uživatel',
- 'Nobody have access to this project.' => 'Nikdo nemá přístup k tomuto projektu.',
'Comments' => 'Komentáře',
'Write your text in Markdown' => 'Můžete použít i Markdown-syntaxi',
'Leave a comment' => 'Zanechte komentář',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Editace úkolu',
'Due Date' => 'Datum splnění',
'Invalid date' => 'Neplatné datum',
- 'Must be done before %B %e, %Y' => 'Musí být dokončeno do %d.%m.%Y ',
- '%B %e, %Y' => '%d.%m.%Y',
- // '%b %e, %Y' => '',
'Automatic actions' => 'Automaticky vykonávané akce',
'Your automatic action have been created successfully.' => 'Vaše akce byla úspěšně vytvořena.',
'Unable to create your automatic action.' => 'Vaší akci nebylo možné vytvořit.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Přiřadit barvu konkrétnímu uživateli',
'Column title' => 'Název sloupce',
'Position' => 'Pozice',
- 'Move Up' => 'Posunout nahoru',
- 'Move Down' => 'Posunout dolu',
'Duplicate to another project' => 'Vytvořit kopii v jiném projektu',
'Duplicate' => 'Vytvořit kopii',
'link' => 'Link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Komentář byl smazán.',
'Unable to remove this comment.' => 'Komentář nelze odebrat.',
'Do you really want to remove this comment?' => 'Skutečně chcete odebrat tento komentář?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Přístup k této stránce mají pouze administrátoři nebo vlastníci komentáře.',
'Current password for the user "%s"' => 'Aktuální heslo pro uživatele "%s"',
'The current password is required' => 'Heslo je vyžadováno',
'Wrong password' => 'Neplatné heslo',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'E-Mail',
- 'Link my Google Account' => 'Propojit s Google účtem',
- 'Unlink my Google Account' => 'Odpojit Google účet',
- 'Login with my Google Account' => 'Přihlášení pomocí Google účtu',
- 'Project not found.' => 'Projekt nebyl nalezen.',
'Task removed successfully.' => 'Úkol byl úspěšně odebrán.',
'Unable to remove this task.' => 'Tento úkol nelze odebrat.',
'Remove a task' => 'Odebrat úkol',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maximální velikost: ',
'Unable to upload the file.' => 'Soubor nelze nahrát.',
'Display another project' => 'Zobrazit jiný projekt',
- // 'Login with my Github Account' => '',
- // 'Link my Github Account' => '',
- // 'Unlink my Github Account' => '',
'Created by %s' => 'Vytvořeno uživatelem %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Poslední úprava dne %d.%m.%Y v čase %H:%M',
'Tasks Export' => 'Export úkolů',
'Tasks exportation for "%s"' => 'Export úkolů pro "%s"',
'Start Date' => 'Počáteční datum',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Přeji si dostávat upozornění pouze pro následující projekty:',
'view the task on Kanboard' => 'Zobrazit úkol na Kanboard',
'Public access' => 'Veřejný přístup',
- 'User management' => 'Správa uživatelů',
'Active tasks' => 'Aktivní úkoly',
'Disable public access' => 'Zakázat veřejný přístup',
'Enable public access' => 'Povolit veřejný přístup',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'e-mail',
'Notifications:' => 'Upozornění:',
'Notifications' => 'Upozornění',
- 'Group:' => 'Skupina',
- 'Regular user' => 'Pravidelný uživatel',
'Account type:' => 'Typ účtu:',
'Edit profile' => 'Upravit profil',
'Change password' => 'Změnit heslo',
'Password modification' => 'Změna hesla',
'External authentications' => 'Vzdálená autorizace',
- 'Google Account' => 'Google účet',
- 'Github Account' => 'github účet',
'Never connected.' => 'Zatím nikdy nespojen.',
- 'No account linked.' => 'Žádné propojení účtu.',
- 'Account linked.' => 'Propojení účtu',
'No external authentication enabled.' => 'Není povolena žádná vzdálená autorizace.',
'Password modified successfully.' => 'Heslo bylo úspěšně změněno.',
'Unable to change the password.' => 'Nelze změnit heslo.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s změnil řešitele úkolu %s na uživatele %s',
'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"',
'Choose an event' => 'Vybrat událost',
- 'Github commit received' => 'Github commit empfangen',
- 'Github issue opened' => 'Github Fehler geöffnet',
- 'Github issue closed' => 'Github Fehler geschlossen',
- 'Github issue reopened' => 'Github Fehler erneut geöffnet',
- 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert',
- 'Github issue label change' => 'Github Fehlerkennzeichnung verändert',
'Create a task from an external provider' => 'Vytvořit úkol externím poskytovatelem',
'Change the assignee based on an external username' => 'Změna přiřazení uživatele závislá na externím uživateli',
'Change the category based on an external label' => 'Změna kategorie závislá na externím popisku',
'Reference' => 'Reference',
- 'Reference: %s' => 'Reference: %s',
// 'Label' => '',
'Database' => 'Datenbank',
'About' => 'O projektu',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frekvence v sekundách (60 sekund ve výchozím nastavení)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvence v sekundách (0 pro zákaz této vlastnosti, 10 sekund ve výchozím nastavení)',
'Application URL' => 'URL aplikace',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Ukázka: http://example.kanboard.net/ (použit v emailovém upozornění)',
'Token regenerated.' => 'Token byl opětovně generován.',
'Date format' => 'Formát datumu',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formát je vždy akceptován, například: "%s" a "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Předpokládaný čas: %s hodin',
- 'Time spent: %s hours' => 'Doba trvání: %s hodin',
- 'Started on %B %e, %Y' => 'Zahájeno %B %e %Y',
'Start date' => 'Počáteční datum',
'Time estimated' => 'Odhadovaný čas',
'There is nothing assigned to you.' => 'Nemáte přiřazenou žádnou položku.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Přístup k tomuto projektu má kdokoliv.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github Webhook',
- 'Help on Github webhooks' => 'Hilfe für Github Webhooks',
'Create a comment from an external provider' => 'Vytvořit komentář pomocí externího poskytovatele',
- 'Github issue comment created' => 'Github Fehler Kommentar hinzugefügt',
'Project management' => 'Správa projektů',
'My projects' => 'Moje projekty',
'Columns' => 'Sloupce',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Rozdělení podle uživatelů pro "%s"',
'Clone this project' => 'Duplokovat projekt',
'Column removed successfully.' => 'Sloupec byl odstraněn.',
- 'Github Issue' => 'Github Issue',
'Not enough data to show the graph.' => 'Pro zobrazení grafu není dostatek dat.',
'Previous' => 'Předchozí',
'The id must be an integer' => 'ID musí být celé číslo',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Výchozí Swimlane',
'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?',
'Inactive swimlanes' => 'Inaktive Swimlane',
- 'Set project manager' => 'Nastavit práva vedoucího projektu',
- 'Set project member' => 'Nastavit práva člena projektu',
'Remove a swimlane' => 'Odstranit swimlane',
- 'Rename' => 'Přejmenovat',
'Show default swimlane' => 'Standard Swimlane anzeigen',
'Swimlane modification for the project "%s"' => 'Swimlane Änderung für das Projekt "%s"',
'Swimlane not found.' => 'Swimlane nicht gefunden',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.',
'The default swimlane have been updated successfully.' => 'Die standard Swimlane wurden erfolgreich aktualisiert. Die standard Swimlane wurden erfolgreich aktualisiert.',
- 'Unable to create your swimlane.' => 'Es ist nicht möglich die Swimlane zu erstellen.',
'Unable to remove this swimlane.' => 'Es ist nicht möglich die Swimlane zu entfernen.',
'Unable to update this swimlane.' => 'Es ist nicht möglich die Swimöane zu ändern.',
'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.',
'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"',
'Default categories for new projects (Comma-separated)' => 'Výchozí kategorie pro nové projekty (oddělené čárkou)',
- 'Gitlab commit received' => 'Gitlab commit erhalten',
- 'Gitlab issue opened' => 'Gitlab Fehler eröffnet',
- 'Gitlab issue closed' => 'Gitlab Fehler geschlossen',
- 'Gitlab webhooks' => 'Gitlab Webhook',
- 'Help on Gitlab webhooks' => 'Hilfe für Gitlab Webhooks',
'Integrations' => 'Integrace',
'Integration with third-party services' => 'Integration von Fremdleistungen',
- 'Role for this project' => 'Role pro tento projekt',
- 'Project manager' => 'Projektový manažer',
- 'Project member' => 'Člen projektu',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Manažer projektu může změnit nastavení projektu a zároveň má více práv než standardní uživatel.',
- 'Gitlab Issue' => 'Gitlab Fehler',
'Subtask Id' => 'Dílčí úkol Id',
'Subtasks' => 'Dílčí úkoly',
'Subtasks Export' => 'Export dílčích úkolů',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'Jeden dílčí úkol již aktuálně řešíte',
'Which parts of the project do you want to duplicate?' => 'Které části projektu chcete duplikovat?',
// 'Disallow login form' => '',
- 'Bitbucket commit received' => 'Bitbucket commit erhalten',
- 'Bitbucket webhooks' => 'Bitbucket webhooks',
- 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket webhooks',
'Start' => 'Začátek',
'End' => 'Konec',
'Task age in days' => 'Doba trvání úkolu ve dnech',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Tento úkol',
'<1h' => '<1h',
'%dh' => '%dh',
- // '%b %e' => '',
'Expand tasks' => 'Rozpbalit úkoly',
'Collapse tasks' => 'Sbalit úkoly',
'Expand/collapse tasks' => 'Rozbalit / sbalit úkoly',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Klávesnicové zkratky',
'Open board switcher' => 'Otevřít přepínač nástěnek',
'Application' => 'Aplikace',
- 'since %B %e, %Y at %k:%M %p' => 'dne %d.%m.%Y v čase %H:%M',
'Compact view' => 'Kompaktní zobrazení',
'Horizontal scrolling' => 'Horizontální rolování',
'Compact/wide view' => 'Kompaktní/plné zobrazení',
'No results match:' => 'Žádná shoda:',
'Currency' => 'Měna',
- 'Files' => 'Soubory',
- 'Images' => 'Obrázky',
'Private project' => 'Soukromý projekt',
// 'AUD - Australian Dollar' => '',
// 'CAD - Canadian Dollar' => '',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.',
'Code' => 'Code',
'Two factor authentication' => 'Dvouúrovňová autorizace',
- 'Enable/disable two factor authentication' => 'Povolit / zakázat dvou úrovňovou autorizaci',
'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Speichere den geheimen Schlüssel in deiner TOTP software (z.B. Google Authenticator oder FreeOTP).',
'Check my code' => 'Kontrola mého kódu',
'Secret key: ' => 'Tajný klíč',
'Test your device' => 'Test Vašeho zařízení',
'Assign a color when the task is moved to a specific column' => 'Přiřadit barvu, když je úkol přesunut do konkrétního sloupce',
'%s via Kanboard' => '%s via Kanboard',
- 'uploaded by: %s' => 'Nahráno uživatelem: %s',
- 'uploaded on: %s' => 'Nahráno dne: %s',
- 'size: %s' => 'Velikost: %s',
'Burndown chart for "%s"' => 'Burndown-Chart für "%s"',
'Burndown chart' => 'Burndown-Chart',
'This chart show the task complexity over the time (Work Remaining).' => 'Graf zobrazuje složitost úkolů v čase (Zbývající práce).',
@@ -722,7 +645,6 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Nimm einen Screenshot auf und drücke STRG+V oder ⌘+V um ihn hier einzufügen.',
'Screenshot uploaded successfully.' => 'Screenshot erfolgreich hochgeladen.',
'SEK - Swedish Krona' => 'SEK - Schwedische Kronen',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identifikátor projektu je volitelný alfanumerický kód používaný k identifikaci vašeho projektu.',
'Identifier' => 'Identifikator',
'Disable two factor authentication' => 'Zrušit dvou stupňovou autorizaci',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
'User that will receive the email' => 'Uživatel, který dostane E-mail',
'Email subject' => 'E-mail Předmět',
'Date' => 'Datum',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
'Add a comment log when moving the task between columns' => 'Přidat komentář když je úkol přesouván mezi sloupci',
'Move the task to another column when the category is changed' => 'Přesun úkolu do jiného sloupce když je změněna kategorie',
'Send a task by email to someone' => 'Poslat někomu úkol poštou',
'Reopen a task' => 'Znovu otevřít úkol',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
'Column change' => 'Spalte geändert',
'Position change' => 'Position geändert',
'Swimlane change' => 'Swimlane geändert',
@@ -826,17 +736,11 @@ return array(
'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert',
'The description have been modified' => 'Die Beschreibung wurde geändert',
'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)',
- // 'Swimlane: %s' => '',
'I want to receive notifications for:' => 'Chci dostávat upozornění na:',
'All tasks' => 'Všechny úkoly',
'Only for tasks assigned to me' => 'pouze pro moje úkoly',
'Only for tasks created by me' => 'pouze pro mnou vytvořené úkoly',
'Only for tasks created by me and assigned to me' => 'pouze pro mnou vytvořené a mě přiřazené úkoly',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- 'New due date: %B %e, %Y' => 'Neues Ablaufdatum: %B %e, %Y',
- 'Start date changed: %B %e, %Y' => 'Neues Beginndatum: %B %e, %Y',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
'Total for all columns' => 'S',
'You need at least 2 days of data to show the chart.' => 'Potřebujete nejméně data ze dvou dnů pro zobrazení grafu',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'Nepřiřazené',
'View advanced search syntax' => 'Zobrazit syntaxi rozšířeného vyhledávání',
'Overview' => 'Přehled',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Nástěnka/Kalendář/Zobrazení seznamu',
'Switch to the board view' => 'Přepnout na nástěnku',
'Switch to the calendar view' => 'Přepnout na kalendář',
@@ -894,10 +797,6 @@ return array(
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Graf ukazuje průměrnou dodací lhůtu a dobu cyklu pro posledních %d úkolů v průběhu času',
'Average time into each column' => 'Průměrná doba v každé fázi',
'Lead and cycle time' => 'Dodací lhůta a doba cyklu',
- 'Google Authentication' => 'Ověřování pomocí služby Google',
- 'Help on Google authentication' => 'Nápověda k ověřování pomocí služby Google',
- 'Github Authentication' => 'Ověřování pomocí služby Github',
- 'Help on Github authentication' => 'Nápověda k ověřování pomocí služby Github',
'Lead time: ' => 'Dodací lhůta: ',
'Cycle time: ' => 'Doba cyklu: ',
'Time spent into each column' => 'Čas strávený v každé fázi',
@@ -906,18 +805,12 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Jestliže není úkol uzavřen, místo termínu dokončení je použit aktuální čas.',
'Set automatically the start date' => 'Nastavit automaticky počáteční datum',
'Edit Authentication' => 'Upravit ověřování',
- 'Google Id' => 'Google ID',
- 'Github Id' => 'Github ID',
'Remote user' => 'Vzdálený uživatel',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Hesla vzdáleným uživatelům se neukládají do databáze Kanboard. Naříklad: LDAP, Google a Github účty.',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Pokud zaškrtnete políčko "Zakázat přihlašovací formulář", budou pověření zadané do přihlašovacího formuláře ignorovány.',
- 'By @%s on Gitlab' => 'uživatelem @%s na Gitlab',
- 'Gitlab issue comment created' => 'Vytvořen komentář problému na Gitlab',
'New remote user' => 'Nový vzdálený uživatel',
'New local user' => 'Nový lokální uživatel',
'Default task color' => 'Výchozí barva úkolu',
- 'Hide sidebar' => 'Schovat postranní panel',
- 'Expand sidebar' => 'Rozbalit postranní panel',
'This feature does not work with all browsers.' => 'Tato funkcionalita nefunguje ve všech prohlížečích.',
'There is no destination project available.' => 'Není dostupný žádný cílový projekt.',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'přispěvatelé',
'License:' => 'Licence:',
'License' => 'Licence',
- 'Project Administrator' => 'Administrátor projektu',
'Enter the text below' => 'Zadejte text níže',
'Gantt chart for %s' => 'Gantt graf pro %s',
'Sort by position' => 'Třídit podle pozice',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 6d221a7a..03180b5a 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Fjern projekt',
'Edit the board for "%s"' => 'Rediger boardet for "%s"',
'All projects' => 'Alle Projekter',
- 'Change columns' => 'Ændre kolonner',
'Add a new column' => 'Tilføj en ny kolonne',
'Title' => 'Titel',
'Nobody assigned' => 'Ingen ansvarlig',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Åben en opgave',
'Do you really want to open this task: "%s"?' => 'Vil du virkelig åbne denne opgave: "%s"?',
'Back to the board' => 'Tilbage til boardet',
- 'Created on %B %e, %Y at %k:%M %p' => 'Oprettet %d.%m.%Y - %H:%M',
'There is nobody assigned' => 'Der er ingen tilføjet',
'Column on the board:' => 'Kolonne:',
- 'Status is open' => 'Status er åbnet',
- 'Status is closed' => 'Status er lukket',
'Close this task' => 'Luk denne opgave',
'Open this task' => 'Åben denne opgave',
'There is no description.' => 'Der er ingen beskrivning.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Id\'et er krævet',
'The project id is required' => 'Projektets id er krævet',
'The project name is required' => 'Projektets navn er krævet',
- 'This project must be unique' => 'Projektets navn skal være unikt',
'The title is required' => 'Titel er krævet',
'Settings saved successfully.' => 'Indstillinger gemt.',
'Unable to save your settings.' => 'Indstillinger kunne ikke gemmes.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Igangværende',
'Done' => 'Færdig',
'Application version:' => 'Version:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Fuldført %d.%m.%Y - %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d.%m.%Y - %H:%M',
- 'Date created' => 'Dato for oprettelse',
- 'Date completed' => 'Dato for fuldført',
'Id' => 'ID',
'%d closed tasks' => '%d lukket opgavet',
'No task for this project' => 'Ingen opgaver i dette projekt',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Kompleksitet',
'Task limit' => 'Opgave begrænsning',
// 'Task count' => '',
- 'Edit project access list' => 'Rediger adgangstilladelser for projektet',
- 'Allow this user' => 'Tillad denne bruger',
- 'Don\'t forget that administrators have access to everything.' => 'Glem ikke at administratorer har adgang til alt.',
- 'Revoke' => 'Fjern',
- 'List of authorized users' => 'Liste over autoriserede brugere',
'User' => 'Bruger',
- 'Nobody have access to this project.' => 'Ingen har adgang til dette projekt.',
'Comments' => 'Kommentarer',
'Write your text in Markdown' => 'Skriv din tekst i markdown',
'Leave a comment' => 'Efterlad en kommentar',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Rediger denne opgave',
'Due Date' => 'Forfaldsdato',
'Invalid date' => 'Ugyldig dato',
- 'Must be done before %B %e, %Y' => 'Skal være fuldført inden %d.%m.%Y',
- '%B %e, %Y' => '%d.%m.%Y',
- // '%b %e, %Y' => '',
'Automatic actions' => 'Automatiske handlinger',
'Your automatic action have been created successfully.' => 'Din automatiske handling er oprettet.',
'Unable to create your automatic action.' => 'Din automatiske handling kunne ikke oprettes.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Tildel en farve til en bestemt bruger',
'Column title' => 'Kolonne titel',
'Position' => 'Position',
- 'Move Up' => 'Ryk op',
- 'Move Down' => 'Ryk ned',
'Duplicate to another project' => 'Kopier til et andet projekt',
'Duplicate' => 'Kopier',
'link' => 'link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Kommentaren blev fjernet.',
'Unable to remove this comment.' => 'Kommentaren kunne ikke fjernes.',
'Do you really want to remove this comment?' => 'Vil du virkelig fjerne denne kommentar?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Kun administratore eller brugeren, som har oprettet kommentaren har adgang til denne side.',
'Current password for the user "%s"' => 'Aktuelle adgangskode for brugeren "%s"',
'The current password is required' => 'Den aktuelle adgangskode er krævet',
'Wrong password' => 'Forkert adgangskode',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'E-Mail',
- 'Link my Google Account' => 'Forbind min Google-konto',
- 'Unlink my Google Account' => 'Fjern forbindelsen til min Google-konto',
- 'Login with my Google Account' => 'Login med min Google-konto',
- 'Project not found.' => 'Projekt ikke fundet.',
'Task removed successfully.' => 'Opgaven er fjernet.',
'Unable to remove this task.' => 'Opgaven kunne ikke fjernes.',
'Remove a task' => 'Fjern en opgave',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maksimum størrelse: ',
'Unable to upload the file.' => 'Filen kunne ikke uploades.',
'Display another project' => 'Vis et andet projekt...',
- 'Login with my Github Account' => 'Login med min Github-konto',
- 'Link my Github Account' => 'Forbind min Github-konto',
- 'Unlink my Github Account' => 'Fjern forbindelsen til min Github-konto',
'Created by %s' => 'Oprettet af %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Sidst redigeret %d.%m.%Y - %H:%M',
'Tasks Export' => 'Opgave eksport',
'Tasks exportation for "%s"' => 'Opgave eksport for "%s"',
'Start Date' => 'Start-dato',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Jeg vil kun have notifikationer for disse projekter:',
'view the task on Kanboard' => 'se opgaven på Kanboard',
'Public access' => 'Offentlig adgang',
- 'User management' => 'Brugerstyring',
'Active tasks' => 'Aktive opgaver',
'Disable public access' => 'Deaktiver offentlig adgang',
'Enable public access' => 'Aktivér offentlig adgang',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Email:',
'Notifications:' => 'Notifikationer:',
'Notifications' => 'Notifikationer',
- 'Group:' => 'Gruppe:',
- 'Regular user' => 'Normal bruger',
'Account type:' => 'Konto type:',
'Edit profile' => 'Rediger profil',
'Change password' => 'Skift adgangskode',
'Password modification' => 'Adgangskode ændring',
'External authentications' => 'Ekstern autentificering',
- 'Google Account' => 'Google-konto',
- 'Github Account' => 'Github-konto',
'Never connected.' => 'Aldrig forbundet.',
- 'No account linked.' => 'Ingen kontoer forfundet.',
- 'Account linked.' => 'Konto forbundet.',
'No external authentication enabled.' => 'Ingen eksterne autentificering aktiveret.',
'Password modified successfully.' => 'Adgangskode ændret.',
'Unable to change the password.' => 'Adgangskoden kunne ikke ændres.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s skift ansvarlig for opgaven %s til %s',
'New password for the user "%s"' => 'Ny adgangskode for brugeren "%s"',
'Choose an event' => 'Vælg et event',
- 'Github commit received' => 'Github commit modtaget',
- 'Github issue opened' => 'Github problem åbet',
- 'Github issue closed' => 'Github problem lukket',
- 'Github issue reopened' => 'Github problem genåbnet',
- 'Github issue assignee change' => 'Github problem ansvarlig skift',
- 'Github issue label change' => 'Github problem label skift',
'Create a task from an external provider' => 'Opret en opgave fra en ekstern udbyder',
'Change the assignee based on an external username' => 'Skift den ansvarlige baseret på et eksternt brugernavn',
'Change the category based on an external label' => 'Skift kategorien baseret på en ekstern label',
'Reference' => 'Reference',
- 'Reference: %s' => 'Reference: %s',
'Label' => 'Label',
'Database' => 'Database',
'About' => 'Om',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frekevens i sekunder (60 sekunder som standard)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvens i sekunder (0 for at deaktivere denne funktion, 10 sekunder som standard)',
'Application URL' => 'Applikation URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Eksempel: http://example.kanboard.net/ (bruges til email notifikationer)',
'Token regenerated.' => 'Token regenereret.',
'Date format' => 'Dato format',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er altid accepteret, eksempelvis: "%s" og "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Estimeret tid: %s timer',
- 'Time spent: %s hours' => 'Tid brugt: %s timer',
- 'Started on %B %e, %Y' => 'Startet %d.%m.%Y ',
'Start date' => 'Start dato',
'Time estimated' => 'Tid estimeret',
'There is nothing assigned to you.' => 'Der er ingenting tildelt til dig.',
@@ -496,10 +449,7 @@ return array(
// 'Everybody have access to this project.' => '',
// 'Webhooks' => '',
// 'API' => '',
- // 'Github webhooks' => '',
- // 'Help on Github webhooks' => '',
// 'Create a comment from an external provider' => '',
- // 'Github issue comment created' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
@@ -517,7 +467,6 @@ return array(
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
- // 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
@@ -547,10 +496,7 @@ return array(
// 'Default swimlane' => '',
// 'Do you really want to remove this swimlane: "%s"?' => '',
// 'Inactive swimlanes' => '',
- // 'Set project manager' => '',
- // 'Set project member' => '',
// 'Remove a swimlane' => '',
- // 'Rename' => '',
// 'Show default swimlane' => '',
// 'Swimlane modification for the project "%s"' => '',
// 'Swimlane not found.' => '',
@@ -558,24 +504,13 @@ return array(
// 'Swimlanes' => '',
// 'Swimlane updated successfully.' => '',
// 'The default swimlane have been updated successfully.' => '',
- // 'Unable to create your swimlane.' => '',
// '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)' => '',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
// 'Integrations' => '',
// 'Integration with third-party services' => '',
- // 'Role for this project' => '',
- // 'Project manager' => '',
- // 'Project member' => '',
- // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '',
- // 'Gitlab Issue' => '',
// 'Subtask Id' => '',
// 'Subtasks' => '',
// 'Subtasks Export' => '',
@@ -603,9 +538,6 @@ return array(
// 'You already have one subtask in progress' => '',
// 'Which parts of the project do you want to duplicate?' => '',
// 'Disallow login form' => '',
- // 'Bitbucket commit received' => '',
- // 'Bitbucket webhooks' => '',
- // 'Help on Bitbucket webhooks' => '',
// 'Start' => '',
// 'End' => '',
// 'Task age in days' => '',
@@ -646,7 +578,6 @@ return array(
// 'This task' => '',
// '<1h' => '',
// '%dh' => '',
- // '%b %e' => '',
// 'Expand tasks' => '',
// 'Collapse tasks' => '',
// 'Expand/collapse tasks' => '',
@@ -656,14 +587,11 @@ return array(
// 'Keyboard shortcuts' => '',
// 'Open board switcher' => '',
// 'Application' => '',
- // 'since %B %e, %Y at %k:%M %p' => '',
// 'Compact view' => '',
// 'Horizontal scrolling' => '',
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
- // 'Files' => '',
- // 'Images' => '',
// 'Private project' => '',
// 'AUD - Australian Dollar' => '',
// 'CAD - Canadian Dollar' => '',
@@ -703,17 +631,12 @@ return array(
// 'The two factor authentication code is valid.' => '',
// 'Code' => '',
// 'Two factor authentication' => '',
- // 'Enable/disable two factor authentication' => '',
// 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
// 'Check my code' => '',
// 'Secret key: ' => '',
// 'Test your device' => '',
// 'Assign a color when the task is moved to a specific column' => '',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -722,7 +645,6 @@ return array(
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
// 'User that will receive the email' => '',
// 'Email subject' => '',
// 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
// 'Add a comment log when moving the task between columns' => '',
// 'Move the task to another column when the category is changed' => '',
// 'Send a task by email to someone' => '',
// 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
// 'Column change' => '',
// 'Position change' => '',
// 'Swimlane change' => '',
@@ -826,17 +736,11 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
// 'I want to receive notifications for:' => '',
// 'All tasks' => '',
// 'Only for tasks assigned to me' => '',
// 'Only for tasks created by me' => '',
// 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
// 'Total for all columns' => '',
// 'You need at least 2 days of data to show the chart.' => '',
@@ -861,7 +765,6 @@ return array(
// 'Not assigned' => '',
// 'View advanced search syntax' => '',
// 'Overview' => '',
- // '%b %e %Y' => '',
// 'Board/Calendar/List view' => '',
// 'Switch to the board view' => '',
// 'Switch to the calendar view' => '',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -906,18 +805,12 @@ return array(
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
// 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
// 'New remote user' => '',
// 'New local user' => '',
// 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
// 'This feature does not work with all browsers.' => '',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
// 'contributors' => '',
// 'License:' => '',
// 'License' => '',
- // 'Project Administrator' => '',
// 'Enter the text below' => '',
// 'Gantt chart for %s' => '',
// 'Sort by position' => '',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index e566c0f6..a1ec2604 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -20,15 +20,15 @@ return array(
'Red' => 'Rot',
'Orange' => 'Orange',
'Grey' => 'Grau',
- 'Brown' => 'Bran',
+ 'Brown' => 'Braun',
'Deep Orange' => 'Dunkelorange',
'Dark Grey' => 'Dunkelgrau',
'Pink' => 'Pink',
'Teal' => 'Türkis',
'Cyan' => 'Cyan',
- 'Lime' => 'Grün (lime)',
+ 'Lime' => 'Limette',
'Light Green' => 'Hellgrün',
- 'Amber' => 'Braun (Amber)',
+ 'Amber' => 'Bernstein',
'Save' => 'Speichern',
'Login' => 'Anmelden',
'Official website:' => 'Offizielle Webseite:',
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Projekt löschen',
'Edit the board for "%s"' => 'Pinnwand für "%s" bearbeiten',
'All projects' => 'Alle Projekte',
- 'Change columns' => 'Spalten ändern',
'Add a new column' => 'Neue Spalte hinzufügen',
'Title' => 'Titel',
'Nobody assigned' => 'Nicht zugeordnet',
@@ -96,17 +95,14 @@ return array(
'Edit a task' => 'Aufgabe bearbeiten',
'Column' => 'Spalte',
'Color' => 'Farbe',
- 'Assignee' => 'Zuständig',
+ 'Assignee' => 'Zuständiger',
'Create another task' => 'Weitere Aufgabe erstellen',
'New task' => 'Neue Aufgabe',
'Open a task' => 'Öffne eine Aufgabe',
'Do you really want to open this task: "%s"?' => 'Soll diese Aufgabe wirklich wieder geöffnet werden: "%s"?',
'Back to the board' => 'Zurück zur Pinnwand',
- 'Created on %B %e, %Y at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M',
'There is nobody assigned' => 'Die Aufgabe wurde niemandem zugewiesen',
'Column on the board:' => 'Spalte:',
- 'Status is open' => 'Status ist geöffnet',
- 'Status is closed' => 'Status ist geschlossen',
'Close this task' => 'Aufgabe schließen',
'Open this task' => 'Aufgabe wieder öffnen',
'There is no description.' => 'Keine Beschreibung vorhanden.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Die ID ist anzugeben',
'The project id is required' => 'Die Projekt ID ist anzugeben',
'The project name is required' => 'Der Projektname ist anzugeben',
- 'This project must be unique' => 'Der Projektname muss eindeutig sein',
'The title is required' => 'Der Titel ist anzugeben',
'Settings saved successfully.' => 'Einstellungen erfolgreich gespeichert.',
'Unable to save your settings.' => 'Speichern der Einstellungen nicht möglich.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'In Arbeit',
'Done' => 'Erledigt',
'Application version:' => 'Version:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Abgeschlossen am %d.%m.%Y um %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d.%m.%Y um %H:%M',
- 'Date created' => 'Erstellt am',
- 'Date completed' => 'Abgeschlossen am',
'Id' => 'ID',
'%d closed tasks' => '%d abgeschlossene Aufgaben',
'No task for this project' => 'Keine Aufgaben in diesem Projekt',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Komplexität',
'Task limit' => 'Maximale Anzahl von Aufgaben',
'Task count' => 'Aufgabenanzahl',
- 'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten',
- 'Allow this user' => 'Diesen Benutzer autorisieren',
- 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugriff.',
- 'Revoke' => 'Entfernen',
- 'List of authorized users' => 'Liste der autorisierten Benutzer',
'User' => 'Benutzer',
- 'Nobody have access to this project.' => 'Niemand hat Zugriff auf dieses Projekt.',
'Comments' => 'Kommentare',
'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax',
'Leave a comment' => 'Kommentar eingeben',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Aufgabe bearbeiten',
'Due Date' => 'Fällig am',
'Invalid date' => 'Ungültiges Datum',
- 'Must be done before %B %e, %Y' => 'Muss vor dem %d.%m.%Y erledigt werden',
- '%B %e, %Y' => '%d.%m.%Y',
- '%b %e, %Y' => '%d.%m.%Y',
'Automatic actions' => 'Automatische Aktionen',
'Your automatic action have been created successfully.' => 'Die automatische Aktion wurde erfolgreich erstellt.',
'Unable to create your automatic action.' => 'Erstellen der automatischen Aktion nicht möglich.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Einem Benutzer eine Farbe zuordnen',
'Column title' => 'Spaltentitel',
'Position' => 'Position',
- 'Move Up' => 'nach oben',
- 'Move Down' => 'nach unten',
'Duplicate to another project' => 'In ein anderes Projekt duplizieren',
'Duplicate' => 'Duplizieren',
'link' => 'Link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Kommentar erfolgreich gelöscht.',
'Unable to remove this comment.' => 'Löschen des Kommentars nicht möglich.',
'Do you really want to remove this comment?' => 'Soll dieser Kommentar wirklich gelöscht werden?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars haben Zugriff auf diese Seite.',
'Current password for the user "%s"' => 'Aktuelles Passwort des Benutzers "%s"',
'The current password is required' => 'Das aktuelle Passwort wird benötigt',
'Wrong password' => 'Falsches Passwort',
@@ -267,10 +246,6 @@ return array(
'External authentication failed' => 'Externe Authentifizierung fehlgeschlagen',
'Your external account is linked to your profile successfully.' => 'Dein externer Account wurde erfolgreich mit deinem Profil verbunden',
'Email' => 'E-Mail',
- 'Link my Google Account' => 'Verbinde meinen Google-Account',
- 'Unlink my Google Account' => 'Verbindung mit meinem Google-Account trennen',
- 'Login with my Google Account' => 'Anmelden mit meinem Google-Account',
- 'Project not found.' => 'Das Projekt wurde nicht gefunden.',
'Task removed successfully.' => 'Aufgabe erfolgreich gelöscht.',
'Unable to remove this task.' => 'Löschen der Aufgabe nicht möglich.',
'Remove a task' => 'Aufgabe löschen',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maximalgröße: ',
'Unable to upload the file.' => 'Hochladen der Datei nicht möglich.',
'Display another project' => 'Zu Projekt wechseln',
- 'Login with my Github Account' => 'Anmelden mit meinem Github-Account',
- 'Link my Github Account' => 'Mit meinem Github-Account verbinden',
- 'Unlink my Github Account' => 'Verbindung mit meinem Github-Account trennen',
'Created by %s' => 'Erstellt durch %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M',
'Tasks Export' => 'Aufgaben exportieren',
'Tasks exportation for "%s"' => 'Aufgaben exportieren für "%s"',
'Start Date' => 'Anfangsdatum',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Ich möchte nur für diese Projekte Benachrichtigungen erhalten:',
'view the task on Kanboard' => 'diese Aufgabe auf dem Kanboard zeigen',
'Public access' => 'Öffentlicher Zugriff',
- 'User management' => 'Benutzer verwalten',
'Active tasks' => 'Aktive Aufgaben',
'Disable public access' => 'Öffentlichen Zugriff deaktivieren',
'Enable public access' => 'Öffentlichen Zugriff aktivieren',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'E-Mail',
'Notifications:' => 'Benachrichtigungen:',
'Notifications' => 'Benachrichtigungen',
- 'Group:' => 'Gruppe',
- 'Regular user' => 'Standardbenutzer',
'Account type:' => 'Accounttyp:',
'Edit profile' => 'Profil bearbeiten',
'Change password' => 'Passwort ändern',
'Password modification' => 'Passwortänderung',
'External authentications' => 'Externe Authentisierungsmethoden',
- 'Google Account' => 'Google-Account',
- 'Github Account' => 'Github-Account',
'Never connected.' => 'Noch nie verbunden.',
- 'No account linked.' => 'Kein Account verbunden.',
- 'Account linked.' => 'Account verbunden',
'No external authentication enabled.' => 'Es sind keine externen Authentisierungsmethoden aktiv.',
'Password modified successfully.' => 'Passwort wurde erfolgreich geändert.',
'Unable to change the password.' => 'Passwort konnte nicht geändert werden.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s',
'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"',
'Choose an event' => 'Aktion wählen',
- 'Github commit received' => 'Github commit empfangen',
- 'Github issue opened' => 'Github Fehler geöffnet',
- 'Github issue closed' => 'Github Fehler geschlossen',
- 'Github issue reopened' => 'Github Fehler erneut geöffnet',
- 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert',
- 'Github issue label change' => 'Github Fehlerkennzeichnung verändert',
'Create a task from an external provider' => 'Eine Aufgabe durch einen externen Provider hinzufügen',
'Change the assignee based on an external username' => 'Zuordnung ändern basierend auf externem Benutzernamen',
'Change the category based on an external label' => 'Kategorie basierend auf einer externen Kennzeichnung ändern',
'Reference' => 'Referenz',
- 'Reference: %s' => 'Referenz: %s',
'Label' => 'Kennzeichnung',
'Database' => 'Datenbank',
'About' => 'Über',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frequenz in Sekunden (standardmäßig 60 Sekunden)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequenz in Sekunden (0 um diese Funktion zu deaktivieren, standardmäßig 10 Sekunden)',
'Application URL' => 'Applikations-URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Beispiel: http://example.kanboard.net/ (wird für E-Mail-Benachrichtigungen verwendet)',
'Token regenerated.' => 'Token wurde neu generiert.',
'Date format' => 'Datumsformat',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Geplante Zeit: %s Stunden',
- 'Time spent: %s hours' => 'Aufgewendete Zeit: %s Stunden',
- 'Started on %B %e, %Y' => 'Gestartet am %B %e %Y',
'Start date' => 'Startdatum',
'Time estimated' => 'Geschätzte Zeit',
'There is nothing assigned to you.' => 'Ihnen ist nichts zugewiesen.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Jeder hat Zugriff zu diesem Projekt',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github-Webhook',
- 'Help on Github webhooks' => 'Hilfe für Github-Webhooks',
'Create a comment from an external provider' => 'Kommentar eines externen Providers hinzufügen',
- 'Github issue comment created' => 'Kommentar zum Github-Issue hinzugefügt',
'Project management' => 'Projektmanagement',
'My projects' => 'Meine Projekte',
'Columns' => 'Spalten',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Benutzerverteilung für "%s"',
'Clone this project' => 'Projekt kopieren',
'Column removed successfully.' => 'Spalte erfolgreich entfernt.',
- 'Github Issue' => 'Github Issue',
'Not enough data to show the graph.' => 'Nicht genügend Daten, um die Grafik zu zeigen.',
'Previous' => 'Vorherige',
'The id must be an integer' => 'Die Id muss eine ganze Zahl sein',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Standard-Swimlane',
'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?',
'Inactive swimlanes' => 'Inaktive Swimlane',
- 'Set project manager' => 'zum Projektmanager machen',
- 'Set project member' => 'zum Projektmitglied machen',
'Remove a swimlane' => 'Swimlane entfernen',
- 'Rename' => 'umbenennen',
'Show default swimlane' => 'Standard-Swimlane anzeigen',
'Swimlane modification for the project "%s"' => 'Swimlane-Änderung für das Projekt "%s"',
'Swimlane not found.' => 'Swimlane nicht gefunden',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.',
'The default swimlane have been updated successfully.' => 'Die Standard-Swimlane wurden erfolgreich aktualisiert. Die Standard-Swimlane wurden erfolgreich aktualisiert.',
- 'Unable to create your swimlane.' => 'Es ist nicht möglich, Swimlane zu erstellen.',
'Unable to remove this swimlane.' => 'Es ist nicht möglich, die Swimlane zu entfernen.',
'Unable to update this swimlane.' => 'Es ist nicht möglich, die Swimlane zu ändern.',
'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.',
'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"',
'Default categories for new projects (Comma-separated)' => 'Standard-Kategorien für neue Projekte (Komma-getrennt)',
- 'Gitlab commit received' => 'Gitlab-Commit erhalten',
- 'Gitlab issue opened' => 'Gitlab-Issue eröffnet',
- 'Gitlab issue closed' => 'Gitlab-Issue geschlossen',
- 'Gitlab webhooks' => 'Gitlab-Webhook',
- 'Help on Gitlab webhooks' => 'Hilfe für Gitlab-Webhooks',
'Integrations' => 'Integration',
'Integration with third-party services' => 'Integration von externen Diensten',
- 'Role for this project' => 'Rolle für dieses Projekt',
- 'Project manager' => 'Projektmanager',
- 'Project member' => 'Projektmitglied',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Ein Projektmanager kann die Projekteinstellungen ändern und hat mehr Rechte als ein normaler Benutzer.',
- 'Gitlab Issue' => 'Gitlab-Issue',
'Subtask Id' => 'Teilaufgaben-ID',
'Subtasks' => 'Teilaufgaben',
'Subtasks Export' => 'Export von Teilaufgaben',
@@ -588,7 +523,7 @@ return array(
'All columns' => 'Alle Spalten',
'Calendar' => 'Kalender',
'Next' => 'Nächste',
- // '#%d' => '',
+ '#%d' => 'Nr %d',
'All swimlanes' => 'Alle Swimlanes',
'All colors' => 'Alle Farben',
'Moved to column %s' => 'In Spalte %s verschoben',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'Bereits eine Teilaufgabe in Bearbeitung',
'Which parts of the project do you want to duplicate?' => 'Welcher Teil des Projekts soll kopiert werden?',
'Disallow login form' => 'Verbiete Login-Formular',
- 'Bitbucket commit received' => 'Bitbucket-Commit erhalten',
- 'Bitbucket webhooks' => 'Bitbucket-Webhooks',
- 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket-Webhooks',
'Start' => 'Start',
'End' => 'Ende',
'Task age in days' => 'Aufgabenalter in Tagen',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Diese Aufgabe',
'<1h' => '<1Std',
'%dh' => '%dStd',
- // '%b %e' => '',
'Expand tasks' => 'Aufgaben aufklappen',
'Collapse tasks' => 'Aufgaben zusammenklappen',
'Expand/collapse tasks' => 'Aufgaben auf/zuklappen',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Tastaturkürzel',
'Open board switcher' => 'Pinnwandauswahl öffnen',
'Application' => 'Anwendung',
- 'since %B %e, %Y at %k:%M %p' => 'seit %B %e, %Y um %k:%M %p',
'Compact view' => 'Kompaktansicht',
'Horizontal scrolling' => 'Horizontales Scrollen',
'Compact/wide view' => 'Kompakt/Breite-Ansicht',
'No results match:' => 'Keine Ergebnisse:',
'Currency' => 'Währung',
- 'Files' => 'Dateien',
- 'Images' => 'Bilder',
'Private project' => 'privates Projekt',
'AUD - Australian Dollar' => 'AUD - Australische Dollar',
'CAD - Canadian Dollar' => 'CAD - Kanadische Dollar',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.',
'Code' => 'Code',
'Two factor authentication' => 'Zwei-Faktor-Authentifizierung',
- 'Enable/disable two factor authentication' => 'Aktiviere/Deaktiviere Zwei-Faktor-Authentifizierung',
'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Speichere den geheimen Schlüssel in deiner TOTP software (z.B. Google Authenticator oder FreeOTP).',
'Check my code' => 'Überprüfe meinen Code',
'Secret key: ' => 'Geheimer Schlüssel',
'Test your device' => 'Teste dein Gerät',
'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',
- 'uploaded by: %s' => 'Hochgeladen von: %s',
- 'uploaded on: %s' => 'Hochgeladen am: %s',
- 'size: %s' => 'Größe: %s',
'Burndown chart for "%s"' => 'Burndown-Diagramm für "%s"',
'Burndown chart' => 'Burndown-Diagramm',
'This chart show the task complexity over the time (Work Remaining).' => 'Dieses Diagramm zeigt die Aufgabenkomplexität über den Faktor Zeit (Verbleibende Arbeit).',
@@ -722,7 +645,6 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Nimm einen Screenshot auf und drücke STRG+V oder ⌘+V um ihn hier einzufügen.',
'Screenshot uploaded successfully.' => 'Screenshot erfolgreich hochgeladen.',
'SEK - Swedish Krona' => 'SEK - Schwedische Kronen',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Der Projektidentifikator ist ein optionaler alphanumerischer Code, der das Projekt identifiziert.',
'Identifier' => 'Identifikator',
'Disable two factor authentication' => 'Deaktiviere Zwei-Faktor-Authentifizierung',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?',
@@ -731,7 +653,6 @@ return array(
'A task cannot be linked to itself' => 'Eine Aufgabe kann nicht mit sich selber verbunden werden',
'The exact same link already exists' => 'Diese Verbindung existiert bereits',
'Recurrent task is scheduled to be generated' => 'Wiederkehrende Aufgabe ist zur Generierung eingeplant',
- 'Recurring information' => 'Wiederkehrende Information',
'Score' => 'Wertung',
'The identifier must be unique' => 'Der Schlüssel muss einzigartig sein',
'This linked task id doesn\'t exists' => 'Die verbundene Aufgabe existiert nicht',
@@ -777,21 +698,10 @@ return array(
'User that will receive the email' => 'Empfänger der E-Mail',
'Email subject' => 'E-Mail-Betreff',
'Date' => 'Datum',
- 'By @%s on Bitbucket' => 'Durch @%s auf Bitbucket',
- 'Bitbucket Issue' => 'Bitbucket-Issue',
- 'Commit made by @%s on Bitbucket' => 'Commit von @%s auf Bitbucket',
- 'Commit made by @%s on Github' => 'Commit von @%s auf Github',
- 'By @%s on Github' => 'Durch @%s auf Github',
- 'Commit made by @%s on Gitlab' => 'Commit von @%s auf Gitlab',
'Add a comment log when moving the task between columns' => 'Kommentar hinzufügen, wenn Aufgabe in andere Spalte verschoben wird',
'Move the task to another column when the category is changed' => 'Aufgabe in andere Spalte verschieben, wenn Kategorie geändert wird',
'Send a task by email to someone' => 'Aufgabe per E-Mail versenden',
'Reopen a task' => 'Aufgabe wieder öffnen',
- 'Bitbucket issue opened' => 'Bitbucket Ticket eröffnet',
- 'Bitbucket issue closed' => 'Bitbucket Ticket geschlossen',
- 'Bitbucket issue reopened' => 'Bitbucket Ticket wieder eröffnet',
- 'Bitbucket issue assignee change' => 'Bitbucket Ticket Zuordnung geändert',
- 'Bitbucket issue comment created' => 'Bitbucket Ticket Kommentar erstellt',
'Column change' => 'Spalte geändert',
'Position change' => 'Position geändert',
'Swimlane change' => 'Swimlane geändert',
@@ -826,17 +736,11 @@ return array(
'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert',
'The description have been modified' => 'Die Beschreibung wurde geändert',
'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)',
- 'Swimlane: %s' => 'Swimlane: %s',
'I want to receive notifications for:' => 'Ich möchte Benachrichtigungen erhalten für:',
'All tasks' => 'Alle Aufgaben',
'Only for tasks assigned to me' => 'nur mir zugeordnete Aufgane',
'Only for tasks created by me' => 'nur von mir erstellte Aufgaben',
'Only for tasks created by me and assigned to me' => 'nur mir zugeordnete und von mir erstellte Aufgaben',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p',
- 'New due date: %B %e, %Y' => 'Neues Ablaufdatum: %B %e, %Y',
- 'Start date changed: %B %e, %Y' => 'Neues Beginndatum: %B %e, %Y',
- '%k:%M %p' => '%k:%M %p',
'%%Y-%%m-%%d' => '%%d.%%m.%%Y',
'Total for all columns' => 'Gesamt für alle Spalten',
'You need at least 2 days of data to show the chart.' => 'Es werden mindestens 2 Tage zur Darstellung benötigt',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'Nicht zugewiesen',
'View advanced search syntax' => 'Zur erweiterten Suchsyntax',
'Overview' => 'Überblick',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Board-/Kalender-/Listen-Ansicht',
'Switch to the board view' => 'Zur Board-Ansicht',
'Switch to the calendar view' => 'Zur Kalender-Ansicht',
@@ -894,10 +797,6 @@ return array(
'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',
- 'Google Authentication' => 'Google-Authentifizierung',
- 'Help on Google authentication' => 'Hilfe bei Google-Authentifizierung',
- 'Github Authentication' => 'Github-Authentifizierung',
- 'Help on Github authentication' => 'Hilfe bei Github-Authentifizierung',
'Lead time: ' => 'Durchlaufzeit:',
'Cycle time: ' => 'Zykluszeit:',
'Time spent into each column' => 'zeit verbracht in jeder Spalte',
@@ -906,18 +805,12 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Wenn die Aufgabe nicht geschlossen ist, wird die aktuelle Zeit statt der Fertigstellung verwendet.',
'Set automatically the start date' => 'Setze Startdatum automatisch',
'Edit Authentication' => 'Authentifizierung bearbeiten',
- 'Google Id' => 'Google Id',
- 'Github Id' => 'Github Id',
'Remote user' => 'Remote-Benutzer',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Goole und Github Accounts',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.',
- 'By @%s on Gitlab' => 'Durch @%s auf Gitlab',
- 'Gitlab issue comment created' => 'Gitlab Ticket Kommentar erstellt',
'New remote user' => 'Neuer Remote-Benutzer',
'New local user' => 'Neuer lokaler Benutzer',
'Default task color' => 'Voreingestellte Aufgabenfarbe',
- 'Hide sidebar' => 'Seitenleiste verstecken',
- 'Expand sidebar' => 'Seitenleiste ausklappen',
'This feature does not work with all browsers.' => 'Diese Funktion funktioniert nicht mit allen Browsern',
'There is no destination project available.' => 'Es ist kein Zielprojekt vorhanden.',
'Trigger automatically subtask time tracking' => 'Teilaufgaben Zeiterfassung automatisch starten',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'Mitwirkende',
'License:' => 'Lizenz:',
'License' => 'Lizenz',
- 'Project Administrator' => 'Projektadministrator',
'Enter the text below' => 'Text unten eingeben',
'Gantt chart for %s' => 'Gantt Diagramm für %s',
'Sort by position' => 'Nach Position sortieren',
@@ -952,11 +844,9 @@ return array(
'open file' => 'Datei öffnen',
'End date' => 'Endedatum',
'Users overview' => 'Benutzerübersicht',
- 'Managers' => 'Manager',
'Members' => 'Mitglieder',
'Shared project' => 'Geteiltes Projekt',
'Project managers' => 'Projektmanager',
- 'Project members' => 'Projektmitglieder',
'Gantt chart for all projects' => 'Gantt Diagramm für alle Projekte',
'Projects list' => 'Projektliste',
'Gantt chart for this project' => 'Gantt Diagramm für dieses Projekt',
@@ -964,26 +854,16 @@ 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',
- 'Start date: %s' => 'Beginndatum: %s',
- 'End date: %s' => 'Enddatum: %s',
'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',
- 'Login with my Gitlab Account' => 'Mit Gitlab Account einloggen',
'Milestone' => 'Meilenstein',
- 'Gitlab Authentication' => 'Gitlab-Authentifizierung',
- 'Help on Gitlab authentication' => 'Hilfe bei Gitlab-Authentifizierung',
- 'Gitlab Id' => 'Gitlab Id',
- 'Gitlab Account' => 'Gitlab Account',
- 'Link my Gitlab Account' => 'Verknüpfe mein Gitlab Account',
- 'Unlink my Gitlab Account' => 'Trenne meinen Gitlab Account',
'Documentation: %s' => 'Dokumentation: %s',
'Switch to the Gantt chart view' => 'Zur Gantt-Diagramm Ansicht wechseln',
'Reset the search/filter box' => 'Suche/Filter-Box zurücksetzen',
'Documentation' => 'Dokumentation',
'Table of contents' => 'Inhaltsverzeichnis',
'Gantt' => 'Gantt',
- 'Help with project permissions' => 'Hilfe bei Projektberechtigungen',
'Author' => 'Autor',
'Version' => 'Version',
'Plugins' => 'Plugins',
@@ -1028,40 +908,244 @@ return array(
'Unread notifications' => 'Ungelesene Benachrichtigungen',
'My filters' => 'Meine Filter',
'Notification methods:' => 'Benachrichtigungs-Methoden:',
- // 'Import tasks from CSV file' => '',
- // 'Unable to read your file' => '',
- // '%d task(s) have been imported successfully.' => '',
- // 'Nothing have been imported!' => '',
- // 'Import users from CSV file' => '',
- // '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
- // '%s attached a file to the task #%d' => '',
- // 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
- // 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
- // 'Usernames must be lowercase and unique' => '',
- // 'Passwords will be encrypted if present' => '',
+ 'Import tasks from CSV file' => 'Importiere Aufgaben aus CSV Datei',
+ 'Unable to read your file' => 'Die Datei kann nicht gelesen werden',
+ '%d task(s) have been imported successfully.' => '%d Aufgabe(n) wurde(n) erfolgreich importiert',
+ 'Nothing have been imported!' => 'Es wurde nichts importiert!',
+ 'Import users from CSV file' => 'Importiere Benutzer aus CSV Datei',
+ '%d user(s) have been imported successfully.' => '%d Benutzer wurde(n) erfolgreich importiert.',
+ 'Comma' => 'Komma',
+ 'Semi-colon' => 'Semikolon',
+ 'Tab' => 'Tabulator',
+ 'Vertical bar' => 'senkrechter Strich',
+ 'Double Quote' => 'Doppelte Anführungszeichen',
+ 'Single Quote' => 'Einfache Anführungszeichen',
+ '%s attached a file to the task #%d' => '%s hat eine Datei zur Aufgabe #%d hinzugefügt',
+ 'There is no column or swimlane activated in your project!' => 'Es ist keine Spalte oder Swimlane in Ihrem Projekt aktiviert!',
+ 'Append filter (instead of replacement)' => 'Filter anhängen (statt zu ersetzen)',
+ 'Append/Replace' => 'Anhängen/Ersetzen',
+ 'Append' => 'Anhängen',
+ 'Replace' => 'Ersetzen',
+ 'Import' => 'Import',
+ 'change sorting' => 'Sortierung ändern',
+ 'Tasks Importation' => 'Aufgaben Import',
+ 'Delimiter' => 'Trennzeichen',
+ 'Enclosure' => 'Anlage',
+ 'CSV File' => 'CSV Datei',
+ 'Instructions' => 'Anweisungen',
+ 'Your file must use the predefined CSV format' => 'Ihre Datei muss das vorgegebene CSV Format haben',
+ 'Your file must be encoded in UTF-8' => 'Ihre Datei muss UTF-8 kodiert sein',
+ 'The first row must be the header' => 'Die erste Zeile muss die Kopfzeile sein',
+ 'Duplicates are not verified for you' => 'Duplikate werden nicht für Sie geprüft',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'Das Fälligkeitsdatum muss das ISO Format haben: YYYY-MM-DD',
+ 'Download CSV template' => 'CSV Vorlage herunterladen',
+ 'No external integration registered.' => 'Keine externe Integration registriert',
+ 'Duplicates are not imported' => 'Duplikate wurden nicht importiert',
+ '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',
+ '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',
+ 'Assignee Name' => 'Name des Zuständigen',
+ 'Groups' => 'Gruppen',
+ 'Members of %s' => 'Mitglied von %s',
+ 'New group' => 'Neue Gruppe',
+ 'Group created successfully.' => 'Gruppe erfolgreich angelegt.',
+ 'Unable to create your group.' => 'Gruppe konnte nicht angelegt werden',
+ 'Edit group' => 'Gruppe bearbeiten',
+ 'Group updated successfully.' => 'Gruppe erfolgreich aktualisiert',
+ 'Unable to update your group.' => 'Gruppe konnte nicht aktualisiert werden',
+ 'Add group member to "%s"' => 'Gruppenmitglied zu "%s" hinzufügen',
+ 'Group member added successfully.' => 'Gruppenmitglied erfolgreich hinzugefügt',
+ 'Unable to add group member.' => 'Gruppenmitglied konnte nicht hinzugefügt werden.',
+ 'Remove user from group "%s"' => 'Benutzer aus Gruppe "%s" löschen',
+ 'User removed successfully from this group.' => 'Benutzer erfolgreich aus dieser Gruppe gelöscht.',
+ 'Unable to remove this user from the group.' => 'Benutzer konnte nicht aus dieser Gruppe gelöscht werden.',
+ 'Remove group' => 'Gruppe löschen',
+ 'Group removed successfully.' => 'Gruppe erfolgreich gelöscht.',
+ 'Unable to remove this group.' => 'Gruppe konnte nicht gelöscht werden.',
+ 'Project Permissions' => 'Projekt Berechtigungen',
+ 'Manager' => 'Manager',
+ 'Project Manager' => 'Projekt Manager',
+ 'Project Member' => 'Projekt Mitglied',
+ 'Project Viewer' => 'Projekt Betrachter',
+ 'Your account is locked for %d minutes' => 'Ihr Zugang wurde für %d Minuten gesperrt',
+ 'Invalid captcha' => 'Ungültiges Captcha',
+ 'The name must be unique' => 'Der Name muss eindeutig sein',
+ 'View all groups' => 'Alle Gruppen anzeigen',
+ 'View group members' => 'Gruppenmitglieder anzeigen',
+ 'There is no user available.' => 'Es ist kein Benutzer verfügbar.',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Wollen Sie den Benutzer "%s" wirklich aus der Gruppe "%s" löschen?',
+ 'There is no group.' => 'Es gibt keine Gruppe.',
+ 'External Id' => 'Externe ID',
+ 'Add group member' => 'Gruppenmitglied hinzufügen',
+ 'Do you really want to remove this group: "%s"?' => 'Wollen Sie die Gruppe "%s" wirklich löschen?',
+ 'There is no user in this group.' => 'Es gibt keinen Benutzer in dieser Gruppe.',
+ 'Remove this user' => 'Diesen Benutzer löschen',
+ 'Permissions' => 'Berechtigungen',
+ 'Allowed Users' => 'Berechtigte Benutzer',
+ 'No user have been allowed specifically.' => 'Keine Benutzer mit ausdrücklicher Berechtigung.',
+ 'Role' => 'Rolle',
+ 'Enter user name...' => 'Geben Sie den Benutzernamem ein...',
+ 'Allowed Groups' => 'Berechtigte Gruppen',
+ 'No group have been allowed specifically.' => 'Keine Gruppen mit ausdrücklicher Berechtigung.',
+ 'Group' => 'Gruppe',
+ 'Group Name' => 'Gruppenname',
+ 'Enter group name...' => 'Geben Sie den Gruppennamen ein...',
+ 'Role:' => 'Rolle:',
+ 'Project members' => 'Projektmitglieder',
+ 'Compare hours for "%s"' => 'Vergleich der Stunden für %s',
+ '%s mentioned you in the task #%d' => '%s erwähnte Sie in Aufgabe #%d',
+ '%s mentioned you in a comment on the task #%d' => '%s erwähnte Sie in einem Kommentar zur Aufgabe #%d',
+ 'You were mentioned in the task #%d' => 'Sie wurden in der Aufgabe #%d erwähnt',
+ 'You were mentioned in a comment on the task #%d' => 'Sie wurden in einem Kommentar zur Aufgabe #%d erwähnt',
+ 'Mentioned' => 'Erwähnt',
+ 'Compare Estimated Time vs Actual Time' => 'Vergleich zwischen erwartetem und tatsächlichem Zeitaufwand',
+ 'Estimated hours: ' => 'Erwarteter Zeitaufwand (Stunden): ',
+ 'Actual hours: ' => 'Tatsächlich aufgewändete Stunden: ',
+ 'Hours Spent' => 'Stunden aufgewändet',
+ 'Hours Estimated' => 'Stunden erwartet',
+ 'Estimated Time' => 'Erwartete Zeit',
+ 'Actual Time' => 'Aktuelle Zeit',
+ 'Estimated vs actual time' => 'Erwarteter vs. tatsächlicher Zeitaufwand',
+ 'RUB - Russian Ruble' => 'Russischer Rubel',
+ 'Assign the task to the person who does the action when the column is changed' => 'Aufgabe der Person zuordnen, die die Aktion durchführt, wenn die Spalte geändert wird',
+ 'Close a task in a specific column' => 'Schliesse eine Aufgabe in einer bestimmten Spalte',
+ 'Time-based One-time Password Algorithm' => 'Zeitbasierter Einmalpasswort Algorithmus',
+ 'Two-Factor Provider: ' => '2FA Anbieter: ',
+ 'Disable two-factor authentication' => 'Zwei-Faktor-Authentifizierung deaktivieren',
+ 'Enable two-factor authentication' => 'Zwei-Faktor-Authentifizierung aktivieren',
+ 'There is no integration registered at the moment.' => 'Derzeit ist kein externer Dienst registriert.',
+ 'Password Reset for Kanboard' => 'Zurücksetzen des Passwortes für Kanboard',
+ 'Forgot password?' => 'Passwort vergessen?',
+ 'Enable "Forget Password"' => 'Passwortrücksetzung aktivieren',
+ 'Password Reset' => 'Passwort zurücksetzen',
+ 'New password' => 'Neues Passwort',
+ 'Change Password' => 'Passwort ändern',
+ 'To reset your password click on this link:' => 'Bitte auf den Link klicken, um Ihr Passwort zurückzusetzen.',
+ 'Last Password Reset' => 'Verlauf der Passwortrücksetzung',
+ 'The password has never been reinitialized.' => 'Das Passwort wurde noch nie zurückgesetzt.',
+ 'Creation' => 'Erstellung',
+ 'Expiration' => 'Ablauf',
+ 'Password reset history' => 'Verlauf Passwortrücksetzung',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Alle Aufgaben der Spalte "%s" und der Swimlane "%s" wurden erfolgreich geschlossen',
+ 'Do you really want to close all tasks of this column?' => 'Wollen Sie wirklich alle Aufgaben in dieser Spalte schließen?',
+ '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d Aufgabe(n) in der Spalte "%s" und in der Swimlane "%s" werden geschlossen.',
+ 'Close all tasks of this column' => 'Alle Aufgaben in dieser Spalte schließen',
+ '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:',
+ '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.',
+ 'Private projects do not have users and groups management.' => 'Private Projekte haben kein Benutzer- und Gruppen-Management.',
+ 'There is no project member.' => 'Es gibt kein Projekt-Mitglied.',
+ 'Priority' => 'Priorität',
+ 'Task priority' => 'Aufgaben-Priorität',
+ 'General' => 'Allgemein',
+ 'Dates' => 'Daten',
+ 'Default priority' => 'Standard-Priorität',
+ 'Lowest priority' => 'Niedrigste Priorität',
+ 'Highest priority' => 'Höchste Priorität',
+ 'If you put zero to the low and high priority, this feature will be disabled.' => 'Wenn Sie Null bei höchster und niedrigster Priorität eintragen, wird diese Funktion deaktiviert.',
+ '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',
+ 'Related' => 'Verbunden',
+ 'Attachment' => 'Anhang',
+ 'Title not found' => 'Titel nicht gefunden',
+ 'Web Link' => 'Weblink',
+ 'External links' => 'Externe Verbindungen',
+ 'Add external link' => 'Externe Verbindung hinzufügen',
+ 'Type' => 'Typ',
+ 'Dependency' => 'Abhängigkeit',
+ 'Add internal link' => 'Füge interne Verbindung hinzu',
+ 'Add a new external link' => 'Füge eine neue externe Verbindung hinzu',
+ 'Edit external link' => 'Externe Verbindung bearbeiten',
+ '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',
+ 'Projects management' => 'Projektmanagement',
+ '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:',
+ 'Reference:' => 'Bezug:',
+ 'Complexity:' => 'Komplexität:',
+ 'Swimlane:' => 'Swimlane:',
+ 'Column:' => 'Spalte:',
+ 'Position:' => 'Position:',
+ 'Creator:' => 'Ersteller:',
+ 'Time estimated:' => 'Geschätzte Zeit:',
+ '%s hours' => '%s Stunden',
+ 'Time spent:' => 'Aufgewendete Zeit:',
+ 'Created:' => 'Erstellt:',
+ 'Modified:' => 'Geändert:',
+ 'Completed:' => 'Abgeschlossen:',
+ '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:',
+ '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',
+ 'User activated successfully.' => 'Benutzer erfolgreich aktiviert.',
+ 'Unable to enable this user.' => 'Dieser Benutzer kann nicht aktiviert werden.',
+ 'User disabled successfully.' => 'Benutzer erfolgreich deaktiviert.',
+ 'Unable to disable this user.' => 'Dieser Benutzer kann nicht deaktiviert werden.',
+ 'All files have been uploaded successfully.' => 'Alle Dateien wurden erfolgreich hochgeladen.',
+ 'View uploaded files' => 'Hochgeladene Dateien ansehen',
+ 'The maximum allowed file size is %sB.' => 'Die maximal erlaubte Dateigröße ist %sB.',
+ 'Choose files again' => 'Wählen Sie erneut Dateien aus',
+ 'Drag and drop your files here' => 'Ziehen Sie Ihre Dateien hier hin',
+ 'choose files' => 'Dateien auswählen',
+ 'View profile' => 'Profil ansehen',
+ 'Two Factor' => 'Zwei-Faktor',
+ 'Disable user' => 'Benutzer deaktivieren',
+ 'Do you really want to disable this user: "%s"?' => 'Wollen Sie diesen Benutzer wirklich deaktivieren: "%s"?',
+ 'Enable user' => 'Benutzer aktivieren',
+ 'Do you really want to enable this user: "%s"?' => 'Wollen Sie diesen Benutzer wirklich aktivieren: "%s"?',
+ 'Download' => 'Runterladen',
+ 'Uploaded: %s' => 'Heraufgeladen: %s',
+ 'Size: %s' => 'Größe: %s',
+ 'Uploaded by %s' => 'Heraufgeladen von %s',
+ 'Filename' => 'Dateiname',
+ 'Size' => 'Größe',
+ 'Column created successfully.' => 'Spalte erfolgreich erstellt.',
+ 'Another column with the same name exists in the project' => 'Es gibt bereits eine Spalte mit demselben Namen im Projekt',
+ 'Default filters' => 'Standard-Filter',
+ 'Your board doesn\'t have any column!' => 'Es gibt keine Spalten in diesem Projekt!',
+ 'Change column position' => 'Position der Spalte ändern',
+ 'Switch to the project overview' => 'Zur Projektübersicht wechseln',
+ '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',
+ 'This value must be greater than %d' => 'Dieser Wert muss größer als %d sein',
+ 'Another swimlane with the same name exists in the project' => 'Es gibt bereits eine Swimlane mit diesem Namen im Projekt',
+ 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Beispiel: http://example.kanboard.net (wird zum Erstellen absoluter URLs genutzt)',
);
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
new file mode 100644
index 00000000..e59949cf
--- /dev/null
+++ b/app/Locale/el_GR/translations.php
@@ -0,0 +1,1151 @@
+<?php
+
+return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => '.',
+ 'None' => 'Τίποτα',
+ 'edit' => 'Διορθωτής',
+ 'Edit' => 'Διόρθωση',
+ 'remove' => 'αφαιρετής',
+ 'Remove' => 'Αφαίρεση',
+ 'Update' => 'Ενημέρωση',
+ '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:' => 'Επίσημο web site :',
+ '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' => 'Ενέργειες',
+ 'Inactive' => 'Ανενεργός',
+ 'Active' => 'Ενεργός',
+ 'Add this column' => 'Προσθήκη αυτής της στήλης',
+ '%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"?' => 'Αφαίρεση του έργου : « %s » ?',
+ 'Remove project' => 'Αφαίρεση του έργου',
+ 'Edit the board for "%s"' => 'Διόρθωση πίνακα από « %s »',
+ 'All projects' => 'Όλα τα έργα',
+ 'Add a new column' => 'Πρόσθήκη στήλης',
+ 'Title' => 'Τίτλος',
+ 'Nobody assigned' => 'Δεν έχει ανατεθεί',
+ '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"?' => 'Θέλετε να αφαιρέσετε τη στήλη : « %s » ?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Αυτή η ενέργεια θα ΑΦΑΙΡΕΣΕΙ ΟΛΕΣ ΤΙΣ ΕΡΓΑΣΙΕΣ που είναι σχετικές με τη στήλη!!',
+ 'Settings' => 'Προτιμήσεις',
+ 'Application settings' => 'Παραμετροποίηση εφαρμογής',
+ 'Language' => 'Γλώσσα',
+ 'Webhook token:' => 'Διακριτικό ασφαλείας (token) webhooks :',
+ 'API token:' => 'Διακριτικό ασφαλείας (token) API :',
+ 'Database size:' => 'Μέγεθος βάσης δεδομένων :',
+ 'Download the database' => 'Κατεβάστε τη βάση δεδομένων',
+ 'Optimize the database' => 'Βελτιστοποίηση της βάσης δεδομένων',
+ '(VACUUM command)' => '(VACUUM command)',
+ '(Gzip compressed Sqlite file)' => '(Gzip compressed Sqlite file)',
+ '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"?' => 'Άνοιγμα της εργασίας : « %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' => 'Ο μέγιστος αριθμός χαρακτήρων είναι %d χαρακτήρες',
+ 'The minimum length is %d characters' => 'Ο ελάχιστος αριθμός χαρακτήρων είναι %d χαρακτήρες',
+ 'The password is required' => 'Ο κωδικός απαιτείται',
+ 'This value must be an integer' => 'Η τιμή πρέπει να είναι ακέραιος',
+ 'The username must be unique' => 'Το όνομα χρήστη πρέπει να είναι μοναδικό',
+ 'The user id is required' => 'το αναγνωριστικό χρήστη απαιτείται',
+ 'Passwords don\'t match' => 'Οι κωδικοί πρόσβασης δεν ταιριάζουν',
+ 'The confirmation is required' => 'Η επειβεβαίωση απαιτείται',
+ 'The project is required' => 'Το έργο απαιτείται',
+ 'The id is required' => 'το αναγνωριστικό απαιτείται',
+ 'The project id is required' => 'το αναγνωριστικό έργου απαιτείται',
+ '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:' => 'Version εφαρμογής :',
+ 'Id' => 'Αναγνωριστικό.',
+ '%d closed tasks' => '%d κλειστές εργασίες',
+ 'No task for this project' => 'Αριθμός εργασιών για το έργο',
+ 'Public link' => 'Δημόσιος σύνδεσμος',
+ 'Change assignee' => 'Αλλαγή ανάθεσης',
+ 'Change assignee for the task "%s"' => 'Αλλαγή ανάθεσης για την εργασία « %s »',
+ 'Timezone' => 'Timezone',
+ 'Sorry, I didn\'t find this information in my database!' => 'Δυστυχώς δεν βρέθηκε αυτή η πληροφορία στη βάση δεδομένων',
+ 'Page not found' => 'Η σελίδα δεν βρέθηκε',
+ 'Complexity' => 'Πολυπλοκότητα',
+ 'Task limit' => 'Όριο εργασιών.',
+ 'Task count' => 'Αρίθμηση εργασιών',
+ 'User' => 'Χρήστης',
+ 'Comments' => 'Σχόλια',
+ 'Write your text in Markdown' => 'Δυνατότητα γραφής και σε Markdown',
+ '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.' => 'Impossible de créer votre action automatisée.',
+ 'Remove an action' => 'Αφαίρεση ενέργειας',
+ 'Unable to remove this action.' => 'Δεν είναι δυνατή η αφαίρεση αυτής της ενέργειας',
+ 'Action removed successfully.' => 'Η ενέργεια αφαιρέθηκε με επιτυχία.',
+ 'Automatic actions for the project "%s"' => 'Αυτόματες ενέργειες για το έργο « %s »',
+ 'Defined actions' => 'Ορισμένες ενέργειες',
+ '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' => 'Ορισμός παραμέτρων ενέργειας',
+ 'Save this action' => 'Αποθήκευση ενέργειας',
+ 'Do you really want to remove this action: "%s"?' => 'Αφαίρεση της ενέργειας: « %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' => 'User agent',
+ 'Persistent connections' => 'Μόνιμες συνδέσεις',
+ 'No session.' => 'Καμμία συνεδρία',
+ 'Expiration date' => 'Ημερομηνία λήξης',
+ 'Remember Me' => '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' => 'Μη αποδεκτή διεύθυνση email',
+ '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' => 'Email',
+ 'Task removed successfully.' => 'Η εργασία αφαιρέθηκε με επιτυχία.',
+ 'Unable to remove this task.' => 'Δεν είναι δυνατή η αφαίρεση της εργασίας.',
+ 'Remove a task' => 'Αφαίρεση εργασίας',
+ 'Do you really want to remove this task: "%s"?' => 'Αφαίρεση της εργασίας « %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"' => 'Τροποποίηση κατηγορίας για το έργο « %s »',
+ 'Category Name' => 'Ονομασία κατηγορίας',
+ 'Add a new category' => 'Προσθήκη νέας κατηγορίας',
+ 'Do you really want to remove this category: "%s"?' => 'Διαγραφή της κατηγορίας « %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' => '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"' => 'Εξαγωγή εργασιών για το έργο « %s »',
+ 'Start Date' => 'Ημερομηνία έναρξης',
+ 'End Date' => 'ημερομηνία λήξης',
+ 'Execute' => 'Εκτέλεση',
+ 'Task Id' => 'Task Id',
+ 'Creator' => 'Δημιουργός',
+ 'Modification date' => 'Ημερομηνία τροποποίησης',
+ 'Completion date' => 'Ημερομηνία ολοκλήρωσης',
+ 'Clone' => 'Clone',
+ 'Project cloned successfully.' => 'Το έργο κλωνοποιήθηκε με επιτυχία.',
+ 'Unable to clone this project.' => 'Αδύνατο να κλωνοποιηθεί το έργο.',
+ 'Enable email notifications' => 'Ενεργοποίηση ειδοποιήσεων ηλεκτρονικού ταχυδρομείου',
+ 'Task position:' => 'Θέση έργου :',
+ 'The task #%d have been opened.' => 'Η εργασία #%d έχει ανοίξει.',
+ 'The task #%d have been closed.' => 'Η εργασία #%d έχει κλείσει.',
+ 'Sub-task updated' => 'Η υπο-εργασία ενημερώθηκε',
+ 'Title:' => 'Τίτλος :',
+ 'Status:' => 'Κατάσταση :',
+ 'Assignee:' => 'Εντολοδόχος :',
+ 'Time tracking:' => 'Παρακολούθηση του χρόνου :',
+ 'New sub-task' => 'Νέα υπο-εργασία',
+ 'New attachment added "%s"' => 'Νέα επικόλληση προστέθηκε « %s »',
+ 'Comment updated' => 'Το σχόλιο ενημερώθηκε',
+ 'New comment posted by %s' => 'Νέο σχόλιο από τον χρήστη « %s »',
+ 'New attachment' => 'New attachment',
+ 'New comment' => 'Νέο σχόλιο',
+ '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"?' => 'Θέλετε πραγματικά να απενεργοποιήσετε το έργο : « %s » ?',
+ 'Do you really want to enable this project: "%s"?' => 'Θέλετε πραγματικά να ενεργοποιήσετε το έργο : « %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:' => 'Username :',
+ 'Name:' => 'Όνομα :',
+ 'Email:' => 'Email :',
+ 'Notifications:' => 'Ειδοποιήσεις :',
+ 'Notifications' => 'Ειδοποιήσεις',
+ 'Account type:' => 'Τύπος λογαριασμού :',
+ 'Edit profile' => 'Επεξεργασία προφίλ',
+ 'Change password' => 'Αλλαγή password',
+ 'Password modification' => 'Τροποποίηση password ',
+ 'External authentications' => 'Εξωτερικές πιστοποιήσεις',
+ 'Never connected.' => 'Ποτέ δεν συνδέθηκε.',
+ 'No external authentication enabled.' => 'Καμία εξωτερική πιστοποιήση ενεργοποιημένη.',
+ 'Password modified successfully.' => 'Το password τροποποιήθηκε με επιτυχία.',
+ 'Unable to change the password.' => 'Αδύνατο να αλλάξει το password.',
+ 'Change category for the task "%s"' => 'Αλλαγή κατηγορίας της εργασίας « %s »',
+ 'Change category' => 'Αλλαγή κατηγορίας',
+ '%s updated the task %s' => '%s ενημέρωσε την εργασία %s',
+ '%s opened the task %s' => '%s άνοιξε την εργασία %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s άλλαξε την εργασία %s στη θέση n°%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' => 'Δεν έχει εκχωρηθεί, εκτίμηση %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 feed',
+ '%s updated a comment on the task #%d' => '%s ενημέρωσε ένα σχόλιο στην εργασία n°%d',
+ '%s commented on the task #%d' => '%s σχολίασε την εργασία n°%d',
+ '%s updated a subtask for the task #%d' => '%s ενημέρωσε μια υπο-εργασία στην εργασία n °%d',
+ '%s created a subtask for the task #%d' => '%s δημιούργησε μια υπο-εργασία στην εργασία n°%d',
+ '%s updated the task #%d' => '%s ενημέρωσε την εργασία n°%d',
+ '%s created the task #%d' => '%s δημιούργησε την εργασία n°%d',
+ '%s closed the task #%d' => '%s έκλεισε την εργασία n°%d',
+ '%s open the task #%d' => '%s άνοιξε την εργασία n°%d',
+ '%s moved the task #%d to the column "%s"' => '%s μετακίνησε την εργασία n°%d στη στήλη « %s »',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s μετακίνησε την εργασία n°%d στη θέση n°%d της στήλης « %s »',
+ 'Activity' => 'Δραστηριότητα',
+ 'Default values are "%s"' => 'Οι προεπιλεγμένες τιμές είναι « %s »',
+ 'Default columns for new projects (Comma-separated)' => 'Προεπιλεγμένες στήλες για νέα έργα (Comma-separated)',
+ 'Task assignee change' => 'Αλλαγή εκδοχέα εργασίας',
+ '%s change the assignee of the task #%d to %s' => '%s άλλαξε τον εκδοχέα της εργασίας n˚%d σε %s',
+ '%s changed the assignee of the task %s to %s' => '%s ενημέρωσε τον εκδοχέα της εργασίας %s σε %s',
+ 'New password for the user "%s"' => 'Νέο password του χρήστη « %s »',
+ 'Choose an event' => 'Επιλογή event',
+ 'Create a task from an external provider' => 'Δημιουργήστε μια εργασία από ένα εξωτερικό πάροχο',
+ 'Change the assignee based on an external username' => 'Αλλάξτε τον εκδοχέα βάση ενός εξωτερικού username',
+ 'Change the category based on an external label' => 'Αλλάξτε τον εκδοχέα βάση ενός εξωτερικού label',
+ 'Reference' => 'Reference',
+ 'Label' => 'Label',
+ 'Database' => 'Database',
+ 'About' => 'About',
+ 'Database driver:' => 'Database driver :',
+ 'Board settings' => 'Board settings',
+ 'URL and token' => 'URL / token',
+ 'Webhook settings' => 'Webhook settings',
+ 'URL for task creation:' => 'URL για δημιουργία εργασίας: :',
+ 'Reset token' => 'Reset token',
+ 'API endpoint:' => 'URL API :',
+ 'Refresh interval for private board' => 'Ανανέωση interval στο private board',
+ 'Refresh interval for public board' => 'Ανανέωση interval στο public board',
+ 'Task highlight period' => 'Περίοδος εργασίας',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Περίοδος (σε δευτερόλεπτα) για να εξετάσει ότι ένα έργο τροποποιήθηκε πρόσφατα (0 για να απενεργοποιήσετε, 2 ημέρες από προεπιλογή)',
+ 'Frequency in second (60 seconds by default)' => 'Συχνότητα σε δευτερόλεπτα (60 δευτερόλεπτα από προεπιλογή)',
+ 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Συχνότητα σε δευτερόλεπτα (0 για να απενεργοποιήσετε αυτή τη λειτουργία, 10 δευτερόλεπτα από προεπιλογή)',
+ 'Application URL' => 'Application URL',
+ 'Token regenerated.' => 'Token regenerated.',
+ 'Date format' => 'Μορφή ημερομηνίας',
+ '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' => 'Εκτιμώμενος χρόνος',
+ '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' => 'Webhooks',
+ '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"' => 'Επανάληψη εργασιών για « %s »',
+ 'Analytics' => 'Αναλύσεις',
+ 'Subtask' => 'Υπο-Εργασία',
+ 'My subtasks' => 'Οι υπο-εργασίες μου',
+ 'User repartition' => 'Επαναλήψεις χρηστών',
+ 'User repartition for "%s"' => 'Επαναλήψεις χρηστών για « %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' => 'Το αναγνωριστικό έργου πρέπει να είναι ακέραιος',
+ 'The status must be an integer' => 'Το status πρέπει να είναι ακέραιος',
+ 'The subtask id is required' => 'Το id της υπο-εργασίας είναι υποχρεωτικό',
+ 'The subtask id must be an integer' => 'Το id της υπο-εργασίας πρέπει να είναι ακέραιος',
+ 'The task id is required' => 'Το id της εργασίας είναι υποχρεωτικό',
+ 'The task id must be an integer' => 'Το 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"' => 'Συγκεντρωτικό διάγραμμα ροής για « %s »',
+ 'Daily project summary' => 'Καθημερινή περίληψη του έργου',
+ 'Daily project summary export' => 'Εξαγωγή της καθημερινής περίληψης του έργου',
+ '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' => 'Ενεργά swimlanes',
+ 'Add a new swimlane' => 'Πρόσθεσε ένα νέο λωρίδα',
+ 'Change default swimlane' => 'Αλλαγή του default λωρίδα',
+ 'Default swimlane' => 'Default λωρίδα',
+ 'Do you really want to remove this swimlane: "%s"?' => 'Σίγουρα θέλετε να αφαιρέσετε τη λωρίδα : « %s » ?',
+ 'Inactive swimlanes' => 'Λωρίδες ανενεργές',
+ 'Remove a swimlane' => 'Αφαιρέστε μια λωρίδα',
+ 'Show default swimlane' => 'Εμφάνιση προεπιλεγμένων λωρίδων',
+ 'Swimlane modification for the project "%s"' => 'Τροποποίηση λωρίδας για το έργο « %s »',
+ 'Swimlane not found.' => 'Η λωρίδα δεν βρέθηκε.',
+ 'Swimlane removed successfully.' => 'Η λωρίδα αφαιρέθηκε με επιτυχία.',
+ 'Swimlanes' => '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"' => 'Παράδειγμα: "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"' => 'Εξαγωγή υπο-εργασίων για το έργο « %s »',
+ 'Task Title' => 'Τίτλος εργασίας',
+ 'Untitled' => 'Χωρίς τίτλο',
+ 'Application default' => 'Προεπιλογή από την εφαρμογή',
+ 'Language:' => 'Γλώσσα :',
+ 'Timezone:' => 'Timezone :',
+ 'All columns' => 'Όλες οι στήλες',
+ 'Calendar' => 'Ημερολόγιο',
+ 'Next' => 'Επόμενο',
+ '#%d' => 'n˚%d',
+ 'All swimlanes' => 'Όλες οι λωρίδες',
+ 'All colors' => 'Όλα τα χρώματα',
+ 'Moved to column %s' => 'Μεταφορά στη στήλη %s',
+ 'Change description' => 'Επεξεργασία περιγραφής',
+ 'User dashboard' => 'Κεντρικό ταμπλό χρήστη',
+ 'Allow only one subtask in progress at the same time for a user' => 'Αφήστε μόνο μία υπο-εργασία σε εξέλιξη ταυτόχρονα για έναν χρήστη',
+ 'Edit column "%s"' => 'Επεξεργασία στήλης « %s »',
+ 'Select the new status of the subtask: "%s"' => 'Επιλογή νέας κατάστασης της υπο-εργασίας : « %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 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 ?',
+ 'Field required' => 'Υποχρεωτικό πεδίο',
+ 'Link added successfully.' => 'Το link προστέθηκε με επιτυχία.',
+ 'Link updated successfully.' => 'Το link ενημερώθηκε με επιτυχία.',
+ 'Link removed successfully.' => 'Το link αφαιρέθηκε με επιτυχία.',
+ 'Link labels' => 'Link labels',
+ 'Link modification' => 'Τροποποίηση Link ',
+ 'Links' => 'Links',
+ 'Link settings' => 'Ρυθμίσεις συνδέσμων',
+ 'Opposite label' => 'Αντίθετο label',
+ 'Remove a link' => 'Αφαίρεση ενός link',
+ 'Task\'s links' => 'Σύνδεσμοι εργασιών',
+ 'The labels must be different' => 'Τα label πρέπει να είναι διαφορετικά',
+ 'There is no link.' => 'Δεν υπάρχει σύνδεσμος.',
+ 'This label must be unique' => 'Το label πρέπει να είναι μοναδικό',
+ 'Unable to create your link.' => 'Αδύνατο να δημιουργήσετε σύνδεσμο.',
+ 'Unable to update your link.' => 'Αδύνατο να ενημερώσετε τον σύνδεσμο.',
+ 'Unable to remove this link.' => 'Αδύνατο να αφαιρέσετε τον σύνδεσμο.',
+ 'relates to' => 'συνδέεται με',
+ 'blocks' => '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' => 'AUD - Australian Dollar',
+ 'CAD - Canadian Dollar' => 'CAD - Canadian Dollar',
+ 'CHF - Swiss Francs' => 'CHF - Swiss Francs',
+ 'Custom Stylesheet' => 'Custom Stylesheet',
+ 'download' => 'download',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'GBP - British Pound' => 'GBP - British Pound',
+ 'INR - Indian Rupee' => 'INR - Indian Rupee',
+ 'JPY - Japanese Yen' => 'JPY - Japanese Yen',
+ 'NZD - New Zealand Dollar' => 'NZD - New Zealand Dollar',
+ 'RSD - Serbian dinar' => 'RSD - Serbian dinar',
+ 'USD - US Dollar' => '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' => 'Ελέγξτε δύο παράγοντες ελέγχου ταυτότητας κωδικού',
+ 'The two factor authentication code is not valid.' => 'Ο κωδικός ελέγχου ταυτότητας δύο παραγόντων δεν είναι σωστός.',
+ 'The two factor authentication code is valid.' => 'Ο κωδικός ελέγχου ταυτότητας δύο παραγόντων είναι σωστός.',
+ 'Code' => 'Κωδικός',
+ 'Two factor authentication' => 'Κωδικός ελέγχου ταυτότητας δύο παραγόντων',
+ 'This QR code contains the key URI: ' => 'Αυτό το QR code περιέχει το url : ',
+ '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 Kanboard',
+ 'Burndown chart for "%s"' => 'Δημιουργία διαγράμματος για « %s »',
+ 'Burndown chart' => 'Δημιουργία διαγράμματος',
+ 'This chart show the task complexity over the time (Work Remaining).' => 'Αυτό το γράφημα δείχνει την πολυπλοκότητα του έργου κατά την πάροδο του χρόνου (Εργασία που παραμένει).',
+ 'Screenshot taken %s' => 'Το screenshot αποθηκεύτηκε από %s',
+ 'Add a screenshot' => 'Προσθήκη ενός screenshot',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Πάρτε ένα screenshot και πατήστε CTRL+V or ⌘+V για να το επικολλήσετε εδώ.',
+ 'Screenshot uploaded successfully.' => 'Το screenshot ανέβηκε με επιτυχία.',
+ 'SEK - Swedish Krona' => 'SEK - Swedish Krona',
+ 'Identifier' => 'Αναγνωριστικό',
+ 'Disable two factor authentication' => 'Απενεργοποίηση κωδικού ελέγχου ταυτότητας δύο παραγόντων',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Είστε σίγουροι ότι θέλετε να απενεργοποίησετε τον κωδικό ελέγχου ταυτότητας δύο παραγόντων : « %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' => 'Score',
+ 'The identifier must be unique' => 'Το αναγνωριστικό πρέπει να είναι μοναδικό',
+ 'This linked task id doesn\'t exists' => 'Αυτό το συνδεδεμένο id της εργασίας δεν υπάρχει',
+ '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' => 'Όταν το έργο έχει μετακινηθεί στην 1η στήλη',
+ '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' => 'iCal feed',
+ 'Preferences' => 'Προτιμήσεις',
+ 'Security' => 'Ασφάλεια',
+ 'Two factor authentication disabled' => 'Η πιστοποίηση δύο παραγόντων απενεργοποιήθηκε',
+ 'Two factor authentication enabled' => 'Η πιστοποίηση δύο παραγόντων ενεργοποιήθηκε',
+ 'Unable to update this user.' => 'Αδύνατο να ενημερωθεί αυτός ο χρήστης.',
+ 'There is no user management for private projects.' => 'Δεν υπάρχει διαχείριση χρηστών για ιδιωτικά έργα.',
+ 'User that will receive the email' => 'Ο χρήστης που θα λάβει το μήνυμα ηλεκτρονικού ταχυδρομείου',
+ 'Email subject' => 'Θέμα ηλεκτρονικού ταχυδρομείου',
+ 'Date' => 'Ημέρα',
+ '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 μετέφερε την εργασία n°%d στην 1η λωρίδα',
+ '%s moved the task #%d to the swimlane "%s"' => '%s μετέφερε την εργασία n°%d στη λωρίδα « %s »',
+ 'Swimlane' => 'Λωρίδα',
+ 'Gravatar' => 'Gravatar',
+ '%s moved the task %s to the first swimlane' => '%s μετέφερε την εργασία %s στην 1η λωρίδα',
+ '%s moved the task %s to the swimlane "%s"' => '%s μετέφερε την εργασία %s στη λωρίδα « %s »',
+ 'This report contains all subtasks information for the given date range.' => 'Η έκθεση αυτή περιέχει όλες τις υπο-εργασίες για το συγκεκριμένο εύρος ημερομηνιών.',
+ 'This report contains all tasks information for the given date range.' => 'Η έκθεση αυτή περιέχει όλες τις πληροφορίες για το συγκεκριμένο εύρος ημερομηνιών.',
+ 'Project activities for %s' => 'Ενέργειες για το έργο « %s »',
+ 'view the board on Kanboard' => 'δείτε τον πίνακα στο Kanboard',
+ 'The task have been moved to the first swimlane' => 'Η εργασία αυτή έχει μετακινηθεί στην πρώτη λωρίδα',
+ 'The task have been moved to another swimlane:' => 'Η εργασία αυτή έχει μετακινηθεί σε άλλη λωρίδα :',
+ 'Overdue tasks for the project "%s"' => 'Εκπρόθεσμες εργασίες για το έργο « %s »',
+ 'New title: %s' => 'Νέος τίτλος : %s',
+ 'The task is not assigned anymore' => 'Η εργασία δεν έχει ανατεθεί πλέον',
+ 'New assignee: %s' => 'Καινούργια ανάθεση : %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' => 'Ο χρόνος που πέρασε έχει αλλάξει : %sh',
+ 'Time estimated changed: %sh' => 'Ο εκτιμώμενος χρόνος άλλαξε : %sh',
+ 'The field "%s" have been updated' => 'Το πεδίο « %s » έχει ενημερωθεί',
+ 'The description have 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' => '%%d/%%m/%%Y',
+ 'Total for all columns' => 'Σύνολο για όλες τις στήλες',
+ 'You need at least 2 days of data to show the chart.' => 'Έχετε τουλάχιστον 2 ημέρες δεδομένων για να εμφανιστούν στο διάγραμμα.',
+ '<15m' => '<15m',
+ '<30m' => '<30m',
+ 'Stop timer' => 'Διακοπή ρολογιού',
+ 'Start timer' => 'Έναρξη ρολογιού',
+ 'Add project member' => 'Προσθήκη νέου μέλους έργου',
+ 'Enable notifications' => 'Ενεργοποίηση ειδοποιήσεων',
+ 'My activity stream' => 'Η ροή δραστηριοτήτων μου',
+ 'My calendar' => 'Το ημερολόγιο μου',
+ 'Search tasks' => 'Αναζήτηση εργασιών',
+ 'Back to the calendar' => 'Πίσω στο ημερολόγιο',
+ 'Filters' => 'Φίλτρα',
+ 'Reset filters' => 'Επαναφορά φίλτρων',
+ 'My tasks due tomorrow' => 'Οι εργασίες καθηκόντων μου αύριο',
+ 'Tasks due today' => 'Οι εργασίες καθηκόντων μου αύριο',
+ 'Tasks due tomorrow' => 'Εργασίες καθηκόντων αύριο',
+ 'Tasks due yesterday' => 'Εργασίες καθηκόντων χτές',
+ 'Closed tasks' => 'Κλειστές εργασίες',
+ 'Open tasks' => 'Ανοιχτές εργασίες',
+ 'Not assigned' => 'Δεν έχουν εκχωρηθεί',
+ 'View advanced search syntax' => 'Δείτε τη σύνταξη "αναζήτησης για προχωρημένους"',
+ 'Overview' => 'Επισκόπηση',
+ '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"' => 'Συντόμευση πληκτρολογίου : « %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"' => 'Lead & cycle time για « %s »',
+ 'Average time spent into each column for "%s"' => 'Μέσος χρόνος παραμονής σε κάθε στήλη για « %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.' => 'Αυτό το γράφημα δείχνει ότι ο μέσος χρόνος που δαπανάται σε κάθε στήλη για τις τελευταίες %d εργασίες',
+ 'Average Lead and Cycle time' => 'Average Lead & Cycle time',
+ 'Average lead time: ' => 'Average lead time : ',
+ 'Average cycle time: ' => 'Average cycle time : ',
+ 'Cycle Time' => 'Cycle time',
+ 'Lead Time' => 'Lead time',
+ 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Αυτό το γράφημα δείχνει το average lead and cycle time για τις τελευταίες %d εργασίες κατά τη διάρκεια του χρόνου.',
+ 'Average time into each column' => 'Μέσος χρόνος σε κάθε στήλη',
+ 'Lead and cycle time' => 'Lead et cycle time',
+ 'Lead time: ' => 'Lead time : ',
+ 'Cycle time: ' => 'Cycle time : ',
+ 'Time spent into each column' => 'Ο χρόνος που δαπανήθηκε σε κάθε στήλη',
+ 'The lead time is the duration between the task creation and the completion.' => 'Το <lead time> είναι η διάρκεια μεταξύ της δημιουργίας του έργου και της ολοκλήρωσης του.',
+ 'The cycle time is the duration between the start date and the completion.' => 'Το <cycle time> είναι η διάρκεια μεταξύ της ημερομηνίας εκκίνησης και της ολοκλήρωσης του.',
+ '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.' => 'Στους απομακρυσμένους χρήστες δεν αποθηκεύονται οι κωδικοί πρόσβασης εντός της βάσης δεδομένων της τρέχουσας εφαρμογής, Παραδείγματα: 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.' => 'Αυτή η δυνατότητα δεν δουλεύει σε όλους τους browsers',
+ 'There is no destination project available.' => 'Δεν υπάρχει διαθέσιμο κανένα έργο προορισμού.',
+ 'Trigger automatically subtask time tracking' => 'Αυτόματη ενεργοποίηση της παρακολούθησης χρόνου σε υπο-εργασίες',
+ 'Include closed tasks in the cumulative flow diagram' => 'Να συμπεριλαμβάνονται οι κλειστές εργασίες στο συσσωρευτικό διάγραμμα ροής',
+ 'Current swimlane: %s' => 'Τρέχουσα λωρίδα : %s',
+ 'Current column: %s' => 'Τρέχουσα στήλη : %s',
+ 'Current category: %s' => 'Τρέχουσα κατηγορία : %s',
+ 'no category' => 'Καμμία κατηγορία',
+ 'Current assignee: %s' => 'Τρέχον εκδοχέας : %s',
+ 'not assigned' => 'δεν έχει εκχωρηθεί',
+ 'Author:' => 'Συγγραφέας :',
+ 'contributors' => 'συνεισφέροντες',
+ 'License:' => 'Άδεια :',
+ 'License' => 'Άδεια',
+ 'Enter the text below' => 'Πληκτρολογήστε το παρακάτω κείμενο',
+ 'Gantt chart for %s' => 'Gantt διάγραμμα για %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' => 'Διαγράμματα Gantt',
+ 'People who are project managers' => 'Οι άνθρωποι που είναι οι διευθυντές έργων',
+ 'People who are project members' => 'Οι άνθρωποι που είναι μέλη των έργων',
+ 'NOK - Norwegian Krone' => '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' => 'Διάγραμμα Gantt για όλα τα έργα',
+ 'Projects list' => 'Λίστα έργων',
+ 'Gantt chart for this project' => 'Διάγραμμα Gantt για το έργο',
+ 'Project board' => 'Κεντρικός πίνακας έργου',
+ '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' => 'Ορόσημο',
+ 'Documentation: %s' => 'Τεκμηρίωση : %s',
+ 'Switch to the Gantt chart view' => 'Μεταφορά σε προβολή διαγράμματος Gantt',
+ 'Reset the search/filter box' => 'Αρχικοποίηση του πεδίου αναζήτησης/φιλτραρίσματος',
+ 'Documentation' => 'Τεκμηρίωση',
+ 'Table of contents' => 'Πίνακας περιεχομένων',
+ 'Gantt' => 'Gantt',
+ 'Author' => 'Δημιουργός',
+ 'Version' => 'Έκδοση',
+ 'Plugins' => 'Πρόσθετα',
+ 'There is no plugin loaded.' => 'Δεν έχει φορτωθεί plugin',
+ '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' => 'Web',
+ 'New attachment on task #%d: %s' => 'Νέο συνημμένο για την εργασία n°%d : %s',
+ 'New comment on task #%d' => 'Νέο σχόλιο για την εργασία n°%d',
+ 'Comment updated on task #%d' => 'Ενημέρωση σχολίου για την εργασία n°%d',
+ 'New subtask on task #%d' => 'Νέα υπο-εργασία για την εργασία n°%d',
+ 'Subtask updated on task #%d' => 'Ενημέρωση υπό-εργασίας για την εργασία n°%d',
+ 'New task #%d: %s' => 'Νέα εργασία n°%d : %s',
+ 'Task updated #%d' => 'Η εργασία n°%d ενημερώθηκε με επιτυχία',
+ 'Task #%d closed' => 'Η εργασία n°%d έκλεισε',
+ 'Task #%d opened' => 'Η εργασία n°%d άνοιξε',
+ 'Column changed for task #%d' => 'Η στήλη άλλαξε για την εργασία n°%d',
+ 'New position for task #%d' => 'Νέα θέση για την εργασία n°%d',
+ 'Swimlane changed for task #%d' => 'Η λωρίδα άλλαξε για την εργασία n°%d',
+ 'Assignee changed on task #%d' => 'Η ανάθεση άλλαξε για την εργασία n°%d',
+ '%d overdue tasks' => '%d εκπρόθεσμες εργασίες',
+ 'Task #%d is overdue' => 'Η εργασία n°%d είναι εκπρόθεσμη',
+ 'No new notifications.' => 'Χωρίς νέες ειδοποιήσεις.',
+ 'Mark all as read' => 'Μαρκάρισμα όλων ως διαβασμένα',
+ 'Mark as read' => 'Μαρκάρισμα ως διαβασμένο',
+ 'Total number of tasks in this column across all swimlanes' => 'Συνολικός αριθμός εργασιών σε αυτήν τη στήλη σε όλες τις λωρίδες',
+ 'Collapse swimlane' => 'Συρίκνωση λωρίδας',
+ 'Expand swimlane' => 'Ανάπτυξη λωρίδας',
+ 'Add a new filter' => 'Προσθήκη νέου φίλτρου',
+ 'Share with all project members' => 'Διαμοίραση με όλους τους χρήστες του έργου',
+ 'Shared' => 'Διαμοιρασμένα',
+ 'Owner' => 'Ιδιοκτήτης',
+ 'Unread notifications' => 'Αδιάβαστες ειδοποιήσεις',
+ 'My filters' => 'Τα φίλτρα μου',
+ 'Notification methods:' => 'Μέθοδοι ειδοποίησης :',
+ 'Import tasks from CSV file' => 'Εισαγωγή εργασιών μέσω αρχείου CSV',
+ 'Unable to read your file' => 'Δεν είναι δυνατή η ανάγνωση του αρχείου',
+ '%d task(s) have been imported successfully.' => '%d η(οι) εργασία(ες) εισήχθησαν με επιτυχία.',
+ 'Nothing have been imported!' => 'Τίποτα δεν εισήχθη',
+ 'Import users from CSV file' => 'Εισαγωγή χρηστών μέσω αρχείου CSV',
+ '%d user(s) have been imported successfully.' => '%d ο(οι) χρήστης(ες) εισήχθησαν με επιτυχία.',
+ 'Comma' => 'Κόμμα',
+ 'Semi-colon' => 'Ερωτηματικό',
+ 'Tab' => 'Tab',
+ 'Vertical bar' => 'Κατακόρυφη μπάρα',
+ 'Double Quote' => 'Διπλά εισαγωγικά',
+ 'Single Quote' => 'Μονά εισαγωγικά',
+ '%s attached a file to the task #%d' => '%s συνημμένο αρχείο στην εργασία n°%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' => 'Delimiter',
+ 'Enclosure' => 'Enclosure',
+ 'CSV File' => 'Αρχείο CSV',
+ 'Instructions' => 'Οδηγίες',
+ 'Your file must use the predefined CSV format' => 'Το αρχείο σας πρέπει να χρησιμοποιεί προκαθορισμένη μορφοποίηση CSV',
+ 'Your file must be encoded in UTF-8' => 'Το αρχείο σας πρέπει να έχει κωδικοποίηση χαρακτήρων UTF-8',
+ 'The first row must be the header' => 'Η πρώτη γραμμή πρέπει να είναι η κεφαλίδα',
+ 'Duplicates are not verified for you' => 'Οι Διπλοεγγραφές δεν ελέγχονται',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'Η ημερομηνία λήξης πρέπει να χρησιμοποιεί τη μορφοποίηση ISO : EEEE-MM-HH ή στα αγγλικά YYYY-MM-DD',
+ 'Download CSV template' => 'Κατέβασμα πρότυπου αρχείου CSV',
+ 'No external integration registered.' => 'No external integration registered.',
+ 'Duplicates are not imported' => 'Διπλοεγγραφές δεν εισήχθησαν',
+ 'Usernames must be lowercase and unique' => 'Οι ονομασίες χρηστών πρέπει να είναι σε μικρά γράμματα (lowercase) και μοναδικά',
+ 'Passwords will be encrypted if present' => 'Οι κωδικοί πρόσβασης κρυπτογραφούνται, αν υπάρχουν',
+ '%s attached a new file to the task %s' => '%s νέο συνημμένο αρχείο της εργασίας %s',
+ 'Assign automatically a category based on a link' => 'Ανατίθεται αυτόματα κατηγορία, βασισμένη στον σύνδεσμο',
+ 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
+ 'Assignee Username' => 'Δικαιοδόχο όνομα χρήστη',
+ 'Assignee Name' => 'Δικαιοδόχο όνομα',
+ 'Groups' => 'Ομάδες',
+ 'Members of %s' => 'Μέλη του %s',
+ 'New group' => 'Νέα ομάδα',
+ 'Group created successfully.' => 'Η ομάδα δημιουργήθηκε με επιτυχία.',
+ 'Unable to create your group.' => 'Δεν είναι δυνατή η δημιουργία της ομάδας.',
+ 'Edit group' => 'Διόρθωση ομάδας',
+ 'Group updated successfully.' => 'Η ομάδα ενημερώθηκε με επιτυχία.',
+ 'Unable to update your group.' => 'Δεν είναι δυνατή η ενημέρωση της ομάδας.',
+ 'Add group member to "%s"' => 'Προσθήκη του μέλους της ομάδας στην « %s »',
+ 'Group member added successfully.' => 'Το μέλος της ομ΄δας προστέθηκε με επιτυχία.',
+ 'Unable to add group member.' => 'Δεν είναι δυνατή η προσθήκη μέλους ομάδας.',
+ 'Remove user from group "%s"' => 'Αφαίρεση μέλους από την ομάδα « %s »',
+ 'User removed successfully from this group.' => 'Ο χρήστης αφαιρέθηκε με επιτυχία από την ομάδα.',
+ 'Unable to remove this user from the group.' => 'Δεν είναι δυνατή η αφαίρεση του χρήστη από την ομάδα.',
+ 'Remove group' => 'Αφαίρεση ομάδας',
+ 'Group removed successfully.' => 'Η ομάδα αφαιρέθηκε με επιτυχία.',
+ 'Unable to remove this group.' => 'Δεν είναι δυνατή η αφαίρεση της ομάδας.',
+ 'Project Permissions' => 'Επιτρέψεις έργου',
+ 'Manager' => 'Διευθυντής',
+ 'Project Manager' => 'Διευθυντής έργου',
+ 'Project Member' => 'Μέλος σε έργο',
+ 'Project Viewer' => 'Μέλος μόνο για προβολή έργου',
+ 'Your account is locked for %d minutes' => 'Ο λογαριασμός σας κλειδώθηκε για %d λεπτά',
+ 'Invalid captcha' => 'Μη αποδεκτό Captcha',
+ 'The name must be unique' => 'Το όνομα πρέπει να είναι μοναδικό',
+ 'View all groups' => 'Προβολή όλων των ομάδων',
+ 'View group members' => 'Προβολή των μελών της ομάδας',
+ 'There is no user available.' => 'Δεν υπάρχει διαθέσιμος χρήστης',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Αφαίρεση του χρήστη « %s » από την ομάδα « %s » ?',
+ 'There is no group.' => 'Δεν υπάρχει ομάδα.',
+ 'External Id' => 'Εξωτερικό αναγνωριστικό',
+ 'Add group member' => 'Προσθήκη μέλους ομάδας',
+ 'Do you really want to remove this group: "%s"?' => 'Αφαίρεση της ομάδας : « %s » ?',
+ 'There is no user in this group.' => 'Δεν υπάρχει χρήστης σε αυτήν την ομάδα',
+ 'Remove this user' => 'Αφαίρεση χρήστη',
+ 'Permissions' => 'Επιτρέψεις',
+ 'Allowed Users' => 'Χρήστες που επιτρέπονται',
+ 'No user have been allowed specifically.' => 'Δεν υπάρχει χρήστης που να επιτρέπεται συγκεκριμένα.',
+ 'Role' => 'Ρόλος',
+ 'Enter user name...' => 'Εισαγωγή ονόματος χρήστη...',
+ 'Allowed Groups' => 'Ομάδες που επιτρέπονται',
+ 'No group have been allowed specifically.' => 'Δεν υπάρχει ομάδα που να επιτρέπεται συγκεκριμένα.',
+ 'Group' => 'Ομάδα',
+ 'Group Name' => 'Ονομασία ομάδας',
+ 'Enter group name...' => 'Εισαγωγή ονομασίας ομάδας...',
+ 'Role:' => 'Ρόλος :',
+ 'Project members' => 'Μέλη έργου',
+ 'Compare hours for "%s"' => 'Σύγκριση ωρών για « %s »',
+ '%s mentioned you in the task #%d' => '%s αναφέρονται σε εσάς, στη εργασία n°%d',
+ '%s mentioned you in a comment on the task #%d' => '%s αναφέρονται σε εσάς σε σχόλιο, στη εργασίας n°%d',
+ 'You were mentioned in the task #%d' => 'Αναφέρεστε στην εργασία n°%d',
+ 'You were mentioned in a comment on the task #%d' => 'Αναφέρεστε σε σχόλιο, στην εργασία n°%d',
+ 'Mentioned' => 'Αναφέρεται',
+ 'Compare Estimated Time vs Actual Time' => 'Σύγκριση προβλεπόμενου χρόνου vs πραγματικού χρόνου',
+ 'Estimated hours: ' => 'Προβλεπόμενες ώρες : ',
+ 'Actual hours: ' => 'Πραγματικές ώρες : ',
+ 'Hours Spent' => 'Δαπανόμενες ώρες',
+ 'Hours Estimated' => 'Προβλεπόμενες ώρες',
+ 'Estimated Time' => 'Προβλεπόμενος χρόνος',
+ 'Actual Time' => 'Πραγματικός χρόνος',
+ 'Estimated vs actual time' => 'Προβλεπόμενος vs πραγματικός χρόνος',
+ 'RUB - Russian Ruble' => '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' => 'Time-based One-time Password Algorithm',
+ 'Two-Factor Provider: ' => 'Two-Factor Provider : ',
+ 'Disable two-factor authentication' => 'Disable two-factor authentication',
+ 'Enable two-factor authentication' => 'Enable two-factor authentication',
+ 'There is no integration registered at the moment.' => 'There is no integration registered at the moment.',
+ 'Password Reset for Kanboard' => 'Αρχικοποίηση κωδικών πρόσβασης για την εφαρμογή 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.' => 'Όλες οι εργασίας της στήλης « %s » και η λωρίδα « %s » έκλεισαν με επιτυχία.',
+ '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.' => '%d εργασία(ες) στη στήλη « %s » και στη λωρίδα « %s » θα κλείσουν.',
+ 'Close all tasks of this column' => 'Κλείσιμο όλων των εργασιών αυτής της στήλης',
+ 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Κανένα plugin δεν έχει καταχωρηθεί με τη μέθοδο της κοινοποίησης του έργου. Μπορείτε ακόμα να διαμορφώσετε τις μεμονωμένες κοινοποιήσεις στο προφίλ χρήστη σας.',
+ '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
+);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index 17554a3e..46cbb151 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Suprimir el proyecto',
'Edit the board for "%s"' => 'Modificar el tablero para « %s »',
'All projects' => 'Todos los proyectos',
- 'Change columns' => 'Cambiar las columnas',
'Add a new column' => 'Añadir una nueva columna',
'Title' => 'Título',
'Nobody assigned' => 'Nadie asignado',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Abrir una tarea',
'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: « %s » ?',
'Back to the board' => 'Volver al tablero',
- 'Created on %B %e, %Y at %k:%M %p' => 'Creado el %e de %B de %Y a las %k:%M %p',
'There is nobody assigned' => 'No hay nadie asignado a esta tarea',
'Column on the board:' => 'Columna en el tablero: ',
- 'Status is open' => 'Estado abierto',
- 'Status is closed' => 'Estado cerrado',
'Close this task' => 'Cerrar esta tarea',
'Open this task' => 'Abrir esta tarea',
'There is no description.' => 'No hay descripción.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'El identificador es obligatorio',
'The project id is required' => 'El identificador del proyecto es obligatorio',
'The project name is required' => 'El nombre del proyecto es obligatorio',
- 'This project must be unique' => 'El nombre del proyecto debe ser único',
'The title is required' => 'El título es obligatorio',
'Settings saved successfully.' => 'Parámetros guardados correctamente.',
'Unable to save your settings.' => 'No se pueden guardar sus parámetros.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'En curso',
'Done' => 'Hecho',
'Application version:' => 'Versión de la aplicación:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Completado el %e de %B de %Y a las %k:%M %p',
- '%B %e, %Y at %k:%M %p' => '%e de %B de %Y a las %k:%M %p',
- 'Date created' => 'Fecha de creación',
- 'Date completed' => 'Fecha de terminación',
'Id' => 'Identificador',
'%d closed tasks' => '%d tareas completadas',
'No task for this project' => 'Ninguna tarea para este proyecto',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Complejidad',
'Task limit' => 'Número máximo de tareas',
'Task count' => 'Contador de tareas',
- 'Edit project access list' => 'Editar los permisos del proyecto',
- 'Allow this user' => 'Autorizar a este usuario',
- 'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.',
- 'Revoke' => 'Revocar',
- 'List of authorized users' => 'Lista de los usuarios autorizados',
'User' => 'Usuario',
- 'Nobody have access to this project.' => 'Nadie tiene acceso a este proyecto',
'Comments' => 'Comentarios',
'Write your text in Markdown' => 'Redacta el texto en Markdown',
'Leave a comment' => 'Dejar un comentario',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Editar esta tarea',
'Due Date' => 'Fecha límite',
'Invalid date' => 'Fecha no válida',
- 'Must be done before %B %e, %Y' => 'Debe de estar hecho antes del %e de %B de %Y',
- '%B %e, %Y' => '%e de %B de %Y',
- '%b %e, %Y' => '%e de %B de %Y',
'Automatic actions' => 'Acciones automatizadas',
'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.',
'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Asignar un color a un usuario específico',
'Column title' => 'Título de la columna',
'Position' => 'Posición',
- 'Move Up' => 'Mover hacia arriba',
- 'Move Down' => 'Mover hacia abajo',
'Duplicate to another project' => 'Duplicar a otro proyecto',
'Duplicate' => 'Duplicar',
'link' => 'vinculación',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'El comentario ha sido suprimido correctamente.',
'Unable to remove this comment.' => 'No se puede suprimir este comentario.',
'Do you really want to remove this comment?' => '¿Desea suprimir este comentario?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Sólo los administradores o el autor del comentario tienen acceso a esta página.',
'Current password for the user "%s"' => 'Contraseña actual para el usuario: « %s »',
'The current password is required' => 'La contraseña es obligatoria',
'Wrong password' => 'contraseña incorrecta',
@@ -267,10 +246,6 @@ return array(
'External authentication failed' => 'Falló la autenticación externa',
'Your external account is linked to your profile successfully.' => 'Su cuenta externa se ha vinculado exitosamente con su perfil.',
'Email' => 'Correo',
- 'Link my Google Account' => 'Vincular con mi Cuenta en Google',
- 'Unlink my Google Account' => 'Desvincular de mi Cuenta en Google',
- 'Login with my Google Account' => 'Ingresar con mi Cuenta de Google',
- 'Project not found.' => 'Proyecto no hallado.',
'Task removed successfully.' => 'Tarea suprimida correctamente.',
'Unable to remove this task.' => 'No pude suprimir esta tarea.',
'Remove a task' => 'Borrar una tarea',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Tamaño máximo',
'Unable to upload the file.' => 'No pude cargar el fichero.',
'Display another project' => 'Mostrar otro proyecto',
- 'Login with my Github Account' => 'Ingresar con mi cuenta de Github',
- 'Link my Github Account' => 'Vincular mi cuenta de Github',
- 'Unlink my Github Account' => 'Desvincular mi cuenta de Github',
'Created by %s' => 'Creado por %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificación %e de %B de %Y a las %k:%M %p',
'Tasks Export' => 'Exportar tareas',
'Tasks exportation for "%s"' => 'Exportación de tareas para "%s"',
'Start Date' => 'Fecha de inicio',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Quiero recibir notificaciones sólo de estos proyectos:',
'view the task on Kanboard' => 'ver la tarea en Kanboard',
'Public access' => 'Acceso público',
- 'User management' => 'Gestión de Usuarios',
'Active tasks' => 'Tareas activas',
'Disable public access' => 'Desactivar acceso público',
'Enable public access' => 'Activar acceso público',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Correo electrónico:',
'Notifications:' => 'Notificaciones:',
'Notifications' => 'Notificaciones',
- 'Group:' => 'Grupo:',
- 'Regular user' => 'Usuario regular',
'Account type:' => 'Tipo de Cuenta:',
'Edit profile' => 'Editar perfil',
'Change password' => 'Cambiar contraseña',
'Password modification' => 'Modificacion de contraseña',
'External authentications' => 'Autenticación externa',
- 'Google Account' => 'Cuenta de Google',
- 'Github Account' => 'Cuenta de Github',
'Never connected.' => 'Nunca se ha conectado.',
- 'No account linked.' => 'Sin vínculo con cuenta.',
- 'Account linked.' => 'Vinculada con Cuenta.',
'No external authentication enabled.' => 'Sin autenticación externa activa.',
'Password modified successfully.' => 'Contraseña cambiada correctamente.',
'Unable to change the password.' => 'No pude cambiar la contraseña.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s cambió el concesionario de la tarea %s por %s',
'New password for the user "%s"' => 'Nueva contraseña para el usuario "%s"',
'Choose an event' => 'Seleccione un evento',
- 'Github commit received' => 'Envío a Github recibido',
- 'Github issue opened' => 'Abierto asunto en Github',
- 'Github issue closed' => 'Cerrado asunto en Github',
- 'Github issue reopened' => 'Reabierto asunto en Github',
- 'Github issue assignee change' => 'Cambio en concesionario de asunto de Github',
- 'Github issue label change' => 'Cambio en etiqueta de asunto de Github',
'Create a task from an external provider' => 'Crear una tarea a partir de un proveedor externo',
'Change the assignee based on an external username' => 'Cambiar el concesionario basado en un nombre de usuario externo',
'Change the category based on an external label' => 'Cambiar la categoría basado en una etiqueta externa',
'Reference' => 'Referencia',
- 'Reference: %s' => 'Referencia: %s',
'Label' => 'Etiqueta',
'Database' => 'Base de Datos',
'About' => 'Acerca de',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frecuencia en segundos (60 segundos por defecto)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frecuencia en segundos (0 para deshabilitar esta característica, 10 segundos por defecto)',
'Application URL' => 'URL de la aplicación',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Ejemplo: http://ejemplo.kanboard.net/ (usado por las notificaciones de correo)',
'Token regenerated.' => 'Ficha regenerada.',
'Date format' => 'Formato de la fecha',
'ISO format is always accepted, example: "%s" and "%s"' => 'El formato ISO siempre es aceptado, ejemplo: "%s" y "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Tiempo estimado: %s horas',
- 'Time spent: %s hours' => 'Tiempo invertido: %s horas',
- 'Started on %B %e, %Y' => 'Iniciado el %e de %B de %Y',
'Start date' => 'Fecha de inicio',
'Time estimated' => 'Tiempo estimado',
'There is nothing assigned to you.' => 'No tiene nada asignado.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Cualquiera tiene acceso a este proyecto',
'Webhooks' => 'Disparadores Web (Webhooks)',
'API' => 'API',
- 'Github webhooks' => 'Disparadores Web (Webhooks) de Github',
- 'Help on Github webhooks' => 'Ayuda con los Disparadores Web (Webhook) de Github',
'Create a comment from an external provider' => 'Crear un comentario a partir de un proveedor externo',
- 'Github issue comment created' => 'Creado el comentario del problema en Github',
'Project management' => 'Administración del proyecto',
'My projects' => 'Mis proyectos',
'Columns' => 'Columnas',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Repartición para "%s"',
'Clone this project' => 'Clonar este proyecto',
'Column removed successfully.' => 'Columna eliminada correctamente',
- 'Github Issue' => 'Problema con Github',
'Not enough data to show the graph.' => 'No hay suficiente información para mostrar el gráfico.',
'Previous' => 'Anterior',
'The id must be an integer' => 'El id debe ser un entero',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Calle por defecto',
'Do you really want to remove this swimlane: "%s"?' => '¿Realmente quiere quitar esta calle: "%s"?',
'Inactive swimlanes' => 'Calles inactivas',
- 'Set project manager' => 'Asignar administrador del proyecto',
- 'Set project member' => 'Asignar miembro del proyecto',
'Remove a swimlane' => 'Quitar un calle',
- 'Rename' => 'Renombrar',
'Show default swimlane' => 'Mostrar calle por defecto',
'Swimlane modification for the project "%s"' => 'Modificación de la calle para el proyecto "%s"',
'Swimlane not found.' => 'Calle no encontrada',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Calles',
'Swimlane updated successfully.' => 'Calle actualizada correctamente',
'The default swimlane have been updated successfully.' => 'La calle por defecto ha sido actualizada correctamente',
- 'Unable to create your swimlane.' => 'Imposible crear su calle',
'Unable to remove this swimlane.' => 'Imposible de quitar esta calle',
'Unable to update this swimlane.' => 'Imposible de actualizar esta calle',
'Your swimlane have been created successfully.' => 'Su calle ha sido creada correctamente',
'Example: "Bug, Feature Request, Improvement"' => 'Ejemplo: "Error, Solicitud de característica, Mejora',
'Default categories for new projects (Comma-separated)' => 'Categorías por defecto para nuevos proyectos (separadas por comas)',
- 'Gitlab commit received' => 'Recibido envío desde Gitlab',
- 'Gitlab issue opened' => 'Abierto asunto de Gitlab',
- 'Gitlab issue closed' => 'Cerrado asunto de Gitlab',
- 'Gitlab webhooks' => 'Disparadores Web (Webhooks) de Gitlab',
- 'Help on Gitlab webhooks' => 'Ayuda sobre Disparadores Web (Webhooks) de Gitlab',
'Integrations' => 'Integraciones',
'Integration with third-party services' => 'Integración con servicios de terceros',
- 'Role for this project' => 'Papel de este proyecto',
- 'Project manager' => 'Administrador de proyecto',
- 'Project member' => 'Miembro de proyecto',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un administrador de proyecto puede cambiar sus valores y tener más privilegios que un usuario estándar.',
- 'Gitlab Issue' => 'Asunto Gitlab',
'Subtask Id' => 'Id de Subtarea',
'Subtasks' => 'Subtareas',
'Subtasks Export' => 'Exportación de Subtareas',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'Ya dispones de una subtarea en progreso',
'Which parts of the project do you want to duplicate?' => '¿Qué partes del proyecto desea duplicar?',
'Disallow login form' => 'Deshabilitar formulario de ingreso',
- 'Bitbucket commit received' => 'Recibido envío desde Bitbucket',
- 'Bitbucket webhooks' => 'Disparadores Web (webhooks) de Bitbucket',
- 'Help on Bitbucket webhooks' => 'Ayuda sobre disparadores web (webhooks) de Bitbucket',
'Start' => 'Inicio',
'End' => 'Fin',
'Task age in days' => 'Edad de la tarea en días',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Esta tarea',
'<1h' => '<1h',
'%dh' => '%dh',
- '%b %e' => '%e de %b',
'Expand tasks' => 'Espande tareas',
'Collapse tasks' => 'Colapsa tareas',
'Expand/collapse tasks' => 'Expande/colapasa tareas',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Atajos de teclado',
'Open board switcher' => 'Abrir conmutador de tablero',
'Application' => 'Aplicación',
- 'since %B %e, %Y at %k:%M %p' => 'desde %e de %B de %Y a las %k:%M %p',
'Compact view' => 'Compactar vista',
'Horizontal scrolling' => 'Desplazamiento horizontal',
'Compact/wide view' => 'Vista compacta/amplia',
'No results match:' => 'No hay resultados coincidentes:',
'Currency' => 'Moneda',
- 'Files' => 'Ficheros',
- 'Images' => 'Imágenes',
'Private project' => 'Proyecto privado',
'AUD - Australian Dollar' => 'AUD - Dólar australiano',
'CAD - Canadian Dollar' => 'CAD - Dólar canadiense',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => 'El código de autenticación de dos factores es válido',
'Code' => 'Código',
'Two factor authentication' => 'Autenticación de dos factores',
- 'Enable/disable two factor authentication' => 'Activar/desactivar autenticación de dos factores',
'This QR code contains the key URI: ' => 'Este código QR contiene la clave URI: ',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Guarda la clave secreta en su software TOTP (por ejemplo Autenticación de Google o FreeOTP).',
'Check my code' => 'Revisar mi código',
'Secret key: ' => 'Clave secreta: ',
'Test your device' => 'Probar su dispositivo',
'Assign a color when the task is moved to a specific column' => 'Asignar un color al mover la tarea a una columna específica',
'%s via Kanboard' => '%s vía Kanboard',
- 'uploaded by: %s' => 'cargado por: %s',
- 'uploaded on: %s' => 'cargado en: %s',
- 'size: %s' => 'tamaño: %s',
'Burndown chart for "%s"' => 'Trabajo pendiente para "%s"',
'Burndown chart' => 'Trabajo pendiente',
'This chart show the task complexity over the time (Work Remaining).' => 'Este diagrama mestra la complejidad de las tareas a lo largo del tiempo (Trabajo restante)',
@@ -722,7 +645,6 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Capture un patallazo y pulse CTRL+V o ⌘+V para pegar aquí.',
'Screenshot uploaded successfully.' => 'Pantallazo cargado con éxito',
'SEK - Swedish Krona' => 'SEK - Corona sueca',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'El identificador del proyecto us un código opcional alfanumérico que se usa para identificar su proyecto.',
'Identifier' => 'Identificador',
'Disable two factor authentication' => 'Desactivar la autenticación de dos factores',
'Do you really want to disable the two factor authentication for this user: "%s"?' => '¿Realmentes quiere desactuvar la autenticación de dos factores para este usuario: "%s?"',
@@ -731,7 +653,6 @@ return array(
'A task cannot be linked to itself' => 'Una tarea no puede se enlazada con sigo misma',
'The exact same link already exists' => 'El mismo enlace ya existe',
'Recurrent task is scheduled to be generated' => 'Tarea recurrente programada para ser generada',
- 'Recurring information' => 'Información recurrente',
'Score' => 'Puntuación',
'The identifier must be unique' => 'El identificador debe ser único',
'This linked task id doesn\'t exists' => 'El id de tarea no existe',
@@ -777,21 +698,10 @@ return array(
'User that will receive the email' => 'Usuario que recibirá el correo',
'Email subject' => 'Asunto del correo',
'Date' => 'Fecha',
- 'By @%s on Bitbucket' => 'Mediante @%s en Bitbucket',
- 'Bitbucket Issue' => 'Asunto de Bitbucket',
- 'Commit made by @%s on Bitbucket' => 'Envío realizado por @%s en Bitbucket',
- 'Commit made by @%s on Github' => 'Envío realizado por @%s en Github',
- 'By @%s on Github' => 'Por @%s en Github',
- 'Commit made by @%s on Gitlab' => 'Envío realizado por @%s en Gitlab',
'Add a comment log when moving the task between columns' => 'Añadir un comentario al mover la tarea entre columnas',
'Move the task to another column when the category is changed' => 'Mover la tarea a otra columna cuando cambia la categoría',
'Send a task by email to someone' => 'Enviar una tarea a alguien por correo',
'Reopen a task' => 'Reabrir tarea',
- 'Bitbucket issue opened' => 'Abierto asunto de Bitbucket',
- 'Bitbucket issue closed' => 'Cerrado asunto de Bitbucket',
- 'Bitbucket issue reopened' => 'Reabierto asunto de Bitbucket',
- 'Bitbucket issue assignee change' => 'Cambiado concesionario de asunto de Bitbucket',
- 'Bitbucket issue comment created' => 'Creado comentario de asunto de Bitbucket',
'Column change' => 'Cambio de columna',
'Position change' => 'Cambio de posición',
'Swimlane change' => 'Cambio de calle',
@@ -826,17 +736,11 @@ return array(
'The field "%s" have been updated' => 'Se ha actualizado el campo "%s"',
'The description have been modified' => 'Se ha modificado la descripción',
'Do you really want to close the task "%s" as well as all subtasks?' => '¿De verdad que quiere cerra la tarea "%s" así como todas las subtareas?',
- 'Swimlane: %s' => 'Calle: %s',
'I want to receive notifications for:' => 'Deseo recibir notificaciones para:',
'All tasks' => 'Todas las tareas',
'Only for tasks assigned to me' => 'Sólo para las tareas que me han sido asignadas',
'Only for tasks created by me' => 'Sólo para las taread creadas por mí',
'Only for tasks created by me and assigned to me' => 'Sólo para las tareas credas por mí y que me han sido asignadas',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%e de %B de %Y a las %k:%M %p',
- 'New due date: %B %e, %Y' => 'Nueva fecha de entrega: %B %e, %Y',
- 'Start date changed: %B %e, %Y' => 'Cambiadad fecha de inicio: %B %e, %Y',
- '%k:%M %p' => '%k:%M %p',
'%%Y-%%m-%%d' => '%%d/%%M/%%Y',
'Total for all columns' => 'Total para todas las columnas',
'You need at least 2 days of data to show the chart.' => 'Necesitas al menos 2 días de datos para mostrar el gráfico.',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'No asignada',
'View advanced search syntax' => 'Ver sintáxis avanzada de búsqueda',
'Overview' => 'Resumen',
- '%b %e %Y' => '%e de %B de %Y',
'Board/Calendar/List view' => 'Vista de Tablero/Calendario/Lista',
'Switch to the board view' => 'Cambiar a vista de tablero',
'Switch to the calendar view' => 'Cambiar a vista de calendario',
@@ -894,10 +797,6 @@ return array(
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Esta gráfica muestra el plazo medio de entrega y de ciclo para las %d últimas tareas transcurridas.',
'Average time into each column' => 'Tiempo medio en cada columna',
'Lead and cycle time' => 'Plazo de entrega y de ciclo',
- 'Google Authentication' => 'Autenticación de Google',
- 'Help on Google authentication' => 'Ayuda con la aAutenticación de Google',
- 'Github Authentication' => 'Autenticación de Github',
- 'Help on Github authentication' => 'Ayuda con la autenticación de Github',
'Lead time: ' => 'Plazo de entrega: ',
'Cycle time: ' => 'Tiempo de Ciclo: ',
'Time spent into each column' => 'Tiempo empleado en cada columna',
@@ -906,18 +805,12 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Si la tarea no se cierra, se usa la fecha actual en lugar de la de terminación.',
'Set automatically the start date' => 'Poner la fecha de inicio de forma automática',
'Edit Authentication' => 'Editar autenticación',
- 'Google Id' => 'Id de Google',
- 'Github Id' => 'Id de Github',
'Remote user' => 'Usuario remoto',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Los usuarios remotos no almacenan sus contraseñas en la base de datos Kanboard, por ejemplo: cuentas de LDAP, Google y Github',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si marcas la caja de edición "Desactivar formulario de ingreso", se ignoran las credenciales entradas en el formulario de ingreso.',
- 'By @%s on Gitlab' => 'Por @%s en Gitlab',
- 'Gitlab issue comment created' => 'Creado comentario de asunto de Gitlab',
'New remote user' => 'Nuevo usuario remoto',
'New local user' => 'Nuevo usuario local',
'Default task color' => 'Color por defecto de tarea',
- 'Hide sidebar' => 'Ocultar barra lateral',
- 'Expand sidebar' => 'Expandir barra lateral',
'This feature does not work with all browsers.' => 'Esta característica no funciona con todos los navegadores',
'There is no destination project available.' => 'No está disponible proyecto destino',
'Trigger automatically subtask time tracking' => 'Disparar de forma automática seguimiento temporal de subtarea',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'contribuyentes',
'License:' => 'Licencia:',
'License' => 'Licencia',
- 'Project Administrator' => 'Administrador del Proyecto',
'Enter the text below' => 'Digita el texto de abajo',
'Gantt chart for %s' => 'Diagrama de Gantt para %s',
'Sort by position' => 'Clasificado mediante posición',
@@ -952,11 +844,9 @@ return array(
'open file' => 'abrir fichero',
'End date' => 'Fecha de fin',
'Users overview' => 'Resumen de usuarios',
- 'Managers' => 'Administradores',
'Members' => 'Miembros',
'Shared project' => 'Proyecto compartido',
'Project managers' => 'Administradores de proyecto',
- 'Project members' => 'Miembros de proyecto',
'Gantt chart for all projects' => 'Diagrama de Gantt para todos los proyectos',
'Projects list' => 'Lista de proyectos',
'Gantt chart for this project' => 'Diagrama de Gantt para este proyecto',
@@ -964,26 +854,16 @@ 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',
- 'Start date: %s' => 'Fecha inicial: %s',
- 'End date: %s' => 'Fecha final: %s',
'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',
- 'Login with my Gitlab Account' => 'Ingresar usando mi Cuenta en Gitlab',
'Milestone' => 'Hito',
- 'Gitlab Authentication' => 'Autenticación Gitlab',
- 'Help on Gitlab authentication' => 'Ayuda con autenticación Gitlab',
- 'Gitlab Id' => 'Id de Gitlab',
- 'Gitlab Account' => 'Cuenta de Gitlab',
- 'Link my Gitlab Account' => 'Enlazar con mi Cuenta en Gitlab',
- 'Unlink my Gitlab Account' => 'Desenlazar con mi Cuenta en Gitlab',
'Documentation: %s' => 'Documentación: %s',
'Switch to the Gantt chart view' => 'Conmutar a vista de diagrama de Gantt',
'Reset the search/filter box' => 'Limpiar la caja del filtro de búsqueda',
'Documentation' => 'Documentación',
'Table of contents' => 'Tabla de contenido',
'Gantt' => 'Gantt',
- 'Help with project permissions' => 'Ayuda con permisos del proyecto',
'Author' => 'Autor',
'Version' => 'Versión',
'Plugins' => 'Plugins',
@@ -1046,7 +926,6 @@ return array(
'Append/Replace' => 'Añadir/Reemplazar',
'Append' => 'Añadir',
'Replace' => 'Reemplazar',
- 'There is no notification method registered.' => 'No hay método de notificación registrado',
'Import' => 'Importar',
'change sorting' => 'Cambiar orden',
'Tasks Importation' => 'Importación de tareas',
@@ -1064,4 +943,209 @@ return array(
'Duplicates are not imported' => 'Los duplicados no son importados',
'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' => '',
+ // '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' => 'Miembros de proyecto',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ 'Estimated vs actual time' => 'Tiempo estimado vs real',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 998684f9..169fa665 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Poista projekti',
'Edit the board for "%s"' => 'Muokkaa taulua projektille "%s"',
'All projects' => 'Kaikki projektit',
- 'Change columns' => 'Muokkaa sarakkeita',
'Add a new column' => 'Lisää uusi sarake',
'Title' => 'Nimi',
'Nobody assigned' => 'Ei suorittajaa',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Avaa tehtävä',
'Do you really want to open this task: "%s"?' => 'Haluatko varmasti avata tehtävän: "%s"?',
'Back to the board' => 'Takaisin tauluun',
- 'Created on %B %e, %Y at %k:%M %p' => 'Luotu %d.%m.%Y kello %H:%M',
'There is nobody assigned' => 'Ei suorittajaa',
'Column on the board:' => 'Sarake taululla: ',
- 'Status is open' => 'Status on avoin',
- 'Status is closed' => 'Status on suljettu',
'Close this task' => 'Sulje tämä tehtävä',
'Open this task' => 'Avaa tämä tehtävä',
'There is no description.' => 'Ei kuvausta.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'ID vaaditaan',
'The project id is required' => 'Projektin ID on pakollinen',
'The project name is required' => 'Projektin nimi on pakollinen',
- 'This project must be unique' => 'Projektin nimi täytyy olla uniikki',
'The title is required' => 'Otsikko vaaditaan',
'Settings saved successfully.' => 'Asetukset tallennettu onnistuneesti.',
'Unable to save your settings.' => 'Asetusten tallentaminen epäonnistui.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Työnalla',
'Done' => 'Tehty',
'Application version:' => 'Ohjelman versio:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Valmistunut %d.%m.%Y kello %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d.%m.%Y kello %H:%M',
- 'Date created' => 'Luomispäivä',
- 'Date completed' => 'Valmistumispäivä',
'Id' => 'Id',
'%d closed tasks' => '%d suljettua tehtävää',
'No task for this project' => 'Ei tehtävää tälle projektille',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Monimutkaisuus',
'Task limit' => 'Tehtävien maksimimäärä',
'Task count' => 'Tehtävien määrä',
- 'Edit project access list' => 'Muuta projektin käyttäjiä',
- 'Allow this user' => 'Salli tämä projekti',
- 'Don\'t forget that administrators have access to everything.' => 'Muista että ylläpitäjät pääsevät kaikkialle.',
- 'Revoke' => 'Poista',
- 'List of authorized users' => 'Sallittujen käyttäjien lista',
'User' => 'Käyttäjät',
- // 'Nobody have access to this project.' => '',
'Comments' => 'Kommentit',
'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla',
'Leave a comment' => 'Lisää kommentti',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Muokkaa tehtävää',
'Due Date' => 'Deadline',
'Invalid date' => 'Virheellinen päiväys',
- 'Must be done before %B %e, %Y' => 'Täytyy suorittaa ennen %d.%m.%Y',
- '%B %e, %Y' => '%d.%m.%Y',
- // '%b %e, %Y' => '',
'Automatic actions' => 'Automaattiset toiminnot',
'Your automatic action have been created successfully.' => 'Toiminto suoritettiin onnistuneesti.',
'Unable to create your automatic action.' => 'Automaattisen toiminnon luominen epäonnistui.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Valitse väri käyttäjälle',
'Column title' => 'Sarakkeen nimi',
'Position' => 'Positio',
- 'Move Up' => 'Siirrä ylös',
- 'Move Down' => 'Siirrä alas',
'Duplicate to another project' => 'Kopioi toiseen projektiin',
'Duplicate' => 'Monista',
'link' => 'linkki',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Kommentti poistettiin onnistuneesti.',
'Unable to remove this comment.' => 'Kommentin poistaminen epäonnistui.',
'Do you really want to remove this comment?' => 'Haluatko varmasti poistaa tämän kommentin?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Vain ylläpitäjillä tai kommentin jättäjällä on pääsy tälle sivulle.',
'Current password for the user "%s"' => 'Käyttäjän "%s" salasana',
'The current password is required' => 'Salasana vaaditaan',
'Wrong password' => 'Väärä salasana',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'Sähköposti',
- 'Link my Google Account' => 'Linkitä Google-tili',
- 'Unlink my Google Account' => 'Poista Google-tilin linkitys',
- 'Login with my Google Account' => 'Kirjaudu Google tunnuksella',
- 'Project not found.' => 'Projektia ei löytynyt.',
'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.',
'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.',
'Remove a task' => 'Poista tehtävä',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maksimikoko: ',
'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.',
'Display another project' => 'Näytä toinen projekti',
- 'Login with my Github Account' => 'Kirjaudu sisään Github-tililläni',
- 'Link my Github Account' => 'Liitä Github-tilini',
- 'Unlink my Github Account' => 'Poista liitos Github-tiliini',
'Created by %s' => 'Luonut: %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Viimeksi muokattu %B %e, %Y kello %H:%M',
'Tasks Export' => 'Tehtävien vienti',
'Tasks exportation for "%s"' => 'Tehtävien vienti projektilta "%s"',
'Start Date' => 'Aloituspäivä',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Haluan vastaanottaa ilmoituksia ainoastaan näistä projekteista:',
'view the task on Kanboard' => 'katso tehtävää Kanboardissa',
'Public access' => 'Julkinen käyttöoikeus',
- 'User management' => 'Käyttäjähallinta',
'Active tasks' => 'Aktiiviset tehtävät',
'Disable public access' => 'Poista käytöstä julkinen käyttöoikeus',
'Enable public access' => 'Ota käyttöön ',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Sähköpostiosoite:',
'Notifications:' => 'Ilmoitukset:',
'Notifications' => 'Ilmoitukset',
- 'Group:' => 'Ryhmä:',
- 'Regular user' => 'Peruskäyttäjä',
'Account type:' => 'Tilin tyyppi:',
'Edit profile' => 'Muokkaa profiilia',
'Change password' => 'Vaihda salasana',
'Password modification' => 'Salasanan vaihto',
'External authentications' => 'Muut tunnistautumistavat',
- 'Google Account' => 'Google-tili',
- 'Github Account' => 'Github-tili',
'Never connected.' => 'Ei koskaan liitetty.',
- 'No account linked.' => 'Tiliä ei ole liitetty.',
- 'Account linked.' => 'Tili on liitetty.',
'No external authentication enabled.' => 'Muita tunnistautumistapoja ei ole otettu käyttöön.',
'Password modified successfully.' => 'Salasana vaihdettu onnistuneesti.',
'Unable to change the password.' => 'Salasanan vaihto epäonnistui.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s vaihtoi tehtävän %s saajaksi %s',
'New password for the user "%s"' => 'Uusi salasana käyttäjälle "%s"',
'Choose an event' => 'Valitse toiminta',
- 'Github commit received' => 'Github-kommitti vastaanotettu',
- 'Github issue opened' => 'Github-issue avattu',
- 'Github issue closed' => 'Github-issue suljettu',
- 'Github issue reopened' => 'Github-issue uudelleenavattu',
- 'Github issue assignee change' => 'Github-issuen saajan vaihto',
- 'Github issue label change' => 'Github-issuen labelin vaihto',
'Create a task from an external provider' => 'Luo tehtävä ulkoiselta tarjoajalta',
'Change the assignee based on an external username' => 'Vaihda tehtävän saajaa perustuen ulkoiseen käyttäjänimeen',
'Change the category based on an external label' => 'Vaihda kategoriaa perustuen ulkoiseen labeliin',
'Reference' => 'Viite',
- 'Reference: %s' => 'Viite: %s',
'Label' => 'Label',
'Database' => 'Tietokanta',
'About' => 'Tietoja',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Päivitystiheys sekunteina (60 sekuntia oletuksena)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Päivitystiheys sekunteina (0 poistaa toiminnon käytöstä, oletuksena 10 sekuntia)',
'Application URL' => 'Sovelluksen URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Esimerkiksi: http://example.kanboard.net/ (käytetään sähköposti-ilmoituksissa)',
'Token regenerated.' => 'Token uudelleenluotu.',
'Date format' => 'Päiväyksen muoto',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-muoto on aina hyväksytty, esimerkiksi %s ja %s',
@@ -482,9 +438,6 @@ return array(
'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ää',
- 'Estimated time: %s hours' => 'Arvioitu aika: %s tuntia',
- 'Time spent: %s hours' => 'Aikaa kulunut: %s tuntia',
- 'Started on %B %e, %Y' => 'Aloitettu %B %e, %Y',
'Start date' => 'Aloituspäivä',
'Time estimated' => 'Arvioitu aika',
'There is nothing assigned to you.' => 'Ei tehtäviä, joihin sinut olisi merkitty tekijäksi.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Kaikilla on käyttöoikeus projektiin.',
// 'Webhooks' => '',
// 'API' => '',
- // 'Github webhooks' => '',
- // 'Help on Github webhooks' => '',
// 'Create a comment from an external provider' => '',
- // 'Github issue comment created' => '',
'Project management' => 'Projektin hallinta',
'My projects' => 'Minun projektini',
'Columns' => 'Sarakkeet',
@@ -517,7 +467,6 @@ return array(
// 'User repartition for "%s"' => '',
'Clone this project' => 'Kahdenna projekti',
'Column removed successfully.' => 'Sarake poistettu onnstuneesti.',
- 'Github Issue' => 'Github-issue',
'Not enough data to show the graph.' => 'Ei riittävästi dataa graafin näyttämiseksi.',
'Previous' => 'Edellinen',
'The id must be an integer' => 'ID:n on oltava kokonaisluku',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Oletuskaista',
'Do you really want to remove this swimlane: "%s"?' => 'Haluatko varmasti poistaa tämän kaistan: "%s"?',
'Inactive swimlanes' => 'Passiiviset kaistat',
- // 'Set project manager' => '',
- // 'Set project member' => '',
'Remove a swimlane' => 'Poista kaista',
- 'Rename' => 'Uudelleennimeä',
'Show default swimlane' => 'Näytä oletuskaista',
'Swimlane modification for the project "%s"' => 'Kaistamuutos projektille "%s"',
'Swimlane not found.' => 'Kaistaa ei löydy',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Kaistat',
'Swimlane updated successfully.' => 'Kaista päivitetty onnistuneesti.',
'The default swimlane have been updated successfully.' => 'Oletuskaista päivitetty onnistuneesti.',
- 'Unable to create your swimlane.' => 'Kaistan luonti epäonnistui.',
'Unable to remove this swimlane.' => 'Kaistan poisto epäonnistui.',
'Unable to update this swimlane.' => 'Kaistan päivittäminen epäonnistui.',
'Your swimlane have been created successfully.' => 'Kaista luotu onnistuneesti.',
'Example: "Bug, Feature Request, Improvement"' => 'Esimerkiksi: "Bugit, Ominaisuuspyynnöt, Parannukset"',
'Default categories for new projects (Comma-separated)' => 'Oletuskategoriat uusille projekteille (pilkuin eroteltu)',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
// 'Integrations' => '',
// 'Integration with third-party services' => '',
- // 'Role for this project' => '',
- // 'Project manager' => '',
- // 'Project member' => '',
- // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '',
- // 'Gitlab Issue' => '',
// 'Subtask Id' => '',
// 'Subtasks' => '',
// 'Subtasks Export' => '',
@@ -603,9 +538,6 @@ return array(
// 'You already have one subtask in progress' => '',
// 'Which parts of the project do you want to duplicate?' => '',
// 'Disallow login form' => '',
- // 'Bitbucket commit received' => '',
- // 'Bitbucket webhooks' => '',
- // 'Help on Bitbucket webhooks' => '',
// 'Start' => '',
// 'End' => '',
// 'Task age in days' => '',
@@ -646,7 +578,6 @@ return array(
// 'This task' => '',
// '<1h' => '',
// '%dh' => '',
- // '%b %e' => '',
// 'Expand tasks' => '',
// 'Collapse tasks' => '',
// 'Expand/collapse tasks' => '',
@@ -656,14 +587,11 @@ return array(
// 'Keyboard shortcuts' => '',
// 'Open board switcher' => '',
// 'Application' => '',
- // 'since %B %e, %Y at %k:%M %p' => '',
// 'Compact view' => '',
// 'Horizontal scrolling' => '',
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
- // 'Files' => '',
- // 'Images' => '',
// 'Private project' => '',
// 'AUD - Australian Dollar' => '',
// 'CAD - Canadian Dollar' => '',
@@ -703,17 +631,12 @@ return array(
// 'The two factor authentication code is valid.' => '',
// 'Code' => '',
// 'Two factor authentication' => '',
- // 'Enable/disable two factor authentication' => '',
// 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
// 'Check my code' => '',
// 'Secret key: ' => '',
// 'Test your device' => '',
// 'Assign a color when the task is moved to a specific column' => '',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -722,7 +645,6 @@ return array(
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
// 'User that will receive the email' => '',
// 'Email subject' => '',
// 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
// 'Add a comment log when moving the task between columns' => '',
// 'Move the task to another column when the category is changed' => '',
// 'Send a task by email to someone' => '',
// 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
// 'Column change' => '',
// 'Position change' => '',
// 'Swimlane change' => '',
@@ -826,17 +736,11 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
// 'I want to receive notifications for:' => '',
// 'All tasks' => '',
// 'Only for tasks assigned to me' => '',
// 'Only for tasks created by me' => '',
// 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
// 'Total for all columns' => '',
// 'You need at least 2 days of data to show the chart.' => '',
@@ -861,7 +765,6 @@ return array(
// 'Not assigned' => '',
// 'View advanced search syntax' => '',
// 'Overview' => '',
- // '%b %e %Y' => '',
// 'Board/Calendar/List view' => '',
// 'Switch to the board view' => '',
// 'Switch to the calendar view' => '',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -906,18 +805,12 @@ return array(
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
// 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
// 'New remote user' => '',
// 'New local user' => '',
// 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
// 'This feature does not work with all browsers.' => '',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
// 'contributors' => '',
// 'License:' => '',
// 'License' => '',
- // 'Project Administrator' => '',
// 'Enter the text below' => '',
// 'Gantt chart for %s' => '',
// 'Sort by position' => '',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index e39c7664..cbda4aa8 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Supprimer le projet',
'Edit the board for "%s"' => 'Modifier le tableau pour « %s »',
'All projects' => 'Tous les projets',
- 'Change columns' => 'Changer les colonnes',
'Add a new column' => 'Ajouter une nouvelle colonne',
'Title' => 'Titre',
'Nobody assigned' => 'Personne assignée',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Ouvrir une tâche',
'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : « %s » ?',
'Back to the board' => 'Retour au tableau',
- 'Created on %B %e, %Y at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M',
'There is nobody assigned' => 'Il n\'y a personne d\'assigné à cette tâche',
'Column on the board:' => 'Colonne sur le tableau : ',
- 'Status is open' => 'État ouvert',
- 'Status is closed' => 'État fermé',
'Close this task' => 'Fermer cette tâche',
'Open this task' => 'Ouvrir cette tâche',
'There is no description.' => 'Il n\'y a pas de description.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'L\'identifiant est obligatoire',
'The project id is required' => 'L\'identifiant du projet est obligatoire',
'The project name is required' => 'Le nom du projet est obligatoire',
- 'This project must be unique' => 'Le nom du projet doit être unique',
'The title is required' => 'Le titre est obligatoire',
'Settings saved successfully.' => 'Paramètres sauvegardés avec succès.',
'Unable to save your settings.' => 'Impossible de sauvegarder vos réglages.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'En cours',
'Done' => 'Terminé',
'Application version:' => 'Version de l\'application :',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M',
- 'Date created' => 'Date de création',
- 'Date completed' => 'Date de clôture',
'Id' => 'Id.',
'%d closed tasks' => '%d tâches terminées',
'No task for this project' => 'Aucune tâche pour ce projet',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Complexité',
'Task limit' => 'Tâches Max.',
'Task count' => 'Nombre de tâches',
- 'Edit project access list' => 'Modifier l\'accès au projet',
- 'Allow this user' => 'Autoriser cet utilisateur',
- 'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.',
- 'Revoke' => 'Révoquer',
- 'List of authorized users' => 'Liste des utilisateurs autorisés',
'User' => 'Utilisateur',
- 'Nobody have access to this project.' => 'Personne n\'est autorisé à accéder au projet.',
'Comments' => 'Commentaires',
'Write your text in Markdown' => 'Écrivez votre texte en Markdown',
'Leave a comment' => 'Laissez un commentaire',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Modifier cette tâche',
'Due Date' => 'Date d\'échéance',
'Invalid date' => 'Date invalide',
- 'Must be done before %B %e, %Y' => 'Doit être fait avant le %d/%m/%Y',
- '%B %e, %Y' => '%d %B %Y',
- '%b %e, %Y' => '%d/%m/%Y',
'Automatic actions' => 'Actions automatisées',
'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajoutée avec succès.',
'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Assigner une couleur à un utilisateur',
'Column title' => 'Titre de la colonne',
'Position' => 'Position',
- 'Move Up' => 'Déplacer vers le haut',
- 'Move Down' => 'Déplacer vers le bas',
'Duplicate to another project' => 'Dupliquer dans un autre projet',
'Duplicate' => 'Dupliquer',
'link' => 'lien',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Commentaire supprimé avec succès.',
'Unable to remove this comment.' => 'Impossible de supprimer ce commentaire.',
'Do you really want to remove this comment?' => 'Voulez-vous vraiment supprimer ce commentaire ?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Seuls les administrateurs ou le créateur du commentaire peuvent accéder à cette page.',
'Current password for the user "%s"' => 'Mot de passe actuel pour l\'utilisateur « %s »',
'The current password is required' => 'Le mot de passe actuel est obligatoire',
'Wrong password' => 'Mot de passe invalide',
@@ -267,10 +246,6 @@ return array(
'External authentication failed' => 'L’authentification externe a échoué',
'Your external account is linked to your profile successfully.' => 'Votre compte externe est désormais lié à votre profil.',
'Email' => 'Email',
- 'Link my Google Account' => 'Lier mon compte Google',
- 'Unlink my Google Account' => 'Ne plus utiliser mon compte Google',
- 'Login with my Google Account' => 'Se connecter avec mon compte Google',
- 'Project not found.' => 'Projet introuvable.',
'Task removed successfully.' => 'Tâche supprimée avec succès.',
'Unable to remove this task.' => 'Impossible de supprimer cette tâche.',
'Remove a task' => 'Supprimer une tâche',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Taille maximum : ',
'Unable to upload the file.' => 'Impossible de transférer le fichier.',
'Display another project' => 'Afficher un autre projet',
- 'Login with my Github Account' => 'Se connecter avec mon compte Github',
- 'Link my Github Account' => 'Lier mon compte Github',
- 'Unlink my Github Account' => 'Ne plus utiliser mon compte Github',
'Created by %s' => 'Créé par %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M',
'Tasks Export' => 'Exportation des tâches',
'Tasks exportation for "%s"' => 'Exportation des tâches pour « %s »',
'Start Date' => 'Date de début',
@@ -376,7 +347,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :',
'view the task on Kanboard' => 'voir la tâche sur Kanboard',
'Public access' => 'Accès public',
- 'User management' => 'Gestion des utilisateurs',
'Active tasks' => 'Tâches actives',
'Disable public access' => 'Désactiver l\'accès public',
'Enable public access' => 'Activer l\'accès public',
@@ -399,18 +369,12 @@ return array(
'Email:' => 'Email :',
'Notifications:' => 'Notifications :',
'Notifications' => 'Notifications',
- 'Group:' => 'Groupe :',
- 'Regular user' => 'Utilisateur normal',
'Account type:' => 'Type de compte :',
'Edit profile' => 'Modifier le profil',
'Change password' => 'Changer le mot de passe',
'Password modification' => 'Changement de mot de passe',
'External authentications' => 'Authentifications externes',
- 'Google Account' => 'Compte Google',
- 'Github Account' => 'Compte Github',
'Never connected.' => 'Jamais connecté.',
- 'No account linked.' => 'Aucun compte attaché.',
- 'Account linked.' => 'Compte attaché.',
'No external authentication enabled.' => 'Aucune authentication externe activée.',
'Password modified successfully.' => 'Mot de passe changé avec succès.',
'Unable to change the password.' => 'Impossible de changer le mot de passe.',
@@ -448,17 +412,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s a changé la personne assignée à la tâche %s pour %s',
'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »',
'Choose an event' => 'Choisir un événement',
- 'Github commit received' => 'Commit reçu via Github',
- 'Github issue opened' => 'Ouverture d\'un ticket sur Github',
- 'Github issue closed' => 'Fermeture d\'un ticket sur Github',
- 'Github issue reopened' => 'Réouverture d\'un ticket sur Github',
- 'Github issue assignee change' => 'Changement d\'assigné sur un ticket Github',
- 'Github issue label change' => 'Changement de libellé sur un ticket Github',
'Create a task from an external provider' => 'Créer une tâche depuis un fournisseur externe',
'Change the assignee based on an external username' => 'Changer l\'assigné en fonction d\'un utilisateur externe',
'Change the category based on an external label' => 'Changer la catégorie en fonction d\'un libellé externe',
'Reference' => 'Référence',
- 'Reference: %s' => 'Référence : %s',
'Label' => 'Libellé',
'Database' => 'Base de données',
'About' => 'À propos',
@@ -476,7 +433,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Fréquence en seconde (60 secondes par défaut)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Fréquence en seconde (0 pour désactiver, 10 secondes par défaut)',
'Application URL' => 'URL de l\'application',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemple : http://exemple.kanboard.net/ (utilisé pour les notifications)',
'Token regenerated.' => 'Jeton de sécurité regénéré.',
'Date format' => 'Format des dates',
'ISO format is always accepted, example: "%s" and "%s"' => 'Le format ISO est toujours accepté, exemple : « %s » et « %s »',
@@ -484,9 +440,6 @@ return array(
'This project is private' => 'Ce projet est privé',
'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici',
'Add' => 'Ajouter',
- 'Estimated time: %s hours' => 'Temps estimé: %s hours',
- 'Time spent: %s hours' => 'Temps passé : %s heures',
- 'Started on %B %e, %Y' => 'Commençé le %d/%m/%Y',
'Start date' => 'Date de début',
'Time estimated' => 'Temps estimé',
'There is nothing assigned to you.' => 'Il n\'y a rien d\'assigné pour vous.',
@@ -495,13 +448,10 @@ return array(
'Dashboard' => 'Tableau de bord',
'Confirmation' => 'Confirmation',
'Allow everybody to access to this project' => 'Autoriser tout le monde à accéder à ce projet',
- 'Everybody have access to this project.' => 'Tout le monde a acccès à ce projet.',
+ 'Everybody have access to this project.' => 'Tout le monde a accès à ce projet.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Webhook Github',
- 'Help on Github webhooks' => 'Aide sur les webhooks Github',
'Create a comment from an external provider' => 'Créer un commentaire depuis un fournisseur externe',
- 'Github issue comment created' => 'Commentaire créé sur un ticket Github',
'Project management' => 'Gestion des projets',
'My projects' => 'Mes projets',
'Columns' => 'Colonnes',
@@ -519,7 +469,6 @@ return array(
'User repartition for "%s"' => 'Répartition des utilisateurs pour « %s »',
'Clone this project' => 'Cloner ce projet',
'Column removed successfully.' => 'Colonne supprimée avec succès.',
- 'Github Issue' => 'Ticket Github',
'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique.',
'Previous' => 'Précédent',
'The id must be an integer' => 'L\'id doit être un entier',
@@ -549,10 +498,7 @@ return array(
'Default swimlane' => 'Swimlane par défaut',
'Do you really want to remove this swimlane: "%s"?' => 'Voulez-vous vraiment supprimer cette swimlane : « %s » ?',
'Inactive swimlanes' => 'Swimlanes inactives',
- 'Set project manager' => 'Mettre chef de projet',
- 'Set project member' => 'Mettre membre du projet',
'Remove a swimlane' => 'Supprimer une swimlane',
- 'Rename' => 'Renommer',
'Show default swimlane' => 'Afficher la swimlane par défaut',
'Swimlane modification for the project "%s"' => 'Modification d\'une swimlane pour le projet « %s »',
'Swimlane not found.' => 'Cette swimlane est introuvable.',
@@ -560,24 +506,13 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane mise à jour avec succès.',
'The default swimlane have been updated successfully.' => 'La swimlane par défaut a été mise à jour avec succès.',
- 'Unable to create your swimlane.' => 'Impossible de créer votre swimlane.',
'Unable to remove this swimlane.' => 'Impossible de supprimer cette swimlane.',
'Unable to update this swimlane.' => 'Impossible de mettre à jour cette swimlane.',
'Your swimlane have been created successfully.' => 'Votre swimlane a été créée avec succès.',
'Example: "Bug, Feature Request, Improvement"' => 'Exemple: « Incident, Demande de fonctionnalité, Amélioration »',
'Default categories for new projects (Comma-separated)' => 'Catégories par défaut pour les nouveaux projets (séparation par des virgules)',
- 'Gitlab commit received' => 'Commit reçu via Gitlab',
- 'Gitlab issue opened' => 'Ouverture d\'un ticket sur Gitlab',
- 'Gitlab issue closed' => 'Fermeture d\'un ticket sur Gitlab',
- 'Gitlab webhooks' => 'Webhook Gitlab',
- 'Help on Gitlab webhooks' => 'Aide sur les webhooks Gitlab',
'Integrations' => 'Intégrations',
'Integration with third-party services' => 'Intégration avec des services externes',
- 'Role for this project' => 'Rôle pour ce projet',
- 'Project manager' => 'Chef de projet',
- 'Project member' => 'Membre du projet',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un chef de projet peut changer les paramètres du projet et possède plus de privilèges qu\'un utilisateur standard.',
- 'Gitlab Issue' => 'Ticket Gitlab',
'Subtask Id' => 'Identifiant de la sous-tâche',
'Subtasks' => 'Sous-tâches',
'Subtasks Export' => 'Exportation des sous-tâches',
@@ -605,9 +540,6 @@ return array(
'You already have one subtask in progress' => 'Vous avez déjà une sous-tâche en progrès',
'Which parts of the project do you want to duplicate?' => 'Quelles parties du projet voulez-vous dupliquer ?',
'Disallow login form' => 'Interdire le formulaire d\'authentification',
- 'Bitbucket commit received' => 'Commit reçu via Bitbucket',
- 'Bitbucket webhooks' => 'Webhook Bitbucket',
- 'Help on Bitbucket webhooks' => 'Aide sur les webhooks Bitbucket',
'Start' => 'Début',
'End' => 'Fin',
'Task age in days' => 'Âge de la tâche en jours',
@@ -648,7 +580,6 @@ return array(
'This task' => 'Cette tâche',
'<1h' => '<1h',
'%dh' => '%dh',
- '%b %e' => '%e %b',
'Expand tasks' => 'Déplier les tâches',
'Collapse tasks' => 'Replier les tâches',
'Expand/collapse tasks' => 'Plier/déplier les tâches',
@@ -658,14 +589,11 @@ return array(
'Keyboard shortcuts' => 'Raccourcis clavier',
'Open board switcher' => 'Ouvrir le sélecteur de tableau',
'Application' => 'Application',
- 'since %B %e, %Y at %k:%M %p' => 'depuis le %d/%m/%Y à %H:%M',
'Compact view' => 'Vue compacte',
'Horizontal scrolling' => 'Défilement horizontal',
'Compact/wide view' => 'Basculer entre la vue compacte et étendue',
'No results match:' => 'Aucun résultat :',
'Currency' => 'Devise',
- 'Files' => 'Fichiers',
- 'Images' => 'Images',
'Private project' => 'Projet privé',
'AUD - Australian Dollar' => 'AUD - Dollar australien',
'CAD - Canadian Dollar' => 'CAD - Dollar canadien',
@@ -705,17 +633,12 @@ return array(
'The two factor authentication code is valid.' => 'Le code pour l\'authentification à deux-facteurs est valide.',
'Code' => 'Code',
'Two factor authentication' => 'Authentification à deux-facteurs',
- 'Enable/disable two factor authentication' => 'Activer/désactiver l\'authentification à deux-facteurs',
'This QR code contains the key URI: ' => 'Ce code QR contient l\'url de la clé : ',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Sauvegardez cette clé secrète dans votre logiciel TOTP (par exemple Google Authenticator ou FreeOTP).',
'Check my code' => 'Vérifier mon code',
'Secret key: ' => 'Clé secrète : ',
'Test your device' => 'Testez votre appareil',
'Assign a color when the task is moved to a specific column' => 'Assigner une couleur lorsque la tâche est déplacée dans une colonne spécifique',
'%s via Kanboard' => '%s via Kanboard',
- 'uploaded by: %s' => 'Téléchargé par %s',
- 'uploaded on: %s' => 'Téléchargé le %s',
- 'size: %s' => 'Taille : %s',
'Burndown chart for "%s"' => 'Graphique d\'avancement pour « %s »',
'Burndown chart' => 'Graphique d\'avancement',
'This chart show the task complexity over the time (Work Remaining).' => 'Ce graphique représente la complexité des tâches en fonction du temps (travail restant).',
@@ -724,20 +647,18 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Prenez une capture d\'écran et appuyez sur CTRL+V ou ⌘+V pour coller ici.',
'Screenshot uploaded successfully.' => 'Capture d\'écran téléchargée avec succès.',
'SEK - Swedish Krona' => 'SEK - Couronne suédoise',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'L\'identificateur du projet est un code alpha-numérique optionnel pour identifier votre projet.',
'Identifier' => 'Identificateur',
'Disable two factor authentication' => 'Désactiver l\'authentification à deux facteurs',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Voulez-vous vraiment désactiver l\'authentification à deux facteurs pour cet utilisateur : « %s » ?',
'Edit link' => 'Modifier un lien',
- 'Start to type task title...' => 'Entrez le titre de la tâche…',
+ 'Start to type task title...' => 'Entrez le titre de la tâche...',
'A task cannot be linked to itself' => 'Une tâche ne peut être liée à elle-même',
'The exact same link already exists' => 'Un lien identique existe déjà',
'Recurrent task is scheduled to be generated' => 'La tâche récurrente est programmée pour être créée',
- 'Recurring information' => 'Information sur la récurrence',
'Score' => 'Complexité',
'The identifier must be unique' => 'L\'identifiant doit être unique',
'This linked task id doesn\'t exists' => 'L\'identifiant de la task liée n\'existe pas',
- 'This value must be alphanumeric' => 'Cette valeur doit être alpha-numérique',
+ 'This value must be alphanumeric' => 'Cette valeur doit être alphanumérique',
'Edit recurrence' => 'Modifier la récurrence',
'Generate recurrent task' => 'Générer une tâche récurrente',
'Trigger to generate recurrent task' => 'Déclencheur pour générer la tâche récurrente',
@@ -779,21 +700,10 @@ return array(
'User that will receive the email' => 'Utilisateur qui va reçevoir l\'email',
'Email subject' => 'Sujet de l\'email',
'Date' => 'Date',
- 'By @%s on Bitbucket' => 'Par @%s sur Bitbucket',
- 'Bitbucket Issue' => 'Ticket Bitbucket',
- 'Commit made by @%s on Bitbucket' => 'Commit fait par @%s sur Bitbucket',
- 'Commit made by @%s on Github' => 'Commit fait par @%s sur Github',
- 'By @%s on Github' => 'Par @%s sur Github',
- 'Commit made by @%s on Gitlab' => 'Commit fait par @%s sur Gitlab',
'Add a comment log when moving the task between columns' => 'Ajouter un commentaire d\'information lorsque une tâche est déplacée dans une autre colonne',
'Move the task to another column when the category is changed' => 'Déplacer une tâche vers une autre colonne lorsque la catégorie a changé',
'Send a task by email to someone' => 'Envoyer une tâche par email à quelqu\'un',
'Reopen a task' => 'Rouvrir une tâche',
- 'Bitbucket issue opened' => 'Ticket Bitbucket ouvert',
- 'Bitbucket issue closed' => 'Ticket Bitbucket fermé',
- 'Bitbucket issue reopened' => 'Ticket Bitbucket rouvert',
- 'Bitbucket issue assignee change' => 'Changement d\'assigné sur un ticket Bitbucket',
- 'Bitbucket issue comment created' => 'Commentaire créé sur un ticket Bitbucket',
'Column change' => 'Changement de colonne',
'Position change' => 'Changement de position',
'Swimlane change' => 'Changement de swimlane',
@@ -828,17 +738,11 @@ return array(
'The field "%s" have been updated' => 'Le champ « %s » a été mis à jour',
'The description have been modified' => 'La description a été modifiée',
'Do you really want to close the task "%s" as well as all subtasks?' => 'Voulez-vous vraiment fermer la tâche « %s » ainsi que toutes ses sous-tâches ?',
- 'Swimlane: %s' => 'Swimlane : %s',
'I want to receive notifications for:' => 'Je veux reçevoir les notifications pour :',
'All tasks' => 'Toutes les Tâches',
'Only for tasks assigned to me' => 'Seulement les tâches qui me sont assignées',
'Only for tasks created by me' => 'Seulement les tâches que j\'ai créées',
'Only for tasks created by me and assigned to me' => 'Seulement les tâches créées par moi-même et celles qui me sont assignées',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M',
- 'New due date: %B %e, %Y' => 'Nouvelle date d\'échéance : %d/%m/%Y',
- 'Start date changed: %B %e, %Y' => 'Date de début modifiée : %d/%m/%Y',
- '%k:%M %p' => '%H:%M',
'%%Y-%%m-%%d' => '%%d/%%m/%%Y',
'Total for all columns' => 'Total pour toutes les colonnes',
'You need at least 2 days of data to show the chart.' => 'Vous avez besoin d\'au minimum 2 jours de données pour afficher le graphique.',
@@ -863,7 +767,6 @@ return array(
'Not assigned' => 'Non assignées',
'View advanced search syntax' => 'Voir la syntaxe pour la recherche avancée',
'Overview' => 'Vue d\'ensemble',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Vue Tableau/Calendrier/Liste',
'Switch to the board view' => 'Basculer vers le tableau',
'Switch to the calendar view' => 'Basculer vers le calendrier',
@@ -896,10 +799,6 @@ return array(
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Ce graphique montre la durée moyenne du lead et cycle time pour les %d dernières tâches.',
'Average time into each column' => 'Temps moyen dans chaque colonne',
'Lead and cycle time' => 'Lead et cycle time',
- 'Google Authentication' => 'Authentification Google',
- 'Help on Google authentication' => 'Aide sur l\'authentification Google',
- 'Github Authentication' => 'Authentification Github',
- 'Help on Github authentication' => 'Aide sur l\'authentification Github',
'Lead time: ' => 'Lead time : ',
'Cycle time: ' => 'Temps de cycle : ',
'Time spent into each column' => 'Temps passé dans chaque colonne',
@@ -908,18 +807,12 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Si la tâche n\'est pas fermée, l\'heure courante est utilisée à la place de la date de complétion.',
'Set automatically the start date' => 'Définir automatiquement la date de début',
'Edit Authentication' => 'Modifier l\'authentification',
- 'Google Id' => 'Identifiant Google',
- 'Github Id' => 'Identifiant Github',
'Remote user' => 'Utilisateur distant',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Les utilisateurs distants ne stockent pas leur mot de passe dans la base de données de Kanboard, exemples : comptes LDAP, Github ou Google.',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si vous cochez la case « Interdire le formulaire d\'authentification », les identifiants entrés dans le formulaire d\'authentification seront ignorés.',
- 'By @%s on Gitlab' => 'Par @%s sur Gitlab',
- 'Gitlab issue comment created' => 'Commentaire créé sur un ticket Gitlab',
'New remote user' => 'Créer un utilisateur distant',
'New local user' => 'Créer un utilisateur local',
'Default task color' => 'Couleur par défaut des tâches',
- 'Hide sidebar' => 'Cacher la barre latérale',
- 'Expand sidebar' => 'Déplier la barre latérale',
'This feature does not work with all browsers.' => 'Cette fonctionnalité n\'est pas compatible avec tous les navigateurs',
'There is no destination project available.' => 'Il n\'y a pas de projet de destination disponible.',
'Trigger automatically subtask time tracking' => 'Déclencher automatiquement le suivi du temps pour les sous-tâches',
@@ -934,7 +827,6 @@ return array(
'contributors' => 'contributeurs',
'License:' => 'Licence :',
'License' => 'Licence',
- 'Project Administrator' => 'Administrateur de projet',
'Enter the text below' => 'Entrez le texte ci-dessous',
'Gantt chart for %s' => 'Diagramme de Gantt pour %s',
'Sort by position' => 'Trier par position',
@@ -954,11 +846,9 @@ return array(
'open file' => 'ouvrir le fichier',
'End date' => 'Date de fin',
'Users overview' => 'Vue d\'ensemble des utilisateurs',
- 'Managers' => 'Gérants',
'Members' => 'Membres',
'Shared project' => 'Projet partagé',
'Project managers' => 'Gestionnaires de projet',
- 'Project members' => 'Membres de projet',
'Gantt chart for all projects' => 'Diagramme de Gantt pour tous les projets',
'Projects list' => 'Liste des projets',
'Gantt chart for this project' => 'Diagramme de Gantt pour ce projet',
@@ -966,26 +856,16 @@ 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',
- 'Start date: %s' => 'Date de début : %s',
- 'End date: %s' => 'Date de fin : %s',
'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',
- 'Login with my Gitlab Account' => 'Se connecter avec mon compte Gitlab',
'Milestone' => 'Étape importante',
- 'Gitlab Authentication' => 'Authentification Gitlab',
- 'Help on Gitlab authentication' => 'Aide sur l\'authentification Gitlab',
- 'Gitlab Id' => 'Identifiant Gitlab',
- 'Gitlab Account' => 'Compte Gitlab',
- 'Link my Gitlab Account' => 'Lier mon compte Gitlab',
- 'Unlink my Gitlab Account' => 'Ne plus utiliser mon compte Gitlab',
'Documentation: %s' => 'Documentation : %s',
'Switch to the Gantt chart view' => 'Passer à la vue en diagramme de Gantt',
'Reset the search/filter box' => 'Réinitialiser le champ de recherche',
'Documentation' => 'Documentation',
'Table of contents' => 'Table des matières',
'Gantt' => 'Gantt',
- 'Help with project permissions' => 'Aide avec les permissions des projets',
'Author' => 'Auteur',
'Version' => 'Version',
'Plugins' => 'Extensions',
@@ -1048,7 +928,6 @@ return array(
'Append/Replace' => 'Ajouter/Remplaçer',
'Append' => 'Ajouter',
'Replace' => 'Remplaçer',
- 'There is no notification method registered.' => 'Il n\'y a aucune méthode de notification enregistrée.',
'Import' => 'Importation',
'change sorting' => 'changer l\'ordre',
'Tasks Importation' => 'Importation des tâches',
@@ -1066,4 +945,210 @@ return array(
'Duplicates are not imported' => 'Les doublons ne sont pas importés',
'Usernames must be lowercase and unique' => 'Les noms d\'utilisateurs doivent être en minuscule et unique',
'Passwords will be encrypted if present' => 'Les mots de passe seront chiffrés si présent',
+ '%s attached a new file to the task %s' => '%s a attaché un nouveau fichier à la tâche %s',
+ 'Link type' => 'Type de lien',
+ 'Assign automatically a category based on a link' => 'Assigner automatiquement une catégorie en fonction d\'un lien',
+ 'BAM - Konvertible Mark' => 'BAM - Mark convertible',
+ 'Assignee Username' => 'Utilisateur assigné',
+ 'Assignee Name' => 'Nom de l\'assigné',
+ 'Groups' => 'Groupes',
+ 'Members of %s' => 'Membres de %s',
+ 'New group' => 'Nouveau groupe',
+ 'Group created successfully.' => 'Groupe créé avec succès.',
+ 'Unable to create your group.' => 'Impossible de créé votre groupe.',
+ 'Edit group' => 'Modifier le groupe',
+ 'Group updated successfully.' => 'Groupe mis à jour avec succès.',
+ 'Unable to update your group.' => 'Impossible de mettre à jour votre groupe.',
+ 'Add group member to "%s"' => 'Ajouter un membre au groupe « %s »',
+ 'Group member added successfully.' => 'Membre ajouté avec succès au groupe.',
+ 'Unable to add group member.' => 'Impossible d\'ajouter ce membre au groupe.',
+ 'Remove user from group "%s"' => 'Supprimer un utilisateur d\'un groupe « %s »',
+ 'User removed successfully from this group.' => 'Utilisateur supprimé avec succès de ce groupe.',
+ 'Unable to remove this user from the group.' => 'Impossible de supprimer cet utilisateur du groupe.',
+ 'Remove group' => 'Supprimer le groupe',
+ 'Group removed successfully.' => 'Groupe supprimé avec succès.',
+ 'Unable to remove this group.' => 'Impossible de supprimer ce groupe.',
+ 'Project Permissions' => 'Permissions du projet',
+ 'Manager' => 'Gestionnaire',
+ 'Project Manager' => 'Chef de projet',
+ 'Project Member' => 'Membre du projet',
+ 'Project Viewer' => 'Visualiseur de projet',
+ 'Your account is locked for %d minutes' => 'Votre compte est vérouillé pour %d minutes',
+ 'Invalid captcha' => 'Captcha invalid',
+ 'The name must be unique' => 'Le nom doit être unique',
+ 'View all groups' => 'Voir tous les groupes',
+ 'View group members' => 'Voir les membres du groupe',
+ 'There is no user available.' => 'Il n\'y a aucun utilisateur disponible',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Voulez-vous vraiment supprimer l\'utilisateur « %s » du groupe « %s » ?',
+ 'There is no group.' => 'Il n\'y a aucun groupe.',
+ 'External Id' => 'Identifiant externe',
+ 'Add group member' => 'Ajouter un membre au groupe',
+ 'Do you really want to remove this group: "%s"?' => 'Voulez-vous vraiment supprimer ce groupe : « %s » ?',
+ 'There is no user in this group.' => 'Il n\'y a aucun utilisateur dans ce groupe',
+ 'Remove this user' => 'Supprimer cet utilisateur',
+ 'Permissions' => 'Permissions',
+ 'Allowed Users' => 'Utilisateurs autorisés',
+ 'No user have been allowed specifically.' => 'Aucun utilisateur a été autorisé spécifiquement.',
+ 'Role' => 'Rôle',
+ 'Enter user name...' => 'Entrez le nom de l\'utilisateur...',
+ 'Allowed Groups' => 'Groupes autorisés',
+ 'No group have been allowed specifically.' => 'Aucun groupe a été autorisé spécifiquement.',
+ 'Group' => 'Groupe',
+ 'Group Name' => 'Nom du groupe',
+ 'Enter group name...' => 'Entrez le nom du groupe...',
+ 'Role:' => 'Rôle :',
+ 'Project members' => 'Membres du projet',
+ 'Compare hours for "%s"' => 'Comparer les heures pour « %s »',
+ '%s mentioned you in the task #%d' => '%s vous a mentionné dans la tâche n°%d',
+ '%s mentioned you in a comment on the task #%d' => '%s vous a mentionné dans un commentaire de la tâche n°%d',
+ 'You were mentioned in the task #%d' => 'Vous avez été mentionné dans la tâche n°%d',
+ 'You were mentioned in a comment on the task #%d' => 'Vous avez été mentionné dans un commentaire de la tâche n°%d',
+ 'Mentioned' => 'Mentionné',
+ 'Compare Estimated Time vs Actual Time' => 'Comparer le temps estimé et le temps actuel',
+ 'Estimated hours: ' => 'Heures estimées : ',
+ 'Actual hours: ' => 'Heures actuelles : ',
+ 'Hours Spent' => 'Heures passées',
+ 'Hours Estimated' => 'Heures estimées',
+ 'Estimated Time' => 'Temps estimé',
+ 'Actual Time' => 'Temps actuel',
+ 'Estimated vs actual time' => 'Temps estimé vs actuel',
+ 'RUB - Russian Ruble' => 'RUB - Rouble russe',
+ 'Assign the task to the person who does the action when the column is changed' => 'Assigner la tâche à la personne qui fait l\'action lorsque la colonne est changée',
+ 'Close a task in a specific column' => 'Fermer une tâche dans une colonne specifique',
+ 'Time-based One-time Password Algorithm' => 'Mot de passe à usage unique basé sur le temps',
+ 'Two-Factor Provider: ' => 'Fournisseur d\'authentification à deux facteurs : ',
+ 'Disable two-factor authentication' => 'Désactiver l\'authentification à deux-facteurs',
+ 'Enable two-factor authentication' => 'Activer l\'authentification à deux-facteurs',
+ 'There is no integration registered at the moment.' => 'Il n\'y a aucune intégration enregistrée pour le moment.',
+ 'Password Reset for Kanboard' => 'Réinitialisation du mot de passe pour Kanboard',
+ 'Forgot password?' => 'Mot de passe oublié ?',
+ 'Enable "Forget Password"' => 'Activer la fonctionnalité « Mot de passe oublié »',
+ 'Password Reset' => 'Réinitialisation du mot de passe',
+ 'New password' => 'Nouveau mot de passe',
+ 'Change Password' => 'Changer de mot de passe',
+ 'To reset your password click on this link:' => 'Pour réinitialiser votre mot de passe cliquer sur ce lien :',
+ 'Last Password Reset' => 'Dernières réinitialisation de mot de passe',
+ 'The password has never been reinitialized.' => 'Le mot de passe n\'a jamais été réinitialisé.',
+ 'Creation' => 'Création',
+ 'Expiration' => 'Expiration',
+ 'Password reset history' => 'Historique de la réinitialisation du mot de passe',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Toutes les tâches de la colonne « %s » et de la swimlane « %s » ont été fermées avec succès.',
+ 'Do you really want to close all tasks of this column?' => 'Voulez-vous vraiment fermer toutes les tâches de cette colonne ?',
+ '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tâche(s) dans la colonne « %s » et la swimlane « %s » seront fermées.',
+ 'Close all tasks of this column' => 'Fermer toutes les tâches de cette colonne',
+ 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Aucun plugin n\'a enregistré une méthode de notification de projet. Vous pouvez toujours configurer les notifications individuelles dans votre profil d\'utilisateur.',
+ 'My dashboard' => 'Mon tableau de bord',
+ 'My profile' => 'Mon profile',
+ 'Project owner: ' => 'Responsable du projet : ',
+ 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'L\'identifiant du projet est optionnel et doit être alphanumérique, example: MONPROJET.',
+ 'Project owner' => 'Responsable du projet',
+ 'Those dates are useful for the project Gantt chart.' => 'Ces dates sont utiles pour le diagramme de Gantt des projets.',
+ 'Private projects do not have users and groups management.' => 'Les projets privés n\'ont pas de gestion d\'utilisateurs et de groupes.',
+ 'There is no project member.' => 'Il y a aucun membre du projet.',
+ 'Priority' => 'Priorité',
+ 'Task priority' => 'Priorité des tâches',
+ 'General' => 'Général',
+ 'Dates' => 'Dates',
+ 'Default priority' => 'Priorité par défaut',
+ 'Lowest priority' => 'Priorité basse',
+ 'Highest priority' => 'Priorité haute',
+ 'If you put zero to the low and high priority, this feature will be disabled.' => 'Si vous mettez zéro pour la priorité basse et haute, cette fonctionnalité sera désactivée.',
+ '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 planifée quotidienne pour les tâches',
+ 'Auto' => 'Auto',
+ 'Related' => 'Relié',
+ 'Attachment' => 'Pièce-jointe',
+ 'Title not found' => 'Titre non trouvé',
+ 'Web Link' => 'Lien web',
+ 'External links' => 'Liens externes',
+ 'Add external link' => 'Ajouter un lien externe',
+ 'Type' => 'Type',
+ 'Dependency' => 'Dépendance',
+ 'Add internal link' => 'Ajouter un lien interne',
+ 'Add a new external link' => 'Ajouter un nouveau lien externe',
+ 'Edit external link' => 'Modifier un lien externe',
+ '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',
+ 'Projects management' => 'Gestion des projets',
+ '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é :',
+ 'Reference:' => 'Référence :',
+ 'Complexity:' => 'Complexité :',
+ 'Swimlane:' => 'Swimlane :',
+ 'Column:' => 'Colonne :',
+ 'Position:' => 'Position :',
+ 'Creator:' => 'Créateur :',
+ 'Time estimated:' => 'Temps estimé :',
+ '%s hours' => '%s heures',
+ 'Time spent:' => 'Temps passé :',
+ 'Created:' => 'Créé le :',
+ 'Modified:' => 'Modifié le :',
+ 'Completed:' => 'Terminé le :',
+ '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 : ',
+ 'End date: ' => 'Date de fin : ',
+ 'New due date: ' => 'Nouvelle date d\'échéance : ',
+ 'Start date changed: ' => 'Date de début modifiée : ',
+ 'Disable private projects' => 'Désactiver les projets privés',
+ 'Do you really want to remove this custom filter: "%s"?' => 'Voulez-vous vraiment supprimer ce filtre personnalisé : « %s » ?',
+ 'Remove a custom filter' => 'Supprimer un filtre personnalisé',
+ 'User activated successfully.' => 'Utilisateur activé avec succès.',
+ 'Unable to enable this user.' => 'Impossible d\'activer cet utilisateur.',
+ 'User disabled successfully.' => 'Utilisateur désactivé avec succès.',
+ 'Unable to disable this user.' => 'Impossible de désactiver cet utilisateur.',
+ 'All files have been uploaded successfully.' => 'Tous les fichiers ont été uploadés avec succès.',
+ 'View uploaded files' => 'Voir les fichiers uploadés',
+ 'The maximum allowed file size is %sB.' => 'La taille maximale autorisée pour les fichiers est de %so.',
+ 'Choose files again' => 'Choisir de nouveau des fichiers',
+ 'Drag and drop your files here' => 'Glissez-déposez vos fichiers ici',
+ 'choose files' => 'choisissez des fichiers',
+ 'View profile' => 'Voir le profile',
+ 'Two Factor' => 'Deux-Facteurs',
+ 'Disable user' => 'Désactiver l\'utilisateur',
+ 'Do you really want to disable this user: "%s"?' => 'Voulez-vous vraiment désactiver cet utilisateur : « %s » ?',
+ 'Enable user' => 'Activer un utilisateur',
+ 'Do you really want to enable this user: "%s"?' => 'Voulez-vous vraiment activer cet utilisateur : « %s » ?',
+ 'Download' => 'Télécharger',
+ 'Uploaded: %s' => 'Uploadé : %s',
+ 'Size: %s' => 'Taille : %s',
+ 'Uploaded by %s' => 'Uploadé par %s',
+ 'Filename' => 'Nom du fichier',
+ 'Size' => 'Taille',
+ 'Column created successfully.' => 'La colonne a été créée avec succès.',
+ 'Another column with the same name exists in the project' => 'Une autre colonne existe avec le même nom dans le projet',
+ 'Default filters' => 'Filtres par défaut',
+ 'Your board doesn\'t have any column!' => 'Votre tableau n\'a aucune colonne',
+ 'Change column position' => 'Changer la position de la colonne',
+ 'Switch to the project overview' => 'Aller à l\'aperçu du projet',
+ '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',
+ 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d',
+ 'Another swimlane with the same name exists in the project' => 'Une autre swimlane existe avec le même nom dans le projet',
+ 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Exemple : http://exemple.kanboard.net/ (utilisé pour générer les URLs absolues)',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 59b1d569..71d068e0 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Projekt törlése',
'Edit the board for "%s"' => 'Tábla szerkesztése: "%s"',
'All projects' => 'Minden projekt',
- 'Change columns' => 'Oszlop módosítása',
'Add a new column' => 'Új oszlop',
'Title' => 'Cím',
'Nobody assigned' => 'Nincs felelős',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Feladat felnyitás',
'Do you really want to open this task: "%s"?' => 'Tényleg meg akarja nyitni ezt a feladatot: "%s"?',
'Back to the board' => 'Vissza a táblához',
- 'Created on %B %e, %Y at %k:%M %p' => 'Létrehozva: %Y. %m. %d. %H:%M',
'There is nobody assigned' => 'Nincs felelős',
'Column on the board:' => 'Tábla oszlopa: ',
- 'Status is open' => 'Nyitott állapot',
- 'Status is closed' => 'Zárt állapot',
'Close this task' => 'Feladat lezárása',
'Open this task' => 'Feladat felnyitása',
'There is no description.' => 'Nincs elérhető leírás.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Az ID-t (azonosítót) meg kell adni',
'The project id is required' => 'A projekt ID-t (azonosítót) meg kell adni',
'The project name is required' => 'A projekt nevét meg kell adni',
- 'This project must be unique' => 'A projekt nevének egyedinek kell lennie',
'The title is required' => 'A címet meg kell adni',
'Settings saved successfully.' => 'A beállítások sikeresen mentve.',
'Unable to save your settings.' => 'A beállítások mentése sikertelen.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Folyamatban',
'Done' => 'Kész',
'Application version:' => 'Alkalmazás verzió:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Elkészült: %Y. %m. %d. %H:%M',
- '%B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M',
- 'Date created' => 'Létrehozás időpontja',
- 'Date completed' => 'Befejezés időpontja',
'Id' => 'ID',
'%d closed tasks' => '%d lezárt feladat',
'No task for this project' => 'Nincs feladat ebben a projektben',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Bonyolultság',
'Task limit' => 'Maximális számú feladat',
'Task count' => 'Feladatok száma',
- 'Edit project access list' => 'Projekt hozzáférés módosítása',
- 'Allow this user' => 'Engedélyezi ezt a felhasználót',
- 'Don\'t forget that administrators have access to everything.' => 'Ne felejtsük el: a rendszergazdák mindenhez hozzáférnek.',
- 'Revoke' => 'Visszavon',
- 'List of authorized users' => 'Az engedélyezett felhasználók',
'User' => 'Felhasználó',
- 'Nobody have access to this project.' => 'Senkinek sincs hozzáférése a projekthez.',
'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 ...',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Feladat módosítása',
'Due Date' => 'Határidő',
'Invalid date' => 'Érvénytelen dátum',
- 'Must be done before %B %e, %Y' => 'Kész kell lennie %Y. %m. %d. előtt',
- '%B %e, %Y' => '%Y. %m. %d.',
- '%b %e, %Y' => '%Y. %m. %d.',
'Automatic actions' => 'Automatikus intézkedések',
'Your automatic action have been created successfully.' => 'Az automatikus intézkedés sikeresen elkészült.',
'Unable to create your automatic action.' => 'Automatikus intézkedés létrehozása nem lehetséges.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Szín hozzárendelése a felhasználóhoz',
'Column title' => 'Oszlopfejléc',
'Position' => 'Pozíció',
- 'Move Up' => 'Fel',
- 'Move Down' => 'Le',
'Duplicate to another project' => 'Másolás másik projektbe',
'Duplicate' => 'Másolás',
'link' => 'link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Megjegyzés sikeresen törölve.',
'Unable to remove this comment.' => 'Megjegyzés törölése nem lehetséges.',
'Do you really want to remove this comment?' => 'Valóban törölni szeretné ezt a megjegyzést?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Csak a rendszergazdák és a megjegyzés létrehozója férhet hozzá az oldalhoz.',
'Current password for the user "%s"' => 'Felhasználó jelenlegi jelszava: "%s"',
'The current password is required' => 'A jelenlegi jelszót meg kell adni',
'Wrong password' => 'Hibás jelszó',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'E-mail',
- 'Link my Google Account' => 'Kapcsold össze a Google fiókkal',
- 'Unlink my Google Account' => 'Válaszd le a Google fiókomat',
- 'Login with my Google Account' => 'Jelentkezzen be Google fiókkal',
- 'Project not found.' => 'A projekt nem található.',
'Task removed successfully.' => 'Feladat sikeresen törölve.',
'Unable to remove this task.' => 'A feladatot nem lehet törölni.',
'Remove a task' => 'Feladat törlése',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maximális méret: ',
'Unable to upload the file.' => 'Fájl feltöltése nem lehetséges.',
'Display another project' => 'Másik projekt megjelenítése',
- 'Login with my Github Account' => 'Jelentkezzen be Github fiókkal',
- 'Link my Github Account' => 'Github fiók csatolása',
- 'Unlink my Github Account' => 'Github fiók leválasztása',
'Created by %s' => 'Készítette: %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Utolsó módosítás: %Y. %m. %d. %H:%M',
'Tasks Export' => 'Feladatok exportálása',
'Tasks exportation for "%s"' => 'Feladatok exportálása: "%s"',
'Start Date' => 'Kezdés dátuma',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Csak ezekről a projektekről kérek értesítést:',
'view the task on Kanboard' => 'feladat megtekintése a Kanboardon',
'Public access' => 'Nyilvános hozzáférés',
- 'User management' => 'Felhasználók kezelése',
'Active tasks' => 'Aktív feladatok',
'Disable public access' => 'Nyilvános hozzáférés letiltása',
'Enable public access' => 'Nyilvános hozzáférés engedélyezése',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'E-mail:',
'Notifications:' => 'Értesítések:',
'Notifications' => 'Értesítések',
- 'Group:' => 'Csoport:',
- 'Regular user' => 'Default User',
'Account type:' => 'Fiók típusa:',
'Edit profile' => 'Profil szerkesztése',
'Change password' => 'Jelszó módosítása',
'Password modification' => 'Jelszó módosítása',
'External authentications' => 'Külső azonosítás',
- 'Google Account' => 'Google fiók',
- 'Github Account' => 'Github fiók',
'Never connected.' => 'Sosem csatlakozva.',
- 'No account linked.' => 'Nincs csatlakoztatott fiók.',
- 'Account linked.' => 'Fiók csatlakoztatva.',
'No external authentication enabled.' => 'A külső azonosítás nincs engedélyezve.',
'Password modified successfully.' => 'A jelszó sikeresen módosítva.',
'Unable to change the password.' => 'A jelszó módosítása sikertelen.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s',
'New password for the user "%s"' => 'Felhasználó új jelszava: %s',
'Choose an event' => 'Válasszon eseményt',
- 'Github commit received' => 'Github commit érkezett',
- 'Github issue opened' => 'Github issue nyitás',
- 'Github issue closed' => 'Github issue zárás',
- 'Github issue reopened' => 'Github issue újranyitva',
- 'Github issue assignee change' => 'Github issue felelős változás',
- 'Github issue label change' => 'Github issue címke változás',
'Create a task from an external provider' => 'Feladat létrehozása külsős számára',
'Change the assignee based on an external username' => 'Felelős módosítása külső felhasználónév alapján',
'Change the category based on an external label' => 'Kategória módosítása külső címke alapján',
'Reference' => 'Hivatkozás',
- 'Reference: %s' => 'Hivatkozás: %s',
'Label' => 'Címke',
'Database' => 'Adatbázis',
'About' => 'Kanboard információ',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Gyakoriság másodpercben (alapértelmezetten 60 másodperc)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Gyakoriság másodpercben (0 funkció letiltva, alapértelmezetten 10 másodperc)',
'Application URL' => 'Alkalmazás URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Példa: http://example.kanboard.net/ (e-mail értesítőben használt)',
'Token regenerated.' => 'Token újragenerálva.',
'Date format' => 'Dátum formátum',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formátum mindig elfogadott, pl: "%s" és "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Becsült idő: %s óra',
- 'Time spent: %s hours' => 'Eltöltött idő: %s óra',
- 'Started on %B %e, %Y' => 'Elkezdve: %Y. %m. %d.',
'Start date' => 'Kezdés dátuma',
'Time estimated' => 'Becsült időtartam',
'There is nothing assigned to you.' => 'Nincs kiosztott feladat.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Mindenki elérheti a projektet',
'Webhooks' => 'Webhook',
'API' => 'API',
- 'Github webhooks' => 'Github webhooks',
- 'Help on Github webhooks' => 'Github Webhook súgó',
'Create a comment from an external provider' => 'Megjegyzés létrehozása külső felhasználótól',
- 'Github issue comment created' => 'Github issue megjegyzés létrehozva',
'Project management' => 'Projekt menedzsment',
'My projects' => 'Projektjeim',
'Columns' => 'Oszlopok',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s',
'Clone this project' => 'Projekt másolása',
'Column removed successfully.' => 'Oszlop sikeresen törölve.',
- 'Github Issue' => 'Github issue',
'Not enough data to show the graph.' => 'Nincs elég adat a grafikonhoz.',
'Previous' => 'Előző',
'The id must be an integer' => 'Az ID csak egész szám lehet',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Alapértelmezett folyamat',
'Do you really want to remove this swimlane: "%s"?' => 'Valóban törli a folyamatot:%s ?',
'Inactive swimlanes' => 'Inaktív folyamatok',
- 'Set project manager' => 'Beállítás projekt kezelőnek',
- 'Set project member' => 'Beállítás projekt felhasználónak',
'Remove a swimlane' => 'Folyamat törlés',
- 'Rename' => 'Átnevezés',
'Show default swimlane' => 'Alapértelmezett folyamat megjelenítése',
'Swimlane modification for the project "%s"' => '%s projekt folyamatainak módosítása',
'Swimlane not found.' => 'Folyamat nem található',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Folyamatok',
'Swimlane updated successfully.' => 'Folyamat sikeresn frissítve',
'The default swimlane have been updated successfully.' => 'Az alapértelmezett folyamat sikeresen frissítve.',
- 'Unable to create your swimlane.' => 'A folyamat létrehozása sikertelen.',
'Unable to remove this swimlane.' => 'A folyamat törlése sikertelen.',
'Unable to update this swimlane.' => 'A folyamat frissítése sikertelen.',
'Your swimlane have been created successfully.' => 'A folyamat sikeresen létrehozva.',
'Example: "Bug, Feature Request, Improvement"' => 'Például: Hiba, Új funkció, Fejlesztés',
'Default categories for new projects (Comma-separated)' => 'Alapértelmezett kategóriák az új projektekben (Vesszővel elválasztva)',
- 'Gitlab commit received' => 'Gitlab commit érkezett',
- 'Gitlab issue opened' => 'Gitlab issue nyitás',
- 'Gitlab issue closed' => 'Gitlab issue zárás',
- 'Gitlab webhooks' => 'Gitlab webhooks',
- 'Help on Gitlab webhooks' => 'Gitlab webhooks súgó',
'Integrations' => 'Integráció',
'Integration with third-party services' => 'Integráció harmadik féllel',
- 'Role for this project' => 'Projekt szerepkör',
- 'Project manager' => 'Projekt kezelő',
- 'Project member' => 'Projekt felhasználó',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'A projekt kezelő képes megváltoztatni a projekt beállításait és több joggal rendelkezik mint az alap felhasználók.',
- 'Gitlab Issue' => 'Gitlab issue',
'Subtask Id' => 'Részfeladat id',
'Subtasks' => 'Részfeladatok',
'Subtasks Export' => 'Részfeladat exportálás',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'Már van egy folyamatban levő részfeladata',
'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné másolni?',
// 'Disallow login form' => '',
- 'Bitbucket commit received' => 'Bitbucket commit érkezett',
- 'Bitbucket webhooks' => 'Bitbucket webhooks',
- 'Help on Bitbucket webhooks' => 'Bitbucket webhooks súgó',
'Start' => 'Kezdet',
'End' => 'Vég',
'Task age in days' => 'Feladat életkora napokban',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Ez a feladat',
'<1h' => '<1ó',
'%dh' => '%dó',
- '%b %e' => '%b %e',
'Expand tasks' => 'Feladatok lenyitása',
'Collapse tasks' => 'Feladatok összecsukása',
'Expand/collapse tasks' => 'Feladatok lenyitása/összecsukása',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Billentyű kombinációk',
'Open board switcher' => 'Tábla választó lenyitása',
'Application' => 'Alkalmazás',
- 'since %B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M óta',
'Compact view' => 'Kompakt nézet',
'Horizontal scrolling' => 'Vízszintes görgetés',
'Compact/wide view' => 'Kompakt/széles nézet',
'No results match:' => 'Nincs találat:',
'Currency' => 'Pénznem',
- 'Files' => 'Fájlok',
- 'Images' => 'Képek',
'Private project' => 'Privát projekt',
'AUD - Australian Dollar' => 'AUD - Ausztrál dollár',
'CAD - Canadian Dollar' => 'CAD - Kanadai dollár',
@@ -703,17 +631,12 @@ return array(
// 'The two factor authentication code is valid.' => '',
// 'Code' => '',
// 'Two factor authentication' => '',
- // 'Enable/disable two factor authentication' => '',
// 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
// 'Check my code' => '',
// 'Secret key: ' => '',
// 'Test your device' => '',
// 'Assign a color when the task is moved to a specific column' => '',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -722,7 +645,6 @@ return array(
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
// 'User that will receive the email' => '',
// 'Email subject' => '',
// 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
// 'Add a comment log when moving the task between columns' => '',
// 'Move the task to another column when the category is changed' => '',
// 'Send a task by email to someone' => '',
// 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
// 'Column change' => '',
// 'Position change' => '',
// 'Swimlane change' => '',
@@ -826,17 +736,11 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
// 'I want to receive notifications for:' => '',
// 'All tasks' => '',
// 'Only for tasks assigned to me' => '',
// 'Only for tasks created by me' => '',
// 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
// 'Total for all columns' => '',
// 'You need at least 2 days of data to show the chart.' => '',
@@ -861,7 +765,6 @@ return array(
// 'Not assigned' => '',
// 'View advanced search syntax' => '',
// 'Overview' => '',
- // '%b %e %Y' => '',
// 'Board/Calendar/List view' => '',
// 'Switch to the board view' => '',
// 'Switch to the calendar view' => '',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -906,18 +805,12 @@ return array(
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
// 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
// 'New remote user' => '',
// 'New local user' => '',
// 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
// 'This feature does not work with all browsers.' => '',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
// 'contributors' => '',
// 'License:' => '',
// 'License' => '',
- // 'Project Administrator' => '',
// 'Enter the text below' => '',
// 'Gantt chart for %s' => '',
// 'Sort by position' => '',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index 8408fc11..936023dc 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Hapus proyek',
'Edit the board for "%s"' => 'Rubah papan untuk « %s »',
'All projects' => 'Semua proyek',
- 'Change columns' => 'Rubah kolom',
'Add a new column' => 'Tambah kolom baru',
'Title' => 'Judul',
'Nobody assigned' => 'Tidak ada yang ditugaskan',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Buka tugas',
'Do you really want to open this task: "%s"?' => 'Apakah anda yakin akan membuka tugas ini : « %s » ?',
'Back to the board' => 'Kembali ke papan',
- 'Created on %B %e, %Y at %k:%M %p' => 'Dibuat pada tanggal %d/%m/%Y à %H:%M',
'There is nobody assigned' => 'Tidak ada orang yand ditugaskan',
'Column on the board:' => 'Kolom di dalam papan : ',
- 'Status is open' => 'Status terbuka',
- 'Status is closed' => 'Status ditutup',
'Close this task' => 'Tutup tugas ini',
'Open this task' => 'Buka tugas ini',
'There is no description.' => 'Tidak ada deskripsi.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Id diperlukan',
'The project id is required' => 'Id proyek diperlukan',
'The project name is required' => 'Nama proyek diperlukan',
- 'This project must be unique' => 'Proyek ini harus unik',
'The title is required' => 'Judul diperlukan',
'Settings saved successfully.' => 'Pengaturan berhasil disimpan.',
'Unable to save your settings.' => 'Tidak dapat menyimpan pengaturan anda.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Sedang dalam pengerjaan',
'Done' => 'Selesai',
'Application version:' => 'Versi aplikasi :',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Diselesaikan pada tanggal %d/%m/%Y à %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M',
- 'Date created' => 'Tanggal dibuat',
- 'Date completed' => 'Tanggal diselesaikan',
'Id' => 'Id.',
'%d closed tasks' => '%d tugas yang ditutup',
'No task for this project' => 'Tidak ada tugas dalam proyek ini',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Kompleksitas',
'Task limit' => 'Batas tugas.',
'Task count' => 'Jumlah tugas',
- 'Edit project access list' => 'Modifikasi hak akses proyek',
- 'Allow this user' => 'Memperbolehkan pengguna ini',
- 'Don\'t forget that administrators have access to everything.' => 'Ingat bahwa administrator memiliki akses ke semua.',
- 'Revoke' => 'Mencabut',
- 'List of authorized users' => 'Daftar pengguna yang berwenang',
'User' => 'Pengguna',
- 'Nobody have access to this project.' => 'Tidak ada yang berwenang untuk mengakses proyek.',
'Comments' => 'Komentar',
'Write your text in Markdown' => 'Menulis teks anda didalam Markdown',
'Leave a comment' => 'Tinggalkan komentar',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Modifikasi tugas ini',
'Due Date' => 'Batas Tanggal Terakhir',
'Invalid date' => 'Tanggal tidak valid',
- 'Must be done before %B %e, %Y' => 'Harus diselesaikan sebelum tanggal %d/%m/%Y',
- '%B %e, %Y' => '%d %B %Y',
- '%b %e, %Y' => '%d/%m/%Y',
'Automatic actions' => 'Tindakan otomatis',
'Your automatic action have been created successfully.' => 'Tindakan otomatis anda berhasil dibuat.',
'Unable to create your automatic action.' => 'Tidak dapat membuat tindakan otomatis anda.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Menetapkan warna untuk pengguna tertentu',
'Column title' => 'Judul kolom',
'Position' => 'Posisi',
- 'Move Up' => 'Pindah ke atas',
- 'Move Down' => 'Pindah ke bawah',
'Duplicate to another project' => 'Duplikasi ke proyek lain',
'Duplicate' => 'Duplikasi',
'link' => 'tautan',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Komentar berhasil dihapus.',
'Unable to remove this comment.' => 'Tidak dapat menghapus komentar ini.',
'Do you really want to remove this comment?' => 'Apakah anda yakin akan menghapus komentar ini ?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Hanya administrator atau pembuat komentar yang dapat mengakses halaman ini.',
'Current password for the user "%s"' => 'Kata sandi saat ini untuk pengguna « %s »',
'The current password is required' => 'Kata sandi saat ini diperlukan',
'Wrong password' => 'Kata sandi salah',
@@ -267,10 +246,6 @@ return array(
'External authentication failed' => 'Otentifikasi eksternal gagal',
'Your external account is linked to your profile successfully.' => 'Akun eksternal anda berhasil dihubungkan ke profil anda.',
'Email' => 'Email',
- 'Link my Google Account' => 'Hubungkan akun Google saya',
- 'Unlink my Google Account' => 'Putuskan akun Google saya',
- 'Login with my Google Account' => 'Masuk menggunakan akun Google saya',
- 'Project not found.' => 'Proyek tidak ditemukan.',
'Task removed successfully.' => 'Tugas berhasil dihapus.',
'Unable to remove this task.' => 'Tidak dapat menghapus tugas ini.',
'Remove a task' => 'Hapus tugas',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Ukuran maksimum: ',
'Unable to upload the file.' => 'Tidak dapat mengunggah berkas.',
'Display another project' => 'Lihat proyek lain',
- 'Login with my Github Account' => 'Masuk menggunakan akun Github saya',
- 'Link my Github Account' => 'Hubungkan akun Github saya ',
- 'Unlink my Github Account' => 'Putuskan akun Github saya',
'Created by %s' => 'Dibuat oleh %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifikasi terakhir pada tanggal %d/%m/%Y à %H:%M',
'Tasks Export' => 'Ekspor Tugas',
'Tasks exportation for "%s"' => 'Tugas di ekspor untuk « %s »',
'Start Date' => 'Tanggal Mulai',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Saya ingin menerima pemberitahuan hanya untuk proyek-proyek yang dipilih :',
'view the task on Kanboard' => 'lihat tugas di Kanboard',
'Public access' => 'Akses publik',
- 'User management' => 'Manajemen pengguna',
'Active tasks' => 'Tugas aktif',
'Disable public access' => 'Nonaktifkan akses publik',
'Enable public access' => 'Aktifkan akses publik',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Email :',
'Notifications:' => 'Pemberitahuan :',
'Notifications' => 'Pemberitahuan',
- 'Group:' => 'Grup :',
- 'Regular user' => 'Pengguna normal',
'Account type:' => 'Tipe akun :',
'Edit profile' => 'Modifikasi profil',
'Change password' => 'Rubah kata sandri',
'Password modification' => 'Modifikasi kata sandi',
'External authentications' => 'Otentifikasi eksternal',
- 'Google Account' => 'Akun Google',
- 'Github Account' => 'Akun Github',
'Never connected.' => 'Tidak pernah terhubung.',
- 'No account linked.' => 'Tidak ada akun terhubung.',
- 'Account linked.' => 'Akun terhubung.',
'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.',
'Password modified successfully.' => 'Kata sandi berhasil dimodifikasi.',
'Unable to change the password.' => 'Tidak dapat merubah kata sandir.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s',
'New password for the user "%s"' => 'Kata sandi baru untuk pengguna « %s »',
'Choose an event' => 'Pilih acara',
- 'Github commit received' => 'Menerima komit dari Github',
- 'Github issue opened' => 'Tiket Github dibuka',
- 'Github issue closed' => 'Tiket Github ditutup',
- 'Github issue reopened' => 'Tiket Github dibuka kembali',
- 'Github issue assignee change' => 'Rubah penugasan tiket Github',
- 'Github issue label change' => 'Perubahan label pada tiket Github',
'Create a task from an external provider' => 'Buat tugas dari pemasok eksternal',
'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal',
'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal',
'Reference' => 'Referensi',
- 'Reference: %s' => 'Referensi : %s',
'Label' => 'Label',
'Database' => 'Basis data',
'About' => 'Tentang',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frequensi dalam detik (standar 60 detik)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequensi dalam detik (0 untuk menonaktifkan fitur ini, standar 10 detik)',
'Application URL' => 'URL Aplikasi',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Contoh : http://exemple.kanboard.net/ (digunakan untuk pemberitahuan email)',
'Token regenerated.' => 'Token diregenerasi.',
'Date format' => 'Format tanggal',
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalu diterima, contoh : « %s » et « %s »',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Perkiraan waktu: %s jam',
- 'Time spent: %s hours' => 'Waktu dihabiskan : %s jam',
- 'Started on %B %e, %Y' => 'Dimulai pada %d/%m/%Y',
'Start date' => 'Tanggal mulai',
'Time estimated' => 'Perkiraan waktu',
'There is nothing assigned to you.' => 'Tidak ada yang diberikan kepada anda.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Semua orang mendapat akses untuk proyek ini.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Webhook Github',
- 'Help on Github webhooks' => 'Bantuan pada webhook Github',
'Create a comment from an external provider' => 'Buat komentar dari pemasok eksternal',
- 'Github issue comment created' => 'Komentar dibuat pada tiket Github',
'Project management' => 'Manajemen proyek',
'My projects' => 'Proyek saya',
'Columns' => 'Kolom',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Partisi ulang pengguna untuk « %s »',
'Clone this project' => 'Gandakan proyek ini',
'Column removed successfully.' => 'Kolom berhasil dihapus.',
- 'Github Issue' => 'Tiket Github',
'Not enough data to show the graph.' => 'Tidak cukup data untuk menampilkan grafik.',
'Previous' => 'Sebelumnya',
'The id must be an integer' => 'Id harus integer',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Standar swimlane',
'Do you really want to remove this swimlane: "%s"?' => 'Apakah anda yakin akan menghapus swimlane ini : « %s » ?',
'Inactive swimlanes' => 'Swimlanes tidak aktif',
- 'Set project manager' => 'Masukan manajer proyek',
- 'Set project member' => 'Masukan anggota proyek ',
'Remove a swimlane' => 'Supprimer une swimlane',
- 'Rename' => 'Ganti nama',
'Show default swimlane' => 'Perlihatkan standar swimlane',
'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk proyek « %s »',
'Swimlane not found.' => 'Swimlane tidak ditemukan.',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane berhasil diperbaharui.',
'The default swimlane have been updated successfully.' => 'Standar swimlane berhasil diperbaharui.',
- 'Unable to create your swimlane.' => 'Tidak dapat membuat swimlane anda.',
'Unable to remove this swimlane.' => 'Tidak dapat menghapus swimlane ini.',
'Unable to update this swimlane.' => 'Tidak dapat memperbaharui swimlane ini.',
'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.',
'Example: "Bug, Feature Request, Improvement"' => 'Contoh: « Insiden, Permintaan Fitur, Perbaikan »',
'Default categories for new projects (Comma-separated)' => 'Standar kategori untuk proyek baru (dipisahkan dengan koma)',
- 'Gitlab commit received' => 'Menerima komit Gitlab',
- 'Gitlab issue opened' => 'Tiket Gitlab dibuka',
- 'Gitlab issue closed' => 'Tiket Gitlab ditutup',
- 'Gitlab webhooks' => 'Webhook Gitlab',
- 'Help on Gitlab webhooks' => 'Bantuan pada webhook Gitlab',
'Integrations' => 'Integrasi',
'Integration with third-party services' => 'Integrasi dengan layanan pihak ketiga',
- 'Role for this project' => 'Peran untuk proyek ini',
- 'Project manager' => 'Manajer proyek',
- 'Project member' => 'Anggota proyek',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Seorang manajer proyek dapat mengubah pengaturan proyek dan memiliki lebih banyak keistimewaan dibandingkan dengan pengguna biasa.',
- 'Gitlab Issue' => 'Tiket Gitlab',
'Subtask Id' => 'Id Subtugas',
'Subtasks' => 'Subtugas',
'Subtasks Export' => 'Ekspor Subtugas',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'Anda sudah ada satu subtugas dalam proses',
'Which parts of the project do you want to duplicate?' => 'Bagian dalam proyek mana yang ingin anda duplikasi?',
'Disallow login form' => 'Larang formulir masuk',
- 'Bitbucket commit received' => 'Menerima komit Bitbucket',
- 'Bitbucket webhooks' => 'Webhook Bitbucket',
- 'Help on Bitbucket webhooks' => 'Bantuan pada webhook Bitbucket',
'Start' => 'Mulai',
'End' => 'Selesai',
'Task age in days' => 'Usia tugas dalam hari',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Tugas ini',
'<1h' => '<1h',
'%dh' => '%dh',
- '%b %e' => '%e %b',
'Expand tasks' => 'Perluas tugas',
'Collapse tasks' => 'Lipat tugas',
'Expand/collapse tasks' => 'Perluas/lipat tugas',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'pintas keyboard',
'Open board switcher' => 'Buka table switcher',
'Application' => 'Aplikasi',
- 'since %B %e, %Y at %k:%M %p' => 'sejak %d/%m/%Y à %H:%M',
'Compact view' => 'Tampilan kompak',
'Horizontal scrolling' => 'Horisontal bergulir',
'Compact/wide view' => 'Beralih antara tampilan kompak dan diperluas',
'No results match:' => 'Tidak ada hasil :',
'Currency' => 'Mata uang',
- 'Files' => 'Arsip',
- 'Images' => 'Gambar',
'Private project' => 'Proyek pribadi',
'AUD - Australian Dollar' => 'AUD - Dollar Australia',
'CAD - Canadian Dollar' => 'CAD - Dollar Kanada',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => 'Kode dua faktor kode otentifikasi valid.',
'Code' => 'Kode',
'Two factor authentication' => 'Dua faktor otentifikasi',
- 'Enable/disable two factor authentication' => 'Matikan/hidupkan dua faktor otentifikasi',
'This QR code contains the key URI: ' => 'kode QR ini mengandung kunci URI : ',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Menyimpan kunci rahasia ini dalam perangkat lunak TOTP anda(misalnya Googel Authenticator atau FreeOTP).',
'Check my code' => 'Memeriksa kode saya',
'Secret key: ' => 'Kunci rahasia : ',
'Test your device' => 'Menguji perangkat anda',
'Assign a color when the task is moved to a specific column' => 'Menetapkan warna ketika tugas tersebut dipindahkan ke kolom tertentu',
'%s via Kanboard' => '%s via Kanboard',
- 'uploaded by: %s' => 'diunggah oleh %s',
- 'uploaded on: %s' => 'diunggah pada %s',
- 'size: %s' => 'ukuran : %s',
'Burndown chart for "%s"' => 'Grafik Burndown untku « %s »',
'Burndown chart' => 'Grafik Burndown',
'This chart show the task complexity over the time (Work Remaining).' => 'Grafik ini menunjukkan kompleksitas tugas dari waktu ke waktu (Sisa Pekerjaan).',
@@ -722,7 +645,6 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Mengambil screenshot dan tekan CTRL + V atau ⌘ + V untuk paste di sini.',
'Screenshot uploaded successfully.' => 'Screenshot berhasil diunggah.',
'SEK - Swedish Krona' => 'SEK - Krona Swedia',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identifier proyek adalah kode alfanumerik opsional digunakan untuk mengidentifikasi proyek Anda.',
'Identifier' => 'Identifier',
'Disable two factor authentication' => 'Matikan dua faktor otentifikasi',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Apakah anda yakin akan mematikan dua faktor otentifikasi untuk pengguna ini : « %s » ?',
@@ -731,7 +653,6 @@ return array(
'A task cannot be linked to itself' => 'Sebuah tugas tidak dapat dikaitkan dengan dirinya sendiri',
'The exact same link already exists' => 'Tautan yang sama persis sudah ada',
'Recurrent task is scheduled to be generated' => 'Tugas berulang dijadwalkan akan dihasilkan',
- 'Recurring information' => 'Informasi berulang',
'Score' => 'Skor',
'The identifier must be unique' => 'Identifier harus unik',
'This linked task id doesn\'t exists' => 'Id tugas terkait tidak ada',
@@ -777,21 +698,10 @@ return array(
'User that will receive the email' => 'Pengguna yang akan menerima email',
'Email subject' => 'Subjek Email',
'Date' => 'Tanggal',
- 'By @%s on Bitbucket' => 'Oleh @%s pada Bitbucket',
- 'Bitbucket Issue' => 'Tiket Bitbucket',
- 'Commit made by @%s on Bitbucket' => 'Komit dibuat oleh @%s pada Bitbucket',
- 'Commit made by @%s on Github' => 'Komit dibuat oleh @%s pada Github',
- 'By @%s on Github' => 'Oleh @%s pada Github',
- 'Commit made by @%s on Gitlab' => 'Komit dibuat oleh @%s pada Gitlab',
'Add a comment log when moving the task between columns' => 'Menambahkan log komentar ketika memindahkan tugas antara kolom',
'Move the task to another column when the category is changed' => 'Pindahkan tugas ke kolom lain ketika kategori berubah',
'Send a task by email to someone' => 'Kirim tugas melalui email ke seseorang',
'Reopen a task' => 'Membuka kembali tugas',
- 'Bitbucket issue opened' => 'Tiket Bitbucket dibuka',
- 'Bitbucket issue closed' => 'Tiket Bitbucket ditutup',
- 'Bitbucket issue reopened' => 'Tiket Bitbucket dibuka kembali',
- 'Bitbucket issue assignee change' => 'Perubahan penugasan tiket Bitbucket',
- 'Bitbucket issue comment created' => 'Komentar dibuat tiket Bitbucket',
'Column change' => 'Kolom berubah',
'Position change' => 'Posisi berubah',
'Swimlane change' => 'Swimlane berubah',
@@ -826,17 +736,11 @@ return array(
'The field "%s" have been updated' => 'Field « %s » telah diperbaharui',
'The description have been modified' => 'Deskripsi telah dimodifikasi',
'Do you really want to close the task "%s" as well as all subtasks?' => 'Apakah anda yakin akan menutup tugas « %s » beserta semua sub-tugasnya ?',
- 'Swimlane: %s' => 'Swimlane : %s',
'I want to receive notifications for:' => 'Saya ingin menerima pemberitahuan untuk :',
'All tasks' => 'Semua tugas',
'Only for tasks assigned to me' => 'Hanya untuk tugas yang ditugaskan ke saya',
'Only for tasks created by me' => 'Hanya untuk tugas yang dibuat oleh saya',
'Only for tasks created by me and assigned to me' => 'Hanya untuk tugas yang dibuat oleh saya dan ditugaskan ke saya',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M',
- 'New due date: %B %e, %Y' => 'Tanggal jatuh tempo baru : %d/%m/%Y',
- 'Start date changed: %B %e, %Y' => 'Tanggal mulai berubah : %d/%m/%Y',
- '%k:%M %p' => '%H:%M',
'%%Y-%%m-%%d' => '%%d/%%m/%%Y',
'Total for all columns' => 'Total untuk semua kolom',
'You need at least 2 days of data to show the chart.' => 'Anda memerlukan setidaknya 2 hari dari data yang menunjukkan grafik.',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'Tidak ditugaskan',
'View advanced search syntax' => 'Lihat sintaks pencarian lanjutan',
'Overview' => 'Ikhtisar',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Tampilan Papan/Kalender/Daftar',
'Switch to the board view' => 'Beralih ke tampilan papan',
'Switch to the calendar view' => 'Beralih ke tampilan kalender',
@@ -894,10 +797,6 @@ return array(
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Grafik ini menunjukkan memimpin rata-rata dan waktu siklus untuk %d tugas terakhir dari waktu ke waktu.',
'Average time into each column' => 'Rata-rata waktu ke setiap kolom',
'Lead and cycle time' => 'Lead dan siklus waktu',
- 'Google Authentication' => 'Google Otentifikasi',
- 'Help on Google authentication' => 'Bantuan pada otentifikasi Google',
- 'Github Authentication' => 'Otentifikasi Github',
- 'Help on Github authentication' => 'Bantuan pada otentifikasi Github',
'Lead time: ' => 'Lead time : ',
'Cycle time: ' => 'Siklus waktu : ',
'Time spent into each column' => 'Waktu yang dihabiskan di setiap kolom',
@@ -906,18 +805,12 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Jika tugas tidak ditutup waktu saat ini yang digunakan sebagai pengganti tanggal penyelesaian.',
'Set automatically the start date' => 'Secara otomatis mengatur tanggal mulai',
'Edit Authentication' => 'Modifikasi Otentifikasi',
- 'Google Id' => 'Id Google',
- 'Github Id' => 'Id Github',
'Remote user' => 'Pengguna jauh',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata sandi mereka dalam basis data Kanboard, contoh: akun LDAP, Google dan Github.',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.',
- 'By @%s on Gitlab' => 'Dengan @%s pada Gitlab',
- 'Gitlab issue comment created' => 'Komentar dibuat pada tiket Gitlab',
'New remote user' => 'Pengguna baru jauh',
'New local user' => 'Pengguna baru lokal',
'Default task color' => 'Standar warna tugas',
- 'Hide sidebar' => 'Sembunyikan sidebar',
- 'Expand sidebar' => 'Perluas sidebar',
'This feature does not work with all browsers.' => 'Fitur ini tidak dapat digunakan di semua browsers',
'There is no destination project available.' => 'Tidak ada destinasi proyek yang tersedia.',
'Trigger automatically subtask time tracking' => 'Otomatis memicu pelacakan untuk subtugas',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'kontributor',
'License:' => 'Lisensi :',
'License' => 'Lisensi',
- 'Project Administrator' => 'Administrator proyek',
'Enter the text below' => 'Masukkan teks di bawah',
'Gantt chart for %s' => 'Grafik Gantt untuk %s',
'Sort by position' => 'Urutkan berdasarkan posisi',
@@ -952,11 +844,9 @@ return array(
'open file' => 'buka berkas',
'End date' => 'Waktu berakhir',
'Users overview' => 'Ikhtisar pengguna',
- 'Managers' => 'Manajer',
'Members' => 'Anggota',
'Shared project' => 'Proyek bersama',
'Project managers' => 'Manajer proyek',
- 'Project members' => 'Anggota proyek',
'Gantt chart for all projects' => 'Grafik Gantt untuk semua proyek',
'Projects list' => 'Daftar proyek',
'Gantt chart for this project' => 'Grafik Gantt untuk proyek ini',
@@ -964,26 +854,16 @@ 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',
- 'Start date: %s' => 'Waktu mulai : %s',
- 'End date: %s' => 'Waktu berakhir : %s',
'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 ',
- 'Login with my Gitlab Account' => 'Masuk menggunakan akun Gitlab saya',
'Milestone' => 'Milestone',
- 'Gitlab Authentication' => 'Authentification Gitlab',
- 'Help on Gitlab authentication' => 'Bantuan pada otentifikasi Gitlab',
- 'Gitlab Id' => 'Id Gitlab',
- 'Gitlab Account' => 'Akun Gitlab',
- 'Link my Gitlab Account' => 'Hubungkan akun Gitlab saya',
- 'Unlink my Gitlab Account' => 'Putuskan akun Gitlab saya',
'Documentation: %s' => 'Dokumentasi : %s',
'Switch to the Gantt chart view' => 'Beralih ke tampilan grafik Gantt',
'Reset the search/filter box' => 'Atur ulang pencarian/kotak filter',
'Documentation' => 'Dokumentasi',
'Table of contents' => 'Daftar isi',
'Gantt' => 'Gantt',
- 'Help with project permissions' => 'Bantuan dengan izin proyek',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ 'Project members' => 'Anggota proyek',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 08bf7225..a735128f 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -4,14 +4,14 @@ return array(
'number.decimals_separator' => ',',
'number.thousands_separator' => '.',
'None' => 'Nessuno',
- 'edit' => 'modificare',
- 'Edit' => 'Modificare',
- 'remove' => 'cancellare',
- 'Remove' => 'Cancellare',
- 'Update' => 'Aggiornare',
+ 'edit' => 'modifica',
+ 'Edit' => 'Modifica',
+ 'remove' => 'cancella',
+ 'Remove' => 'Cancella',
+ 'Update' => 'Aggiorna',
'Yes' => 'Si',
'No' => 'No',
- 'cancel' => 'annullare',
+ 'cancel' => 'annulla',
'or' => 'o',
'Yellow' => 'Giallo',
'Blue' => 'Blu',
@@ -20,68 +20,67 @@ return array(
'Red' => 'Rosso',
'Orange' => 'Arancione',
'Grey' => 'Grigio',
- // 'Brown' => '',
- // 'Deep Orange' => '',
- // 'Dark Grey' => '',
- // 'Pink' => '',
- // 'Teal' => '',
- // 'Cyan' => '',
+ 'Brown' => 'Marrone',
+ 'Deep Orange' => 'Arancio scuro',
+ 'Dark Grey' => 'Grigio scuro',
+ 'Pink' => 'Rosa',
+ 'Teal' => 'Verde foglia di tè',
+ 'Cyan' => 'Ciano',
// 'Lime' => '',
- // 'Light Green' => '',
- // 'Amber' => '',
- 'Save' => 'Salvare',
- 'Login' => 'Entra',
+ 'Light Green' => 'Verde chiaro',
+ 'Amber' => 'Ambra',
+ 'Save' => 'Salva',
+ 'Login' => 'Accedi',
'Official website:' => 'Sito web ufficiale:',
'Unassigned' => 'Non assegnato',
- 'View this task' => 'Vedere questo compito',
- 'Remove user' => 'Cancellare un utente',
- 'Do you really want to remove this user: "%s"?' => 'Veramente vuoi cancellare questo utente: « %s » ?',
- 'New user' => 'Aggiungere un utente',
+ 'View this task' => 'Visualizza questo task',
+ 'Remove user' => 'Cancella un utente',
+ 'Do you really want to remove this user: "%s"?' => 'Veramente vuoi cancellare questo utente: "%s" ?',
+ 'New user' => 'Aggiungi un utente',
'All users' => 'Tutti gli utenti',
'Username' => 'Nome utente',
'Password' => 'Password',
'Administrator' => 'Amministratore',
- 'Sign in' => 'Iscriversi',
+ 'Sign in' => 'Accedi',
'Users' => 'Utenti',
'No user' => 'Nessun utente',
'Forbidden' => 'Vietato',
'Access Forbidden' => 'Accesso vietato',
- 'Edit user' => 'Modificare un utente',
- 'Logout' => 'Uscire',
+ 'Edit user' => 'Modifica un utente',
+ 'Logout' => 'Esci',
'Bad username or password' => 'Utente o password errati',
- 'Edit project' => 'Modificare progetto',
+ 'Edit project' => 'Modifica progetto',
'Name' => 'Nome',
'Projects' => 'Progetti',
'No project' => 'Nessun progetto',
'Project' => 'Progetto',
'Status' => 'Stato',
- 'Tasks' => 'Compiti',
+ 'Tasks' => 'Tasks',
'Board' => 'Bacheca',
'Actions' => 'Azioni',
'Inactive' => 'Inattivo',
'Active' => 'Attivo',
- 'Add this column' => 'Aggiungere questa colonna',
- '%d tasks on the board' => '%d compiti sulla bacheca',
- '%d tasks in total' => '%d compiti in totale',
- 'Unable to update this board.' => 'Non si può aggiornare questa bacheca.',
- 'Edit board' => 'Modificare questa bacheca',
- 'Disable' => 'Disattivare',
- 'Enable' => 'Attivare',
+ 'Add this column' => 'Aggiungi questa colonna',
+ '%d tasks on the board' => '%d task sulla bacheca',
+ '%d tasks in total' => '%d task in totale',
+ 'Unable to update this board.' => 'Impossibile aggiornare questa bacheca.',
+ 'Edit board' => 'Modifica questa bacheca',
+ 'Disable' => 'Disattiva',
+ 'Enable' => 'Attiva',
'New project' => 'Nuovo progetto',
- 'Do you really want to remove this project: "%s"?' => 'Veramente vuoi eliminare questo progetto: « %s » ?',
- 'Remove project' => 'Cancellare il progetto',
- 'Edit the board for "%s"' => 'Modificare la bacheca per « %s »',
+ 'Do you really want to remove this project: "%s"?' => 'Veramente vuoi eliminare il seguente progetto: "%s" ?',
+ 'Remove project' => 'Cancella il progetto',
+ 'Edit the board for "%s"' => 'Modifica la bacheca per "%s"',
'All projects' => 'Tutti i progetti',
- 'Change columns' => 'Cambiare le colonne',
- 'Add a new column' => 'Aggiungere una nuova colonna',
+ 'Add a new column' => 'Aggiungi una nuova colonna',
'Title' => 'Titolo',
'Nobody assigned' => 'Nessuno assegnato',
'Assigned to %s' => 'Assegnato a %s',
- 'Remove a column' => 'Cancellare questa colonna',
- 'Remove a column from a board' => 'Cancellare una colonna da una bacheca',
- 'Unable to remove this column.' => 'Non si può cancellare questa colonna.',
- 'Do you really want to remove this column: "%s"?' => 'Veramente desideri cancellare questa colonna : « %s » ?',
- 'This action will REMOVE ALL TASKS associated to this column!' => 'Questa azione cancellerà TUTTI I COMPITI legati a questa colonna!',
+ 'Remove a column' => 'Cancella questa colonna',
+ 'Remove a column from a board' => 'Cancella una colonna da una bacheca',
+ 'Unable to remove this column.' => 'Impossibile cancellare questa colonna.',
+ 'Do you really want to remove this column: "%s"?' => 'Veramente desideri cancellare questa colonna: "%s" ?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Questa azione cancellerà TUTTI I TASK legati a questa colonna!',
'Settings' => 'Impostazioni',
'Application settings' => 'Impostazioni dell\'applicazione',
'Language' => 'Lingua',
@@ -92,25 +91,22 @@ return array(
'Optimize the database' => 'Ottimizare la base dati',
'(VACUUM command)' => '(Comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)',
- 'Close a task' => 'Chiudere un compito',
- 'Edit a task' => 'Modificare un compito',
- 'Column' => 'colonna',
+ 'Close a task' => 'Chiudi un task',
+ 'Edit a task' => 'Modifica un task',
+ 'Column' => 'Colonna',
'Color' => 'Colore',
- 'Assignee' => 'Persona assegnata',
- 'Create another task' => 'Creare un nuovo compito',
- 'New task' => 'Nuovo compito',
- 'Open a task' => 'Aprire un compito',
- 'Do you really want to open this task: "%s"?' => 'Veramente desideri aprire questo compito: « %s » ?',
- 'Back to the board' => 'Tornare alla bacheca',
- 'Created on %B %e, %Y at %k:%M %p' => 'Creato il %B %e, %Y alle %k:%M %p',
- 'There is nobody assigned' => 'Non c\'è nessuno assegnato a questo compito',
+ 'Assignee' => 'Assegnatario',
+ 'Create another task' => 'Crea un nuovo task',
+ 'New task' => 'Nuovo task',
+ 'Open a task' => 'Apri un task',
+ 'Do you really want to open this task: "%s"?' => 'Veramente desideri aprire questo task: "%s" ?',
+ 'Back to the board' => 'Torna alla bacheca',
+ 'There is nobody assigned' => 'Nessuno è assegnato a questo task',
'Column on the board:' => 'Colonna sulla bacheca: ',
- 'Status is open' => 'Stato aperto',
- 'Status is closed' => 'stato chiuso',
- 'Close this task' => 'Chiudere questo compito',
- 'Open this task' => 'Aprire questo compito',
- 'There is no description.' => 'Non c\'è descrizione.',
- 'Add a new task' => 'Aggiungere un nuovo compito',
+ 'Close this task' => 'Chiudi questo task',
+ 'Open this task' => 'Apri questo task',
+ 'There is no description.' => 'Nessuna descrizione presente.',
+ 'Add a new task' => 'Aggiungere un nuovo task',
'The username is required' => 'Si richiede un nome di utente',
'The maximum length is %d characters' => 'La lunghezza massima è di %d caratteri',
'The minimum length is %d characters' => 'La lunghezza minima è di %d caratteri',
@@ -124,84 +120,70 @@ return array(
'The id is required' => 'Si richiede l\'identificatore',
'The project id is required' => 'Si richiede l\'identificatore del progetto',
'The project name is required' => 'Si richiede il nome del progetto',
- 'This project must be unique' => 'Il nome del progetto deve essere unico',
'The title is required' => 'Si richiede un titolo',
'Settings saved successfully.' => 'Impostazioni salvate correttamente.',
- 'Unable to save your settings.' => 'Non si possono salvare le impostazioni.',
+ 'Unable to save your settings.' => 'Impossibile salvare le impostazioni.',
'Database optimization done.' => 'Ottimizzazione della base dati conclusa.',
'Your project have been created successfully.' => 'Il tuo progetto è stato creato correttamente.',
- 'Unable to create your project.' => 'Non si può creare il progetto.',
+ 'Unable to create your project.' => 'Impossibile creare il progetto.',
'Project updated successfully.' => 'Progetto aggiornato correttamente.',
- 'Unable to update this project.' => 'Non si può aggiornare il progetto.',
- 'Unable to remove this project.' => 'Non si può cancellare questo progetto.',
+ 'Unable to update this project.' => 'Impossibile aggiornare il progetto.',
+ 'Unable to remove this project.' => 'Impossibile cancellare questo progetto.',
'Project removed successfully.' => 'Progetto cancellato correttamente.',
'Project activated successfully.' => 'Progetto attivato correttamente.',
- 'Unable to activate this project.' => 'Non si può attivare il progetto.',
+ 'Unable to activate this project.' => 'Impossibile attivare il progetto.',
'Project disabled successfully.' => 'Progetto disattivato correttamente.',
- 'Unable to disable this project.' => 'Non si può disattivare il progetto.',
- 'Unable to open this task.' => 'Non si può aprire questo compito.',
- 'Task opened successfully.' => 'Il compito è stato aperto correttamente.',
- 'Unable to close this task.' => 'Non si può chiudere questo compito.',
- 'Task closed successfully.' => 'Compito chiuso correttamente.',
- 'Unable to update your task.' => 'Non si può modificare questo compito.',
- 'Task updated successfully.' => 'Compito modificato correttamente.',
- 'Unable to create your task.' => 'Non si può creare questo compito.',
- 'Task created successfully.' => 'Compito creato correttamente.',
+ 'Unable to disable this project.' => 'Impossibile disattivare il progetto.',
+ 'Unable to open this task.' => 'Impossibile aprire questo task.',
+ 'Task opened successfully.' => 'Il task è stato aperto correttamente.',
+ 'Unable to close this task.' => 'Impossibile chiudere questo task.',
+ 'Task closed successfully.' => 'Task chiuso correttamente.',
+ 'Unable to update your task.' => 'Impossibile modificare questo task.',
+ 'Task updated successfully.' => 'Task modificato correttamente.',
+ 'Unable to create your task.' => 'Impossibile creare questo task.',
+ 'Task created successfully.' => 'Task creato correttamente.',
'User created successfully.' => 'Utente creato correttamente.',
- 'Unable to create your user.' => 'Non si può creare l\'utente.',
+ 'Unable to create your user.' => 'Impossibile creare l\'utente.',
'User updated successfully.' => 'Utente aggiornato correttamente.',
- 'Unable to update your user.' => 'Non si può aggiornare questo utente.',
+ 'Unable to update your user.' => 'Impossibile aggiornare questo utente.',
'User removed successfully.' => 'Utente cancellato correttamente.',
- 'Unable to remove this user.' => 'Non si può cancellare questo utente.',
+ 'Unable to remove this user.' => 'Impossibile cancellare questo utente.',
'Board updated successfully.' => 'Bacheca aggiornata correttamente.',
'Ready' => 'Pronto',
'Backlog' => 'In attesa',
'Work in progress' => 'In corso',
'Done' => 'Fatto',
'Application version:' => 'Versione dell\'applicazione:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Completato il %B %e, %Y alle %k:%M %p',
- // '%B %e, %Y at %k:%M %p' => '',
- 'Date created' => 'Data di creazione',
- 'Date completed' => 'Data di termine',
- 'Id' => 'Identificatore',
- '%d closed tasks' => '%d compiti chiusi',
- 'No task for this project' => 'Nessun compito per questo progetto',
+ // 'Id' => '',
+ '%d closed tasks' => '%d task chiusi',
+ 'No task for this project' => 'Nessun task per questo progetto',
'Public link' => 'Link pubblico',
- 'Change assignee' => 'Cambiare la persona assegnata',
- 'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »',
+ 'Change assignee' => 'Cambia l\'assegnatario',
+ 'Change assignee for the task "%s"' => 'Cambia l\'assegnatario per il task "%s"',
'Timezone' => 'Fuso orario',
- 'Sorry, I didn\'t find this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!',
+ 'Sorry, I didn\'t find this information in my database!' => 'Spiacente, non ho trovato questa informazione sul database!',
'Page not found' => 'Pagina non trovata',
'Complexity' => 'Complessità',
- 'Task limit' => 'Numero massimo di compiti',
- 'Task count' => 'Numero di compiti',
- 'Edit project access list' => 'Modificare i permessi del progetto',
- 'Allow this user' => 'Permettere a questo utente',
- 'Don\'t forget that administrators have access to everything.' => 'Non dimenticare che gli amministratori hanno accesso a tutto.',
- 'Revoke' => 'Revocare',
- 'List of authorized users' => 'Lista di utenti autorizzati',
+ 'Task limit' => 'Limite di task',
+ 'Task count' => 'Numero di task',
'User' => 'Utente',
- 'Nobody have access to this project.' => 'Nessuno ha accesso a questo progetto.',
'Comments' => 'Commenti',
'Write your text in Markdown' => 'Scrivi il testo in Markdown',
- 'Leave a comment' => 'Lasciare un commento',
+ 'Leave a comment' => 'Lascia un commento',
'Comment is required' => 'Si richiede un commento',
- 'Leave a description' => 'Lasciare una descrizione',
+ 'Leave a description' => 'Lascia una descrizione',
'Comment added successfully.' => 'Commenti aggiunti correttamente.',
- 'Unable to create your comment.' => 'Non si può creare questo commento.',
- 'Edit this task' => 'Modificare questo compito',
+ 'Unable to create your comment.' => 'Impossibile creare questo commento.',
+ 'Edit this task' => 'Modifica questo task',
'Due Date' => 'Data di scadenza',
- 'Invalid date' => 'Data sbagliata',
- 'Must be done before %B %e, %Y' => 'Deve essere completato prima del %B %e, %Y',
- // '%B %e, %Y' => '',
- // '%b %e, %Y' => '',
+ 'Invalid date' => 'Data non valida',
'Automatic actions' => 'Azioni automatiche',
'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata correttamente.',
- 'Unable to create your automatic action.' => 'Non si può creare quest\'azione automatica.',
+ 'Unable to create your automatic action.' => 'Impossibile creare quest\'azione automatica.',
'Remove an action' => 'Cancellare un\'azione',
- 'Unable to remove this action.' => 'Non si può cancellare questa azione.',
+ 'Unable to remove this action.' => 'Impossibile cancellare questa azione.',
'Action removed successfully.' => 'Azione cancellata correttamente.',
- 'Automatic actions for the project "%s"' => 'Azioni automatiche per questo progetto « %s »',
+ 'Automatic actions for the project "%s"' => 'Azioni automatiche per il progetto "%s"',
'Defined actions' => 'Azioni definite',
'Add an action' => 'Aggiungi un\'azione',
'Event name' => 'Nome dell\'evento',
@@ -209,43 +191,40 @@ return array(
'Action parameters' => 'Parametri d\'azione',
'Action' => 'Azione',
'Event' => 'Evento',
- 'When the selected event occurs execute the corresponding action.' => 'Quando accade l\'evento selezionato, eseguire l\'azione corrispondente.',
- 'Next step' => 'Passo seguente',
+ 'When the selected event occurs execute the corresponding action.' => 'Quando si verifica l\'evento selezionato, eseguire l\'azione corrispondente.',
+ 'Next step' => 'Passo successivo',
'Define action parameters' => 'Definire i parametri dell\'azione',
- 'Save this action' => 'Salvare questa azione',
- 'Do you really want to remove this action: "%s"?' => 'Veramente vuole cancellare questa azione « %s » ?',
- 'Remove an automatic action' => 'Cancellare un\'azione automatica',
- 'Assign the task to a specific user' => 'Assegnare questo compito a un utente specifico',
- 'Assign the task to the person who does the action' => 'Assegnare il compito all\'utente che svolge l\'azione',
- 'Duplicate the task to another project' => 'Duplicare il compito in altro progetto',
- 'Move a task to another column' => 'Muovere un compito in un\'altra colonna',
- 'Task modification' => 'Modifica di un compito',
- 'Task creation' => 'Creazione di un compito',
- 'Closing a task' => 'Chiudere un compito',
+ 'Save this action' => 'Salva questa azione',
+ 'Do you really want to remove this action: "%s"?' => 'Vuoi veramente cancellare la seguente azione: "%s"?',
+ 'Remove an automatic action' => 'Cancella un\'azione automatica',
+ 'Assign the task to a specific user' => 'Assegna il task ad un utente specifico',
+ 'Assign the task to the person who does the action' => 'Assegna il task all\'utente che compie l\'azione',
+ 'Duplicate the task to another project' => 'Duplica il task in altro progetto',
+ 'Move a task to another column' => 'Sposta un task in un\'altra colonna',
+ 'Task modification' => 'Modifica di un task',
+ 'Task creation' => 'Creazione di un task',
+ 'Closing a task' => 'Chiusura di un task',
'Assign a color to a specific user' => 'Assegna un colore ad un utente specifico',
'Column title' => 'Titolo della colonna',
'Position' => 'Posizione',
- 'Move Up' => 'Alzare',
- 'Move Down' => 'Abassare',
- 'Duplicate to another project' => 'Duplicare in un altro progetto',
- 'Duplicate' => 'Duplicare',
- 'link' => 'link',
+ 'Duplicate to another project' => 'Duplica in un altro progetto',
+ 'Duplicate' => 'Duplica',
+ 'link' => 'relazione',
'Comment updated successfully.' => 'Commento aggiornato correttamente.',
- 'Unable to update your comment.' => 'Non si può aggiornare questo commento.',
- 'Remove a comment' => 'Cancellare un commento',
+ 'Unable to update your comment.' => 'Impossibile aggiornare questo commento.',
+ 'Remove a comment' => 'Cancella un commento',
'Comment removed successfully.' => 'Commento cancellato correttamente.',
- 'Unable to remove this comment.' => 'Non si può cancellare questo commento.',
- 'Do you really want to remove this comment?' => 'Desidera cancellare questo commento?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Solo gli amministratori o l\'autore del commento hanno accesso a questa pagina.',
- 'Current password for the user "%s"' => 'Password attuale per l\'utente: « %s »',
+ 'Unable to remove this comment.' => 'Impossibile cancellare questo commento.',
+ 'Do you really want to remove this comment?' => 'Vuoi davvero cancellare questo commento?',
+ 'Current password for the user "%s"' => 'Password attuale per l\'utente "%s"',
'The current password is required' => 'Si richiede la password attuale',
- 'Wrong password' => 'password sbagliata',
+ 'Wrong password' => 'Password errata',
'Unknown' => 'Sconociuto',
- 'Last logins' => 'Ultimi ingressi',
- 'Login date' => 'Data di ingresso',
- 'Authentication method' => 'Metodo di autenticazzione',
+ 'Last logins' => 'Ultimi accessi',
+ 'Login date' => 'Data di accesso',
+ 'Authentication method' => 'Metodo di autenticazione',
'IP address' => 'Indirizzo IP',
- 'User agent' => 'Navigatore',
+ 'User agent' => 'User agent',
'Persistent connections' => 'Connessioni persistenti',
'No session.' => 'Non esiste sessione.',
'Expiration date' => 'Data di scadenza',
@@ -254,97 +233,89 @@ return array(
'Everybody' => 'Tutti',
'Open' => 'Aperto',
'Closed' => 'Chiuso',
- 'Search' => 'Cercare',
+ 'Search' => 'Cerca',
'Nothing found.' => 'Non si è trovato nulla.',
'Due date' => 'Data di scadenza',
- 'Others formats accepted: %s and %s' => 'Altri formati accettati: %s y %s',
+ 'Others formats accepted: %s and %s' => 'Altri formati accettati: %s e %s',
'Description' => 'Descrizione',
'%d comments' => '%d commenti',
'%d comment' => '%d commento',
- 'Email address invalid' => 'Indirizzo e-mail sbagliato',
- // '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 address invalid' => 'Indirizzo Email non valido',
+ 'Your external account is not linked anymore to your profile.' => 'Il tuo account esterno non è più collegato al tuo profilo.',
+ 'Unable to unlink your external account.' => 'Impossibile scollegare il tuo account esterno.',
+ 'External authentication failed' => 'Autenticazione esterna fallita',
+ 'Your external account is linked to your profile successfully.' => 'Il tuo account esterno è stato collegato al tuo profilo con successo.',
'Email' => 'E-mail',
- 'Link my Google Account' => 'Collegare il mio Account di Google',
- 'Unlink my Google Account' => 'Scollegare il mio account di Google',
- 'Login with my Google Account' => 'Entra con il mio Account di Google',
- 'Project not found.' => 'progetto non trovato.',
- 'Task removed successfully.' => 'Compito cancellato correttamente.',
- 'Unable to remove this task.' => 'Non si può cancellare questo compito.',
- 'Remove a task' => 'Cancellare un compito',
- 'Do you really want to remove this task: "%s"?' => 'Veramente vuoi cancellare questo compito: "%s"?',
- 'Assign automatically a color based on a category' => 'Assegnare un colore in modo automatico basandosi sulla categoria',
- 'Assign automatically a category based on a color' => 'Assegnare una categoria in modo automatico basandosi sul colore',
- 'Task creation or modification' => 'Creazione o modifica di compito',
+ 'Task removed successfully.' => 'Task cancellato correttamente.',
+ 'Unable to remove this task.' => 'Impossibile cancellare questo task.',
+ 'Remove a task' => 'Cancella un task',
+ 'Do you really want to remove this task: "%s"?' => 'Vuoi davvero cancellare questo task: "%s"?',
+ 'Assign automatically a color based on a category' => 'Assegna un colore in modo automatico basandosi sulla categoria',
+ 'Assign automatically a category based on a color' => 'Assegna una categoria in modo automatico basandosi sul colore',
+ 'Task creation or modification' => 'Creazione o modifica di task',
'Category' => 'Categoria',
'Category:' => 'Categoria:',
'Categories' => 'Categorie',
'Category not found.' => 'Categoria non trovata.',
'Your category have been created successfully.' => 'La tua categoria è stata creata correttamente.',
- 'Unable to create your category.' => 'Non si può creare la tua categoria.',
+ 'Unable to create your category.' => 'Impossibile creare la tua categoria.',
'Your category have been updated successfully.' => 'La tua categoria è stata aggiornata correttamente.',
- 'Unable to update your category.' => 'Non si può aggiornare la tua categoria.',
- 'Remove a category' => 'Cancellare una categoria',
+ 'Unable to update your category.' => 'Impossibile aggiornare la tua categoria.',
+ 'Remove a category' => 'Cancella una categoria',
'Category removed successfully.' => 'Categoria cancellata correttamente.',
- 'Unable to remove this category.' => 'Non si può cancellare questa categoria.',
- 'Category modification for the project "%s"' => 'Modifica di categoria per il progetto "%s"',
- 'Category Name' => 'Nome di categoria',
+ 'Unable to remove this category.' => 'Impossibile cancellare questa categoria.',
+ 'Category modification for the project "%s"' => 'Modifica della categoria per il progetto "%s"',
+ 'Category Name' => 'Nome della categoria',
'Add a new category' => 'Aggiungere una nuova categoria',
- 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare questa categoria: "%s"?',
+ 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare la seguente categoria: "%s"?',
'All categories' => 'Tutte le categorie',
'No category' => 'Senza categoria',
'The name is required' => 'Si richiede un nome',
- 'Remove a file' => 'Cancellare un file',
- 'Unable to remove this file.' => 'Non si può cancellare questo file.',
+ 'Remove a file' => 'Cancella un file',
+ 'Unable to remove this file.' => 'Impossibile cancellare questo file.',
'File removed successfully.' => 'File cancellato correttamente.',
- 'Attach a document' => 'Allegare un documento',
+ 'Attach a document' => 'Allega un documento',
'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?',
'Attachments' => 'Allegati',
- 'Edit the task' => 'Modificare il compito',
- 'Edit the description' => 'Modificare la descrizione',
- 'Add a comment' => 'Aggiungere un commento',
- 'Edit a comment' => 'Modificare un commento',
+ 'Edit the task' => 'Modifica il task',
+ 'Edit the description' => 'Modifica la descrizione',
+ 'Add a comment' => 'Aggiungi un commento',
+ 'Edit a comment' => 'Modifica un commento',
'Summary' => 'Sommario',
- 'Time tracking' => 'Time tracking',
+ // 'Time tracking' => '',
'Estimate:' => 'Stimato:',
'Spent:' => 'Trascorso:',
- 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sotto-compito?',
+ 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sotto-task?',
'Remaining:' => 'Rimangono',
'hours' => 'ore',
'spent' => 'trascorse',
'estimated' => 'stimate',
- 'Sub-Tasks' => 'Sotto-compiti',
- 'Add a sub-task' => 'Aggiungere un sotto-compito',
+ 'Sub-Tasks' => 'Sotto-tasks',
+ 'Add a sub-task' => 'Aggiungi un sotto-task',
'Original estimate' => 'Stima originale',
- 'Create another sub-task' => 'Creare un altro sotto-compito',
- 'Time spent' => 'Tempo Trascorso',
- 'Edit a sub-task' => 'Modificare un sotto-compito',
- 'Remove a sub-task' => 'Cancellare un sotto-compito',
+ 'Create another sub-task' => 'Crea un altro sotto-task',
+ 'Time spent' => 'Tempo trascorso',
+ 'Edit a sub-task' => 'Modifica un sotto-task',
+ 'Remove a sub-task' => 'Cancella un sotto-task',
'The time must be a numeric value' => 'Il tempo deve essere un valore numerico',
'Todo' => 'Da fare',
'In progress' => 'In corso',
- 'Sub-task removed successfully.' => 'Sotto-compito cancellato correttamente.',
- 'Unable to remove this sub-task.' => 'Non si può cancellare questo sotto-compito.',
- 'Sub-task updated successfully.' => 'Sotto-compito aggiornato correttamente.',
- 'Unable to update your sub-task.' => 'Non si può aggiornare il tuo sotto-compito.',
- 'Unable to create your sub-task.' => 'Non si può creare il tuo sotto-compito.',
- 'Sub-task added successfully.' => 'Sotto-compito aggiunto correttamente.',
- 'Maximum size: ' => 'Dimensioni massime',
- 'Unable to upload the file.' => 'Non si può caricare il file.',
- 'Display another project' => 'Mostrare un altro progetto',
- 'Login with my Github Account' => 'Entrare col tuo account di Github',
- 'Link my Github Account' => 'Collegare il mio account Github',
- 'Unlink my Github Account' => 'Scollegare il mio account di Github',
+ 'Sub-task removed successfully.' => 'Sotto-task cancellato correttamente.',
+ 'Unable to remove this sub-task.' => 'Impossibile cancellare questo sotto-task.',
+ 'Sub-task updated successfully.' => 'Sotto-task aggiornato correttamente.',
+ 'Unable to update your sub-task.' => 'Impossibile aggiornare il tuo sotto-task.',
+ 'Unable to create your sub-task.' => 'Impossibile creare il tuo sotto-task.',
+ 'Sub-task added successfully.' => 'Sotto-task aggiunto correttamente.',
+ 'Maximum size: ' => 'Dimensioni massime: ',
+ 'Unable to upload the file.' => 'Impossibile caricare il file.',
+ 'Display another project' => 'Mostra un altro progetto',
'Created by %s' => 'Creato da %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Ultima modifica il %d/%m/%Y alle %H:%M',
- 'Tasks Export' => 'Esportazione di compiti',
- 'Tasks exportation for "%s"' => 'Esportazione di compiti per « %s »',
+ 'Tasks Export' => 'Export dei task',
+ 'Tasks exportation for "%s"' => 'Export dei task per "%s"',
'Start Date' => 'Data d\'inizio',
'End Date' => 'Data di fine',
- 'Execute' => 'Eseguire',
- 'Task Id' => 'Identificatore del compito',
+ 'Execute' => 'Esegui',
+ 'Task Id' => 'Id del task',
'Creator' => 'Creatore',
'Modification date' => 'Data di modifica',
'Completion date' => 'Data di termine',
@@ -352,235 +323,199 @@ return array(
'Project cloned successfully.' => 'Progetto clonato con successo.',
'Unable to clone this project.' => 'Impossibile clonare questo progetto',
'Enable email notifications' => 'Abilita le notifiche via email',
- 'Task position:' => 'Posizione del compito:',
- 'The task #%d have been opened.' => 'Il compito #%d è stato aperto.',
- 'The task #%d have been closed.' => 'Il compito #%d è stato chiuso.',
- 'Sub-task updated' => 'Sotto-compito aggiornato',
+ 'Task position:' => 'Posizione del task:',
+ 'The task #%d have been opened.' => 'Il task #%d è stato aperto.',
+ 'The task #%d have been closed.' => 'Il task #%d è stato chiuso.',
+ 'Sub-task updated' => 'Sotto-task aggiornato',
'Title:' => 'Titolo',
'Status:' => 'Stato',
- 'Assignee:' => 'Assegnatario',
- 'Time tracking:' => 'Gestione del tempo:',
- 'New sub-task' => 'Nuovo sotto-compito',
- 'New attachment added "%s"' => 'Nuovo allegato aggiunto « %s »',
+ 'Assignee:' => 'Assegnatario:',
+ // '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 comment posted by %s' => 'Nuovo commento aggiunto da "%s"',
'New attachment' => 'Nuovo allegato',
'New comment' => 'Nuovo commento',
- 'New subtask' => 'Nuovo sotto-compito',
- 'Subtask updated' => 'Sotto-compito aggiornato',
- 'Task updated' => 'Compito aggiornato',
- 'Task closed' => 'Compito chiuso',
- 'Task opened' => 'Compito aperto',
+ 'New subtask' => 'Nuovo sotto-task',
+ 'Subtask updated' => 'Sotto-task aggiornato',
+ 'Task updated' => 'Task aggiornato',
+ 'Task closed' => 'Task chiuso',
+ 'Task opened' => 'Task aperto',
'I want to receive notifications only for those projects:' => 'Vorrei ricevere le notifiche solo da questi progetti:',
- 'view the task on Kanboard' => 'vedi il compito su Kanboard',
+ 'view the task on Kanboard' => 'vedi il task su Kanboard',
'Public access' => 'Accesso pubblico',
- 'User management' => 'Gestione utenti',
- 'Active tasks' => 'Compiti attivi',
+ 'Active tasks' => 'Task attivi',
'Disable public access' => 'Disabilita l\'accesso pubblico',
'Enable public access' => 'Abilita l\'accesso pubblico',
'Public access disabled' => 'Accesso pubblico disattivato',
- 'Do you really want to disable this project: "%s"?' => 'Vuoi davvero disabilitare questo progetto: "%s"?',
- 'Do you really want to enable this project: "%s"?' => 'Vuoi davvero abilitare questo progetto: "%s"?',
+ 'Do you really want to disable this project: "%s"?' => 'Vuoi davvero disabilitare il seguente progetto: "%s"?',
+ 'Do you really want to enable this project: "%s"?' => 'Vuoi davvero abilitare il seguente progetto: "%s"?',
'Project activation' => 'Attivazione progetto',
- 'Move the task to another project' => 'Muovi il compito in un altro progetto',
- 'Move to another project' => 'Muovi in un altro progetto',
- 'Do you really want to duplicate this task?' => 'Vuoi davvero duplicare questo compito?',
- 'Duplicate a task' => 'Duplica il compito',
+ 'Move the task to another project' => 'Sposta il task in un altro progetto',
+ 'Move to another project' => 'Sposta in un altro progetto',
+ 'Do you really want to duplicate this task?' => 'Vuoi davvero duplicare questo task?',
+ 'Duplicate a task' => 'Duplica il task',
'External accounts' => 'Account esterni',
'Account type' => 'Tipo di account',
'Local' => 'Locale',
'Remote' => 'Remoto',
'Enabled' => 'Abilitato',
'Disabled' => 'Disabilitato',
- // 'Username:' => '',
+ 'Username:' => 'Nome utente:',
'Name:' => 'Nome:',
// 'Email:' => '',
'Notifications:' => 'Notifiche:',
'Notifications' => 'Notifiche',
- 'Group:' => 'Gruppo',
- 'Regular user' => 'Utente regolare',
'Account type:' => 'Tipo di account',
- 'Edit profile' => 'Modifica il profilo',
+ 'Edit profile' => 'Modifica profilo',
'Change password' => 'Cambia password',
'Password modification' => 'Modifica della password',
'External authentications' => 'Autenticazione esterna',
- 'Google Account' => 'Account Google',
- 'Github Account' => 'Account Github',
'Never connected.' => 'Mai connesso.',
- 'No account linked.' => 'Nessun account collegato.',
- 'Account linked.' => 'Account collegato.',
'No external authentication enabled.' => 'Nessuna autenticazione esterna abilitata.',
'Password modified successfully.' => 'Password modificata con successo.',
'Unable to change the password.' => 'Impossibile cambiare la password.',
- 'Change category for the task "%s"' => 'Cambia categoria per il compito "%s"',
+ 'Change category for the task "%s"' => 'Cambia categoria per il task "%s"',
'Change category' => 'Cambia categoria',
- '%s updated the task %s' => '%s ha aggiornato il compito %s',
- '%s opened the task %s' => '%s ha aperto il compito %s',
- '%s moved the task %s to the position #%d in the column "%s"' => '%s ha spostato il compito %s nella posizione #%d della colonna "%s"',
- '%s moved the task %s to the column "%s"' => '%s ha spostato il compito %s nella colonna "%s"',
- '%s created the task %s' => '%s ha creato il compito %s',
- '%s closed the task %s' => '%s ha chiuso il compito %s',
- '%s created a subtask for the task %s' => '%s ha creato un sotto-compito per il compito %s',
- '%s updated a subtask for the task %s' => '%s ha aggiornato un sotto-compito per il compito %s',
+ '%s updated the task %s' => '%s ha aggiornato il task %s',
+ '%s opened the task %s' => '%s ha aperto il task %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s ha spostato il task %s nella posizione #%d della colonna "%s"',
+ '%s moved the task %s to the column "%s"' => '%s ha spostato il task %s nella colonna "%s"',
+ '%s created the task %s' => '%s ha creato il task %s',
+ '%s closed the task %s' => '%s ha chiuso il task %s',
+ '%s created a subtask for the task %s' => '%s ha creato un sotto-task per il task %s',
+ '%s updated a subtask for the task %s' => '%s ha aggiornato un sotto-task per il task %s',
'Assigned to %s with an estimate of %s/%sh' => 'Assegnato a %s con una stima di %s/%sh',
'Not assigned, estimate of %sh' => 'Non assegnato, stima %sh',
- '%s updated a comment on the task %s' => '%s ha aggiornato un commento del compito %s',
- '%s commented the task %s' => '%s ha commentato il compito %s',
+ '%s updated a comment on the task %s' => '%s ha aggiornato un commento nel task %s',
+ '%s commented the task %s' => '%s ha commentato il task %s',
'%s\'s activity' => 'Attività di %s',
'RSS feed' => 'Feed RSS',
- '%s updated a comment on the task #%d' => '%s ha aggiornato un commento del compito #%d',
- '%s commented on the task #%d' => '%s ha commentato il compito #%d',
- '%s updated a subtask for the task #%d' => '%s ha aggiornato un sotto-compito del compito #%d',
- '%s created a subtask for the task #%d' => '%s ha creato un sotto-compito del compito #%d',
- '%s updated the task #%d' => '%s ha aggiornato il compito #%d',
- '%s created the task #%d' => '%s ha creato il compito #%d',
- '%s closed the task #%d' => '%s ha chiuso il compito #%d',
- '%s open the task #%d' => '%s ha aperto il compito #%d',
- '%s moved the task #%d to the column "%s"' => '%s ha spostato il compito #%d nella colonna "%s"',
- '%s moved the task #%d to the position %d in the column "%s"' => '%s ha spostato il compito #%d nella posizione %d della colonna "%s"',
+ '%s updated a comment on the task #%d' => '%s ha aggiornato un commento del task #%d',
+ '%s commented on the task #%d' => '%s ha commentato il task #%d',
+ '%s updated a subtask for the task #%d' => '%s ha aggiornato un sotto-task del task #%d',
+ '%s created a subtask for the task #%d' => '%s ha creato un sotto-task del task #%d',
+ '%s updated the task #%d' => '%s ha aggiornato il task #%d',
+ '%s created the task #%d' => '%s ha creato il task #%d',
+ '%s closed the task #%d' => '%s ha chiuso il task #%d',
+ '%s open the task #%d' => '%s ha aperto il task #%d',
+ '%s moved the task #%d to the column "%s"' => '%s ha spostato il task #%d nella colonna "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s ha spostato il task #%d nella posizione %d della colonna "%s"',
'Activity' => 'Attività',
'Default values are "%s"' => 'Valori di default "%s"',
'Default columns for new projects (Comma-separated)' => 'Colonne di default per i nuovi progetti (Separati da virgola)',
- 'Task assignee change' => 'Cambiare l\'assegnatario del compito',
- '%s change the assignee of the task #%d to %s' => '%s dai l\'assegnazione del compito #%d a %s',
- '%s changed the assignee of the task %s to %s' => '%s ha cambiato l\'assegnatario del compito %s a %s',
+ 'Task assignee change' => 'Cambia l\'assegnatario del task',
+ '%s change the assignee of the task #%d to %s' => '%s dai l\'assegnazione del task #%d a %s',
+ '%s changed the assignee of the task %s to %s' => '%s ha cambiato l\'assegnatario del task %s a %s',
'New password for the user "%s"' => 'Nuova password per l\'utente "%s"',
'Choose an event' => 'Scegli un evento',
- 'Github commit received' => 'Commit di Github ricevuto',
- 'Github issue opened' => 'Issue di Github ricevuto',
- 'Github issue closed' => 'Issue di Github chiusa',
- 'Github issue reopened' => 'Issue di Github riaperta',
- 'Github issue assignee change' => 'Assegnatario dell\'issue di Github cambiato',
- 'Github issue label change' => 'Etichetta dell\'issue di Github cambiata',
- 'Create a task from an external provider' => 'Crea un compito da un provider esterno',
+ 'Create a task from an external provider' => 'Crea un task da un provider esterno',
'Change the assignee based on an external username' => 'Cambia l\'assegnatario basandosi su un username esterno',
'Change the category based on an external label' => 'Cambia la categoria basandosi su un\'etichetta esterna',
- 'Reference' => 'Referenza',
- 'Reference: %s' => 'Referenza :%s',
+ 'Reference' => 'Riferimento',
'Label' => 'Etichetta',
// 'Database' => '',
- 'About' => 'Info',
+ 'About' => 'Informazioni',
'Database driver:' => 'Driver per Database',
'Board settings' => 'Impostazioni bacheca',
'URL and token' => 'URL e token',
'Webhook settings' => 'Impostazione Webhook',
- 'URL for task creation:' => 'URL per la creazione dei compiti:',
+ 'URL for task creation:' => 'URL per la creazione dei tasks:',
'Reset token' => 'Rigenera il token',
'API endpoint:' => 'Endpoint dell\'API:',
'Refresh interval for private board' => 'Intervallo di refresh per le bacheche private',
'Refresh interval for public board' => 'Intervallo di refresh per le bacheche pubbliche',
- 'Task highlight period' => 'Periodo di evidenza per il compito',
- 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (in secondi) per considerare un compito come modificato recentemente (0 per disabilitare, 2 giorni di default)',
+ 'Task highlight period' => 'Periodo di evidenza per il task',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (in secondi) per considerare un task come modificato recentemente (0 per disabilitare, 2 giorni di default)',
'Frequency in second (60 seconds by default)' => 'Frequenza in secondi (60 secondi di default)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequenza in secondi (0 secondi di default)',
'Application URL' => 'URL dell\'applicazione',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Esempio: http://example.kanboard.net/ (usato dalle notifiche email)',
- 'Token regenerated.' => 'Token rigenerato',
+ 'Token regenerated.' => 'Token rigenerato.',
'Date format' => 'Formato data',
'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-compito',
+ 'Type here to create a new sub-task' => 'Scrivi qui per creare un sotto-task',
'Add' => 'Aggiungi',
- 'Estimated time: %s hours' => 'Tempo stimato: %s ore',
- 'Time spent: %s hours' => 'Tempo trascorso: %s ore',
- 'Started on %B %e, %Y' => 'Avviato il %B %e, %Y',
'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 compiti',
+ 'My tasks' => 'I miei task',
'Activity stream' => 'Flusso di attività',
'Dashboard' => 'Bacheca',
'Confirmation' => 'Conferma',
'Allow everybody to access to this project' => 'Abilita tutti ad accedere a questo progetto',
- 'Everybody have access to this project.' => 'Tutti hanno accesso a questo progetto',
+ 'Everybody have access to this project.' => 'Tutti hanno accesso a questo progetto.',
// 'Webhooks' => '',
// 'API' => '',
- 'Github webhooks' => 'Webhooks di Github',
- 'Help on Github webhooks' => 'Guida ai Webhooks di Github',
'Create a comment from an external provider' => 'Crea un commit da un provider esterno',
- 'Github issue comment created' => 'Commento ad un Issue di Github creato',
- 'Project management' => 'Gestione del progetto',
+ 'Project management' => 'Gestione progetti',
'My projects' => 'I miei progetti',
'Columns' => 'Colonne',
- 'Task' => 'Compito',
- 'Your are not member of any project.' => 'Non sei membro di alcun progetto',
+ 'Task' => 'Task',
+ 'Your are not member of any project.' => 'Non sei membro di alcun progetto.',
'Percentage' => 'Percentuale',
- 'Number of tasks' => 'Numero di compiti',
- 'Task distribution' => 'Distribuzione dei compiti',
+ 'Number of tasks' => 'Numero di task',
+ 'Task distribution' => 'Distribuzione dei task',
'Reportings' => 'Rapporti',
- 'Task repartition for "%s"' => 'Ripartizione compiti per "%s"',
+ 'Task repartition for "%s"' => 'Ripartizione task per "%s"',
// 'Analytics' => '',
- 'Subtask' => 'Sotto-compiti',
- 'My subtasks' => 'I miei sotto-compiti',
+ 'Subtask' => 'Sotto-task',
+ 'My subtasks' => 'I miei sotto-task',
'User repartition' => 'Ripartizione per utente',
'User repartition for "%s"' => 'Ripartizione utente per "%s"',
'Clone this project' => 'Clona questo progetto',
'Column removed successfully.' => 'Colonna rimossa con successo',
- 'Github Issue' => 'Issue di Github',
'Not enough data to show the graph.' => 'Non ci sono abbastanza dati per visualizzare il grafico.',
'Previous' => 'Precendete',
'The id must be an integer' => 'L\'id deve essere un intero',
'The project id must be an integer' => 'L\'id del progetto deve essere un intero',
'The status must be an integer' => 'Lo status deve essere un intero',
- 'The subtask id is required' => 'L\'id del sotto-compito è necessario',
- 'The subtask id must be an integer' => 'L\'id del sotto-compito deve essere un intero',
- 'The task id is required' => 'Richiesto l\'id del compito',
- 'The task id must be an integer' => 'L\'id del compito deve essere un intero',
+ 'The subtask id is required' => 'L\'id del sotto-task è necessario',
+ 'The subtask id must be an integer' => 'L\'id del sotto-task deve essere un intero',
+ 'The task id is required' => 'Richiesto l\'id del task',
+ 'The task id must be an integer' => 'L\'id del task deve essere un intero',
'The user id must be an integer' => 'L\'id dell\'utente deve essere un intero',
'This value is required' => 'Questo valore è necessario',
'This value must be numeric' => 'Questo valore deve essere numerico',
- 'Unable to create this task.' => 'Impossibile creare questo compito',
+ 'Unable to create this task.' => 'Impossibile creare questo task',
'Cumulative flow diagram' => 'Diagramma di flusso cumulativo',
'Cumulative flow diagram for "%s"' => 'Diagramma di flusso comulativo per "%s"',
'Daily project summary' => 'Sommario giornaliero del progetto',
- 'Daily project summary export' => 'Esportazione del sommario giornaliero del progetto',
- 'Daily project summary export for "%s"' => 'Esportazione del sommario giornaliero del progetto per "%s"',
+ 'Daily project summary export' => 'Export del sommario giornaliero del progetto',
+ '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 compiti per colonna raggruppati per giorno',
+ '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 di default',
- 'Default swimlane' => 'Corsia di default',
- 'Do you really want to remove this swimlane: "%s"?' => 'Vuoi davvero rimuovere questa corsia: "%s"?',
+ 'Change default swimlane' => 'Cambia la corsia predefinita',
+ 'Default swimlane' => 'Corsia predefinita',
+ 'Do you really want to remove this swimlane: "%s"?' => 'Vuoi davvero rimuovere la seguente corsia: "%s"?',
'Inactive swimlanes' => 'Corsie inattive',
- 'Set project manager' => 'Imposta un manager del progetto',
- 'Set project member' => 'Imposta un membro del progetto',
'Remove a swimlane' => 'Rimuovi una corsia',
- 'Rename' => 'Rinomina',
- 'Show default swimlane' => 'Mostra le corsie di default',
+ 'Show default swimlane' => 'Mostra la corsia predefinita',
'Swimlane modification for the project "%s"' => 'Modifica corsia per il progetto "%s"',
- 'Swimlane not found.' => 'Corsia non trovata',
- 'Swimlane removed successfully.' => 'Corsia rimossa con successo',
+ 'Swimlane not found.' => 'Corsia non trovata.',
+ 'Swimlane removed successfully.' => 'Corsia rimossa con successo.',
'Swimlanes' => 'Corsie',
- 'Swimlane updated successfully.' => 'Corsia aggiornata con successo',
- 'The default swimlane have been updated successfully.' => 'La corsia di default è stata aggiornata con successo.',
- 'Unable to create your swimlane.' => 'Impossibile creare la sua corsia.',
+ 'Swimlane updated successfully.' => 'Corsia aggiornata con successo.',
+ 'The default swimlane have been updated successfully.' => 'La corsia predefinita è stata aggiornata con successo.',
'Unable to remove this swimlane.' => 'Impossibile rimuovere questa corsia.',
'Unable to update this swimlane.' => 'Impossibile aggiornare questa corsia.',
- 'Your swimlane have been created successfully.' => 'La sua corsia è stata creata con successo',
- 'Example: "Bug, Feature Request, Improvement"' => 'Esempio: "Bug, Richiesta di Funzioni, Migliorie"',
+ 'Your swimlane have been created successfully.' => 'La tua corsia è stata creata con successo',
+ 'Example: "Bug, Feature Request, Improvement"' => 'Esempi: "Bug, Richiesta di Funzioni, Migliorie"',
'Default categories for new projects (Comma-separated)' => 'Categorie di default per i progetti (Separati da virgola)',
- 'Gitlab commit received' => 'Commit ricevuto da Gitlab',
- 'Gitlab issue opened' => 'Issue di Gitlab aperta',
- 'Gitlab issue closed' => 'Issue di Gitlab chiusa',
- 'Gitlab webhooks' => 'Webhooks di Gitlab',
- 'Help on Gitlab webhooks' => 'Guida ai Webhooks di Gitlab',
'Integrations' => 'Integrazioni',
'Integration with third-party services' => 'Integrazione con servizi di terze parti',
- 'Role for this project' => 'Ruolo per questo progetto',
- 'Project manager' => 'Manager del progetto',
- 'Project member' => 'Membro del progetto',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un manager del progetto può cambiare le impostazioni del progetto ed avere più privilegi di un utente standard.',
- 'Gitlab Issue' => 'Issue di Gitlab',
- 'Subtask Id' => 'Id del sotto-compito',
- 'Subtasks' => 'Sotto-compiti',
- 'Subtasks Export' => 'Esporta sotto-compiti',
- 'Subtasks exportation for "%s"' => 'Esportazione dei sotto-compiti per "%s"',
- 'Task Title' => 'Titolo del compito',
+ 'Subtask Id' => 'Id del sotto-task',
+ 'Subtasks' => 'Sotto-task',
+ 'Subtasks Export' => 'Esporta i sotto-task',
+ 'Subtasks exportation for "%s"' => 'Export dei sotto-task per "%s"',
+ 'Task Title' => 'Titolo del task',
'Untitled' => 'Senza titolo',
'Application default' => 'Default dell\'applicazione',
'Language:' => 'Lingua',
@@ -594,44 +529,41 @@ return array(
'Moved to column %s' => 'Spostato sulla colonna "%s"',
'Change description' => 'Cambia descrizione',
'User dashboard' => 'Bacheca utente',
- 'Allow only one subtask in progress at the same time for a user' => 'Permetti un solo sotto-compito in progresso per utente nello stesso tempo',
+ 'Allow only one subtask in progress at the same time for a user' => 'Permetti un solo sotto-task in corso per utente alla volta',
'Edit column "%s"' => 'Modifica la colonna "%s"',
- 'Select the new status of the subtask: "%s"' => 'Selziona il nuovo status per il sotto-compito: "%s"',
- 'Subtask timesheet' => 'Timesheet del sotto-compito',
+ 'Select the new status of the subtask: "%s"' => 'Seleziona il nuovo status per il sotto-task: "%s"',
+ 'Subtask timesheet' => 'Timesheet del sotto-task',
'There is nothing to show.' => 'Nulla da mostrare.',
- 'Time Tracking' => 'Gestione del tempo',
- 'You already have one subtask in progress' => 'Hai già un sotto-compito in progresso',
+ // 'Time Tracking' => '',
+ 'You already have one subtask in progress' => 'Hai già un sotto-task in corso',
'Which parts of the project do you want to duplicate?' => 'Quali parti del progetto vuoi duplicare?',
- // 'Disallow login form' => '',
- 'Bitbucket commit received' => 'Commit ricevuto da Bitbucket',
- 'Bitbucket webhooks' => 'Webhooks di Bitbucket',
- 'Help on Bitbucket webhooks' => 'Guida ai Webhooks di Bitbucket',
+ 'Disallow login form' => 'Disabilita il form di login',
'Start' => 'Inizio',
'End' => 'Fine',
- 'Task age in days' => 'Anzianità del compito in giorni',
+ 'Task age in days' => 'Anzianità del task in giorni',
'Days in this column' => 'Giorni in questa colonna',
// '%dd' => '',
- 'Add a link' => 'Aggiungi un link',
- 'Add a new link' => 'Aggiungi un nuovo link',
- 'Do you really want to remove this link: "%s"?' => 'Vuoi davvero rimuovere questo link: "%s"?',
- 'Do you really want to remove this link with task #%d?' => 'Vuoi davvero rimuovere questo link dal compito #%d?',
+ '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?',
'Field required' => 'Campo necessario',
- 'Link added successfully.' => 'Link aggiunto con successo.',
- 'Link updated successfully.' => 'Linka aggiornato con successo.',
- 'Link removed successfully.' => 'Link rimosso con successo.',
- 'Link labels' => 'Etichette dei link',
- 'Link modification' => 'Modifica link',
- 'Links' => 'Link',
- 'Link settings' => 'Impostazioni link',
- // 'Opposite label' => '',
- 'Remove a link' => 'Rimuovi un link',
- 'Task\'s links' => 'Link del compito',
+ 'Link added successfully.' => 'Relazione aggiunta con successo.',
+ 'Link updated successfully.' => 'Relazione aggiornata con successo.',
+ 'Link removed successfully.' => 'Relazione rimosso con successo.',
+ 'Link labels' => 'Etichette delle relazioni',
+ 'Link modification' => 'Modifica relazione',
+ 'Links' => 'Relazioni',
+ 'Link settings' => 'Impostazioni relazioni',
+ 'Opposite label' => 'Etichetta contraria',
+ 'Remove a link' => 'Rimuovi una relazione',
+ 'Task\'s links' => 'Relazioni del task',
'The labels must be different' => 'Le etichette devono essere diverse',
- 'There is no link.' => 'Non c\'è alcun link',
- 'This label must be unique' => 'Questa etichetta deve essere unica',
- 'Unable to create your link.' => 'Impossibile creare il suo link.',
- 'Unable to update your link.' => 'Impossibile aggiornare il suo link.',
- 'Unable to remove this link.' => 'Impossibile rimuovere il suo link.',
+ 'There is no link.' => 'Nessuna relazione presente.',
+ 'This label must be unique' => 'Questa etichetta deve essere univoca',
+ 'Unable to create your link.' => 'Impossibile creare la relazione.',
+ 'Unable to update your link.' => 'Impossibile aggiornare la relazione.',
+ 'Unable to remove this link.' => 'Impossibile rimuovere la relazione.',
'relates to' => 'si riferisce a',
'blocks' => 'blocca',
'is blocked by' => 'è bloccato da',
@@ -643,32 +575,28 @@ return array(
'is a milestone of' => 'è una milestone di',
'fixes' => 'sistema',
'is fixed by' => 'è sistemato da',
- 'This task' => 'Questo compito',
+ 'This task' => 'Questo task',
// '<1h' => '',
// '%dh' => '',
- // '%b %e' => '',
- 'Expand tasks' => 'Espandi i compiti',
- 'Collapse tasks' => 'Minimizza i compiti',
- 'Expand/collapse tasks' => 'Espandi/Minimizza compiti',
- 'Close dialog box' => 'Chiudi dialog box',
+ 'Expand tasks' => 'Espandi i task',
+ 'Collapse tasks' => 'Minimizza i task',
+ 'Expand/collapse tasks' => 'Espandi/minimizza i task',
+ 'Close dialog box' => 'Chiudi la finestra di dialogo',
'Submit a form' => 'Invia i dati',
'Board view' => 'Vista bacheca',
'Keyboard shortcuts' => 'Scorciatoie da tastiera',
- 'Open board switcher' => 'Apri il selezionatore di bacheche',
+ 'Open board switcher' => 'Apri il selettore di bacheche',
'Application' => 'Applicazione',
- 'since %B %e, %Y at %k:%M %p' => 'dal %B %e, %Y alle %k:%M %p',
'Compact view' => 'Vista compatta',
'Horizontal scrolling' => 'Scrolling orizzontale',
'Compact/wide view' => 'Vista compatta/estesa',
'No results match:' => 'Nessun risultato trovato:',
'Currency' => 'Valuta',
- // 'Files' => '',
- 'Images' => 'Immagini',
'Private project' => 'Progetto privato',
'AUD - Australian Dollar' => 'AUD - Dollari Australiani',
'CAD - Canadian Dollar' => 'CAD - Dollari Canadesi',
'CHF - Swiss Francs' => 'CHF - Franchi Svizzeri',
- 'Custom Stylesheet' => 'CSS personalizzato',
+ 'Custom Stylesheet' => 'Foglio di stile personalizzato',
// 'download' => '',
// 'EUR - Euro' => '',
'GBP - British Pound' => 'GBP - Pound Inglesi',
@@ -678,15 +606,15 @@ return array(
'RSD - Serbian dinar' => 'RSD - Dinar Serbi',
'USD - US Dollar' => 'USD - Dollari Americani',
'Destination column' => 'Colonna destinazione',
- 'Move the task to another column when assigned to a user' => 'Sposta il compito in un\'altra colonna quando viene assegnato ad un utente',
- 'Move the task to another column when assignee is cleared' => 'Sposta il compito in un\'altra colonna quando l\'assegnatario cancellato',
+ 'Move the task to another column when assigned to a user' => 'Sposta il task in un\'altra colonna quando viene assegnato ad un utente',
+ 'Move the task to another column when assignee is cleared' => 'Sposta il task in un\'altra colonna quando l\'assegnatario viene cancellato',
'Source column' => 'Colonna sorgente',
'Transitions' => 'Transizioni',
'Executer' => 'Esecutore',
'Time spent in the column' => 'Tempo trascorso nella colonna',
- 'Task transitions' => 'Transizioni del compito',
- 'Task transitions export' => 'Esporta le transizioni del compito',
- 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Questo report contiene tutti i movimenti di colonna per ogni compito con le date, l\'utente ed il tempo trascorso per ogni transizione',
+ 'Task transitions' => 'Transizioni dei task',
+ 'Task transitions export' => 'Export delle transizioni dei task',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Questo report contiene tutti gli spotamenti di colonna per ogni task con le date, l\'utente ed il tempo trascorso per ogni transizione',
'Currency rates' => 'Tassi di cambio',
'Rate' => 'Cambio',
'Change reference currency' => 'Cambia la valuta di riferimento',
@@ -695,373 +623,529 @@ return array(
'The currency rate have been added successfully.' => 'Il tasso di cambio è stato aggiunto con successo.',
'Unable to add this currency rate.' => 'Impossibile aggiungere questo tasso di cambio.',
'Webhook URL' => 'URL Webhook',
- '%s remove the assignee of the task %s' => '%s rimuove l\'assegnatario del compito %s',
+ '%s remove the assignee of the task %s' => '%s rimuove l\'assegnatario del task %s',
'Enable Gravatar images' => 'Abilita immagini Gravatar',
'Information' => 'Informazioni',
- 'Check two factor authentication code' => 'Controlla il codice di autenticazione a due fattori',
- 'The two factor authentication code is not valid.' => 'Il codice di autenticazione a due fattori non è valido',
- 'The two factor authentication code is valid.' => 'Il codice di autenticazione a due fattori è valido',
+ 'Check two factor authentication code' => 'Controlla il codice di autenticazione "two-factor"',
+ 'The two factor authentication code is not valid.' => 'Il codice di autenticazione "two-factor" non è valido',
+ 'The two factor authentication code is valid.' => 'Il codice di autenticazione "two-factor" è valido',
'Code' => 'Codice',
- 'Two factor authentication' => 'Autenticazione a due fattori',
- 'Enable/disable two factor authentication' => 'Abilita/disabilita autenticazione a due fattori',
+ 'Two factor authentication' => 'Autenticazione "two-factor"',
'This QR code contains the key URI: ' => 'Questo QR code contiene l\'URI: ',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salva la chiave privata nel tuo software TOTP (per esempio Google Authenticator oppure FreeOTP).',
'Check my code' => 'Controlla il mio codice',
'Secret key: ' => 'Chiave privata:',
'Test your device' => 'Testa il tuo dispositivo',
- 'Assign a color when the task is moved to a specific column' => 'Assegna un colore quando il compito viene spostato in una colonna specifica',
- // '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
- // 'Burndown chart for "%s"' => '',
- // 'Burndown chart' => '',
- // 'This chart show the task complexity over the time (Work Remaining).' => '',
- // 'Screenshot taken %s' => '',
- // 'Add a screenshot' => '',
- // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
- // 'Screenshot uploaded successfully.' => '',
- // 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
- // 'Identifier' => '',
- // 'Disable two factor authentication' => '',
- // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
- // 'Edit link' => '',
- // 'Start to type task title...' => '',
- // 'A task cannot be linked to itself' => '',
- // 'The exact same link already exists' => '',
- // 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
- // 'Score' => '',
- // 'The identifier must be unique' => '',
- // 'This linked task id doesn\'t exists' => '',
- // 'This value must be alphanumeric' => '',
- // 'Edit recurrence' => '',
- // 'Generate recurrent task' => '',
- // 'Trigger to generate recurrent task' => '',
- // 'Factor to calculate new due date' => '',
- // 'Timeframe to calculate new due date' => '',
- // 'Base date to calculate new due date' => '',
- // 'Action date' => '',
- // 'Base date to calculate new due date: ' => '',
- // 'This task has created this child task: ' => '',
- // 'Day(s)' => '',
- // 'Existing due date' => '',
- // 'Factor to calculate new due date: ' => '',
- // 'Month(s)' => '',
- // 'Recurrence' => '',
- // 'This task has been created by: ' => '',
- // 'Recurrent task has been generated:' => '',
- // 'Timeframe to calculate new due date: ' => '',
- // 'Trigger to generate recurrent task: ' => '',
- // 'When task is closed' => '',
- // 'When task is moved from first column' => '',
- // 'When task is moved to last column' => '',
- // 'Year(s)' => '',
- // 'Calendar settings' => '',
- // 'Project calendar view' => '',
- // 'Project settings' => '',
- // 'Show subtasks based on the time tracking' => '',
- // 'Show tasks based on the creation date' => '',
- // 'Show tasks based on the start date' => '',
- // 'Subtasks time tracking' => '',
- // 'User calendar view' => '',
- // 'Automatically update the start date' => '',
- // 'iCal feed' => '',
- // 'Preferences' => '',
- // 'Security' => '',
- // 'Two factor authentication disabled' => '',
- // 'Two factor authentication enabled' => '',
- // 'Unable to update this user.' => '',
- // 'There is no user management for private projects.' => '',
- // 'User that will receive the email' => '',
- // 'Email subject' => '',
- // 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
- // 'Add a comment log when moving the task between columns' => '',
- // 'Move the task to another column when the category is changed' => '',
- // 'Send a task by email to someone' => '',
- // 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
- // 'Column change' => '',
- // 'Position change' => '',
- // 'Swimlane change' => '',
- // 'Assignee change' => '',
- // '[%s] Overdue tasks' => '',
- // 'Notification' => '',
- // '%s moved the task #%d to the first swimlane' => '',
- // '%s moved the task #%d to the swimlane "%s"' => '',
- // 'Swimlane' => '',
+ 'Assign a color when the task is moved to a specific column' => 'Assegna un colore quando il task viene spostato in una colonna specifica',
+ '%s via Kanboard' => '%s tramite Kanboard',
+ 'Burndown chart for "%s"' => 'Grafico Burndown per "%s"',
+ 'Burndown chart' => 'Grafico Burndown',
+ 'This chart show the task complexity over the time (Work Remaining).' => 'Questo grafico mostra la complessità dei task nel tempo (Lavoro residuo).',
+ 'Screenshot taken %s' => 'Schermata catturata %s',
+ 'Add a screenshot' => 'Aggiungi una schermata',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Cattura una schermata e premi CTRL+V o ⌘+V per incollarla qui.',
+ 'Screenshot uploaded successfully.' => 'Schermata caricata correttamente.',
+ 'SEK - Swedish Krona' => 'SEK - Corona svedese',
+ 'Identifier' => 'Identificatore',
+ 'Disable two factor authentication' => 'Disabilita l\'autenticazione "two-factor"',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Vuoi davvero disabilitare l\'autenticazione "two-factor" per questo utente: "%s"?',
+ 'Edit link' => 'Modifica relazione',
+ 'Start to type task title...' => 'Inzia a digitare il titolo di un task...',
+ 'A task cannot be linked to itself' => 'Un task non può essere correlato a se stesso',
+ 'The exact same link already exists' => 'La stessa relazione risulta già esistente',
+ 'Recurrent task is scheduled to be generated' => 'Il task ricorrente è pianificato per essere generato',
+ 'Score' => 'Punteggio',
+ 'The identifier must be unique' => 'L\'identificatore deve essere univoco',
+ 'This linked task id doesn\'t exists' => 'L\'id del task correlato non esiste',
+ 'This value must be alphanumeric' => 'Questo valore deve essere alfanumerico',
+ 'Edit recurrence' => 'Modifica ricorrenza',
+ 'Generate recurrent task' => 'Genera task ricorrente',
+ 'Trigger to generate recurrent task' => 'Trigger per generare il task ricorrente',
+ 'Factor to calculate new due date' => 'Fattore numerico per calcolare la nuova data di scadenza',
+ 'Timeframe to calculate new due date' => 'Lasso temporale per calcolare la nuova data di scadenza',
+ 'Base date to calculate new due date' => 'Data di partenza per calcolare la nuova data di scadenza',
+ 'Action date' => 'Data di azione (action date)',
+ 'Base date to calculate new due date: ' => 'Data di base per calcolare la nuova data di scadenza: ',
+ 'This task has created this child task: ' => 'Questo task ha creato il seguente task figlio: ',
+ 'Day(s)' => 'Giorno/i',
+ 'Existing due date' => 'Data di scadenza esistente',
+ 'Factor to calculate new due date: ' => 'Fattore numerico per calcolare la nuova data di scadenza: ',
+ 'Month(s)' => 'Mese/i',
+ 'Recurrence' => 'Ricorrenza',
+ 'This task has been created by: ' => 'Questo task è stato creato da: ',
+ 'Recurrent task has been generated:' => 'Il task ricorrente è stato generato:',
+ 'Timeframe to calculate new due date: ' => 'Lasso temporale per calcolare la nuova data di scadenza: ',
+ 'Trigger to generate recurrent task: ' => 'Trigger per generare il task ricorrente: ',
+ 'When task is closed' => 'Quando un task è chiuso',
+ 'When task is moved from first column' => 'Quando un task è spostato dalla prima colonna',
+ 'When task is moved to last column' => 'Quando un task è spostato nell\'ultima colonna',
+ 'Year(s)' => 'Anno/i',
+ 'Calendar settings' => 'Impostazioni del calendario',
+ 'Project calendar view' => 'Vista di progetto a calendario',
+ 'Project settings' => 'Impostazioni di progetto',
+ 'Show subtasks based on the time tracking' => 'Mostra i sotto-task in base al time tracking',
+ 'Show tasks based on the creation date' => 'Mostra i task in base alla data di creazione',
+ 'Show tasks based on the start date' => 'Mostra i task in base alla data di inzio',
+ 'Subtasks time tracking' => 'Time tracking per i sotto-task',
+ 'User calendar view' => 'Vista utente a calendario',
+ 'Automatically update the start date' => 'Aggiorna automaticamente la data di inzio',
+ 'iCal feed' => 'feed iCal',
+ 'Preferences' => 'Preferenze',
+ 'Security' => 'Sicurezza',
+ 'Two factor authentication disabled' => 'Two factor authentication disabilitata',
+ 'Two factor authentication enabled' => 'Two factor authentication abilitata',
+ 'Unable to update this user.' => 'Impossibile aggiornare questo utente.',
+ 'There is no user management for private projects.' => 'Non è prevista la gestione di utenti per i progetti privati.',
+ 'User that will receive the email' => 'Utente che riceverà l\'email',
+ 'Email subject' => 'Soggetto dell\'email',
+ 'Date' => 'Data',
+ 'Add a comment log when moving the task between columns' => 'Aggiungi un log quando si sposta un task tra colonne',
+ 'Move the task to another column when the category is changed' => 'Sposta il task in un\'altra colonna quando la categoria viene modificata',
+ 'Send a task by email to someone' => 'Invia un task via email a qualcuno',
+ 'Reopen a task' => 'Riapri un task',
+ 'Column change' => 'Cambio di colonna',
+ 'Position change' => 'Cambio di posizione',
+ 'Swimlane change' => 'Cambio di corsia',
+ 'Assignee change' => 'Cambio assegnatario',
+ '[%s] Overdue tasks' => '[%s] Task scaduti',
+ 'Notification' => 'Notifica',
+ '%s moved the task #%d to the first swimlane' => '%s ha spostato il task #%d nella prima corsia',
+ '%s moved the task #%d to the swimlane "%s"' => '%s ha spostato il task #%d nella corsia "%s"',
+ 'Swimlane' => 'Corsia',
// 'Gravatar' => '',
- // '%s moved the task %s to the first swimlane' => '',
- // '%s moved the task %s to the swimlane "%s"' => '',
- // 'This report contains all subtasks information for the given date range.' => '',
- // 'This report contains all tasks information for the given date range.' => '',
- // 'Project activities for %s' => '',
- // 'view the board on Kanboard' => '',
- // 'The task have been moved to the first swimlane' => '',
- // 'The task have been moved to another swimlane:' => '',
- // 'Overdue tasks for the project "%s"' => '',
- // 'New title: %s' => '',
- // 'The task is not assigned anymore' => '',
- // 'New assignee: %s' => '',
- // 'There is no category now' => '',
- // 'New category: %s' => '',
- // 'New color: %s' => '',
- // 'New complexity: %d' => '',
- // 'The due date have been removed' => '',
- // 'There is no description anymore' => '',
- // 'Recurrence settings have been modified' => '',
- // 'Time spent changed: %sh' => '',
- // 'Time estimated changed: %sh' => '',
- // 'The field "%s" have been updated' => '',
- // 'The description have been modified' => '',
- // 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
- // 'I want to receive notifications for:' => '',
- // 'All tasks' => '',
- // 'Only for tasks assigned to me' => '',
- // 'Only for tasks created by me' => '',
- // 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
+ '%s moved the task %s to the first swimlane' => '%s ha spostato il task %s nella prima corsia',
+ '%s moved the task %s to the swimlane "%s"' => '%s ha spostato il task %s nella corsia %s',
+ 'This report contains all subtasks information for the given date range.' => 'Questo report contiente tutte le informazioni sui sotto-task nell\'arco temporale indicato.',
+ 'This report contains all tasks information for the given date range.' => 'Questo report contiente tutte le informazioni sui task nell\'arco temporale indicato.',
+ 'Project activities for %s' => 'Attività di progetto per %s',
+ 'view the board on Kanboard' => 'guarda la bacheca su Kanboard',
+ 'The task have been moved to the first swimlane' => 'Il task è stato spostato nella prima corsia',
+ 'The task have been moved to another swimlane:' => 'Il task è stato spostato in un\'altra corsia:',
+ 'Overdue tasks for the project "%s"' => 'Task scaduti per il progetto "%s"',
+ 'New title: %s' => 'Nuovo titolo: %s',
+ 'The task is not assigned anymore' => 'Il task non è più assegnato a nessuno',
+ 'New assignee: %s' => 'Nuovo assegnatario: %s',
+ 'There is no category now' => 'Non è presente più nessuna categoria',
+ 'New category: %s' => 'Nuova categoria: %s',
+ 'New color: %s' => 'Nuovo colorei: %s',
+ 'New complexity: %d' => 'Nuova complessità: %d',
+ 'The due date have been removed' => 'La data di scadenza è stata rimossa',
+ 'There is no description anymore' => 'Non è presente più alcuna descrizione.',
+ 'Recurrence settings have been modified' => 'Le impostazioni di ricorrenza sono state modificate',
+ 'Time spent changed: %sh' => 'Tempo trascorso modificato: %sh',
+ 'Time estimated changed: %sh' => 'Tempo stimato modificato: %sh',
+ 'The field "%s" have been updated' => 'Il campo %s è stato aggiornato',
+ 'The description have been modified' => 'La descrizione è stata modificata',
+ 'Do you really want to close the task "%s" as well as all subtasks?' => 'Vuoi veramente chiudere il task "%s" e i relativi sotto-task?',
+ 'I want to receive notifications for:' => 'Voglio ricevere le notifiche per:',
+ 'All tasks' => 'Tutti i task',
+ 'Only for tasks assigned to me' => 'Solo per i task assegnati a me',
+ 'Only for tasks created by me' => 'Solo per i task creati da me',
+ 'Only for tasks created by me and assigned to me' => 'Solo per i task creati da me e assegnati a me',
// '%%Y-%%m-%%d' => '',
- // 'Total for all columns' => '',
- // 'You need at least 2 days of data to show the chart.' => '',
+ 'Total for all columns' => 'Totale per tutte le colonne',
+ 'You need at least 2 days of data to show the chart.' => 'Hai bisogno di almeno 2 giorni di dati per mostrare il grafico.',
// '<15m' => '',
// '<30m' => '',
- // 'Stop timer' => '',
- // 'Start timer' => '',
- // 'Add project member' => '',
- // 'Enable notifications' => '',
- // 'My activity stream' => '',
- // 'My calendar' => '',
- // 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
- // 'Reset filters' => '',
- // 'My tasks due tomorrow' => '',
- // 'Tasks due today' => '',
- // 'Tasks due tomorrow' => '',
- // 'Tasks due yesterday' => '',
- // 'Closed tasks' => '',
- // 'Open tasks' => '',
- // 'Not assigned' => '',
- // 'View advanced search syntax' => '',
- // 'Overview' => '',
- // '%b %e %Y' => '',
+ 'Stop timer' => 'Ferma il timer',
+ '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 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',
+ 'Tasks due tomorrow' => 'Task da completare per domani',
+ 'Tasks due yesterday' => 'Task da completare ieri',
+ 'Closed tasks' => 'Task chiusi',
+ 'Open tasks' => 'Task aperti',
+ 'Not assigned' => 'Non assegnato',
+ 'View advanced search syntax' => 'Visualizza la sintassi di ricerca avanzata',
+ 'Overview' => 'Panoramica',
// 'Board/Calendar/List view' => '',
- // 'Switch to the board view' => '',
- // 'Switch to the calendar view' => '',
- // 'Switch to the list view' => '',
- // 'Go to the search/filter box' => '',
- // 'There is no activity yet.' => '',
- // 'No tasks found.' => '',
- // 'Keyboard shortcut: "%s"' => '',
- // 'List' => '',
- // 'Filter' => '',
- // 'Advanced search' => '',
- // 'Example of query: ' => '',
- // 'Search by project: ' => '',
- // 'Search by column: ' => '',
- // 'Search by assignee: ' => '',
- // 'Search by color: ' => '',
- // 'Search by category: ' => '',
- // 'Search by description: ' => '',
- // 'Search by due date: ' => '',
- // 'Lead and Cycle time for "%s"' => '',
- // 'Average time spent into each column for "%s"' => '',
- // 'Average time spent into each column' => '',
- // 'Average time spent' => '',
- // 'This chart show the average time spent into each column for the last %d tasks.' => '',
- // 'Average Lead and Cycle time' => '',
- // 'Average lead time: ' => '',
- // 'Average cycle time: ' => '',
- // 'Cycle Time' => '',
- // 'Lead Time' => '',
- // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
- // 'Average time into each column' => '',
- // 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
- // 'Lead time: ' => '',
- // 'Cycle time: ' => '',
- // 'Time spent into each column' => '',
- // 'The lead time is the duration between the task creation and the completion.' => '',
- // 'The cycle time is the duration between the start date and the completion.' => '',
- // 'If the task is not closed the current time is used instead of the completion date.' => '',
- // 'Set automatically the start date' => '',
- // 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
- // 'Remote user' => '',
- // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
- // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
- // 'New remote user' => '',
- // 'New local user' => '',
- // 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
- // 'This feature does not work with all browsers.' => '',
- // 'There is no destination project available.' => '',
- // 'Trigger automatically subtask time tracking' => '',
- // 'Include closed tasks in the cumulative flow diagram' => '',
- // 'Current swimlane: %s' => '',
- // 'Current column: %s' => '',
- // 'Current category: %s' => '',
- // 'no category' => '',
- // 'Current assignee: %s' => '',
- // 'not assigned' => '',
- // 'Author:' => '',
- // 'contributors' => '',
- // 'License:' => '',
- // 'License' => '',
- // 'Project Administrator' => '',
- // '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' => '',
- // 'Managers' => '',
- // 'Members' => '',
- // 'Shared project' => '',
- // 'Project managers' => '',
- // 'Project members' => '',
- // 'Gantt chart for all projects' => '',
- // 'Projects list' => '',
- // 'Gantt chart for this project' => '',
- // 'Project board' => '',
- // 'End date:' => '',
- // 'There is no start date or end date for this project.' => '',
- // 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
- // 'Link type' => '',
- // 'Change task color when using a specific task link' => '',
- // 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
+ 'Switch to the board view' => 'Passa alla vista "bacheca"',
+ 'Switch to the calendar view' => 'Passa alla vista "calendario"',
+ 'Switch to the list view' => 'Passa alla vista "elenco"',
+ 'Go to the search/filter box' => 'Vai alla casella di ricerca/filtro',
+ 'There is no activity yet.' => 'Non è presente ancora nessuna attività.',
+ 'No tasks found.' => 'Nessun task trovato.',
+ 'Keyboard shortcut: "%s"' => 'Scorciatoia da tastiera: "%s"',
+ 'List' => 'Lista',
+ 'Filter' => 'Filtro',
+ 'Advanced search' => 'Riceca avanzata',
+ 'Example of query: ' => 'Esempio di query: ',
+ 'Search by project: ' => 'Ricerca per progetto: ',
+ 'Search by column: ' => 'Ricerca per colonna: ',
+ 'Search by assignee: ' => 'Ricerca per assegnatario: ',
+ 'Search by color: ' => 'Ricerca per colore: ',
+ 'Search by category: ' => 'Ricerca per categoria: ',
+ 'Search by description: ' => 'Ricerca per descrizione: ',
+ 'Search by due date: ' => 'Ricerca per data di scadenza: ',
+ 'Lead and Cycle time for "%s"' => 'Tempo di consegna (Lead Time) e lavorazione (Cycle Time) per "%s"',
+ 'Average time spent into each column for "%s"' => 'Tempo medio trascorso in ogni colonna per "%s"',
+ 'Average time spent into each column' => 'Tempo medio trascorso in ogni colonna',
+ 'Average time spent' => 'Tempo medio trascorso',
+ 'This chart show the average time spent into each column for the last %d tasks.' => 'Questo grafico mostra il tempo medio trascorso in ogni colonna per gli ultimi %d task.',
+ 'Average Lead and Cycle time' => 'Tempo medio di consegna (Lead Time) e lavorazione (Cycle Time)',
+ 'Average lead time: ' => 'Tempo medio di consegna (Lead Time): ',
+ 'Average cycle time: ' => 'Tempo medio di lavorazione (Cycle Time): ',
+ 'Cycle Time' => 'Tempo di lavorazione (Cycle Time)',
+ 'Lead Time' => 'Tempo di consegna (Lead Time)',
+ 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Questo grafico mostra i tempi medi di consegna (Lead Time) e lavorazione (Cycle Time) per gli ultimi %d task.',
+ 'Average time into each column' => 'Tempo medio in ogni colonna',
+ 'Lead and cycle time' => 'Tempo di consegna e lavorazione',
+ 'Lead time: ' => 'Tempo di consegna (Lead Time): ',
+ 'Cycle time: ' => 'Tempo di lavorazione (Cycle Time): ',
+ 'Time spent into each column' => 'Tempo trascorso in ogni colonna',
+ 'The lead time is the duration between the task creation and the completion.' => 'Il tempo di consegna (Lead Time) è la durata tra la creazione di un task ed il suo completamento.',
+ 'The cycle time is the duration between the start date and the completion.' => 'Il tempo di lavorazione (Cycle Time) è la durata tra la data di inzio della lavorazione di un task ed il suo completamento.',
+ 'If the task is not closed the current time is used instead of the completion date.' => 'Se il task non è chiuso sarà usata la data attuale invece della data di completamento.',
+ 'Set automatically the start date' => 'Imposta automaticamente la data di inzio',
+ 'Edit Authentication' => 'Modifica Autenticazione',
+ 'Remote user' => 'Utente remoto',
+ 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'La password degli utenti remoti (ad esempio: LDAP, account Google e Github) non è salvata nel database di Kanboard',
+ 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se imposti l\'opzione "Disabilita il form di login", le credenzali inserite nella saranno ignorate.',
+ 'New remote user' => 'Nuovo utente remoto',
+ 'New local user' => 'Nuovo utente locale',
+ 'Default task color' => 'Colore predefinito dei task',
+ 'This feature does not work with all browsers.' => 'Questa feature non funziona con tutti i browser.',
+ 'There is no destination project available.' => 'Non ci sono progetti disponbili come destinazione.',
+ 'Trigger automatically subtask time tracking' => 'Attiva automaticamente il time-tracking per i sotto-task',
+ 'Include closed tasks in the cumulative flow diagram' => 'Includi i task chiusi nel diagramma di flusso cumulativo',
+ 'Current swimlane: %s' => 'Corsia attuale: %s',
+ 'Current column: %s' => 'Colonna attuale: %s',
+ 'Current category: %s' => 'Categoria attuale: %s',
+ 'no category' => 'nessuna categoria',
+ 'Current assignee: %s' => 'Assegnatario attuale: %s',
+ 'not assigned' => 'non assegnato',
+ 'Author:' => 'Autore',
+ 'contributors' => 'contributori',
+ 'License:' => 'Licenza:',
+ 'License' => 'Licenza',
+ 'Enter the text below' => 'Inserisci il testo qui sotto',
+ 'Gantt chart for %s' => 'Grafico Gantt per %s',
+ 'Sort by position' => 'Ordina per posizione',
+ 'Sort by date' => 'Ordina per data',
+ 'Add task' => 'Aggiungi task',
+ 'Start date:' => 'Data di inizio:',
+ 'Due date:' => 'Data di completamento:',
+ 'There is no start date or due date for this task.' => 'Nessuna data di inzio o di scadenza per questo task.',
+ 'Moving or resizing a task will change the start and due date of the task.' => 'Spostando o ridimensionado un task ne modifca la data di inzio e di scadenza.',
+ 'There is no task in your project.' => 'Non ci sono task nel tuo progetto.',
+ 'Gantt chart' => 'Grafici Gantt',
+ 'People who are project managers' => 'Persone che sono manager di progetto',
+ 'People who are project members' => 'Persone che sono membri di progetto',
+ 'NOK - Norwegian Krone' => 'NOK - Corone norvegesi',
+ 'Show this column' => 'Mostra questa colonna',
+ 'Hide this column' => 'Nascondi questa colonna',
+ 'open file' => 'apri file',
+ 'End date' => 'Data di fine',
+ 'Users overview' => 'Panoramica utenti',
+ 'Members' => 'Membri',
+ 'Shared project' => 'Progetto condiviso',
+ 'Project managers' => 'Manager del 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',
+ 'Project board' => 'Bacheca del progetto',
+ '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' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
- // 'Documentation: %s' => '',
- // 'Switch to the Gantt chart view' => '',
- // 'Reset the search/filter box' => '',
- // 'Documentation' => '',
- // 'Table of contents' => '',
- // 'Gantt' => '',
- // 'Help with project permissions' => '',
- // '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.' => '',
+ 'Documentation: %s' => 'Documentazione: %s',
+ 'Switch to the Gantt chart view' => 'Passa alla vista Grafico Gantt',
+ 'Reset the search/filter box' => 'Resetta la riceca/filtro',
+ 'Documentation' => 'Documentazione',
+ 'Table of contents' => 'Indice dei contenuti',
+ 'Gantt' => 'Gantt',
+ 'Author' => 'Autore',
+ 'Version' => 'Versione',
+ 'Plugins' => 'Plugin',
+ 'There is no plugin loaded.' => 'Nessun plugin è stato caricato.',
+ 'Set maximum column height' => 'Imposta l\'altezza massima della colonna',
+ 'Remove maximum column height' => 'Rimuovi l\'altezza massima della colonna',
+ 'My notifications' => 'Le mie notifiche',
+ 'Custom filters' => 'Filtri personalizzati',
+ 'Your custom filter have been created successfully.' => 'Il filtro personalizzato è stato creato con successo.',
+ 'Unable to create your custom filter.' => 'Impossibile creare il filtro personalizzato.',
+ 'Custom filter removed successfully.' => 'Filtro personalizzato rimosso con successo.',
+ 'Unable to remove this custom filter.' => 'Impossibile rimuovere questo filtro personalizzato',
+ 'Edit custom filter' => 'Modifica il filtro personalizzato',
+ 'Your custom filter have been updated successfully.' => 'Il filtro personalizzato è stato aggiornato con successo.',
+ 'Unable to update custom filter.' => 'Impossibile aggiornare il filtro personalizzato.',
// 'Web' => '',
- // 'New attachment on task #%d: %s' => '',
- // 'New comment on task #%d' => '',
- // 'Comment updated on task #%d' => '',
- // 'New subtask on task #%d' => '',
- // 'Subtask updated on task #%d' => '',
- // 'New task #%d: %s' => '',
- // 'Task updated #%d' => '',
- // 'Task #%d closed' => '',
- // 'Task #%d opened' => '',
- // 'Column changed for task #%d' => '',
- // 'New position for task #%d' => '',
- // 'Swimlane changed for task #%d' => '',
- // 'Assignee changed on task #%d' => '',
- // '%d overdue tasks' => '',
- // 'Task #%d is overdue' => '',
- // 'No new notifications.' => '',
- // 'Mark all as read' => '',
- // 'Mark as read' => '',
- // 'Total number of tasks in this column across all swimlanes' => '',
- // 'Collapse swimlane' => '',
- // 'Expand swimlane' => '',
- // 'Add a new filter' => '',
- // 'Share with all project members' => '',
- // 'Shared' => '',
- // 'Owner' => '',
- // 'Unread notifications' => '',
- // 'My filters' => '',
- // 'Notification methods:' => '',
- // 'Import tasks from CSV file' => '',
- // 'Unable to read your file' => '',
- // '%d task(s) have been imported successfully.' => '',
- // 'Nothing have been imported!' => '',
- // 'Import users from CSV file' => '',
- // '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
- // '%s attached a file to the task #%d' => '',
- // 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
+ 'New attachment on task #%d: %s' => 'Nuovo allegato nel task #%d: %s',
+ 'New comment on task #%d' => 'Nuovo commento nel task #%d',
+ 'Comment updated on task #%d' => 'Commento aggiornato nel task #%d',
+ 'New subtask on task #%d' => 'Nuovo sotto-task nel task #%d',
+ 'Subtask updated on task #%d' => 'Sotto-task aggiornato nel task #%d',
+ 'New task #%d: %s' => 'Nuovo task #%d: %s',
+ 'Task updated #%d' => 'Task #%d aggiornato',
+ 'Task #%d closed' => 'Task #%d chiuso',
+ 'Task #%d opened' => 'Task #%d aperto',
+ 'Column changed for task #%d' => 'Colonna modificata per il task #%d',
+ 'New position for task #%d' => 'Nuova posizione per il task #%d',
+ 'Swimlane changed for task #%d' => 'Corsia modificata per il task #%d',
+ 'Assignee changed on task #%d' => 'Assegnatario modificato per il task #%d',
+ '%d overdue tasks' => '%d task scaduti',
+ 'Task #%d is overdue' => 'Il task #%d è scaduto',
+ 'No new notifications.' => 'Nessuna nuova notifica.',
+ 'Mark all as read' => 'Segna tutti come letti',
+ 'Mark as read' => 'Segna come letto',
+ 'Total number of tasks in this column across all swimlanes' => 'Numero totale di task in questa colonna per tutte le corsie',
+ 'Collapse swimlane' => 'Minimizza corsia',
+ 'Expand swimlane' => 'Espandi corsia',
+ 'Add a new filter' => 'Aggiungi un nuovo filtro',
+ 'Share with all project members' => 'Condividi con tutti i membri del progetto',
+ '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',
+ '%d task(s) have been imported successfully.' => '%d task sono stati importati con sucesso.',
+ 'Nothing have been imported!' => 'Non è stato importato nulla!',
+ 'Import users from CSV file' => 'Importa utenti da file CSV',
+ '%d user(s) have been imported successfully.' => '%d utenti importati con successo.',
+ 'Comma' => 'Virgola',
+ 'Semi-colon' => 'Punto e virgola',
+ 'Tab' => 'Tabulazione',
+ 'Vertical bar' => 'Barra verticale',
+ 'Double Quote' => 'Apice singolo',
+ 'Single Quote' => 'Doppio apice',
+ '%s attached a file to the task #%d' => '%s ha allegato un file al task #%d',
+ 'There is no column or swimlane activated in your project!' => 'Non ci sono colonne o corsie attive all\'interno del tuo progetto!',
+ 'Append filter (instead of replacement)' => 'Aggiungi filtro (anzichè sostituirlo)',
+ 'Append/Replace' => 'Aggiungi/Sostituisci',
+ 'Append' => 'Aggiungi',
+ 'Replace' => 'Sostituisci',
+ 'Import' => 'Importa',
+ 'change sorting' => 'cambia ordinamento',
+ 'Tasks Importation' => 'Importazione task',
+ 'Delimiter' => 'Delimitatore',
// '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' => '',
+ 'CSV File' => 'File CSV',
+ 'Instructions' => 'Istruzioni',
+ 'Your file must use the predefined CSV format' => 'Il file deve rispettare il formato CSV predefinito',
+ 'Your file must be encoded in UTF-8' => 'Il file deve essere codificato in formato UTF-8',
+ 'The first row must be the header' => 'La prima riga deve contenere l\'intestazione',
+ 'Duplicates are not verified for you' => 'Le righe duplicate non verranno controllate',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'La data di scadenza deve usare il formato ISO: YYYY-MM-DD',
+ 'Download CSV template' => 'Scarica il template CSV',
+ 'No external integration registered.' => 'Nessuna integrazione esterna presente.',
+ 'Duplicates are not imported' => 'I duplicati non veranno importati',
+ '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',
+ 'Assign automatically a category based on a link' => 'Assegna automaticamente una categoria sulla base di una relazione',
+ // 'BAM - Konvertible Mark' => '',
+ 'Assignee Username' => 'Nome utente dell\'assegnatario',
+ 'Assignee Name' => 'Nome dell\'assegnatario',
+ 'Groups' => 'Gruppi',
+ 'Members of %s' => 'Membri di %s',
+ 'New group' => 'Nuovo gruppo',
+ 'Group created successfully.' => 'Gruppo creato con successo',
+ 'Unable to create your group.' => 'Impossibile creare il gruppo',
+ 'Edit group' => 'Modifica gruppo',
+ 'Group updated successfully.' => 'Gruppo aggiornato con sucesso.',
+ 'Unable to update your group.' => 'Impossibile aggiornare il gruppo',
+ 'Add group member to "%s"' => 'Aggiungi un membro al gruppo "%s"',
+ 'Group member added successfully.' => 'Membro del gruppo aggiunto con successo.',
+ 'Unable to add group member.' => 'Impossibile aggiungere un membro del gruppo',
+ 'Remove user from group "%s"' => 'Rimuovi utente dal gruppo "%s"',
+ 'User removed successfully from this group.' => 'Utente rimosso dal gruppo con successo.',
+ 'Unable to remove this user from the group.' => 'Impossibile rimuovere l\'utentei dal gruppo.',
+ 'Remove group' => 'Rimuovi gruppo',
+ 'Group removed successfully.' => 'Gruppo rimosso con sucesso.',
+ 'Unable to remove this group.' => 'Impossibile rimuovere questo gruppo.',
+ 'Project Permissions' => 'Permessi del progetto',
+ // 'Manager' => '',
+ 'Project Manager' => 'Manager del progetto',
+ 'Project Member' => 'Membro del progetto',
+ 'Project Viewer' => 'Osservatore del progetto',
+ 'Your account is locked for %d minutes' => 'Il tuo account è bloccato per %d minuti',
+ 'Invalid captcha' => 'Captcha non valido',
+ 'The name must be unique' => 'Il nome deve essere univoco',
+ 'View all groups' => 'Visualiza tutti i gruppi',
+ 'View group members' => 'Visualizza i membri del gruppo',
+ 'There is no user available.' => 'Nessun utente disponibile.',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Vuoi davvero rimuovere l\'utente "%s" dal gruppo "%s"?',
+ 'There is no group.' => 'Nessun gruppo presente',
+ 'External Id' => 'Id esterno',
+ 'Add group member' => 'Aggiungi un membro del gruppo',
+ 'Do you really want to remove this group: "%s"?' => 'Vuoi davvero rimuovere questo gruppo: "%s"?',
+ 'There is no user in this group.' => 'Nessun utente in questo gruppo.',
+ 'Remove this user' => 'Rimuovi questo utente',
+ 'Permissions' => 'Permessi',
+ 'Allowed Users' => 'Utenti autorizzati',
+ 'No user have been allowed specifically.' => 'Nessun utente è stato esplicitamente autorizzato.',
+ 'Role' => 'Ruolo',
+ 'Enter user name...' => 'Inserisci il nome utente...',
+ 'Allowed Groups' => 'Gruppi autorizzati',
+ 'No group have been allowed specifically.' => 'Nessun gruppo è stato esplicitamente autorizzato.',
+ 'Group' => 'Gruppo',
+ 'Group Name' => 'Nome del gruppo',
+ 'Enter group name...' => 'Inserisci il nome del gruppo...',
+ 'Role:' => 'Ruolo:',
+ 'Project members' => 'Membri del 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',
+ 'You were mentioned in the task #%d' => 'Sei stato menzionato nel task #%d',
+ 'You were mentioned in a comment on the task #%d' => 'Sei stato menzionato in un commento del task #%d',
+ 'Mentioned' => 'Menzionato',
+ 'Compare Estimated Time vs Actual Time' => 'Confronta il Tempo Stimato vs Tempo Effettivo',
+ 'Estimated hours: ' => 'Ore stimate: ',
+ 'Actual hours: ' => 'Ore effettive: ',
+ 'Hours Spent' => 'Ore impiegate',
+ 'Hours Estimated' => 'Ore stimate',
+ 'Estimated Time' => 'Tempo stimato',
+ 'Actual Time' => 'Tempo effettivo',
+ 'Estimated vs actual time' => 'Tempo stimato vs Tempo effettivo',
+ 'RUB - Russian Ruble' => 'RUB - Rublo russo',
+ 'Assign the task to the person who does the action when the column is changed' => 'Assegna il task alla persona che esegue l\'azione quando la colonna viene modificata',
+ 'Close a task in a specific column' => 'Chiudi un task in una specifica colonna',
+ 'Time-based One-time Password Algorithm' => 'Algoritmo per la Time-based One-time Password',
+ // 'Two-Factor Provider: ' => '',
+ 'Disable two-factor authentication' => 'Disabilita l\'autenticazione "two-factor"',
+ 'Enable two-factor authentication' => 'Abilita l\'autenticazione "two-factor"',
+ 'There is no integration registered at the moment.' => 'Nessuna integrazione disponibile al momento.',
+ 'Password Reset for Kanboard' => 'Reimposta password per Kanboard',
+ 'Forgot password?' => 'Password dimenticata?',
+ 'Enable "Forget Password"' => 'Abilita funzione "Password dimenticata"',
+ 'Password Reset' => 'Reimposta password',
+ 'New password' => 'Nuova password',
+ 'Change Password' => 'Cambia password',
+ 'To reset your password click on this link:' => 'Per reimpostare la tua password clicca su questo link:',
+ 'Last Password Reset' => 'Ultimo cambio password',
+ 'The password has never been reinitialized.' => 'La password non è mai stata reimpostata.',
+ 'Creation' => 'Creazione',
+ 'Expiration' => 'Scadenza',
+ 'Password reset history' => 'Storico cambio password',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Tutti i task della colonna "%s" e della corsia "%s" sono stati chiusi con successo.',
+ 'Do you really want to close all tasks of this column?' => 'Vuoi veramente chiudere tutti i task di questa colonna?',
+ '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d task della colonna "%s" e della corsia "%s" saranno chiusi.',
+ 'Close all tasks of this column' => 'Chiudi tutti i task di questa colonna',
+ 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Nessun plugin ha caricato un metodo di notifica di progetto. Puoi tuttavia configurare le notifiche personali dal tuo profilo utente.',
+ 'My dashboard' => 'La mia bacheca',
+ 'My profile' => 'Il mio profilo',
+ 'Project owner: ' => 'Proprietario del progetto: ',
+ 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'L\'identificativo del progetto è opzionale e deve essere alfanumerico, ad esempio: MIOPROGETTO.',
+ 'Project owner' => 'Proprietario del progetto',
+ 'Those dates are useful for the project Gantt chart.' => 'Le seguenti date sono utilizzate per i grafici Gantt di progetto.',
+ 'Private projects do not have users and groups management.' => 'Per i progetti privati non è prevista la gestione di utenti e gruppi.',
+ 'There is no project member.' => 'Non è impostato un membro del progetto.',
+ 'Priority' => 'Priorità',
+ 'Task priority' => 'Priorità del task',
+ 'General' => 'Generale',
+ 'Dates' => 'Date',
+ 'Default priority' => 'Priorità predefinita',
+ 'Lowest priority' => 'Priorità minima',
+ 'Highest priority' => 'Priorità massima',
+ 'If you put zero to the low and high priority, this feature will be disabled.' => 'Se imposti a zero la priorità massima e minima, questa funzionalità sarà disabilitata.',
+ // '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' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index fb40271d..3d22ce32 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'プロジェクトの削除',
'Edit the board for "%s"' => 'ボード「%s」を変更する',
'All projects' => 'すべてのプロジェクト',
- 'Change columns' => 'カラムの変更',
'Add a new column' => 'カラムの追加',
'Title' => 'タイトル',
'Nobody assigned' => '担当なし',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'タスクをオープンする',
'Do you really want to open this task: "%s"?' => 'タスク「%s」をオープンしますか?',
'Back to the board' => 'ボードに戻る',
- 'Created on %B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M に作成',
'There is nobody assigned' => '担当者がいません',
'Column on the board:' => 'カラム: ',
- 'Status is open' => 'ステータスはオープンです',
- 'Status is closed' => 'ステータスはクローズです',
'Close this task' => 'タスクをクローズする',
'Open this task' => 'タスクをオープンする',
'There is no description.' => '説明がありません',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'ID が必要です',
'The project id is required' => 'プロジェクト ID が必要です',
'The project name is required' => 'プロジェクト名が必要です',
- 'This project must be unique' => 'プロジェクト名がすでに使われています',
'The title is required' => 'タイトルが必要です',
'Settings saved successfully.' => '設定を保存しました。',
'Unable to save your settings.' => '設定の保存に失敗しました。',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Work in progress',
'Done' => 'Done',
'Application version:' => 'アプリケーションのバージョン:',
- 'Completed on %B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M に完了',
- '%B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M',
- 'Date created' => '作成日',
- 'Date completed' => '完了日',
'Id' => 'ID',
'%d closed tasks' => '%d 個のクローズしたタスク',
'No task for this project' => 'このプロジェクトにタスクがありません',
@@ -175,13 +166,7 @@ return array(
'Complexity' => '複雑さ',
'Task limit' => 'タスク数制限',
'Task count' => 'タスク数',
- 'Edit project access list' => 'プロジェクトのアクセス許可を変更',
- 'Allow this user' => 'このユーザを許可する',
- 'Don\'t forget that administrators have access to everything.' => '管理者には全ての権限が与えられます。',
- 'Revoke' => '許可を取り下げる',
- 'List of authorized users' => '許可されたユーザ',
'User' => 'ユーザ',
- 'Nobody have access to this project.' => 'だれもプロジェクトにアクセスできません。',
'Comments' => 'コメント',
'Write your text in Markdown' => 'Markdown 記法で書く',
'Leave a comment' => 'コメントを書く',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'タスクを変更する',
'Due Date' => '期限',
'Invalid date' => '日付が無効です',
- 'Must be done before %B %e, %Y' => '%Y/%m/%d までに完了',
- '%B %e, %Y' => '%Y %B %e',
- '%b %e, %Y' => '%Y %b %e',
'Automatic actions' => '自動アクションを管理する',
'Your automatic action have been created successfully.' => '自動アクションを作成しました。',
'Unable to create your automatic action.' => '自動アクションの作成に失敗しました。',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => '色をユーザに割り当てる',
'Column title' => 'カラムのタイトル',
'Position' => '位置',
- 'Move Up' => '上に動かす',
- 'Move Down' => '下に動かす',
'Duplicate to another project' => '別のプロジェクトに複製する',
'Duplicate' => '複製する',
'link' => 'リンク',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'コメントを削除しました。',
'Unable to remove this comment.' => 'コメントの削除に失敗しました。',
'Do you really want to remove this comment?' => 'コメントを削除しますか?',
- 'Only administrators or the creator of the comment can access to this page.' => '管理者かコメントの作成者のみがこのページアクセスできます。',
'Current password for the user "%s"' => 'ユーザ「%s」の現在のパスワード',
'The current password is required' => '現在のパスワードを入力してください',
'Wrong password' => 'パスワードが違います',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'Email',
- 'Link my Google Account' => 'Google アカウントをリンクする',
- 'Unlink my Google Account' => 'Google アカウントのリンクを解除する',
- 'Login with my Google Account' => 'Google アカウントでログインする',
- 'Project not found.' => 'プロジェクトが見つかりません。',
'Task removed successfully.' => 'タスクを削除しました。',
'Unable to remove this task.' => 'タスクの削除に失敗しました。',
'Remove a task' => 'タスクの削除',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => '最大: ',
'Unable to upload the file.' => 'ファイルのアップロードに失敗しました。',
'Display another project' => '別のプロジェクトを表示',
- 'Login with my Github Account' => 'Github アカウントでログインする',
- 'Link my Github Account' => 'Github アカウントをリンクする',
- 'Unlink my Github Account' => 'Github アカウントとのリンクを解除する',
'Created by %s' => '%s が作成',
- 'Last modified on %B %e, %Y at %k:%M %p' => ' %Y/%m/%d %H:%M に変更',
'Tasks Export' => 'タスクの出力',
'Tasks exportation for "%s"' => '「%s」のタスク出力',
'Start Date' => '開始日',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => '以下のプロジェクトにのみ通知を受け取る:',
'view the task on Kanboard' => 'Kanboard でタスクを見る',
'Public access' => '公開アクセス設定',
- 'User management' => 'ユーザを管理する',
'Active tasks' => 'アクティブなタスク',
'Disable public access' => '公開アクセスを無効にする',
'Enable public access' => '公開アクセスを有効にする',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Email:',
'Notifications:' => '通知:',
'Notifications' => '通知',
- 'Group:' => 'グループ:',
- 'Regular user' => '通常のユーザ',
'Account type:' => 'アカウントの種類:',
'Edit profile' => 'プロフィールの変更',
'Change password' => 'パスワードの変更',
'Password modification' => 'パスワードの変更',
'External authentications' => '外部認証',
- 'Google Account' => 'Google アカウント',
- 'Github Account' => 'Github アカウント',
'Never connected.' => '未接続。',
- 'No account linked.' => 'アカウントがリンクしていません。',
- 'Account linked.' => 'アカウントがリンクしました。',
'No external authentication enabled.' => '外部認証が設定されていません。',
'Password modified successfully.' => 'パスワードを変更しました。',
'Unable to change the password.' => 'パスワードが変更できませんでした。',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s がタスク %s の担当を %s に変更しました',
'New password for the user "%s"' => 'ユーザ「%s」の新しいパスワード',
'Choose an event' => 'イベントの選択',
- 'Github commit received' => 'Github のコミットを受け取った',
- 'Github issue opened' => 'Github Issue がオープンされた',
- 'Github issue closed' => 'Github Issue がクローズされた',
- 'Github issue reopened' => 'Github Issue が再オープンされた',
- 'Github issue assignee change' => 'Github Issue の担当が変更された',
- 'Github issue label change' => 'Github のラベルが変更された',
'Create a task from an external provider' => 'タスクを外部サービスから作成する',
'Change the assignee based on an external username' => '担当者を外部サービスに基いて変更する',
'Change the category based on an external label' => 'カテゴリを外部サービスに基いて変更する',
'Reference' => '参照',
- 'Reference: %s' => '参照: %s',
'Label' => 'ラベル',
'Database' => 'データベース',
'About' => '情報',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => '秒数 (デフォルト 60 秒)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => '秒数 (0 は機能を無効化、デフォルト 10 秒)',
'Application URL' => 'アプリケーションの URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemple : http://exemple.kanboard.net/ (Email 通知に利用)',
'Token regenerated.' => 'トークンが再生成されました。',
'Date format' => 'データのフォーマット',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO フォーマットが入力できます(例: %s または %s)',
@@ -482,9 +438,6 @@ return array(
'This project is private' => 'このプロジェクトは非公開です',
'Type here to create a new sub-task' => 'サブタスクを追加するにはここに入力してください',
'Add' => '追加',
- 'Estimated time: %s hours' => '予想時間: %s 時間',
- 'Time spent: %s hours' => '経過: %s 時間',
- 'Started on %B %e, %Y' => '開始 %Y/%m/%d',
'Start date' => '開始時間',
'Time estimated' => '予想時間',
'There is nothing assigned to you.' => '何もアサインされていません。',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => '誰でもこのプロジェクトにアクセスできます。',
'Webhooks' => 'Webhook',
'API' => 'API',
- 'Github webhooks' => 'Github Webhook',
- 'Help on Github webhooks' => 'Github webhook のヘルプ',
'Create a comment from an external provider' => '外部サービスからコメントを作成する',
- 'Github issue comment created' => 'Github Issue コメントが作られました',
'Project management' => 'プロジェクト・マネジメント',
'My projects' => '自分のプロジェクト',
'Columns' => 'カラム',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => '「%s」の担当者分布',
'Clone this project' => 'このプロジェクトを複製する',
'Column removed successfully.' => 'カラムを削除しました',
- 'Github Issue' => 'Github Issue',
'Not enough data to show the graph.' => 'グラフを描画するには出たが足りません',
'Previous' => '戻る',
'The id must be an integer' => 'id は数字でなければなりません',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'デフォルトスイムレーン',
'Do you really want to remove this swimlane: "%s"?' => 'このスイムレーン「%s」を本当に削除しますか?',
'Inactive swimlanes' => 'インタラクティブなスイムレーン',
- 'Set project manager' => 'プロジェクトマネジャーをセット',
- 'Set project member' => 'プロジェクトメンバーをセット',
'Remove a swimlane' => 'スイムレーンの削除',
- 'Rename' => '名前の変更',
'Show default swimlane' => 'デフォルトスイムレーンの表示',
'Swimlane modification for the project "%s"' => '「%s」に対するスイムレーン変更',
'Swimlane not found.' => 'スイムレーンが見つかりません。',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'スイムレーン',
'Swimlane updated successfully.' => 'スイムレーンを更新しました。',
'The default swimlane have been updated successfully.' => 'デフォルトスイムレーンを更新しました。',
- 'Unable to create your swimlane.' => 'スイムレーンを追加できませんでした。',
'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)' => '新しいプロジェクトのデフォルトカテゴリー (コンマ区切り)',
- 'Gitlab commit received' => 'Gitlab コミットを受診しました',
- 'Gitlab issue opened' => 'Gitlab Issue がオープンされました',
- 'Gitlab issue closed' => 'Gitlab Issue がクローズされました',
- 'Gitlab webhooks' => 'Gitlab Webhooks',
- 'Help on Gitlab webhooks' => 'Gitlab Webhooks のヘルプ',
'Integrations' => '連携',
'Integration with third-party services' => 'サードパーティサービスとの連携',
- 'Role for this project' => 'このプロジェクトの役割',
- 'Project manager' => 'プロジェクトマネジャー',
- 'Project member' => 'プロジェクトメンバー',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'プロジェクトマネジャーはプロジェクトの設定を変更するなどの通常ユーザにはない権限があります。',
- 'Gitlab Issue' => 'Gitlab Issue',
'Subtask Id' => 'サブタスク Id',
'Subtasks' => 'サブタスク',
'Subtasks Export' => 'サブタスクの出力',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'すでに進行中のサブタスクがあります。',
'Which parts of the project do you want to duplicate?' => 'プロジェクトの何を複製しますか?',
// 'Disallow login form' => '',
- 'Bitbucket commit received' => 'Bitbucket コミットを受信しました',
- 'Bitbucket webhooks' => 'Bitbucket Webhooks',
- 'Help on Bitbucket webhooks' => 'Bitbucket Webhooks のヘルプ',
'Start' => '開始',
'End' => '終了',
'Task age in days' => 'タスクの経過日数',
@@ -646,7 +578,6 @@ return array(
'This task' => 'このタスクは',
'<1h' => '<1時間',
'%dh' => '%d 時間',
- '%b %e' => '%b/%e',
'Expand tasks' => 'タスクを展開する',
'Collapse tasks' => 'タスクを閉じる',
'Expand/collapse tasks' => 'タスクの展開/閉じる',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'キーボードショートカット',
'Open board switcher' => 'ボード切り替えを開く',
'Application' => 'アプリケーション',
- 'since %B %e, %Y at %k:%M %p' => '%Y/%m/%d %k:%M から',
'Compact view' => 'コンパクトビュー',
'Horizontal scrolling' => '縦スクロール',
'Compact/wide view' => 'コンパクト/ワイドビュー',
'No results match:' => '結果が一致しませんでした',
'Currency' => '通貨',
- 'Files' => 'ファイル',
- 'Images' => '画像',
'Private project' => 'プライベートプロジェクト',
'AUD - Australian Dollar' => 'AUD - 豪ドル',
'CAD - Canadian Dollar' => 'CAD - 加ドル',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => '2 段認証コードは有効です。',
'Code' => 'コード',
'Two factor authentication' => '2 段認証',
- 'Enable/disable two factor authentication' => '2 段認証の有効/無効',
'This QR code contains the key URI: ' => 'この QR コードが URI キーを含んでいます: ',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '秘密鍵を TOTP ソフトに保存 (Google Authenticator や FreeOTP など)',
'Check my code' => '自分のコードをチェック',
'Secret key: ' => '秘密鍵: ',
'Test your device' => 'デバイスをテストする',
// 'Assign a color when the task is moved to a specific column' => '',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -722,7 +645,6 @@ return array(
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
// 'User that will receive the email' => '',
// 'Email subject' => '',
// 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
// 'Add a comment log when moving the task between columns' => '',
// 'Move the task to another column when the category is changed' => '',
// 'Send a task by email to someone' => '',
// 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
// 'Column change' => '',
// 'Position change' => '',
// 'Swimlane change' => '',
@@ -826,17 +736,11 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
// 'I want to receive notifications for:' => '',
// 'All tasks' => '',
// 'Only for tasks assigned to me' => '',
// 'Only for tasks created by me' => '',
// 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
// 'Total for all columns' => '',
// 'You need at least 2 days of data to show the chart.' => '',
@@ -861,7 +765,6 @@ return array(
// 'Not assigned' => '',
// 'View advanced search syntax' => '',
// 'Overview' => '',
- // '%b %e %Y' => '',
// 'Board/Calendar/List view' => '',
// 'Switch to the board view' => '',
// 'Switch to the calendar view' => '',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -906,18 +805,12 @@ return array(
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
// 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
// 'New remote user' => '',
// 'New local user' => '',
// 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
// 'This feature does not work with all browsers.' => '',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
// 'contributors' => '',
// 'License:' => '',
// 'License' => '',
- // 'Project Administrator' => '',
// 'Enter the text below' => '',
// 'Gantt chart for %s' => '',
// 'Sort by position' => '',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
new file mode 100644
index 00000000..e2bc6117
--- /dev/null
+++ b/app/Locale/my_MY/translations.php
@@ -0,0 +1,1151 @@
+<?php
+
+return array(
+ 'number.decimals_separator' => '.',
+ 'number.thousands_separator' => ',',
+ 'None' => 'Tiada',
+ 'edit' => 'sunting',
+ 'Edit' => 'Sunting',
+ 'remove' => 'hapus',
+ 'Remove' => 'Hapus',
+ 'Update' => 'Kemaskini',
+ 'Yes' => 'Ya',
+ 'No' => 'Tidak',
+ 'cancel' => 'batal',
+ 'or' => 'atau',
+ 'Yellow' => 'Kuning',
+ 'Blue' => 'Biru',
+ 'Green' => 'Hijau',
+ 'Purple' => 'Ungu',
+ 'Red' => 'Merah',
+ 'Orange' => 'Oren',
+ 'Grey' => 'Kelabu',
+ 'Brown' => 'Coklat',
+ 'Deep Orange' => 'Oren Gelap',
+ 'Dark Grey' => 'Kelabu Malap',
+ 'Pink' => 'Merah Jambu',
+ 'Teal' => 'Teal',
+ 'Cyan' => 'Sian',
+ 'Lime' => 'Lime',
+ 'Light Green' => 'Hijau Muda',
+ 'Amber' => 'Amber',
+ 'Save' => 'Simpan',
+ 'Login' => 'Masuk',
+ 'Official website:' => 'Laman rasmi :',
+ 'Unassigned' => 'Belum ditugaskan',
+ 'View this task' => 'Lihat tugas ini',
+ 'Remove user' => 'Hapus pengguna',
+ 'Do you really want to remove this user: "%s"?' => 'Anda yakin mahu menghapus pengguna ini : « %s » ?',
+ 'New user' => 'Pengguna baru',
+ 'All users' => 'Semua pengguna',
+ 'Username' => 'Nama pengguna',
+ 'Password' => 'Kata laluan',
+ 'Administrator' => 'Pentadbir',
+ 'Sign in' => 'Masuk',
+ 'Users' => 'Para Pengguna',
+ 'No user' => 'Tiada pengguna',
+ 'Forbidden' => 'Larangan',
+ 'Access Forbidden' => 'Akses Dilarang',
+ 'Edit user' => 'Ubah Pengguna',
+ 'Logout' => 'Keluar',
+ 'Bad username or password' => 'Nama pengguna atau kata laluan tidak sepadan',
+ 'Edit project' => 'Ubah projek',
+ 'Name' => 'Nama',
+ 'Projects' => 'Projek',
+ 'No project' => 'Tiada projek',
+ 'Project' => 'Projek',
+ 'Status' => 'Status',
+ 'Tasks' => 'Tugasan',
+ 'Board' => 'Papan',
+ 'Actions' => 'Tindakan',
+ 'Inactive' => 'Tidak Aktif',
+ 'Active' => 'Aktif',
+ 'Add this column' => 'Tambahkan kolom ini',
+ '%d tasks on the board' => '%d tugasan di papan',
+ '%d tasks in total' => 'Sejumlah %d tugasan',
+ 'Unable to update this board.' => 'Tidak berupaya mengemaskini papan ini',
+ 'Edit board' => 'ubah papan',
+ 'Disable' => 'Nyah-Upaya',
+ 'Enable' => 'Aktifkan',
+ 'New project' => 'Projek Baru',
+ 'Do you really want to remove this project: "%s"?' => 'Anda yakin mahu menghapus projek ini : « %s » ?',
+ 'Remove project' => 'Hapus projek',
+ 'Edit the board for "%s"' => 'Ubah papan untuk « %s »',
+ 'All projects' => 'Semua projek',
+ 'Add a new column' => 'Tambah kolom baru',
+ 'Title' => 'Judul',
+ 'Nobody assigned' => 'Tidak ada yang ditugaskan',
+ 'Assigned to %s' => 'Ditugaskan ke %s',
+ 'Remove a column' => 'Hapus kolom',
+ 'Remove a column from a board' => 'Hapus kolom dari papan',
+ 'Unable to remove this column.' => 'Tidak dapat menghapus kolom ini.',
+ 'Do you really want to remove this column: "%s"?' => 'Apakah anda yakin akan menghapus kolom ini : « %s » ?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'tindakan ini akan MENGHAPUS SEMUA TUGAS yang terkait dengan kolom ini!',
+ 'Settings' => 'Penetapan',
+ 'Application settings' => 'Penetapan aplikasi',
+ 'Language' => 'Bahasa',
+ 'Webhook token:' => 'Token webhook :',
+ 'API token:' => 'Token API :',
+ 'Database size:' => 'Saiz pengkalan data:',
+ 'Download the database' => 'Muat turun pengkalan data',
+ 'Optimize the database' => 'Optimakan pengkalan data',
+ '(VACUUM command)' => '(perintah VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(File Sqlite yang termampat Gzip)',
+ 'Close a task' => 'Tutup tugas',
+ 'Edit a task' => 'Sunting tugas',
+ 'Column' => 'Kolom',
+ 'Color' => 'Warna',
+ 'Assignee' => 'Orang yang ditugaskan',
+ 'Create another task' => 'Buat tugas lain',
+ 'New task' => 'Tugasan baru',
+ 'Open a task' => 'Buka tugas',
+ 'Do you really want to open this task: "%s"?' => 'Anda yakin untuk buka tugas ini : « %s » ?',
+ 'Back to the board' => 'Kembali ke papan',
+ 'There is nobody assigned' => 'Tidak ada orang yand ditugaskan',
+ 'Column on the board:' => 'Kolom di dalam papan : ',
+ 'Close this task' => 'Tutup tugas ini',
+ 'Open this task' => 'Buka tugas ini',
+ 'There is no description.' => 'Tidak ada keterangan.',
+ 'Add a new task' => 'Tambah tugas baru',
+ 'The username is required' => 'Nama pengguna adalah wajib',
+ 'The maximum length is %d characters' => 'Panjang maksimum adalah %d karakter',
+ 'The minimum length is %d characters' => 'Panjang minimum adalah %d karakter',
+ 'The password is required' => 'Kata laluan adalah wajib',
+ 'This value must be an integer' => 'Nilai ini harus integer',
+ 'The username must be unique' => 'Nama pengguna semestinya unik',
+ 'The user id is required' => 'Id Pengguna adalah wajib',
+ 'Passwords don\'t match' => 'Kata laluan tidak sepadan',
+ 'The confirmation is required' => 'Pengesahan diperlukan',
+ 'The project is required' => 'Projek diperlukan',
+ 'The id is required' => 'Id diperlukan',
+ 'The project id is required' => 'Id projek diperlukan',
+ 'The project name is required' => 'Nama projek diperlukan',
+ 'The title is required' => 'Judul diperlukan',
+ 'Settings saved successfully.' => 'Penetapan berjaya disimpan.',
+ 'Unable to save your settings.' => 'Tidak dapat menyimpan penetapan anda.',
+ 'Database optimization done.' => 'Optimasi pengkalan data selesai.',
+ 'Your project have been created successfully.' => 'Projek anda berhasil dibuat.',
+ 'Unable to create your project.' => 'Tidak dapat membuat projek anda.',
+ 'Project updated successfully.' => 'projek berhasil diperbaharui.',
+ 'Unable to update this project.' => 'Tidak dapat memperbaharui projek ini.',
+ 'Unable to remove this project.' => 'Tidak dapat menghapus projek ini.',
+ 'Project removed successfully.' => 'projek berhasil dihapus.',
+ 'Project activated successfully.' => 'projek berhasil diaktivasi.',
+ 'Unable to activate this project.' => 'Tidak dapat mengaktifkan projek ini.',
+ 'Project disabled successfully.' => 'projek berhasil dinonaktifkan.',
+ 'Unable to disable this project.' => 'Tidak dapat menonaktifkan projek ini.',
+ 'Unable to open this task.' => 'Tidak dapat membuka tugas ini.',
+ 'Task opened successfully.' => 'Tugas berhasil dibuka.',
+ 'Unable to close this task.' => 'Tidak dapat menutup tugas ini.',
+ 'Task closed successfully.' => 'Tugas berhasil ditutup.',
+ 'Unable to update your task.' => 'Tidak dapat memperbaharui tugas ini.',
+ 'Task updated successfully.' => 'Tugas berhasil diperbaharui.',
+ 'Unable to create your task.' => 'Tidak dapat membuat tugas anda.',
+ 'Task created successfully.' => 'Tugas berhasil dibuat.',
+ 'User created successfully.' => 'Pengguna berhasil dibuat.',
+ 'Unable to create your user.' => 'Tidak dapat membuat pengguna anda.',
+ 'User updated successfully.' => 'Pengguna berhasil diperbaharui.',
+ 'Unable to update your user.' => 'Tidak dapat memperbaharui pengguna anda.',
+ 'User removed successfully.' => 'pengguna berhasil dihapus.',
+ 'Unable to remove this user.' => 'Tidak dapat menghapus pengguna ini.',
+ 'Board updated successfully.' => 'Papan berhasil diperbaharui.',
+ 'Ready' => 'Siap',
+ 'Backlog' => 'Tertunda',
+ 'Work in progress' => 'Sedang dalam pengerjaan',
+ 'Done' => 'Selesai',
+ 'Application version:' => 'Versi aplikasi :',
+ 'Id' => 'Id.',
+ '%d closed tasks' => '%d tugas yang ditutup',
+ 'No task for this project' => 'Tidak ada tugas dalam projek ini',
+ 'Public link' => 'Pautan publik',
+ 'Change assignee' => 'Mengubah orang yand ditugaskan',
+ 'Change assignee for the task "%s"' => 'Mengubah orang yang ditugaskan untuk tugas « %s »',
+ 'Timezone' => 'Zona waktu',
+ 'Sorry, I didn\'t find this information in my database!' => 'Maaf, saya tidak menemukan informasi ini dalam basis data saya !',
+ 'Page not found' => 'Halaman tidak ditemukan',
+ 'Complexity' => 'Kompleksitas',
+ 'Task limit' => 'Batas tugas.',
+ 'Task count' => 'Jumlah tugas',
+ 'User' => 'Pengguna',
+ 'Comments' => 'Komentar',
+ 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown',
+ 'Leave a comment' => 'Tinggalkan komentar',
+ 'Comment is required' => 'Komentar diperlukan',
+ 'Leave a description' => 'Tinggalkan deskripsi',
+ 'Comment added successfully.' => 'Komentar berhasil ditambahkan.',
+ 'Unable to create your comment.' => 'Tidak dapat menambahkan komentar anda.',
+ 'Edit this task' => 'Modifikasi tugas ini',
+ 'Due Date' => 'Batas Tanggal Terakhir',
+ 'Invalid date' => 'Tanggal tidak valid',
+ 'Automatic actions' => 'Tindakan otomatis',
+ 'Your automatic action have been created successfully.' => 'Tindakan otomatis anda berhasil dibuat.',
+ 'Unable to create your automatic action.' => 'Tidak dapat membuat tindakan otomatis anda.',
+ 'Remove an action' => 'Hapus tindakan',
+ 'Unable to remove this action.' => 'Tidak dapat menghapus tindakan ini',
+ 'Action removed successfully.' => 'Tindakan berhasil dihapus.',
+ 'Automatic actions for the project "%s"' => 'Tindakan otomatis untuk projek ini « %s »',
+ 'Defined actions' => 'Tindakan didefinisikan',
+ 'Add an action' => 'Tambah tindakan',
+ 'Event name' => 'Nama acara',
+ 'Action name' => 'Nama tindakan',
+ 'Action parameters' => 'Parameter tindakan',
+ 'Action' => 'Tindakan',
+ 'Event' => 'Acara',
+ 'When the selected event occurs execute the corresponding action.' => 'Ketika acara yang dipilih terjadi, melakukan tindakan yang sesuai.',
+ 'Next step' => 'Langkah selanjutnya',
+ 'Define action parameters' => 'Definisi parameter tindakan',
+ 'Save this action' => 'Simpan tindakan ini',
+ 'Do you really want to remove this action: "%s"?' => 'Apakah anda yakin akan menghapus tindakan ini « %s » ?',
+ 'Remove an automatic action' => 'Hapus tindakan otomatis',
+ 'Assign the task to a specific user' => 'Menetapkan tugas untuk pengguna tertentu',
+ 'Assign the task to the person who does the action' => 'Memberikan tugas untuk orang yang melakukan tindakan',
+ 'Duplicate the task to another project' => 'Duplikasi tugas ke projek lain',
+ 'Move a task to another column' => 'Pindahkan tugas ke kolom lain',
+ 'Task modification' => 'Modifikasi tugas',
+ 'Task creation' => 'Membuat tugas',
+ 'Closing a task' => 'Menutup tugas',
+ 'Assign a color to a specific user' => 'Menetapkan warna untuk pengguna tertentu',
+ 'Column title' => 'Judul kolom',
+ 'Position' => 'Posisi',
+ 'Duplicate to another project' => 'Duplikasi ke projek lain',
+ 'Duplicate' => 'Duplikasi',
+ 'link' => 'Pautan',
+ 'Comment updated successfully.' => 'Komentar berhasil diperbaharui.',
+ 'Unable to update your comment.' => 'Tidak dapat memperbaharui komentar anda.',
+ 'Remove a comment' => 'Hapus komentar',
+ 'Comment removed successfully.' => 'Komentar berhasil dihapus.',
+ 'Unable to remove this comment.' => 'Tidak dapat menghapus komentar ini.',
+ 'Do you really want to remove this comment?' => 'Apakah anda yakin akan menghapus komentar ini ?',
+ 'Current password for the user "%s"' => 'Kata laluan saat ini untuk pengguna « %s »',
+ 'The current password is required' => 'Kata laluan saat ini diperlukan',
+ 'Wrong password' => 'Kata laluan salah',
+ 'Unknown' => 'Tidak diketahui',
+ 'Last logins' => 'Masuk terakhir',
+ 'Login date' => 'Tanggal masuk',
+ 'Authentication method' => 'Metode otentifikasi',
+ 'IP address' => 'Alamat IP',
+ 'User agent' => 'Agen Pengguna',
+ 'Persistent connections' => 'Koneksi persisten',
+ 'No session.' => 'Tidak ada sesi.',
+ 'Expiration date' => 'Tanggal kadaluarsa',
+ 'Remember Me' => 'Ingat Saya',
+ 'Creation date' => 'Tanggal dibuat',
+ 'Everybody' => 'Semua orang',
+ 'Open' => 'Terbuka',
+ 'Closed' => 'Ditutup',
+ 'Search' => 'Cari',
+ 'Nothing found.' => 'Tidak ditemukan.',
+ 'Due date' => 'Batas tanggal terakhir',
+ 'Others formats accepted: %s and %s' => 'Format lain yang didukung : %s et %s',
+ 'Description' => 'Deskripsi',
+ '%d comments' => '%d komentar',
+ '%d comment' => '%d komentar',
+ 'Email address invalid' => 'Alamat email tidak valid',
+ 'Your external account is not linked anymore to your profile.' => 'Akaun eksternal anda tidak lagi terhubung ke profil anda.',
+ 'Unable to unlink your external account.' => 'Tidak dapat memutuskan Akaun eksternal anda.',
+ 'External authentication failed' => 'Otentifikasi eksternal gagal',
+ 'Your external account is linked to your profile successfully.' => 'Akaun eksternal anda berhasil dihubungkan ke profil anda.',
+ 'Email' => 'Email',
+ 'Task removed successfully.' => 'Tugas berhasil dihapus.',
+ 'Unable to remove this task.' => 'Tidak dapat menghapus tugas ini.',
+ 'Remove a task' => 'Hapus tugas',
+ 'Do you really want to remove this task: "%s"?' => 'Apakah anda yakin akan menghapus tugas ini « %s » ?',
+ 'Assign automatically a color based on a category' => 'Otomatis menetapkan warna berdasarkan kategori',
+ 'Assign automatically a category based on a color' => 'Otomatis menetapkan kategori berdasarkan warna',
+ 'Task creation or modification' => 'Tugas dibuat atau di mofifikasi',
+ 'Category' => 'Kategori',
+ 'Category:' => 'Kategori :',
+ 'Categories' => 'Kategori',
+ 'Category not found.' => 'Kategori tidak ditemukan',
+ 'Your category have been created successfully.' => 'Kategori anda berhasil dibuat.',
+ 'Unable to create your category.' => 'Tidak dapat membuat kategori anda.',
+ 'Your category have been updated successfully.' => 'Kategori anda berhasil diperbaharui.',
+ 'Unable to update your category.' => 'Tidak dapat memperbaharui kategori anda.',
+ 'Remove a category' => 'Hapus kategori',
+ 'Category removed successfully.' => 'Kategori berhasil dihapus.',
+ 'Unable to remove this category.' => 'Tidak dapat menghapus kategori ini.',
+ 'Category modification for the project "%s"' => 'Modifikasi kategori untuk projek « %s »',
+ 'Category Name' => 'Nama Kategori',
+ 'Add a new category' => 'Tambah kategori baru',
+ 'Do you really want to remove this category: "%s"?' => 'Apakah anda yakin akan menghapus kategori ini « %s » ?',
+ 'All categories' => 'Semua kategori',
+ 'No category' => 'Tidak ada kategori',
+ 'The name is required' => 'Nama diperlukan',
+ 'Remove a file' => 'Hapus berkas',
+ 'Unable to remove this file.' => 'Tidak dapat menghapus berkas ini.',
+ 'File removed successfully.' => 'Berkas berhasil dihapus.',
+ 'Attach a document' => 'Lampirkan dokumen',
+ 'Do you really want to remove this file: "%s"?' => 'Apakah anda yakin akan menghapus berkas ini « %s » ?',
+ 'Attachments' => 'Lampiran',
+ 'Edit the task' => 'Modifikasi tugas',
+ 'Edit the description' => 'Modifikasi deskripsi',
+ 'Add a comment' => 'Tambahkan komentar',
+ 'Edit a comment' => 'Modifikasi komentar',
+ 'Summary' => 'Ringkasan',
+ 'Time tracking' => 'Pelacakan waktu',
+ 'Estimate:' => 'Estimasi :',
+ 'Spent:' => 'Menghabiskan:',
+ 'Do you really want to remove this sub-task?' => 'Apakah anda yakin akan menghapus sub-tugas ini ?',
+ 'Remaining:' => 'Tersisa:',
+ 'hours' => 'jam',
+ 'spent' => 'menghabiskan',
+ 'estimated' => 'perkiraan',
+ 'Sub-Tasks' => 'Sub-tugas',
+ 'Add a sub-task' => 'Tambahkan sub-tugas',
+ 'Original estimate' => 'Perkiraan semula',
+ 'Create another sub-task' => 'Tambahkan sub-tugas lainnya',
+ 'Time spent' => 'Waktu yang dihabiskan',
+ 'Edit a sub-task' => 'Modifikasi sub-tugas',
+ 'Remove a sub-task' => 'Hapus sub-tugas',
+ 'The time must be a numeric value' => 'Waktu harus berisikan numerik',
+ 'Todo' => 'Yang harus dilakukan',
+ 'In progress' => 'Sedang proses',
+ 'Sub-task removed successfully.' => 'Sub-tugas berhasil dihapus.',
+ 'Unable to remove this sub-task.' => 'Tidak dapat menghapus sub-tugas.',
+ 'Sub-task updated successfully.' => 'Sub-tugas berhasil diperbaharui.',
+ 'Unable to update your sub-task.' => 'Tidak dapat memperbaharui sub-tugas anda.',
+ 'Unable to create your sub-task.' => 'Tidak dapat membuat sub-tugas anda.',
+ 'Sub-task added successfully.' => 'Sub-tugas berhasil dibuat.',
+ 'Maximum size: ' => 'Ukuran maksimum: ',
+ 'Unable to upload the file.' => 'Tidak dapat mengunggah berkas.',
+ 'Display another project' => 'Lihat projek lain',
+ 'Created by %s' => 'Dibuat oleh %s',
+ 'Tasks Export' => 'Ekspor Tugas',
+ 'Tasks exportation for "%s"' => 'Tugas di ekspor untuk « %s »',
+ 'Start Date' => 'Tanggal Mulai',
+ 'End Date' => 'Tanggal Berakhir',
+ 'Execute' => 'Eksekusi',
+ 'Task Id' => 'Id Tugas',
+ 'Creator' => 'Pembuat',
+ 'Modification date' => 'Tanggal modifikasi',
+ 'Completion date' => 'Tanggal penyelesaian',
+ 'Clone' => 'Klon',
+ 'Project cloned successfully.' => 'Kloning projek berhasil.',
+ 'Unable to clone this project.' => 'Tidak dapat mengkloning projek.',
+ 'Enable email notifications' => 'Aktifkan pemberitahuan dari email',
+ 'Task position:' => 'Posisi tugas :',
+ 'The task #%d have been opened.' => 'Tugas #%d telah dibuka.',
+ 'The task #%d have been closed.' => 'Tugas #%d telah ditutup.',
+ 'Sub-task updated' => 'Sub-tugas diperbaharui',
+ 'Title:' => 'Judul :',
+ 'Status:' => 'Status :',
+ 'Assignee:' => 'Ditugaskan ke :',
+ 'Time tracking:' => 'Pelacakan waktu :',
+ 'New sub-task' => 'Sub-tugas baru',
+ 'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »',
+ 'Comment updated' => 'Komentar diperbaharui',
+ 'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »',
+ 'New attachment' => 'Lampirkan baru',
+ 'New comment' => 'Komentar baru',
+ 'New subtask' => 'Sub-tugas baru',
+ 'Subtask updated' => 'Sub-tugas diperbaharui',
+ 'Task updated' => 'Tugas diperbaharui',
+ 'Task closed' => 'Tugas ditutup',
+ 'Task opened' => 'Tugas dibuka',
+ 'I want to receive notifications only for those projects:' => 'Saya ingin menerima pemberitahuan hanya untuk projek-projek yang dipilih :',
+ 'view the task on Kanboard' => 'lihat tugas di Kanboard',
+ 'Public access' => 'Akses awam',
+ 'Active tasks' => 'Tugas aktif',
+ 'Disable public access' => 'Nyahaktifkan akses awam',
+ 'Enable public access' => 'Aktifkan akses awam',
+ 'Public access disabled' => 'Akses awam dinyahaktif',
+ 'Do you really want to disable this project: "%s"?' => 'Anda yakin menyah-aktifkan projek ini : « %s » ?',
+ 'Do you really want to enable this project: "%s"?' => 'Anda yakin untuk mengaktifkan projek ini : « %s » ?',
+ 'Project activation' => 'Aktifkan projek',
+ 'Move the task to another project' => 'Pindahkan tugas ke projek lain',
+ 'Move to another project' => 'Pindahkan ke projek lain',
+ 'Do you really want to duplicate this task?' => 'Anda yakin mengembarkan tugas ini ?',
+ 'Duplicate a task' => 'Kembarkan tugas',
+ 'External accounts' => 'Akaun luaran',
+ 'Account type' => 'Jenis Akaun',
+ 'Local' => 'Lokal',
+ 'Remote' => 'Jauh',
+ 'Enabled' => 'Aktif',
+ 'Disabled' => 'Tidak aktif',
+ 'Username:' => 'Nama pengguna :',
+ 'Name:' => 'Nama:',
+ 'Email:' => 'Emel:',
+ 'Notifications:' => 'Makluman:',
+ 'Notifications' => 'Makluman',
+ 'Account type:' => 'Jenis Akaun :',
+ 'Edit profile' => 'Sunting profil',
+ 'Change password' => 'Rubah kata sandri',
+ 'Password modification' => 'Modifikasi kata laluan',
+ 'External authentications' => 'Otentifikasi eksternal',
+ 'Never connected.' => 'Tidak pernah terhubung.',
+ 'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.',
+ 'Password modified successfully.' => 'Kata laluan telah berjaya ditukar.',
+ 'Unable to change the password.' => 'Tidak dapat merubah kata laluanr.',
+ 'Change category for the task "%s"' => 'Rubah kategori untuk tugas « %s »',
+ 'Change category' => 'Tukar kategori',
+ '%s updated the task %s' => '%s memperbaharui tugas %s',
+ '%s opened the task %s' => '%s membuka tugas %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s memindahkan tugas %s ke posisi n°%d dalam kolom « %s »',
+ '%s moved the task %s to the column "%s"' => '%s memindahkan tugas %s ke kolom « %s »',
+ '%s created the task %s' => '%s membuat tugas %s',
+ '%s closed the task %s' => '%s menutup tugas %s',
+ '%s created a subtask for the task %s' => '%s membuat subtugas untuk tugas %s',
+ '%s updated a subtask for the task %s' => '%s memperbaharui subtugas untuk tugas %s',
+ 'Assigned to %s with an estimate of %s/%sh' => 'Ditugaskan untuk %s dengan perkiraan %s/%sh',
+ 'Not assigned, estimate of %sh' => 'Tiada yang ditugaskan, perkiraan %sh',
+ '%s updated a comment on the task %s' => '%s memperbaharui komentar pada tugas %s',
+ '%s commented the task %s' => '%s memberikan komentar pada tugas %s',
+ '%s\'s activity' => 'Aktifitas dari %s',
+ 'RSS feed' => 'RSS feed',
+ '%s updated a comment on the task #%d' => '%s memperbaharui komentar pada tugas n°%d',
+ '%s commented on the task #%d' => '%s memberikan komentar pada tugas n°%d',
+ '%s updated a subtask for the task #%d' => '%s memperbaharui subtugas untuk tugas n°%d',
+ '%s created a subtask for the task #%d' => '%s membuat subtugas untuk tugas n°%d',
+ '%s updated the task #%d' => '%s memperbaharui tugas n°%d',
+ '%s created the task #%d' => '%s membuat tugas n°%d',
+ '%s closed the task #%d' => '%s menutup tugas n°%d',
+ '%s open the task #%d' => '%s membuka tugas n°%d',
+ '%s moved the task #%d to the column "%s"' => '%s memindahkan tugas n°%d ke kolom « %s »',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s memindahkan tugas n°%d ke posisi n°%d dalam kolom « %s »',
+ 'Activity' => 'Aktifitas',
+ 'Default values are "%s"' => 'Standar nilai adalah« %s »',
+ 'Default columns for new projects (Comma-separated)' => 'Kolom default untuk projek baru (dipisahkan dengan koma)',
+ 'Task assignee change' => 'Mengubah orang ditugaskan untuk tugas',
+ '%s change the assignee of the task #%d to %s' => '%s rubah orang yang ditugaskan dari tugas n%d ke %s',
+ '%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s',
+ 'New password for the user "%s"' => 'Kata laluan baru untuk pengguna « %s »',
+ 'Choose an event' => 'Pilih sebuah acara',
+ 'Create a task from an external provider' => 'Buat tugas dari pemasok eksternal',
+ 'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal',
+ 'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal',
+ 'Reference' => 'Referensi',
+ 'Label' => 'Label',
+ 'Database' => 'Pengkalan data',
+ 'About' => 'Tentang',
+ 'Database driver:' => 'Driver pengkalan data:',
+ 'Board settings' => 'Pengaturan papan',
+ 'URL and token' => 'URL dan token',
+ 'Webhook settings' => 'Penetapan webhook',
+ 'URL for task creation:' => 'URL untuk cipta tugas:',
+ 'Reset token' => 'Menetap semula token',
+ 'API endpoint:' => 'API endpoint :',
+ 'Refresh interval for private board' => 'Interval pembaruan untuk papan pribadi',
+ 'Refresh interval for public board' => 'Interval pembaruan untuk papan publik',
+ 'Task highlight period' => 'Periode puncak tugas',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periode (dalam detik) untuk mempertimbangkan tugas yang baru dimodifikasi (0 untuk menonaktifkan, standar 2 hari)',
+ 'Frequency in second (60 seconds by default)' => 'Frequensi dalam detik (standar 60 saat)',
+ 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekuensi dalam detik (0 untuk menonaktifkan fitur ini, standar 10 detik)',
+ 'Application URL' => 'URL Aplikasi',
+ 'Token regenerated.' => 'Token diregenerasi.',
+ 'Date format' => 'Format tarikh',
+ 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalunya diterima, contoh: « %s » et « %s »',
+ 'New private project' => 'Projek peribadi baharu',
+ 'This project is private' => 'projek ini adalah peribadi',
+ 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru',
+ 'Add' => 'Tambah',
+ 'Start date' => 'Tarikh mula',
+ 'Time estimated' => 'Anggaran masa',
+ 'There is nothing assigned to you.' => 'Tidak ada yang diberikan kepada anda.',
+ 'My tasks' => 'Tugas saya',
+ 'Activity stream' => 'Arus aktifitas',
+ 'Dashboard' => 'Dasbor',
+ 'Confirmation' => 'Konfirmasi',
+ 'Allow everybody to access to this project' => 'Memungkinkan semua orang untuk mengakses projek ini',
+ 'Everybody have access to this project.' => 'Semua orang mendapat akses untuk projek ini.',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Create a comment from an external provider' => 'Buat komentar dari pemasok eksternal',
+ 'Project management' => 'Manajemen projek',
+ 'My projects' => 'projek saya',
+ 'Columns' => 'Kolom',
+ 'Task' => 'Tugas',
+ 'Your are not member of any project.' => 'Anda bukan anggota dari setiap projek.',
+ 'Percentage' => 'Persentasi',
+ 'Number of tasks' => 'Jumlah dari tugas',
+ 'Task distribution' => 'Pembagian tugas',
+ 'Reportings' => 'Pelaporan',
+ 'Task repartition for "%s"' => 'Pembagian tugas untuk « %s »',
+ 'Analytics' => 'Analitis',
+ 'Subtask' => 'Subtugas',
+ 'My subtasks' => 'Subtugas saya',
+ 'User repartition' => 'Partisi ulang pengguna',
+ 'User repartition for "%s"' => 'Partisi ulang pengguna untuk « %s »',
+ 'Clone this project' => 'Gandakan projek ini',
+ 'Column removed successfully.' => 'Kolom berhasil dihapus.',
+ 'Not enough data to show the graph.' => 'Tidak cukup data untuk menampilkan grafik.',
+ 'Previous' => 'Sebelumnya',
+ 'The id must be an integer' => 'Id harus integer',
+ 'The project id must be an integer' => 'Id projek harus integer',
+ 'The status must be an integer' => 'Status harus integer',
+ 'The subtask id is required' => 'Id subtugas diperlukan',
+ 'The subtask id must be an integer' => 'Id subtugas harus integer',
+ 'The task id is required' => 'Id tugas diperlukan',
+ 'The task id must be an integer' => 'Id tugas harus integer',
+ 'The user id must be an integer' => 'Id user harus integer',
+ 'This value is required' => 'Nilai ini diperlukan',
+ 'This value must be numeric' => 'Nilai ini harus angka',
+ 'Unable to create this task.' => 'Tidak dapat membuat tugas ini',
+ 'Cumulative flow diagram' => 'Diagram alir kumulatif',
+ 'Cumulative flow diagram for "%s"' => 'Diagram alir kumulatif untuk « %s »',
+ 'Daily project summary' => 'Ringkasan projek harian',
+ 'Daily project summary export' => 'Ekspot ringkasan projek harian',
+ 'Daily project summary export for "%s"' => 'Ekspor ringkasan projek harian untuk « %s »',
+ 'Exports' => 'Ekspor',
+ 'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.',
+ 'Nothing to preview...' => 'Tiada yang dapat diintai...',
+ 'Preview' => 'Intai',
+ 'Write' => 'Tulis',
+ 'Active swimlanes' => 'Swimlanes aktif',
+ 'Add a new swimlane' => 'Tambah swimlane baharu',
+ 'Change default swimlane' => 'Tukar piawai swimlane',
+ 'Default swimlane' => 'Piawai swimlane',
+ 'Do you really want to remove this swimlane: "%s"?' => 'Anda yakin untuk menghapus swimlane ini : « %s » ?',
+ 'Inactive swimlanes' => 'Swimlanes tidak aktif',
+ 'Remove a swimlane' => 'Padam swimlane',
+ 'Show default swimlane' => 'Tampilkan piawai swimlane',
+ 'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk projek « %s »',
+ 'Swimlane not found.' => 'Swimlane tidak ditemui.',
+ 'Swimlane removed successfully.' => 'Swimlane telah dipadamkan.',
+ 'Swimlanes' => 'Swimlanes',
+ 'Swimlane updated successfully.' => 'Swimlane telah dikemaskini.',
+ 'The default swimlane have been updated successfully.' => 'Standar swimlane berhasil diperbaharui.',
+ 'Unable to remove this swimlane.' => 'Tidak dapat menghapus swimlane ini.',
+ 'Unable to update this swimlane.' => 'Tidak dapat memperbaharui swimlane ini.',
+ 'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.',
+ 'Example: "Bug, Feature Request, Improvement"' => 'Contoh: « Insiden, Permintaan Ciri, Pembaikan »',
+ 'Default categories for new projects (Comma-separated)' => 'Piawaian kategori untuk projek baru (asingkan guna koma)',
+ 'Integrations' => 'Integrasi',
+ 'Integration with third-party services' => 'Integrasi dengan khidmat pihak ketiga',
+ 'Subtask Id' => 'Id Subtugas',
+ 'Subtasks' => 'Subtugas',
+ 'Subtasks Export' => 'Ekspot Subtugas',
+ 'Subtasks exportation for "%s"' => 'Ekspor subtugas untuk « %s »',
+ 'Task Title' => 'Judul Tugas',
+ 'Untitled' => 'Tanpa nama',
+ 'Application default' => 'Aplikasi Piawaian',
+ 'Language:' => 'Bahasa:',
+ 'Timezone:' => 'Zon masa:',
+ 'All columns' => 'Semua kolom',
+ 'Calendar' => 'Kalender',
+ 'Next' => 'Selanjutnya',
+ '#%d' => 'n°%d',
+ 'All swimlanes' => 'Semua swimlane',
+ 'All colors' => 'Semua warna',
+ 'Moved to column %s' => 'Pindah ke kolom %s',
+ 'Change description' => 'Ubah keterangan',
+ 'User dashboard' => 'Papan Kenyataan pengguna',
+ 'Allow only one subtask in progress at the same time for a user' => 'Izinkan hanya satu subtugas dalam proses secara bersamaan untuk satu pengguna',
+ 'Edit column "%s"' => 'Modifikasi kolom « %s »',
+ 'Select the new status of the subtask: "%s"' => 'Pilih status baru untuk subtugas : « %s »',
+ 'Subtask timesheet' => 'Subtugas absen',
+ 'There is nothing to show.' => 'Tidak ada yang dapat diperlihatkan.',
+ 'Time Tracking' => 'Pelacakan waktu',
+ 'You already have one subtask in progress' => 'Anda sudah ada satu subtugas dalam proses',
+ 'Which parts of the project do you want to duplicate?' => 'Bagian dalam projek mana yang ingin anda duplikasi?',
+ 'Disallow login form' => 'Larang formulir masuk',
+ 'Start' => 'Mula',
+ 'End' => 'Selesai',
+ 'Task age in days' => 'Usia tugas dalam bentuk harian',
+ 'Days in this column' => 'Hari dalam kolom ini',
+ '%dd' => '%dj',
+ 'Add a link' => 'Menambahkan pautan',
+ 'Add a new link' => 'Tambah Pautan baru',
+ 'Do you really want to remove this link: "%s"?' => 'Anda yakin akan menghapus Pautan ini : « %s » ?',
+ 'Do you really want to remove this link with task #%d?' => 'Anda yakin akan menghapus Pautan ini dengan tugas n°%d ?',
+ 'Field required' => 'Medan diperlukan',
+ 'Link added successfully.' => 'Pautan berhasil ditambahkan.',
+ 'Link updated successfully.' => 'Pautan berhasil diperbaharui.',
+ 'Link removed successfully.' => 'Pautan berhasil dihapus.',
+ 'Link labels' => 'Label Pautan',
+ 'Link modification' => 'Modifikasi Pautan',
+ 'Links' => 'Pautan',
+ 'Link settings' => 'Pengaturan Pautan',
+ 'Opposite label' => 'Label berlawanan',
+ 'Remove a link' => 'Hapus Pautan',
+ 'Task\'s links' => 'Pautan tugas',
+ 'The labels must be different' => 'Label harus berbeda',
+ 'There is no link.' => 'Tidak ada Pautan.',
+ 'This label must be unique' => 'Label ini harus unik',
+ 'Unable to create your link.' => 'Tidak dapat membuat Pautan anda.',
+ 'Unable to update your link.' => 'Tidak dapat memperbaharui Pautan anda.',
+ 'Unable to remove this link.' => 'Tidak dapat menghapus Pautan ini.',
+ 'relates to' => 'berhubungan dengan',
+ 'blocks' => 'blok',
+ 'is blocked by' => 'diblokir oleh',
+ 'duplicates' => 'duplikat',
+ 'is duplicated by' => 'diduplikasi oleh',
+ 'is a child of' => 'anak dari',
+ 'is a parent of' => 'orant tua dari',
+ 'targets milestone' => 'milestone target',
+ 'is a milestone of' => 'adalah milestone dari',
+ 'fixes' => 'perbaikan',
+ 'is fixed by' => 'diperbaiki oleh',
+ 'This task' => 'Tugas ini',
+ '<1h' => '<1h',
+ '%dh' => '%dh',
+ 'Expand tasks' => 'Perluas tugas',
+ 'Collapse tasks' => 'Lipat tugas',
+ 'Expand/collapse tasks' => 'Perluas/lipat tugas',
+ 'Close dialog box' => 'Tutup kotak dialog',
+ 'Submit a form' => 'Submit formulir',
+ 'Board view' => 'Table halaman',
+ 'Keyboard shortcuts' => 'pintas keyboard',
+ 'Open board switcher' => 'Buka table switcher',
+ 'Application' => 'Aplikasi',
+ 'Compact view' => 'Tampilan kompak',
+ 'Horizontal scrolling' => 'Horisontal bergulir',
+ 'Compact/wide view' => 'Beralih antara tampilan kompak dan diperluas',
+ 'No results match:' => 'Tidak ada hasil :',
+ 'Currency' => 'Mata uang',
+ 'Private project' => 'projek pribadi',
+ 'AUD - Australian Dollar' => 'AUD - Dollar Australia',
+ 'CAD - Canadian Dollar' => 'CAD - Dollar Kanada',
+ 'CHF - Swiss Francs' => 'CHF - Swiss Prancis',
+ 'Custom Stylesheet' => 'Kustomisasi Stylesheet',
+ 'download' => 'unduh',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'GBP - British Pound' => 'GBP - Poundsterling inggris',
+ 'INR - Indian Rupee' => 'INR - Rupe India',
+ 'JPY - Japanese Yen' => 'JPY - Yen Jepang',
+ 'NZD - New Zealand Dollar' => 'NZD - Dollar Selandia baru',
+ 'RSD - Serbian dinar' => 'RSD - Dinar Serbia',
+ 'USD - US Dollar' => 'USD - Dollar Amerika',
+ 'Destination column' => 'Kolom tujuan',
+ 'Move the task to another column when assigned to a user' => 'Pindahkan tugas ke kolom lain ketika ditugaskan ke pengguna',
+ 'Move the task to another column when assignee is cleared' => 'Pindahkan tugas ke kolom lain ketika orang yang ditugaskan dibersihkan',
+ 'Source column' => 'Sumber kolom',
+ 'Transitions' => 'Transisi',
+ 'Executer' => 'Eksekusi',
+ 'Time spent in the column' => 'Waktu yang dihabiskan dalam kolom',
+ 'Task transitions' => 'Transisi tugas',
+ 'Task transitions export' => 'Ekspor transisi tugas',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Laporan ini berisi semua kolom yang pindah untuk setiap tugas dengan tanggal, pengguna dan waktu yang dihabiskan untuk setiap transisi.',
+ 'Currency rates' => 'Nilai tukar mata uang',
+ 'Rate' => 'Tarif',
+ 'Change reference currency' => 'Mengubah referensi mata uang',
+ 'Add a new currency rate' => 'Tambahkan nilai tukar mata uang baru',
+ 'Reference currency' => 'Referensi mata uang',
+ 'The currency rate have been added successfully.' => 'Nilai tukar mata uang berhasil ditambahkan.',
+ 'Unable to add this currency rate.' => 'Tidak dapat menambahkan nilai tukar mata uang',
+ 'Webhook URL' => 'URL webhook',
+ '%s remove the assignee of the task %s' => '%s menghapus penugasan dari tugas %s',
+ 'Enable Gravatar images' => 'Mengaktifkan gambar Gravatar',
+ 'Information' => 'Informasi',
+ 'Check two factor authentication code' => 'Cek dua faktor kode otentifikasi',
+ 'The two factor authentication code is not valid.' => 'Kode dua faktor kode otentifikasi tidak valid.',
+ 'The two factor authentication code is valid.' => 'Kode dua faktor kode otentifikasi valid.',
+ 'Code' => 'Kode',
+ 'Two factor authentication' => 'Dua faktor otentifikasi',
+ 'This QR code contains the key URI: ' => 'kode QR ini mengandung kunci URI : ',
+ 'Check my code' => 'Memeriksa kode saya',
+ 'Secret key: ' => 'Kunci rahasia : ',
+ 'Test your device' => 'Menguji perangkat anda',
+ 'Assign a color when the task is moved to a specific column' => 'Menetapkan warna ketika tugas tersebut dipindahkan ke kolom tertentu',
+ '%s via Kanboard' => '%s via Kanboard',
+ 'Burndown chart for "%s"' => 'Grafik Burndown untku « %s »',
+ 'Burndown chart' => 'Grafik Burndown',
+ 'This chart show the task complexity over the time (Work Remaining).' => 'Grafik ini menunjukkan kompleksitas tugas dari waktu ke waktu (Sisa Pekerjaan).',
+ 'Screenshot taken %s' => 'Screenshot diambil %s',
+ 'Add a screenshot' => 'Tambah screenshot',
+ // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
+ 'Screenshot uploaded successfully.' => 'Screenshot berhasil diunggah.',
+ 'SEK - Swedish Krona' => 'SEK - Krona Swedia',
+ 'Identifier' => 'Identifier',
+ 'Disable two factor authentication' => 'Matikan dua faktor otentifikasi',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Apakah anda yakin akan mematikan dua faktor otentifikasi untuk pengguna ini : « %s » ?',
+ 'Edit link' => 'Modifikasi Pautan',
+ 'Start to type task title...' => 'Mulai mengetik judul tugas...',
+ 'A task cannot be linked to itself' => 'Sebuah tugas tidak dapat dikaitkan dengan dirinya sendiri',
+ 'The exact same link already exists' => 'Pautan yang sama persis sudah ada',
+ 'Recurrent task is scheduled to be generated' => 'Tugas berulang dijadwalkan akan dihasilkan',
+ 'Score' => 'Skor',
+ 'The identifier must be unique' => 'Identifier harus unik',
+ 'This linked task id doesn\'t exists' => 'Id tugas terkait tidak ada',
+ 'This value must be alphanumeric' => 'Nilai harus alfanumerik',
+ 'Edit recurrence' => 'Modifikasi pengulangan',
+ 'Generate recurrent task' => 'Menghasilkan tugas berulang',
+ 'Trigger to generate recurrent task' => 'Memicu untuk menghasilkan tugas berulang',
+ 'Factor to calculate new due date' => 'Faktor untuk menghitung tanggal jatuh tempo baru',
+ 'Timeframe to calculate new due date' => 'Jangka waktu untuk menghitung tanggal jatuh tempo baru',
+ 'Base date to calculate new due date' => 'Tanggal dasar untuk menghitung tanggal jatuh tempo baru',
+ 'Action date' => 'Tanggal aksi',
+ 'Base date to calculate new due date: ' => 'Tanggal dasar untuk menghitung tanggal jatuh tempo baru: ',
+ 'This task has created this child task: ' => 'Tugas ini telah menciptakan tugas anak ini: ',
+ 'Day(s)' => 'Hari',
+ 'Existing due date' => 'Batas waktu yang ada',
+ 'Factor to calculate new due date: ' => 'Faktor untuk menghitung tanggal jatuh tempo baru: ',
+ 'Month(s)' => 'Bulan',
+ 'Recurrence' => 'Pengulangan',
+ 'This task has been created by: ' => 'Tugas ini telah dibuat oleh:',
+ 'Recurrent task has been generated:' => 'Tugas berulang telah dihasilkan:',
+ 'Timeframe to calculate new due date: ' => 'Jangka waktu untuk menghitung tanggal jatuh tempo baru: ',
+ 'Trigger to generate recurrent task: ' => 'Pemicu untuk menghasilkan tugas berulang: ',
+ 'When task is closed' => 'Ketika tugas ditutup',
+ 'When task is moved from first column' => 'Ketika tugas dipindahkan dari kolom pertama',
+ 'When task is moved to last column' => 'Ketika tugas dipindahkan ke kolom terakhir',
+ 'Year(s)' => 'Tahun',
+ 'Calendar settings' => 'Pengaturan kalender',
+ 'Project calendar view' => 'Tampilan kalender projek',
+ 'Project settings' => 'Pengaturan projek',
+ 'Show subtasks based on the time tracking' => 'Tampilkan subtugas berdasarkan pelacakan waktu',
+ 'Show tasks based on the creation date' => 'Tampilkan tugas berdasarkan tanggal pembuatan',
+ 'Show tasks based on the start date' => 'Tampilkan tugas berdasarkan tanggal mulai',
+ 'Subtasks time tracking' => 'Pelacakan waktu subtgas',
+ 'User calendar view' => 'Pengguna tampilan kalender',
+ 'Automatically update the start date' => 'Otomatikkan pengemaskinian tanggal',
+ 'iCal feed' => 'iCal feed',
+ 'Preferences' => 'Keutamaan',
+ 'Security' => 'Keamanan',
+ 'Two factor authentication disabled' => 'Otentifikasi dua faktor dimatikan',
+ 'Two factor authentication enabled' => 'Otentifikasi dua faktor dihidupkan',
+ 'Unable to update this user.' => 'Tidak dapat memperbarui pengguna ini.',
+ 'There is no user management for private projects.' => 'Tidak ada manajemen pengguna untuk projek-projek pribadi.',
+ 'User that will receive the email' => 'Pengguna yang akan menerima email',
+ 'Email subject' => 'Subjek Emel',
+ 'Date' => 'Tanggal',
+ 'Add a comment log when moving the task between columns' => 'Menambahkan log komentar ketika memindahkan tugas antara kolom',
+ 'Move the task to another column when the category is changed' => 'Pindahkan tugas ke kolom lain ketika kategori berubah',
+ 'Send a task by email to someone' => 'Kirim tugas melalui email ke seseorang',
+ 'Reopen a task' => 'Membuka kembali tugas',
+ 'Column change' => 'Kolom berubah',
+ 'Position change' => 'Posisi berubah',
+ 'Swimlane change' => 'Swimlane berubah',
+ 'Assignee change' => 'Penerima berubah',
+ '[%s] Overdue tasks' => '[%s] Tugas terlambat',
+ 'Notification' => 'Pemberitahuan',
+ '%s moved the task #%d to the first swimlane' => '%s memindahkan tugas n°%d ke swimlane pertama',
+ '%s moved the task #%d to the swimlane "%s"' => '%s memindahkan tugas n°%d ke swimlane « %s »',
+ 'Swimlane' => 'Swimlane',
+ 'Gravatar' => 'Gravatar',
+ '%s moved the task %s to the first swimlane' => '%s memindahkan tugas %s ke swimlane pertama',
+ '%s moved the task %s to the swimlane "%s"' => '%s memindahkan tugas %s ke swimlane « %s »',
+ 'This report contains all subtasks information for the given date range.' => 'Laporan ini berisi semua informasi subtugas untuk rentang tanggal tertentu.',
+ 'This report contains all tasks information for the given date range.' => 'Laporan ini berisi semua informasi tugas untuk rentang tanggal tertentu.',
+ 'Project activities for %s' => 'Aktifitas projek untuk « %s »',
+ 'view the board on Kanboard' => 'lihat papan di Kanboard',
+ 'The task have been moved to the first swimlane' => 'Tugas telah dipindahkan ke swimlane pertama',
+ 'The task have been moved to another swimlane:' => 'Tugas telah dipindahkan ke swimlane lain:',
+ 'Overdue tasks for the project "%s"' => 'Tugas terlambat untuk projek « %s »',
+ 'New title: %s' => 'Judul baru : %s',
+ 'The task is not assigned anymore' => 'Tugas tidak ditugaskan lagi',
+ 'New assignee: %s' => 'Penerima baru : %s',
+ 'There is no category now' => 'Tidak ada kategori untuk sekarang',
+ 'New category: %s' => 'Kategori baru : %s',
+ 'New color: %s' => 'Warna baru : %s',
+ 'New complexity: %d' => 'Kompleksitas baru : %d',
+ 'The due date have been removed' => 'Tanggal jatuh tempo telah dihapus',
+ 'There is no description anymore' => 'Tidak ada deskripsi lagi',
+ 'Recurrence settings have been modified' => 'Pengaturan pengulangan telah dimodifikasi',
+ 'Time spent changed: %sh' => 'Waktu yang dihabiskan berubah : %sh',
+ 'Time estimated changed: %sh' => 'Perkiraan waktu berubah : %sh',
+ 'The field "%s" have been updated' => 'Field « %s » telah diperbaharui',
+ 'The description have been modified' => 'Deskripsi telah dimodifikasi',
+ 'Do you really want to close the task "%s" as well as all subtasks?' => 'Apakah anda yakin akan menutup tugas « %s » beserta semua sub-tugasnya ?',
+ 'I want to receive notifications for:' => 'Saya ingin menerima pemberitahuan untuk :',
+ 'All tasks' => 'Semua tugas',
+ 'Only for tasks assigned to me' => 'Hanya untuk tugas yang ditugaskan ke saya',
+ 'Only for tasks created by me' => 'Hanya untuk tugas yang dibuat oleh saya',
+ 'Only for tasks created by me and assigned to me' => 'Hanya untuk tugas yang dibuat oleh saya dan ditugaskan ke saya',
+ '%%Y-%%m-%%d' => '%%d/%%m/%%Y',
+ 'Total for all columns' => 'Total untuk semua kolom',
+ 'You need at least 2 days of data to show the chart.' => 'Anda memerlukan setidaknya 2 hari dari data yang menunjukkan grafik.',
+ '<15m' => '<15m',
+ '<30m' => '<30m',
+ 'Stop timer' => 'Hentikan timer',
+ 'Start timer' => 'Mulai timer',
+ 'Add project member' => 'Tambahkan anggota projek',
+ 'Enable notifications' => 'Aktifkan pemberitahuan',
+ 'My activity stream' => 'Aliran kegiatan saya',
+ 'My calendar' => 'Kalender saya',
+ 'Search tasks' => 'Cari tugas',
+ 'Back to the calendar' => 'Kembali ke kalender',
+ 'Filters' => 'Filter',
+ 'Reset filters' => 'Reset ulang filter',
+ 'My tasks due tomorrow' => 'Tugas saya yang berakhir besok',
+ 'Tasks due today' => 'Tugas yang berakhir hari ini',
+ 'Tasks due tomorrow' => 'Tugas yang berakhir besok',
+ 'Tasks due yesterday' => 'Tugas yang berakhir kemarin',
+ 'Closed tasks' => 'Tugas yang ditutup',
+ 'Open tasks' => 'Buka Tugas',
+ 'Not assigned' => 'Tidak ditugaskan',
+ 'View advanced search syntax' => 'Lihat sintaks pencarian lanjutan',
+ 'Overview' => 'Ikhtisar',
+ 'Board/Calendar/List view' => 'Tampilan Papan/Kalender/Daftar',
+ 'Switch to the board view' => 'Beralih ke tampilan papan',
+ 'Switch to the calendar view' => 'Beralih ke tampilan kalender',
+ 'Switch to the list view' => 'Beralih ke tampilan daftar',
+ 'Go to the search/filter box' => 'Pergi ke kotak pencarian/filter',
+ 'There is no activity yet.' => 'Tidak ada aktifitas saat ini.',
+ 'No tasks found.' => 'Tidak ada tugas yang ditemukan.',
+ 'Keyboard shortcut: "%s"' => 'Keyboard shortcut : « %s »',
+ 'List' => 'Daftar',
+ 'Filter' => 'Filter',
+ 'Advanced search' => 'Pencarian lanjutan',
+ 'Example of query: ' => 'Contoh dari query : ',
+ 'Search by project: ' => 'Pencarian berdasarkan projek : ',
+ 'Search by column: ' => 'Pencarian berdasarkan kolom : ',
+ 'Search by assignee: ' => 'Pencarian berdasarkan penerima : ',
+ 'Search by color: ' => 'Pencarian berdasarkan warna : ',
+ 'Search by category: ' => 'Pencarian berdasarkan kategori : ',
+ 'Search by description: ' => 'Pencarian berdasarkan deskripsi : ',
+ 'Search by due date: ' => 'Pencarian berdasarkan tanggal jatuh tempo : ',
+ 'Lead and Cycle time for "%s"' => 'Memimpin dan Siklus waktu untuk « %s »',
+ 'Average time spent into each column for "%s"' => 'Rata-rata waktu yang dihabiskan dalam setiap kolom untuk « %s »',
+ 'Average time spent into each column' => 'Rata-rata waktu yang dihabiskan dalam setiap kolom',
+ 'Average time spent' => 'Rata-rata waktu yang dihabiskan',
+ 'This chart show the average time spent into each column for the last %d tasks.' => 'Grafik ini menunjukkan rata-rata waktu yang dihabiskan dalam setiap kolom untuk %d tugas.',
+ 'Average Lead and Cycle time' => 'Rata-rata Memimpin dan Siklus waktu',
+ 'Average lead time: ' => 'Rata-rata waktu pimpinan : ',
+ 'Average cycle time: ' => 'Rata-rata siklus waktu : ',
+ 'Cycle Time' => 'Siklus Waktu',
+ 'Lead Time' => 'Lead Time',
+ 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Grafik ini menunjukkan memimpin rata-rata dan waktu siklus untuk %d tugas terakhir dari waktu ke waktu.',
+ 'Average time into each column' => 'Rata-rata waktu ke setiap kolom',
+ 'Lead and cycle time' => 'Lead dan siklus waktu',
+ 'Lead time: ' => 'Lead time : ',
+ 'Cycle time: ' => 'Siklus waktu : ',
+ 'Time spent into each column' => 'Waktu yang dihabiskan di setiap kolom',
+ 'The lead time is the duration between the task creation and the completion.' => 'Lead time adalah durasi antara pembuatan tugas dan penyelesaian.',
+ 'The cycle time is the duration between the start date and the completion.' => 'Siklus waktu adalah durasi antara tanggal mulai dan tanggal penyelesaian.',
+ 'If the task is not closed the current time is used instead of the completion date.' => 'Jika tugas tidak ditutup waktu saat ini yang digunakan sebagai pengganti tanggal penyelesaian.',
+ 'Set automatically the start date' => 'Secara otomatis mengatur tanggal mulai',
+ 'Edit Authentication' => 'Modifikasi Otentifikasi',
+ 'Remote user' => 'Pengguna jauh',
+ 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata laluan mereka dalam basis data Kanboard, contoh: Akaun LDAP, Google dan Github.',
+ 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.',
+ 'New remote user' => 'Pengguna baru jauh',
+ 'New local user' => 'Pengguna baru lokal',
+ 'Default task color' => 'Standar warna tugas',
+ 'This feature does not work with all browsers.' => 'Ciri ini tidak dapat digunakan pada semua browsers',
+ 'There is no destination project available.' => 'Tiada destinasi projek yang tersedia.',
+ 'Trigger automatically subtask time tracking' => 'Picu pengesanan subtugas secara otomatik',
+ 'Include closed tasks in the cumulative flow diagram' => 'Termasuk tugas yang ditutup pada diagram aliran kumulatif',
+ 'Current swimlane: %s' => 'Swimlane saat ini : %s',
+ 'Current column: %s' => 'Kolom saat ini : %s',
+ 'Current category: %s' => 'Kategori saat ini : %s',
+ 'no category' => 'tiada kategori',
+ 'Current assignee: %s' => 'Saat ini ditugaskan pada: %s',
+ 'not assigned' => 'Belum ditugaskan',
+ 'Author:' => 'Penulis:',
+ 'contributors' => 'Penggiat',
+ 'License:' => 'Lesen:',
+ 'License' => 'Lesen',
+ 'Enter the text below' => 'Masukkan teks di bawah',
+ 'Gantt chart for %s' => 'Carta Gantt untuk %s',
+ 'Sort by position' => 'Urutkan berdasarkan posisi',
+ 'Sort by date' => 'Urutkan berdasarkan tanggal',
+ 'Add task' => 'Tambah tugas',
+ 'Start date:' => 'Tanggal mulai:',
+ 'Due date:' => 'Batas waktu:',
+ 'There is no start date or due date for this task.' => 'Tiada tanggal mulai dan batas waktu untuk tugas ini.',
+ 'Moving or resizing a task will change the start and due date of the task.' => 'Memindahkan atau mengubah ukuran tugas anda akan mengubah tanggal mulai dan batas waktu dari tugas ini.',
+ 'There is no task in your project.' => 'Tiada tugas didalam projek anda.',
+ 'Gantt chart' => 'Carta Gantt',
+ 'People who are project managers' => 'Orang-orang yang menjadi pengurus projek',
+ 'People who are project members' => 'Orang-orang yang menjadi anggota projek',
+ 'NOK - Norwegian Krone' => 'NOK - Krone Norwegia',
+ 'Show this column' => 'Perlihatkan kolom ini',
+ 'Hide this column' => 'Sembunyikan kolom ini',
+ 'open file' => 'buka fail',
+ 'End date' => 'Waktu berakhir',
+ 'Users overview' => 'Ikhtisar pengguna',
+ 'Members' => 'Anggota',
+ 'Shared project' => 'projek bersama',
+ 'Project managers' => 'Pengurus projek',
+ 'Gantt chart for all projects' => 'Carta Gantt untuk kesemua projek',
+ 'Projects list' => 'Senarai projek',
+ 'Gantt chart for this project' => 'Carta Gantt untuk projek ini',
+ 'Project board' => 'Papan projek',
+ 'End date:' => 'Waktu berakhir :',
+ 'There is no start date or end date for this project.' => 'Tidak ada waktu mula atau waktu berakhir pada projek ini',
+ 'Projects Gantt chart' => 'projekkan carta Gantt',
+ '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',
+ 'Documentation: %s' => 'Dokumentasi : %s',
+ 'Switch to the Gantt chart view' => 'Beralih ke tampilan Carta Gantt',
+ 'Reset the search/filter box' => 'Tetap semula pencarian/saringan',
+ 'Documentation' => 'Dokumentasi',
+ 'Table of contents' => 'Isi kandungan',
+ 'Gantt' => 'Gantt',
+ // 'Author' => '',
+ // 'Version' => '',
+ // 'Plugins' => '',
+ // 'There is no plugin loaded.' => '',
+ // 'Set maximum column height' => '',
+ // 'Remove maximum column height' => '',
+ // 'My notifications' => '',
+ // 'Custom filters' => '',
+ // 'Your custom filter have been created successfully.' => '',
+ // 'Unable to create your custom filter.' => '',
+ // 'Custom filter removed successfully.' => '',
+ // 'Unable to remove this custom filter.' => '',
+ // 'Edit custom filter' => '',
+ // 'Your custom filter have been updated successfully.' => '',
+ // 'Unable to update custom filter.' => '',
+ // 'Web' => '',
+ // 'New attachment on task #%d: %s' => '',
+ // 'New comment on task #%d' => '',
+ // 'Comment updated on task #%d' => '',
+ // 'New subtask on task #%d' => '',
+ // 'Subtask updated on task #%d' => '',
+ // 'New task #%d: %s' => '',
+ // 'Task updated #%d' => '',
+ // 'Task #%d closed' => '',
+ // 'Task #%d opened' => '',
+ // 'Column changed for task #%d' => '',
+ // 'New position for task #%d' => '',
+ // 'Swimlane changed for task #%d' => '',
+ // 'Assignee changed on task #%d' => '',
+ // '%d overdue tasks' => '',
+ // 'Task #%d is overdue' => '',
+ // 'No new notifications.' => '',
+ // 'Mark all as read' => '',
+ // 'Mark as read' => '',
+ // 'Total number of tasks in this column across all swimlanes' => '',
+ // 'Collapse swimlane' => '',
+ // 'Expand swimlane' => '',
+ // 'Add a new filter' => '',
+ // 'Share with all project members' => '',
+ // 'Shared' => '',
+ // 'Owner' => '',
+ // 'Unread notifications' => '',
+ // 'My filters' => '',
+ // 'Notification methods:' => '',
+ // 'Import tasks from CSV file' => '',
+ // 'Unable to read your file' => '',
+ // '%d task(s) have been imported successfully.' => '',
+ // 'Nothing have been imported!' => '',
+ // 'Import users from CSV file' => '',
+ // '%d user(s) have been imported successfully.' => '',
+ // 'Comma' => '',
+ // 'Semi-colon' => '',
+ // 'Tab' => '',
+ // 'Vertical bar' => '',
+ // 'Double Quote' => '',
+ // 'Single Quote' => '',
+ // '%s attached a file to the task #%d' => '',
+ // 'There is no column or swimlane activated in your project!' => '',
+ // 'Append filter (instead of replacement)' => '',
+ // 'Append/Replace' => '',
+ // 'Append' => '',
+ // 'Replace' => '',
+ // 'Import' => '',
+ // 'change sorting' => '',
+ // 'Tasks Importation' => '',
+ // 'Delimiter' => '',
+ // 'Enclosure' => '',
+ // 'CSV File' => '',
+ // 'Instructions' => '',
+ // 'Your file must use the predefined CSV format' => '',
+ // 'Your file must be encoded in UTF-8' => '',
+ // 'The first row must be the header' => '',
+ // 'Duplicates are not verified for you' => '',
+ // 'The due date must use the ISO format: YYYY-MM-DD' => '',
+ // 'Download CSV template' => '',
+ // 'No external integration registered.' => '',
+ // 'Duplicates are not imported' => '',
+ // 'Usernames must be lowercase and unique' => '',
+ // 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ 'Role:' => 'Peranan',
+ 'Project members' => 'Anggota projek',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ 'Creation' => 'Ciptaan',
+ 'Expiration' => 'Jangka hayat',
+ 'Password reset history' => 'Sirah tetap semula kata laluan',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
+);
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 598a15c9..229e4994 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Fjern prosjekt',
'Edit the board for "%s"' => 'Endre prosjektsiden for "%s"',
'All projects' => 'Alle prosjekter',
- 'Change columns' => 'Endre kolonner',
'Add a new column' => 'Legg til en ny kolonne',
'Title' => 'Tittel',
'Nobody assigned' => 'Ikke tildelt',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Åpne en oppgave',
'Do you really want to open this task: "%s"?' => 'Vil du åpne denne oppgaven: "%s"?',
'Back to the board' => 'Tilbake til prosjektsiden',
- 'Created on %B %e, %Y at %k:%M %p' => 'Opprettet %d.%m.%Y - %H:%M',
'There is nobody assigned' => 'Mangler tildeling',
'Column on the board:' => 'Kolonne:',
- 'Status is open' => 'Status: åpen',
- 'Status is closed' => 'Status: lukket',
'Close this task' => 'Lukk oppgaven',
'Open this task' => 'Åpne denne oppgaven',
'There is no description.' => 'Det er ingen beskrivelse.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Id\'en er pøøkrevet',
'The project id is required' => 'Prosjektet-id er påkrevet',
'The project name is required' => 'Prosjektnavn er påkrevet',
- 'This project must be unique' => 'Prosjektnavnet skal være unikt',
'The title is required' => 'Tittel er pårevet',
'Settings saved successfully.' => 'Innstillinger lagret.',
'Unable to save your settings.' => 'Innstillinger kunne ikke lagres.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Under arbeid',
'Done' => 'Utført',
'Application version:' => 'Versjon:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Fullført %d.%m.%Y - %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d.%m.%Y - %H:%M',
- 'Date created' => 'Dato for opprettelse',
- 'Date completed' => 'Dato for fullført',
'Id' => 'ID',
'%d closed tasks' => '%d lukkede oppgaver',
'No task for this project' => 'Ingen oppgaver i dette prosjektet',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Kompleksitet',
'Task limit' => 'Oppgave begrensning',
'Task count' => 'Antall oppgaver',
- 'Edit project access list' => 'Endre tillatelser for prosjektet',
- 'Allow this user' => 'Tillat denne brukeren',
- 'Don\'t forget that administrators have access to everything.' => 'Husk at administratorer har tilgang til alt.',
- 'Revoke' => 'Fjern',
- 'List of authorized users' => 'Liste over autoriserte brukere',
'User' => 'Bruker',
- 'Nobody have access to this project.' => 'Ingen har tilgang til dette prosjektet.',
'Comments' => 'Kommentarer',
'Write your text in Markdown' => 'Skriv din tekst i markdown',
'Leave a comment' => 'Legg inn en kommentar',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Rediger oppgaven',
'Due Date' => 'Forfallsdato',
'Invalid date' => 'Ugyldig dato',
- 'Must be done before %B %e, %Y' => 'Skal være utført innen %d.%m.%Y',
- '%B %e, %Y' => '%d.%m.%Y',
- // '%b %e, %Y' => '',
'Automatic actions' => 'Automatiske handlinger',
'Your automatic action have been created successfully.' => 'Din automatiske handling er opprettet.',
'Unable to create your automatic action.' => 'Din automatiske handling kunne ikke opprettes.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Tildel en farge til en bestemt bruker',
'Column title' => 'Kolonne tittel',
'Position' => 'Posisjon',
- 'Move Up' => 'Flytt opp',
- 'Move Down' => 'Flytt ned',
'Duplicate to another project' => 'Kopier til et annet prosjekt',
'Duplicate' => 'Kopier',
'link' => 'link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Kommentaren ble fjernet.',
'Unable to remove this comment.' => 'Kommentaren kunne ikke fjernes.',
'Do you really want to remove this comment?' => 'Vil du fjerne denne kommentaren?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Kun administrator eller brukeren, som har oprettet kommentaren har adgang til denne siden.',
'Current password for the user "%s"' => 'Aktivt passord for brukeren "%s"',
'The current password is required' => 'Passord er påkrevet',
'Wrong password' => 'Feil passord',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'Epost',
- 'Link my Google Account' => 'Knytt til min Google-konto',
- 'Unlink my Google Account' => 'Fjern knytningen til min Google-konto',
- 'Login with my Google Account' => 'Login med min Google-konto',
- 'Project not found.' => 'Prosjekt ikke funnet.',
'Task removed successfully.' => 'Oppgaven er fjernet.',
'Unable to remove this task.' => 'Oppgaven kunne ikke fjernes.',
'Remove a task' => 'Fjern en oppgave',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maksimum størrelse: ',
'Unable to upload the file.' => 'Filen kunne ikke lastes opp.',
'Display another project' => 'Vis annet prosjekt...',
- // 'Login with my Github Account' => '',
- // 'Link my Github Account' => '',
- // 'Unlink my Github Account' => '',
'Created by %s' => 'Opprettet av %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Sist endret %d.%m.%Y - %H:%M',
'Tasks Export' => 'Oppgave eksport',
'Tasks exportation for "%s"' => 'Oppgaveeksportering for "%s"',
'Start Date' => 'Start-dato',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Jeg vil kun ha varslinger for disse prosjekter:',
'view the task on Kanboard' => 'se oppgaven påhovedsiden',
'Public access' => 'Offentlig tilgang',
- 'User management' => 'Brukere',
'Active tasks' => 'Aktive oppgaver',
'Disable public access' => 'Deaktiver offentlig tilgang',
'Enable public access' => 'Aktiver offentlig tilgang',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Epost:',
'Notifications:' => 'Varslinger:',
'Notifications' => 'Varslinger',
- 'Group:' => 'Gruppe:',
- 'Regular user' => 'Normal bruker',
'Account type:' => 'Konto type:',
'Edit profile' => 'Rediger profil',
'Change password' => 'Endre passord',
'Password modification' => 'Passordendring',
'External authentications' => 'Ekstern godkjenning',
- 'Google Account' => 'Google-konto',
- 'Github Account' => 'GitHub-konto',
'Never connected.' => 'Aldri innlogget.',
- 'No account linked.' => 'Ingen kontoer knyttet.',
- 'Account linked.' => 'Konto knyttet.',
'No external authentication enabled.' => 'Ingen eksterne godkjenninger aktiveret.',
'Password modified successfully.' => 'Passord er endret.',
'Unable to change the password.' => 'Passordet kuenne ikke endres.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s endret ansvarlig for oppgaven %s til %s',
'New password for the user "%s"' => 'Nytt passord for brukeren "%s"',
'Choose an event' => 'Velg en hendelse',
- 'Github commit received' => 'Github forpliktelse mottatt',
- 'Github issue opened' => 'Github problem åpnet',
- 'Github issue closed' => 'Github problem lukket',
- 'Github issue reopened' => 'Github problem gjenåpnet',
- 'Github issue assignee change' => 'Endre ansvarlig for Github problem',
- 'Github issue label change' => 'Endre etikett for Github problem',
'Create a task from an external provider' => 'Oppret en oppgave fra en ekstern tilbyder',
'Change the assignee based on an external username' => 'Endre ansvarlige baseret på et eksternt brukernavn',
'Change the category based on an external label' => 'Endre kategorien basert på en ekstern etikett',
'Reference' => 'Referanse',
- 'Reference: %s' => 'Referanse: %s',
'Label' => 'Etikett',
'Database' => 'Database',
'About' => 'Om',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frekevens i sekunder (60 sekunder som standard)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvens i sekunder (0 for øt deaktivere denne funksjonen, 10 sekunder som standard)',
'Application URL' => 'Applikasjons URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Eksempel: http://example.kanboard.net/ (bruges til email notifikationer)',
'Token regenerated.' => 'Token regenerert.',
'Date format' => 'Datoformat',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er alltid akseptert, eksempelvis: "%s" og "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Estimert tid: %s timer',
- 'Time spent: %s hours' => 'Tid brukt: %s timer',
- 'Started on %B %e, %Y' => 'Startet %d.%m.%Y ',
'Start date' => 'Start dato',
'Time estimated' => 'Tid estimert',
'There is nothing assigned to you.' => 'Ingen er tildelt deg.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Alle har tilgang til dette prosjektet',
// 'Webhooks' => '',
// 'API' => '',
- // 'Github webhooks' => '',
- // 'Help on Github webhooks' => '',
'Create a comment from an external provider' => 'Opprett en kommentar fra en ekstern tilbyder',
- // 'Github issue comment created' => '',
'Project management' => 'Prosjektinnstillinger',
'My projects' => 'Mine prosjekter',
'Columns' => 'Kolonner',
@@ -517,7 +467,6 @@ return array(
// 'User repartition for "%s"' => '',
'Clone this project' => 'Kopier dette prosjektet',
'Column removed successfully.' => 'Kolonne flyttet',
- // 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
'Previous' => 'Forrige',
// 'The id must be an integer' => '',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Standard svømmebane',
// 'Do you really want to remove this swimlane: "%s"?' => '',
// 'Inactive swimlanes' => '',
- 'Set project manager' => 'Velg prosjektleder',
- 'Set project member' => 'Velg prosjektmedlem',
'Remove a swimlane' => 'Fjern en svømmebane',
- 'Rename' => 'Endre navn',
'Show default swimlane' => 'Vis standard svømmebane',
// 'Swimlane modification for the project "%s"' => '',
'Swimlane not found.' => 'Svømmebane ikke funnet',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Svømmebaner',
'Swimlane updated successfully.' => 'Svømmebane oppdatert',
// 'The default swimlane have been updated successfully.' => '',
- // 'Unable to create your swimlane.' => '',
// '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)' => '',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
'Integrations' => 'Integrasjoner',
'Integration with third-party services' => 'Integrasjoner med tredje-parts tjenester',
- 'Role for this project' => 'Rolle for dette prosjektet',
- 'Project manager' => 'Prosjektleder',
- 'Project member' => 'Prosjektmedlem',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Prosjektlederen kan endre flere innstillinger for prosjektet enn den en vanlig bruker kan.',
- // 'Gitlab Issue' => '',
'Subtask Id' => 'Deloppgave ID',
'Subtasks' => 'Deloppgaver',
'Subtasks Export' => 'Eksporter deloppgaver',
@@ -603,9 +538,6 @@ return array(
// 'You already have one subtask in progress' => '',
'Which parts of the project do you want to duplicate?' => 'Hvilke deler av dette prosjektet ønsker du å kopiere?',
// 'Disallow login form' => '',
- // 'Bitbucket commit received' => '',
- // 'Bitbucket webhooks' => '',
- // 'Help on Bitbucket webhooks' => '',
'Start' => 'Start',
'End' => 'Slutt',
'Task age in days' => 'Dager siden oppgaven ble opprettet',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Denne oppgaven',
// '<1h' => '',
// '%dh' => '',
- // '%b %e' => '',
'Expand tasks' => 'Utvid oppgavevisning',
'Collapse tasks' => 'Komprimer oppgavevisning',
'Expand/collapse tasks' => 'Utvide/komprimere oppgavevisning',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Hurtigtaster',
// 'Open board switcher' => '',
// 'Application' => '',
- 'since %B %e, %Y at %k:%M %p' => 'siden %B %e, %Y at %k:%M %p',
'Compact view' => 'Kompakt visning',
'Horizontal scrolling' => 'Bla horisontalt',
'Compact/wide view' => 'Kompakt/bred visning',
'No results match:' => 'Ingen resultater',
'Currency' => 'Valuta',
- 'Files' => 'Filer',
- 'Images' => 'Bilder',
'Private project' => 'Privat prosjekt',
// 'AUD - Australian Dollar' => '',
// 'CAD - Canadian Dollar' => '',
@@ -703,17 +631,12 @@ return array(
// 'The two factor authentication code is valid.' => '',
// 'Code' => '',
'Two factor authentication' => 'Dobbel godkjenning',
- // 'Enable/disable two factor authentication' => '',
// 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
// 'Check my code' => '',
// 'Secret key: ' => '',
// 'Test your device' => '',
'Assign a color when the task is moved to a specific column' => 'Endre til en valgt farge hvis en oppgave flyttes til en spesifikk kolonne',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -722,7 +645,6 @@ return array(
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
'Screenshot uploaded successfully.' => 'Skjermbilde opplastet',
// 'SEK - Swedish Krona' => '',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Prosjektkoden er en alfanumerisk kode som kan brukes for å identifisere prosjektet',
'Identifier' => 'Prosjektkode',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
// 'User that will receive the email' => '',
// 'Email subject' => '',
'Date' => 'Dato',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
'Add a comment log when moving the task between columns' => 'Legg til en kommentar i loggen når en oppgave flyttes mellom kolonnene',
'Move the task to another column when the category is changed' => 'Flytt oppgaven til en annen kolonne når kategorien endres',
'Send a task by email to someone' => 'Send en oppgave på epost til noen',
// 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
'Column change' => 'Endret kolonne',
'Position change' => 'Posisjonsendring',
'Swimlane change' => 'Endret svømmebane',
@@ -826,17 +736,11 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- 'Swimlane: %s' => 'Svømmebane: %s',
'I want to receive notifications for:' => 'Jeg vil motta varslinger om:',
'All tasks' => 'Alle oppgaver',
'Only for tasks assigned to me' => 'Kun oppgaver som er tildelt meg',
'Only for tasks created by me' => 'Kun oppgaver som er opprettet av meg',
'Only for tasks created by me and assigned to me' => 'Kun oppgaver som er opprettet av meg og tildelt meg',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
'Total for all columns' => 'Totalt for alle kolonner',
// 'You need at least 2 days of data to show the chart.' => '',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'Ikke tildelt',
'View advanced search syntax' => 'Vis hjelp for avansert søk ',
'Overview' => 'Oversikt',
- // '%b %e %Y' => '',
'Board/Calendar/List view' => 'Oversikt/kalender/listevisning',
'Switch to the board view' => 'Oversiktsvisning',
'Switch to the calendar view' => 'Kalendevisning',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -906,18 +805,12 @@ return array(
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
// 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
'New remote user' => 'Ny eksternbruker',
'New local user' => 'Ny internbruker',
'Default task color' => 'Standard oppgavefarge',
- 'Hide sidebar' => 'Skjul sidemeny',
- 'Expand sidebar' => 'Vis sidemeny',
// 'This feature does not work with all browsers.' => '',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'bidragsytere',
'License:' => 'Lisens:',
'License' => 'Lisens',
- 'Project Administrator' => 'Prosjektadministrator',
'Enter the text below' => 'Legg inn teksten nedenfor',
'Gantt chart for %s' => 'Gantt skjema for %s',
// 'Sort by position' => '',
@@ -952,11 +844,9 @@ return array(
'open file' => 'Åpne fil',
'End date' => 'Sluttdato',
'Users overview' => 'Brukeroversikt',
- 'Managers' => 'Ledere',
'Members' => 'Medlemmer',
'Shared project' => 'Delt prosjekt',
'Project managers' => 'Prosjektledere',
- 'Project members' => 'Prosjektmedlemmer',
// 'Gantt chart for all projects' => '',
'Projects list' => 'Prosjektliste',
'Gantt chart for this project' => 'Gantt skjema for dette prosjektet',
@@ -964,26 +854,16 @@ return array(
'End date:' => 'Sluttdato:',
// 'There is no start date or end date for this project.' => '',
'Projects Gantt chart' => 'Gantt skjema for prosjekter',
- 'Start date: %s' => 'Startdato: %s',
- 'End date: %s' => 'Sluttdato: %s',
'Link type' => 'Relasjonstype',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
'Milestone' => 'Milepæl',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
'Documentation: %s' => 'Dokumentasjon: %s',
'Switch to the Gantt chart view' => 'Gantt skjema visning',
'Reset the search/filter box' => 'Nullstill søk/filter',
'Documentation' => 'Dokumentasjon',
'Table of contents' => 'Innholdsfortegnelse',
'Gantt' => 'Gantt',
- 'Help with project permissions' => 'Hjelp med prosjekttilganger',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ 'Project members' => 'Prosjektmedlemmer',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index fd74c977..d1d47a34 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Project verwijderen',
'Edit the board for "%s"' => 'Bord bewerken voor « %s »',
'All projects' => 'Alle projecten',
- 'Change columns' => 'Kolommen veranderen',
'Add a new column' => 'Kolom toevoegen',
'Title' => 'Titel',
'Nobody assigned' => 'Niemand toegewezen',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Een taak openen',
'Do you really want to open this task: "%s"?' => 'Weet u zeker dat u deze taak wil openen : « %s » ?',
'Back to the board' => 'Terug naar het bord',
- 'Created on %B %e, %Y at %k:%M %p' => 'Aangemaakt op %d/%m/%Y à %H:%M',
'There is nobody assigned' => 'Er is niemand toegewezen',
'Column on the board:' => 'Kolom op het bord : ',
- 'Status is open' => 'Status is open',
- 'Status is closed' => 'Status is gesloten',
'Close this task' => 'Deze taak sluiten',
'Open this task' => 'Deze taak openen',
'There is no description.' => 'Er is geen omschrijving.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Het id is verplicht',
'The project id is required' => 'Het project id is verplicht',
'The project name is required' => 'De projectnaam is verplicht',
- 'This project must be unique' => 'Dit project moet uniek zijn',
'The title is required' => 'De titel is verplicht',
'Settings saved successfully.' => 'Instellingen succesvol opgeslagen.',
'Unable to save your settings.' => 'Instellingen opslaan niet gelukt.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'In behandeling',
'Done' => 'Afgewerkt',
'Application version:' => 'Applicatie versie :',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Voltooid op %d/%m/%Y à %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d/%m/%Y op %H:%M',
- 'Date created' => 'Datum aangemaakt',
- 'Date completed' => 'Datum voltooid',
'Id' => 'Id',
'%d closed tasks' => '%d gesloten taken',
'No task for this project' => 'Geen taken voor dit project',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Complexiteit',
'Task limit' => 'Taak limiet.',
'Task count' => 'Aantal taken',
- 'Edit project access list' => 'Aanpassen toegangsrechten project',
- 'Allow this user' => 'Deze gebruiker toestaan',
- 'Don\'t forget that administrators have access to everything.' => 'Vergeet niet dat administrators overal toegang hebben.',
- 'Revoke' => 'Intrekken',
- 'List of authorized users' => 'Lijst met geautoriseerde gebruikers',
'User' => 'Gebruiker',
- 'Nobody have access to this project.' => 'Niemand heeft toegang tot dit project',
'Comments' => 'Commentaar',
'Write your text in Markdown' => 'Schrijf uw tekst in Markdown',
'Leave a comment' => 'Schrijf een commentaar',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Deze taak aanpassen',
'Due Date' => 'Vervaldag',
'Invalid date' => 'Ongeldige datum',
- 'Must be done before %B %e, %Y' => 'Moet voltooid zijn voor %d/%m/%Y',
- '%B %e, %Y' => '%d %B %Y',
- '%b %e, %Y' => '%d/%m/%Y',
'Automatic actions' => 'Geautomatiseerd acties',
'Your automatic action have been created successfully.' => 'Geautomatiseerde actie succesvol aangemaakt.',
'Unable to create your automatic action.' => 'Geautomatiseerde actie aanmaken niet gelukt.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Wijs een kleur toe aan een gebruiker',
'Column title' => 'Kolom titel',
'Position' => 'Positie',
- 'Move Up' => 'Omhoog verplaatsen',
- 'Move Down' => 'Omlaag verplaatsen',
'Duplicate to another project' => 'Dupliceren in een ander project',
'Duplicate' => 'Dupliceren',
'link' => 'koppelen',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Commentaar succesvol verwijder.',
'Unable to remove this comment.' => 'Commentaar verwijderen niet gelukt.',
'Do you really want to remove this comment?' => 'Weet u zeker dat u dit commentaar wil verwijderen ?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Alleen administrators of de aanmaker van het commentaar hebben toegang tot deze pagina.',
'Current password for the user "%s"' => 'Huidig wachtwoord voor gebruiker « %s »',
'The current password is required' => 'Huidig wachtwoord is verplicht',
'Wrong password' => 'Onjuist wachtwoord',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'Email',
- 'Link my Google Account' => 'Link mijn Google Account',
- 'Unlink my Google Account' => 'Link met Google Account verwijderen',
- 'Login with my Google Account' => 'Inloggen met mijn Google Account',
- 'Project not found.' => 'Project niet gevonden.',
'Task removed successfully.' => 'Taak succesvol verwijderd.',
'Unable to remove this task.' => 'Taak verwijderen niet gelukt.',
'Remove a task' => 'Taak verwijderen',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maximale grootte : ',
'Unable to upload the file.' => 'Uploaden van bestand niet gelukt.',
'Display another project' => 'Een ander project weergeven',
- 'Login with my Github Account' => 'Login met mijn Github Account',
- 'Link my Github Account' => 'Link met mijn Github',
- 'Unlink my Github Account' => 'Link met mijn Github verwijderen',
'Created by %s' => 'Aangemaakt door %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Laatst gewijzigd op %d/%m/%Y à %H:%M',
'Tasks Export' => 'Taken exporteren',
'Tasks exportation for "%s"' => 'Taken exporteren voor « %s »',
'Start Date' => 'Startdatum',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Ik wil notificaties ontvangen van de volgende projecten :',
'view the task on Kanboard' => 'taak bekijken op Kanboard',
'Public access' => 'Publieke toegang',
- 'User management' => 'Gebruikers management',
'Active tasks' => 'Actieve taken',
'Disable public access' => 'Publieke toegang uitschakelen',
'Enable public access' => 'Publieke toegang inschakelen',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Email :',
'Notifications:' => 'Notificaties :',
'Notifications' => 'Notificaties',
- 'Group:' => 'Groep :',
- 'Regular user' => 'Normale gebruiker',
'Account type:' => 'Account type:',
'Edit profile' => 'Profiel aanpassen',
'Change password' => 'Wachtwoord aanpassen',
'Password modification' => 'Wachtwoord aanpassen',
'External authentications' => 'Externe authenticatie',
- 'Google Account' => 'Google Account',
- 'Github Account' => 'Github Account',
'Never connected.' => 'Nooit verbonden.',
- 'No account linked.' => 'Geen account gelinkt.',
- 'Account linked.' => 'Account gelinkt.',
'No external authentication enabled.' => 'Geen externe authenticatie aangezet.',
'Password modified successfully.' => 'Wachtwoord succesvol aangepast.',
'Unable to change the password.' => 'Aanpassen van wachtwoord niet gelukt.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s heeft de toegewezene voor taak %s veranderd in %s',
'New password for the user "%s"' => 'Nieuw wachtwoord voor gebruiker « %s »',
'Choose an event' => 'Kies een gebeurtenis',
- 'Github commit received' => 'Github commentaar ontvangen',
- 'Github issue opened' => 'Github issue geopend',
- 'Github issue closed' => 'Github issue gesloten',
- 'Github issue reopened' => 'Github issue heropend',
- 'Github issue assignee change' => 'Github toegewezen veranderd',
- 'Github issue label change' => 'Github issue label verander',
'Create a task from an external provider' => 'Maak een taak aan vanuit een externe provider',
'Change the assignee based on an external username' => 'Verander de toegewezene aan de hand van de externe gebruikersnaam',
'Change the category based on an external label' => 'Verander de categorie aan de hand van een extern label',
'Reference' => 'Referentie',
- 'Reference: %s' => 'Referentie : %s',
'Label' => 'Label',
'Database' => 'Database',
'About' => 'Over',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frequentie in seconden (stadaard 60)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequentie in seconden (0 om uit te schakelen, standaard 10)',
'Application URL' => 'Applicatie URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Voorbeeld: http://example.kanboard.net/ (gebruikt voor email notificaties)',
'Token regenerated.' => 'Token opnieuw gegenereerd.',
'Date format' => 'Datum formaat',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formaat is altijd geaccepteerd, bijvoorbeeld : « %s » et « %s »',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Geschatte tijd: %s hours',
- 'Time spent: %s hours' => 'Tijd besteed : %s heures',
- 'Started on %B %e, %Y' => 'Gestart op %d/%m/%Y',
'Start date' => 'Startdatum',
'Time estimated' => 'Geschatte tijd',
'There is nothing assigned to you.' => 'Er is niets aan u toegewezen.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Iedereen heeft toegang tot dit project.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github webhooks',
- 'Help on Github webhooks' => 'Hulp bij Github webhooks',
'Create a comment from an external provider' => 'Voeg een commentaar toe van een externe provider',
- 'Github issue comment created' => 'Github issue commentaar aangemaakt',
'Project management' => 'Project management',
'My projects' => 'Mijn projecten',
'Columns' => 'Kolommen',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Gebruikerverdeling voor « %s »',
'Clone this project' => 'Kloon dit project',
'Column removed successfully.' => 'Kolom succesvol verwijderd.',
- 'Github Issue' => 'Github issue',
'Not enough data to show the graph.' => 'Niet genoeg data om de grafiek te laten zien.',
// 'Previous' => '',
'The id must be an integer' => 'Het id moet een integer zijn',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Standaard swinlane',
'Do you really want to remove this swimlane: "%s"?' => 'Weet u zeker dat u deze swimlane wil verwijderen : « %s » ?',
'Inactive swimlanes' => 'Inactieve swinlanes',
- 'Set project manager' => 'Project manager instellen',
- 'Set project member' => 'Project lid instellen',
'Remove a swimlane' => 'Verwijder swinlane',
- 'Rename' => 'Hernoemen',
'Show default swimlane' => 'Standaard swimlane tonen',
'Swimlane modification for the project "%s"' => 'Swinlane aanpassing voor project « %s »',
'Swimlane not found.' => 'Swimlane niet gevonden.',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane succesvol aangepast.',
'The default swimlane have been updated successfully.' => 'De standaard swimlane is succesvol aangepast.',
- 'Unable to create your swimlane.' => 'Swimlane aanmaken niet gelukt.',
'Unable to remove this swimlane.' => 'Swimlane verwijderen niet gelukt.',
'Unable to update this swimlane.' => 'Swimlane aanpassen niet gelukt.',
'Your swimlane have been created successfully.' => 'Swimlane succesvol aangemaakt.',
'Example: "Bug, Feature Request, Improvement"' => 'Voorbeeld: « Bug, Feature Request, Improvement »',
'Default categories for new projects (Comma-separated)' => 'Standaard categorieën voor nieuwe projecten (komma gescheiden)',
- 'Gitlab commit received' => 'Gitlab commir ontvangen',
- 'Gitlab issue opened' => 'Gitlab issue geopend',
- 'Gitlab issue closed' => 'Gitlab issue gesloten',
- 'Gitlab webhooks' => 'Gitlab webhooks',
- 'Help on Gitlab webhooks' => 'Hulp bij Gitlab webhooks',
'Integrations' => 'Integraties',
'Integration with third-party services' => 'Integratie met derde-partij-services',
- 'Role for this project' => 'Rol voor dit project',
- 'Project manager' => 'Project manager',
- 'Project member' => 'Project lid',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Een project manager kan de instellingen van het project wijzigen en heeft meer rechten dan een normale gebruiker.',
- 'Gitlab Issue' => 'Gitlab issue',
'Subtask Id' => 'Subtaak id',
'Subtasks' => 'Subtaken',
'Subtasks Export' => 'Subtaken exporteren',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'U heeft al een subtaak in behandeling',
'Which parts of the project do you want to duplicate?' => 'Welke onderdelen van het project wilt u dupliceren?',
// 'Disallow login form' => '',
- 'Bitbucket commit received' => 'Bitbucket commit ontvangen',
- 'Bitbucket webhooks' => 'Bitbucket webhooks',
- 'Help on Bitbucket webhooks' => 'Help bij Bitbucket webhooks',
'Start' => 'Start',
'End' => 'Eind',
'Task age in days' => 'Leeftijd taak in dagen',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Deze taal',
'<1h' => '<1h',
'%dh' => '%dh',
- '%b %e' => '%e %b',
'Expand tasks' => 'Taken uitklappen',
'Collapse tasks' => 'Taken inklappen',
'Expand/collapse tasks' => 'Taken in/uiklappen',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Keyboard snelkoppelingen',
'Open board switcher' => 'Open bord switcher',
'Application' => 'Applicatie',
- 'since %B %e, %Y at %k:%M %p' => 'sinds %d/%m/%Y à %H:%M',
// 'Compact view' => '',
// 'Horizontal scrolling' => '',
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
- // 'Files' => '',
- // 'Images' => '',
// 'Private project' => '',
// 'AUD - Australian Dollar' => '',
// 'CAD - Canadian Dollar' => '',
@@ -703,17 +631,12 @@ return array(
// 'The two factor authentication code is valid.' => '',
// 'Code' => '',
// 'Two factor authentication' => '',
- // 'Enable/disable two factor authentication' => '',
// 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
// 'Check my code' => '',
// 'Secret key: ' => '',
// 'Test your device' => '',
// 'Assign a color when the task is moved to a specific column' => '',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -722,7 +645,6 @@ return array(
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
// 'User that will receive the email' => '',
// 'Email subject' => '',
// 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
// 'Add a comment log when moving the task between columns' => '',
// 'Move the task to another column when the category is changed' => '',
// 'Send a task by email to someone' => '',
// 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
// 'Column change' => '',
// 'Position change' => '',
// 'Swimlane change' => '',
@@ -826,17 +736,11 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
// 'I want to receive notifications for:' => '',
// 'All tasks' => '',
// 'Only for tasks assigned to me' => '',
// 'Only for tasks created by me' => '',
// 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
// 'Total for all columns' => '',
// 'You need at least 2 days of data to show the chart.' => '',
@@ -861,7 +765,6 @@ return array(
// 'Not assigned' => '',
// 'View advanced search syntax' => '',
// 'Overview' => '',
- // '%b %e %Y' => '',
// 'Board/Calendar/List view' => '',
// 'Switch to the board view' => '',
// 'Switch to the calendar view' => '',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -906,18 +805,12 @@ return array(
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
// 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
// 'New remote user' => '',
// 'New local user' => '',
// 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
// 'This feature does not work with all browsers.' => '',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
// 'contributors' => '',
// 'License:' => '',
// 'License' => '',
- // 'Project Administrator' => '',
// 'Enter the text below' => '',
// 'Gantt chart for %s' => '',
// 'Sort by position' => '',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index bcc0c433..9caa6b88 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -20,15 +20,15 @@ return array(
'Red' => 'Czerwony',
'Orange' => 'Pomarańczowy',
'Grey' => 'Szary',
- // 'Brown' => '',
- // 'Deep Orange' => '',
- // 'Dark Grey' => '',
- // 'Pink' => '',
- // 'Teal' => '',
- // 'Cyan' => '',
- // 'Lime' => '',
- // 'Light Green' => '',
- // 'Amber' => '',
+ 'Brown' => 'Brąz',
+ 'Deep Orange' => 'Ciemnopomarańczowy',
+ 'Dark Grey' => 'Ciemnoszary',
+ 'Pink' => 'Różowy',
+ 'Teal' => 'Turkusowy',
+ 'Cyan' => 'Cyjan',
+ 'Lime' => 'Limonkowy',
+ 'Light Green' => 'Jasnozielony',
+ 'Amber' => 'Bursztynowy',
'Save' => 'Zapisz',
'Login' => 'Login',
'Official website:' => 'Oficjalna strona:',
@@ -72,9 +72,8 @@ return array(
'Remove project' => 'Usuń projekt',
'Edit the board for "%s"' => 'Edytuj tablicę dla "%s"',
'All projects' => 'Wszystkie projekty',
- 'Change columns' => 'Zmień kolumny',
'Add a new column' => 'Dodaj nową kolumnę',
- 'Title' => 'Tytuł',
+ 'Title' => 'Nazwa',
'Nobody assigned' => 'Nikt nie przypisany',
'Assigned to %s' => 'Przypisane do %s',
'Remove a column' => 'Usuń kolumnę',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Otwórz zadanie',
'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?',
'Back to the board' => 'Powrót do tablicy',
- 'Created on %B %e, %Y at %k:%M %p' => 'Utworzono dnia %e %B %Y o %k:%M',
'There is nobody assigned' => 'Nikt nie jest przypisany',
'Column on the board:' => 'Kolumna na tablicy:',
- 'Status is open' => 'Status otwarty',
- 'Status is closed' => 'Status zamknięty',
'Close this task' => 'Zamknij zadanie',
'Open this task' => 'Otwórz zadanie',
'There is no description.' => 'Brak opisu.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'ID jest wymagane',
'The project id is required' => 'ID projektu jest wymagane',
'The project name is required' => 'Nazwa projektu jest wymagana',
- 'This project must be unique' => 'Projekt musi być unikalny',
'The title is required' => 'Tutył jest wymagany',
'Settings saved successfully.' => 'Ustawienia zapisane.',
'Unable to save your settings.' => 'Nie udało się zapisać ustawień.',
@@ -149,7 +144,7 @@ return array(
'Task created successfully.' => 'Zadanie zostało utworzone.',
'User created successfully.' => 'Użytkownik dodany',
'Unable to create your user.' => 'Nie udało się dodać użytkownika.',
- 'User updated successfully.' => 'Użytkownik zaktualizowany.',
+ 'User updated successfully.' => 'Profil użytkownika został zaaktualizowany.',
'Unable to update your user.' => 'Nie udało się zaktualizować użytkownika.',
'User removed successfully.' => 'Użytkownik usunięty.',
'Unable to remove this user.' => 'Nie udało się usunąć użytkownika.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'W trakcie',
'Done' => 'Zakończone',
'Application version:' => 'Wersja aplikacji:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Zakończono dnia %e %B %Y o %k:%M',
- '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M',
- 'Date created' => 'Data utworzenia',
- 'Date completed' => 'Data zakończenia',
'Id' => 'Id',
'%d closed tasks' => '%d zamkniętych zadań',
'No task for this project' => 'Brak zadań dla tego projektu',
@@ -175,16 +166,10 @@ return array(
'Complexity' => 'Poziom trudności',
'Task limit' => 'Limit zadań',
'Task count' => 'Liczba zadań',
- 'Edit project access list' => 'Edycja list dostępu dla projektu',
- 'Allow this user' => 'Dodaj użytkownika',
- 'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!',
- 'Revoke' => 'Odbierz dostęp',
- 'List of authorized users' => 'Lista użytkowników mających dostęp',
'User' => 'Użytkownik',
- 'Nobody have access to this project.' => 'Żaden użytkownik nie ma dostępu do tego projektu',
'Comments' => 'Komentarze',
- 'Write your text in Markdown' => 'Możesz użyć Markdown',
- 'Leave a comment' => 'Zostaw komentarz',
+ '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',
'Comment added successfully.' => 'Komentarz dodany',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Edytuj zadanie',
'Due Date' => 'Termin',
'Invalid date' => 'Błędna data',
- 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y',
- '%B %e, %Y' => '%e %B %Y',
- '%b %e, %Y' => '%e %B %Y',
'Automatic actions' => 'Akcje automatyczne',
'Your automatic action have been created successfully.' => 'Twoja akcja została dodana',
'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji',
@@ -225,18 +207,15 @@ return array(
'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika',
'Column title' => 'Tytuł kolumny',
'Position' => 'Pozycja',
- 'Move Up' => 'Przenieś wyżej',
- 'Move Down' => 'Przenieś niżej',
'Duplicate to another project' => 'Skopiuj do innego projektu',
'Duplicate' => 'Utwórz kopię',
'link' => 'link',
'Comment updated successfully.' => 'Komentarz został zapisany.',
- 'Unable to update your comment.' => 'Nie udało się zapisanie komentarza.',
+ 'Unable to update your comment.' => 'Nie udało się zapisać komentarza.',
'Remove a comment' => 'Usuń komentarz',
'Comment removed successfully.' => 'Komentarz został usunięty.',
'Unable to remove this comment.' => 'Nie udało się usunąć komentarza.',
'Do you really want to remove this comment?' => 'Czy na pewno usunąć ten komentarz?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Tylko administratorzy oraz autor komentarza ma dostęp do tej strony.',
'Current password for the user "%s"' => 'Aktualne hasło dla użytkownika "%s"',
'The current password is required' => 'Wymanage jest aktualne hasło',
'Wrong password' => 'Błędne hasło',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'Email',
- 'Link my Google Account' => 'Połącz z kontem Google',
- 'Unlink my Google Account' => 'Rozłącz z kontem Google',
- 'Login with my Google Account' => 'Zaloguj przy pomocy konta Google',
- 'Project not found.' => 'Projek nieznaleziony.',
'Task removed successfully.' => 'Zadanie usunięto pomyślnie.',
'Unable to remove this task.' => 'Nie można usunąć tego zadania.',
'Remove a task' => 'Usuń zadanie',
@@ -302,7 +277,7 @@ return array(
'Attach a document' => 'Dołącz plik',
'Do you really want to remove this file: "%s"?' => 'Czy na pewno chcesz usunąć plik: "%s"?',
'Attachments' => 'Załączniki',
- 'Edit the task' => 'Edytuj Zadanie',
+ 'Edit the task' => 'Edytuj zadanie',
'Edit the description' => 'Edytuj opis',
'Add a comment' => 'Dodaj komentarz',
'Edit a comment' => 'Edytuj komentarz',
@@ -312,11 +287,11 @@ return array(
'Spent:' => 'Przeznaczony:',
'Do you really want to remove this sub-task?' => 'Czy na pewno chcesz usunąć to pod-zadanie?',
'Remaining:' => 'Pozostało:',
- 'hours' => 'godzin',
+ 'hours' => 'godzin(y)',
'spent' => 'przeznaczono',
'estimated' => 'szacowany',
- 'Sub-Tasks' => 'Pod-zadanie',
- 'Add a sub-task' => 'Dodaj pod-zadanie',
+ 'Sub-Tasks' => 'Podzadania',
+ 'Add a sub-task' => 'Dodaj podzadanie',
'Original estimate' => 'Szacowanie początkowe',
'Create another sub-task' => 'Dodaj kolejne pod-zadanie',
'Time spent' => 'Przeznaczony czas',
@@ -326,19 +301,15 @@ return array(
'Todo' => 'Do zrobienia',
'In progress' => 'W trakcie',
'Sub-task removed successfully.' => 'Pod-zadanie usunięte pomyślnie.',
- 'Unable to remove this sub-task.' => 'Nie można usunąć tego pod-zadania.',
+ 'Unable to remove this sub-task.' => 'Nie można usunąć tego podzadania.',
'Sub-task updated successfully.' => 'Pod-zadanie zaktualizowane pomyślnie.',
- 'Unable to update your sub-task.' => 'Nie można zaktualizować tego pod-zadania.',
- 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.',
+ 'Unable to update your sub-task.' => 'Nie można zaktualizować tego podzadania.',
+ 'Unable to create your sub-task.' => 'Nie można utworzyć tego podzadania.',
'Sub-task added successfully.' => 'Pod-zadanie utworzone pomyślnie',
'Maximum size: ' => 'Maksymalny rozmiar: ',
'Unable to upload the file.' => 'Nie można wczytać pliku.',
'Display another project' => 'Wyświetl inny projekt',
- 'Login with my Github Account' => 'Zaloguj przy użyciu konta Github',
- 'Link my Github Account' => 'Podłącz konto Github',
- 'Unlink my Github Account' => 'Odłącz konto Github',
'Created by %s' => 'Utworzone przez %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Ostatnio zmienione %e %B %Y o %k:%M',
'Tasks Export' => 'Eksport zadań',
'Tasks exportation for "%s"' => 'Eksport zadań dla "%s"',
'Start Date' => 'Data początkowa',
@@ -356,7 +327,7 @@ return array(
'The task #%d have been opened.' => 'Zadania #%d zostały otwarte.',
'The task #%d have been closed.' => 'Zadania #%d zostały zamknięte.',
'Sub-task updated' => 'Pod-zadanie zaktualizowane',
- 'Title:' => 'Tytuł:',
+ 'Title:' => 'Nazwa:',
// 'Status:' => '',
'Assignee:' => 'Przypisano do:',
'Time tracking:' => 'Śledzenie czasu: ',
@@ -371,10 +342,9 @@ return array(
'Task updated' => 'Zaktualizowane zadanie',
'Task closed' => 'Zadanie zamknięte',
'Task opened' => 'Zadanie otwarte',
- 'I want to receive notifications only for those projects:' => 'Chcę otrzymywać powiadiomienia tylko dla tych projektów:',
+ 'I want to receive notifications only for those projects:' => 'Chcę otrzymywać powiadomienia tylko dla poniższych projektów:',
'view the task on Kanboard' => 'Zobacz zadanie',
'Public access' => 'Dostęp publiczny',
- 'User management' => 'Zarządzanie użytkownikami',
'Active tasks' => 'Aktywne zadania',
'Disable public access' => 'Zablokuj dostęp publiczny',
'Enable public access' => 'Odblokuj dostęp publiczny',
@@ -392,23 +362,17 @@ return array(
'Remote' => 'Zdalne',
'Enabled' => 'Odblokowane',
'Disabled' => 'Zablokowane',
- 'Username:' => 'Nazwa Użytkownika:',
+ 'Username:' => 'Nazwa Użytkownika (login):',
'Name:' => 'Imię i Nazwisko',
'Email:' => 'Email: ',
'Notifications:' => 'Powiadomienia: ',
'Notifications' => 'Powiadomienia',
- 'Group:' => 'Grupa:',
- 'Regular user' => 'Zwykły użytkownik',
'Account type:' => 'Typ konta:',
'Edit profile' => 'Edytuj profil',
'Change password' => 'Zmień hasło',
'Password modification' => 'Zmiana hasła',
'External authentications' => 'Autentykacja zewnętrzna',
- 'Google Account' => 'Konto Google',
- 'Github Account' => 'Konto Github',
'Never connected.' => 'Nigdy nie połączone.',
- 'No account linked.' => 'Brak połączonych kont.',
- 'Account linked.' => 'Konto połączone.',
'No external authentication enabled.' => 'Brak autentykacji zewnętrznych.',
'Password modified successfully.' => 'Hasło zmienione pomyślne.',
'Unable to change the password.' => 'Nie można zmienić hasła.',
@@ -420,8 +384,8 @@ return array(
'%s moved the task %s to the column "%s"' => '%s przeniósł zadanie %s do kolumny "%s"',
'%s created the task %s' => '%s utworzył zadanie %s',
'%s closed the task %s' => '%s zamknął zadanie %s',
- '%s created a subtask for the task %s' => '%s utworzył pod-zadanie dla zadania %s',
- '%s updated a subtask for the task %s' => '%s zaktualizował pod-zadanie dla zadania %s',
+ '%s created a subtask for the task %s' => '%s utworzył podzadanie dla zadania %s',
+ '%s updated a subtask for the task %s' => '%s zaktualizował podzadanie dla zadania %s',
'Assigned to %s with an estimate of %s/%sh' => 'Przypisano do %s z szacowanym czasem wykonania %s/%sh',
'Not assigned, estimate of %sh' => 'Nie przypisane, szacowany czas wykonania %sh',
'%s updated a comment on the task %s' => '%s zaktualizował komentarz do zadania %s',
@@ -430,8 +394,8 @@ return array(
'RSS feed' => 'Kanał RSS',
'%s updated a comment on the task #%d' => '%s zaktualizował komentarz do zadania #%d',
'%s commented on the task #%d' => '%s skomentował zadanie #%d',
- '%s updated a subtask for the task #%d' => '%s zaktualizował pod-zadanie dla zadania #%d',
- '%s created a subtask for the task #%d' => '%s utworzył pod-zadanie dla zadania #%d',
+ '%s updated a subtask for the task #%d' => '%s zaktualizował podzadanie dla zadania #%d',
+ '%s created a subtask for the task #%d' => '%s utworzył podzadanie dla zadania #%d',
'%s updated the task #%d' => '%s zaktualizował zadanie #%d',
'%s created the task #%d' => '%s utworzył zadanie #%d',
'%s closed the task #%d' => '%s zamknął zadanie #%d',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s zmienił osobę odpowiedzialną za zadanie %s na %s',
'New password for the user "%s"' => 'Nowe hasło użytkownika "%s"',
'Choose an event' => 'Wybierz zdarzenie',
- // 'Github commit received' => '',
- // 'Github issue opened' => '',
- // 'Github issue closed' => '',
- // 'Github issue reopened' => '',
- // 'Github issue assignee change' => '',
- // 'Github issue label change' => '',
'Create a task from an external provider' => 'Utwórz zadanie z dostawcy zewnętrznego',
'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika',
'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrznej etykiety',
// 'Reference' => '',
- // 'Reference: %s' => '',
'Label' => 'Etykieta',
'Database' => 'Baza danych',
'About' => 'Informacje',
@@ -474,36 +431,29 @@ return array(
'Frequency in second (60 seconds by default)' => 'Częstotliwość w sekundach (domyślnie 60 sekund)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Częstotliwość w sekundach (0 aby zablokować, domyślnie 10 sekund)',
'Application URL' => 'Adres URL aplikacji',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Przykład: http://example.kanboard.net/ (Używane przez powiadomienia email)',
'Token regenerated.' => 'Token wygenerowany ponownie.',
'Date format' => 'Format daty',
'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' => 'Wpisz tutaj aby utworzyć pod-zadanie',
+ 'Type here to create a new sub-task' => 'Nazwa podzadania',
'Add' => 'Dodaj',
- 'Estimated time: %s hours' => 'Szacowany czas: %s godzin',
- 'Time spent: %s hours' => 'Przeznaczony czas: %s godzin',
- 'Started on %B %e, %Y' => 'Rozpoczęto %e %B %Y',
'Start date' => 'Data rozpoczęcia',
'Time estimated' => 'Szacowany czas',
'There is nothing assigned to you.' => 'Nie ma przypisanych zadań',
'My tasks' => 'Moje zadania',
- 'Activity stream' => 'Dziennik aktywności',
- 'Dashboard' => 'Panel',
+ 'Activity stream' => 'Strumień aktywności',
+ 'Dashboard' => 'Dashboard',
'Confirmation' => 'Potwierdzenie',
'Allow everybody to access to this project' => 'Udostępnij ten projekt wszystkim',
'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.',
// 'Webhooks' => '',
// 'API' => '',
- // 'Github webhooks' => '',
- // 'Help on Github webhooks' => '',
'Create a comment from an external provider' => 'Utwórz komentarz od zewnętrznego dostawcy',
- // 'Github issue comment created' => '',
'Project management' => 'Menadżer projektu',
'My projects' => 'Moje projekty',
'Columns' => 'Kolumny',
- 'Task' => 'zadania',
+ 'Task' => 'Zadanie',
'Your are not member of any project.' => 'Nie bierzesz udziału w żadnym projekcie',
'Percentage' => 'Procent',
'Number of tasks' => 'Liczba zadań',
@@ -511,20 +461,19 @@ return array(
'Reportings' => 'Raporty',
'Task repartition for "%s"' => 'Przydział zadań dla "%s"',
'Analytics' => 'Analizy',
- 'Subtask' => 'Pod-zadanie',
- 'My subtasks' => 'Moje pod-zadania',
+ 'Subtask' => 'Podzadanie',
+ 'My subtasks' => 'Moje podzadania',
'User repartition' => 'Przydział użytkownika',
'User repartition for "%s"' => 'Przydział użytkownika dla "%s"',
'Clone this project' => 'Sklonuj ten projekt',
'Column removed successfully.' => 'Kolumna usunięta pomyślnie.',
- // 'Github Issue' => '',
'Not enough data to show the graph.' => 'Za mało danych do utworzenia wykresu.',
'Previous' => 'Poprzedni',
'The id must be an integer' => 'ID musi być liczbą całkowitą',
'The project id must be an integer' => 'ID projektu musi być liczbą całkowitą',
'The status must be an integer' => 'Status musi być liczbą całkowitą',
'The subtask id is required' => 'ID pod-zadanie jest wymagane',
- 'The subtask id must be an integer' => 'ID pod-zadania musi być liczbą całkowitą',
+ 'The subtask id must be an integer' => 'ID podzadania musi być liczbą całkowitą',
'The task id is required' => 'ID zadania jest wymagane',
'The task id must be an integer' => 'ID zadania musi być liczbą całkowitą',
'The user id must be an integer' => 'ID użytkownika musi być liczbą całkowitą',
@@ -533,9 +482,9 @@ return array(
'Unable to create this task.' => 'Nie można tworzyć zadania.',
'Cumulative flow diagram' => 'Zbiorowy diagram przepływu',
'Cumulative flow diagram for "%s"' => 'Zbiorowy diagram przepływu dla "%s"',
- 'Daily project summary' => 'Dzienne podsumowanie projektu',
+ 'Daily project summary' => 'Dzienne raport z projektu',
'Daily project summary export' => 'Eksport dziennego podsumowania projektu',
- 'Daily project summary export for "%s"' => 'Eksport dziennego podsumowania projektu dla "%s"',
+ '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...',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Domyślny proces',
'Do you really want to remove this swimlane: "%s"?' => 'Czy na pewno chcesz usunąć proces: "%s"?',
'Inactive swimlanes' => 'Nieaktywne procesy',
- 'Set project manager' => 'Ustaw menadżera projektu',
- 'Set project member' => 'Ustaw członka projektu',
'Remove a swimlane' => 'Usuń proces',
- 'Rename' => 'Zmień nazwe',
'Show default swimlane' => 'Pokaż domyślny proces',
'Swimlane modification for the project "%s"' => 'Edycja procesów dla projektu "%s"',
'Swimlane not found.' => 'Nie znaleziono procesu.',
@@ -558,30 +504,19 @@ return array(
'Swimlanes' => 'Procesy',
'Swimlane updated successfully.' => 'Proces zaktualizowany pomyślnie.',
'The default swimlane have been updated successfully.' => 'Domyślny proces zaktualizowany pomyślnie.',
- 'Unable to create your swimlane.' => 'Nie można utworzyć procesu.',
'Unable to remove this swimlane.' => 'Nie można usunąć procesu.',
'Unable to update this swimlane.' => 'Nie można zaktualizować procesu.',
'Your swimlane have been created successfully.' => 'Proces tworzony pomyślnie.',
'Example: "Bug, Feature Request, Improvement"' => 'Przykład: "Błąd, Żądanie Funkcjonalności, Udoskonalenia"',
'Default categories for new projects (Comma-separated)' => 'Domyślne kategorie dla nowych projektów (oddzielone przecinkiem)',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
'Integrations' => 'Integracje',
'Integration with third-party services' => 'Integracja z usługami firm trzecich',
- 'Role for this project' => 'Rola w tym projekcie',
- 'Project manager' => 'Manadżer projektu',
- 'Project member' => 'Członek projektu',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Menadżer projektu może zmieniać ustawienia projektu i posiada większe uprawnienia od zwykłego użytkownika',
- // 'Gitlab Issue' => '',
'Subtask Id' => 'ID pod-zadania',
- 'Subtasks' => 'Pod-zadania',
- 'Subtasks Export' => 'Eksport pod-zadań',
- 'Subtasks exportation for "%s"' => 'Eksporty pod-zadań dla "%s"',
- 'Task Title' => 'Tytuł zadania',
- 'Untitled' => 'Bez tytułu',
+ 'Subtasks' => 'Podzadania',
+ 'Subtasks Export' => 'Eksport podzadań',
+ 'Subtasks exportation for "%s"' => 'Wygeneruj raport podzadań dla projektu "%s"',
+ 'Task Title' => 'Nazwa zadania',
+ 'Untitled' => 'Bez nazwy',
'Application default' => 'Domyślne dla aplikacji',
'Language:' => 'Język:',
'Timezone:' => 'Strefa czasowa:',
@@ -602,10 +537,7 @@ return array(
'Time Tracking' => 'Śledzenie czasu',
'You already have one subtask in progress' => 'Masz już zadanie o statusie "w trakcie"',
'Which parts of the project do you want to duplicate?' => 'Które elementy projektu chcesz zduplikować?',
- // 'Disallow login form' => '',
- // 'Bitbucket commit received' => '',
- // 'Bitbucket webhooks' => '',
- // 'Help on Bitbucket webhooks' => '',
+ 'Disallow login form' => 'Zablokuj możliwość logowania',
'Start' => 'Początek',
'End' => 'Koniec',
'Task age in days' => 'Wiek zadania w dniach',
@@ -646,7 +578,6 @@ return array(
'This task' => 'To zadanie',
// '<1h' => '',
// '%dh' => '',
- '%b %e' => '%e %b',
'Expand tasks' => 'Rozwiń zadania',
'Collapse tasks' => 'Zwiń zadania',
'Expand/collapse tasks' => 'Zwiń/Rozwiń zadania',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Skróty klawiszowe',
'Open board switcher' => 'Przełącz tablice',
'Application' => 'Aplikacja',
- 'since %B %e, %Y at %k:%M %p' => 'od %e %B %Y o %k:%M',
'Compact view' => 'Widok kompaktowy',
'Horizontal scrolling' => 'Przewijanie poziome',
'Compact/wide view' => 'Pełny/Kompaktowy widok',
'No results match:' => 'Brak wyników:',
'Currency' => 'Waluta',
- 'Files' => 'Pliki',
- 'Images' => 'Obrazy',
'Private project' => 'Projekt prywatny',
'AUD - Australian Dollar' => 'AUD - Dolar australijski',
'CAD - Canadian Dollar' => 'CAD - Dolar kanadyjski',
@@ -684,8 +612,8 @@ return array(
'Transitions' => 'Przeniesienia',
'Executer' => 'Wykonał',
'Time spent in the column' => 'Czas spędzony w tej kolumnie',
- 'Task transitions' => 'Przeniesienia zadania',
- 'Task transitions export' => 'Export przeniesień zadania',
+ 'Task transitions' => 'Przeniesienia zadań',
+ 'Task transitions export' => 'Wygeneruj raport z przeniesień zadań',
'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Ten raport zawiera wszystkie przeniesienia pomiędzy kolumnami wraz z datą oraz osobą odpowiedzialną',
'Currency rates' => 'Kursy walut',
'Rate' => 'Kurs',
@@ -703,26 +631,20 @@ return array(
'The two factor authentication code is valid.' => 'Kod weryfikujący poprawny',
'Code' => 'Kod',
'Two factor authentication' => 'Uwierzytelnianie dwustopniowe',
- 'Enable/disable two factor authentication' => 'Włącz/Wyłącz uwierzytelnianie dwustopniowe',
'This QR code contains the key URI: ' => 'Ten kod QR zawiera URI klucza: ',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Zapisz sekretny klucz w swoim oprogramowaniu TOTP (na przykład FreeOTP lub Google Authenticator)',
'Check my code' => 'Sprawdź kod',
'Secret key: ' => 'Tajny kod: ',
'Test your device' => 'Przetestuj urządzenie',
'Assign a color when the task is moved to a specific column' => 'Przypisz kolor gdy zadanie jest przeniesione do danej kolumny',
'%s via Kanboard' => '%s poprzez Kanboard',
- 'uploaded by: %s' => 'Dodane przez: %s',
- 'uploaded on: %s' => 'Data dodania: %s',
- 'size: %s' => 'Rozmiar: %s',
- // 'Burndown chart for "%s"' => '',
- // 'Burndown chart' => '',
+ 'Burndown chart for "%s"' => 'Wykres Burndown dla "%s"',
+ 'Burndown chart' => 'Wykres Burndown',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
'Screenshot taken %s' => 'Zrzut ekranu zapisany %s',
- 'Add a screenshot' => 'Dodaj zrzut ekranu',
+ 'Add a screenshot' => 'Dołącz zrzut ekranu',
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Zrób zrzut ekranu i wciśnij CTRL+V by dodać go tutaj.',
'Screenshot uploaded successfully.' => 'Zrzut ekranu dodany.',
'SEK - Swedish Krona' => 'SEK - Korona szwedzka',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identyfikator projektu to opcjonalny kod alfanumeryczny do identyfikacji projektu.',
'Identifier' => 'Identyfikator',
'Disable two factor authentication' => 'Wyłącz uwierzytelnianie dwuetapowe',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Czy na pewno chcesz wyłączyć uwierzytelnianie dwuetapowe dla tego użytkownika: "%s"?',
@@ -731,7 +653,6 @@ return array(
'A task cannot be linked to itself' => 'Link do zadania nie może wskazywać na samego siebie',
'The exact same link already exists' => 'Taki link już istnieje',
'Recurrent task is scheduled to be generated' => 'Zaplanowano zadanie cykliczne',
- 'Recurring information' => 'Informacje o zadaniu cyklicznym',
'Score' => 'Wynik',
'The identifier must be unique' => 'Identyfikator musi być unikatowy',
'This linked task id doesn\'t exists' => 'Id zadania nie istnieje',
@@ -761,7 +682,7 @@ return array(
'Calendar settings' => 'Ustawienia kalendarza',
'Project calendar view' => 'Widok kalendarza projektu',
'Project settings' => 'Ustawienia Projektu',
- 'Show subtasks based on the time tracking' => 'Pokaż pod-zadania w śledzeniu czasu',
+ 'Show subtasks based on the time tracking' => 'Pokaż podzadania w śledzeniu czasu',
'Show tasks based on the creation date' => 'Pokaż zadania względem daty utworzenia',
'Show tasks based on the start date' => 'Pokaż zadania względem daty rozpoczęcia',
'Subtasks time tracking' => 'Śledzenie czasu pod-zadań',
@@ -773,34 +694,23 @@ return array(
'Two factor authentication disabled' => 'Uwierzytelnianie dwuetapowe wyłączone',
'Two factor authentication enabled' => 'Uwierzytelnianie dwuetapowe włączone',
'Unable to update this user.' => 'Nie można zaktualizować tego użytkownika',
- 'There is no user management for private projects.' => 'Dla projektów prywatnych nie występuje zarządzanie użytkownikami.',
+ 'There is no user management for private projects.' => 'Projekty prywatne nie wspierają zarządzania użytkownikami. Projekt prywatny ma tylko jednego użytkownika.',
// 'User that will receive the email' => '',
// 'Email subject' => '',
- // 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
+ 'Date' => 'Data',
// 'Add a comment log when moving the task between columns' => '',
// 'Move the task to another column when the category is changed' => '',
- // 'Send a task by email to someone' => '',
- // 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
- // 'Column change' => '',
- // 'Position change' => '',
- // 'Swimlane change' => '',
- // 'Assignee change' => '',
+ 'Send a task by email to someone' => 'Wyślij zadanie mailem do kogokolwiek',
+ 'Reopen a task' => 'Otwórz ponownie zadanie',
+ 'Column change' => 'Zmiana kolumny',
+ 'Position change' => 'Zmiana pozycji',
+ 'Swimlane change' => 'Zmiana Swimlane',
+ 'Assignee change' => 'Zmiana przypisanego użytkownika',
// '[%s] Overdue tasks' => '',
- // 'Notification' => '',
+ 'Notification' => 'Powiadomienie',
// '%s moved the task #%d to the first swimlane' => '',
// '%s moved the task #%d to the swimlane "%s"' => '',
- // 'Swimlane' => '',
+ 'Swimlane' => 'Proces',
// 'Gravatar' => '',
// '%s moved the task %s to the first swimlane' => '',
// '%s moved the task %s to the swimlane "%s"' => '',
@@ -811,12 +721,12 @@ return array(
// 'The task have been moved to the first swimlane' => '',
// 'The task have been moved to another swimlane:' => '',
// 'Overdue tasks for the project "%s"' => '',
- // 'New title: %s' => '',
- // 'The task is not assigned anymore' => '',
- // 'New assignee: %s' => '',
- // 'There is no category now' => '',
- // 'New category: %s' => '',
- // 'New color: %s' => '',
+ 'New title: %s' => 'Nowy tytuł: %s',
+ 'The task is not assigned anymore' => 'Brak osoby odpowiedzialnej za zadanie',
+ 'New assignee: %s' => 'Nowy odpowiedzialny: %s',
+ 'There is no category now' => 'Aktualnie zadanie nie posiada kategorii',
+ 'New category: %s' => 'Nowa kategoria: %s',
+ 'New color: %s' => 'Nowy kolor: %s',
// 'New complexity: %d' => '',
// 'The due date have been removed' => '',
// 'There is no description anymore' => '',
@@ -826,61 +736,54 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
- // 'I want to receive notifications for:' => '',
- // 'All tasks' => '',
- // 'Only for tasks assigned to me' => '',
- // 'Only for tasks created by me' => '',
- // 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
+ 'I want to receive notifications for:' => 'Wysyłaj powiadomienia dla:',
+ 'All tasks' => 'Wszystkich zadań',
+ 'Only for tasks assigned to me' => 'Tylko zadań przypisanych do mnie',
+ 'Only for tasks created by me' => 'Tylko zadań utworzonych przeze mnie',
+ 'Only for tasks created by me and assigned to me' => 'Tylko zadań przypisanych lub utworzonych przeze mnie',
// '%%Y-%%m-%%d' => '',
// 'Total for all columns' => '',
// 'You need at least 2 days of data to show the chart.' => '',
// '<15m' => '',
// '<30m' => '',
- // 'Stop timer' => '',
- // 'Start timer' => '',
- // 'Add project member' => '',
- // 'Enable notifications' => '',
- // 'My activity stream' => '',
- // 'My calendar' => '',
- // 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
- // 'Reset filters' => '',
- // 'My tasks due tomorrow' => '',
- // 'Tasks due today' => '',
- // 'Tasks due tomorrow' => '',
- // 'Tasks due yesterday' => '',
- // 'Closed tasks' => '',
- // 'Open tasks' => '',
- // 'Not assigned' => '',
- // 'View advanced search syntax' => '',
- // 'Overview' => '',
- // '%b %e %Y' => '',
- // 'Board/Calendar/List view' => '',
- // 'Switch to the board view' => '',
- // 'Switch to the calendar view' => '',
- // 'Switch to the list view' => '',
- // 'Go to the search/filter box' => '',
- // 'There is no activity yet.' => '',
- // 'No tasks found.' => '',
- // 'Keyboard shortcut: "%s"' => '',
- // 'List' => '',
- // 'Filter' => '',
- // 'Advanced search' => '',
- // 'Example of query: ' => '',
- // 'Search by project: ' => '',
- // 'Search by column: ' => '',
- // 'Search by assignee: ' => '',
- // 'Search by color: ' => '',
- // 'Search by category: ' => '',
- // 'Search by description: ' => '',
- // 'Search by due date: ' => '',
+ 'Stop timer' => 'Zatrzymaj pomiar czasu',
+ 'Start timer' => 'Uruchom pomiar czasu',
+ 'Add project member' => 'Dodaj członka projektu',
+ 'Enable notifications' => 'Włącz powiadomienia',
+ 'My activity stream' => 'Moja aktywność',
+ 'My calendar' => 'Mój kalendarz',
+ 'Search tasks' => 'Szukaj zadań',
+ 'Back to the calendar' => 'Wróć do kalendarza',
+ 'Filters' => 'Filtry',
+ 'Reset filters' => 'Resetuj zastosowane filtry',
+ 'My tasks due tomorrow' => 'Moje zadania do jutra',
+ 'Tasks due today' => 'Zadania do dzisiaj',
+ 'Tasks due tomorrow' => 'Zadania do jutra',
+ 'Tasks due yesterday' => 'Zadania na wczoraj',
+ 'Closed tasks' => 'Zamknięte zadania',
+ 'Open tasks' => 'Otwarte zadania',
+ 'Not assigned' => 'Nieprzypisane zadania',
+ 'View advanced search syntax' => 'Pomoc dotycząca budowania filtrów',
+ 'Overview' => 'Podsumowanie',
+ 'Board/Calendar/List view' => 'Widok: Tablica/Kalendarz/Lista',
+ 'Switch to the board view' => 'Przełącz na tablicę',
+ 'Switch to the calendar view' => 'Przełącz na kalendarz',
+ 'Switch to the list view' => 'Przełącz na listę',
+ 'Go to the search/filter box' => 'Użyj pola wyszukiwania/filtrów',
+ 'There is no activity yet.' => 'Brak powiadomień',
+ 'No tasks found.' => 'Nie znaleziono zadań',
+ 'Keyboard shortcut: "%s"' => 'Skrót klawiaturowy: "%s"',
+ 'List' => 'Lista',
+ 'Filter' => 'Filtr',
+ 'Advanced search' => 'Zaawansowane wyszukiwanie',
+ 'Example of query: ' => 'Przykładowe zapytanie:',
+ 'Search by project: ' => 'Szukaj wg projektów:',
+ 'Search by column: ' => 'Szukaj wg kolumn:',
+ 'Search by assignee: ' => 'Szukaj wg użytkownika:',
+ 'Search by color: ' => 'Szukaj wg koloru:',
+ 'Search by category: ' => 'Szukaj wg kategorii:',
+ 'Search by description: ' => 'Szukaj wg opisu:',
+ 'Search by due date: ' => 'Szukaj wg terminu:',
// 'Lead and Cycle time for "%s"' => '',
// 'Average time spent into each column for "%s"' => '',
// 'Average time spent into each column' => '',
@@ -894,104 +797,81 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
- // 'Time spent into each column' => '',
+ 'Time spent into each column' => 'Czas spędzony przez zadanie w każdej z kolumn',
// 'The lead time is the duration between the task creation and the completion.' => '',
// 'The cycle time is the duration between the start date and the completion.' => '',
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
- // 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
+ 'Edit Authentication' => 'Edycja autoryzacji',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
- // 'New remote user' => '',
- // 'New local user' => '',
- // 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
- // 'This feature does not work with all browsers.' => '',
+ 'New remote user' => 'Nowy użytkownik zdalny',
+ 'New local user' => 'Nowy użytkownik lokalny',
+ 'Default task color' => 'Domyślny kolor zadań',
+ 'This feature does not work with all browsers.' => 'Ta funkcja może nie działać z każdą przeglądarką',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
// 'Include closed tasks in the cumulative flow diagram' => '',
- // 'Current swimlane: %s' => '',
- // 'Current column: %s' => '',
- // 'Current category: %s' => '',
- // 'no category' => '',
+ 'Current swimlane: %s' => 'Bieżący swimlane: %s',
+ 'Current column: %s' => 'Bieżąca kolumna: %s',
+ 'Current category: %s' => 'Bieżąca kategoria: %s',
+ 'no category' => 'brak kategorii',
// 'Current assignee: %s' => '',
- // 'not assigned' => '',
- // 'Author:' => '',
- // 'contributors' => '',
- // 'License:' => '',
- // 'License' => '',
- // 'Project Administrator' => '',
- // 'Enter the text below' => '',
- // 'Gantt chart for %s' => '',
- // 'Sort by position' => '',
- // 'Sort by date' => '',
- // 'Add task' => '',
- // 'Start date:' => '',
- // 'Due date:' => '',
- // 'There is no start date or due date for this task.' => '',
+ 'not assigned' => 'Brak osoby odpowiedzialnej',
+ 'Author:' => 'Autor',
+ 'contributors' => 'współautorzy',
+ 'License:' => 'Licencja:',
+ 'License' => 'Licencja',
+ 'Enter the text below' => 'Wpisz tekst poniżej',
+ 'Gantt chart for %s' => 'Wykres Gantt dla %s',
+ 'Sort by position' => 'Sortuj wg pozycji',
+ 'Sort by date' => 'Sortuj wg daty',
+ 'Add task' => 'Dodaj zadanie',
+ 'Start date:' => 'Data rozpoczęcia:',
+ 'Due date:' => 'Termin',
+ 'There is no start date or due date for this task.' => 'Brak daty rozpoczęcia lub terminu zadania',
// 'Moving or resizing a task will change the start and due date of the task.' => '',
// 'There is no task in your project.' => '',
- // 'Gantt chart' => '',
- // 'People who are project managers' => '',
- // 'People who are project members' => '',
+ 'Gantt chart' => 'Wykres Gantta',
+ 'People who are project managers' => 'Użytkownicy będący menedżerami projektu',
+ 'People who are project members' => 'Użytkownicy będący uczestnikami projektu',
// 'NOK - Norwegian Krone' => '',
- // 'Show this column' => '',
- // 'Hide this column' => '',
- // 'open file' => '',
- // 'End date' => '',
- // 'Users overview' => '',
- // 'Managers' => '',
- // 'Members' => '',
- // 'Shared project' => '',
- // 'Project managers' => '',
- // 'Project members' => '',
- // 'Gantt chart for all projects' => '',
- // 'Projects list' => '',
- // 'Gantt chart for this project' => '',
- // 'Project board' => '',
- // 'End date:' => '',
- // 'There is no start date or end date for this project.' => '',
- // 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
- // 'Link type' => '',
- // 'Change task color when using a specific task link' => '',
- // 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
- // 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
+ 'Show this column' => 'Pokaż tą kolumnę',
+ 'Hide this column' => 'Ukryj tą kolumnę',
+ 'open file' => 'otwórz plik',
+ 'End date' => 'Data zakończenia',
+ 'Users overview' => 'Przegląd użytkowników',
+ 'Members' => 'Uczestnicy',
+ 'Shared project' => 'Projekt udostępniony',
+ 'Project managers' => 'Menedżerowie projektu',
+ 'Gantt chart for all projects' => 'Wykres Gantta dla wszystkich projektów',
+ 'Projects list' => 'Lista projektów',
+ 'Gantt chart for this project' => 'Wykres Gantta dla bieżacego projektu',
+ 'Project board' => 'Talica projektu',
+ 'End date:' => 'Data zakończenia:',
+ 'There is no start date or end date for this project.' => 'Nie zdefiniowano czasu trwania projektu',
+ 'Projects Gantt chart' => 'Wykres Gantta dla projektów',
+ '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',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
- // 'Documentation' => '',
+ 'Documentation' => 'Dokumentacja',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
- // 'Author' => '',
- // 'Version' => '',
- // 'Plugins' => '',
- // 'There is no plugin loaded.' => '',
- // 'Set maximum column height' => '',
- // 'Remove maximum column height' => '',
- // 'My notifications' => '',
- // 'Custom filters' => '',
+ 'Author' => 'Autor',
+ 'Version' => 'Wersja',
+ 'Plugins' => 'Wtyczki',
+ 'There is no plugin loaded.' => 'Nie wykryto żadnych wtyczek.',
+ 'Set maximum column height' => 'Rozwiń kolumny',
+ 'Remove maximum column height' => 'Zwiń kolumny',
+ 'My notifications' => 'Powiadomienia',
+ 'Custom filters' => 'Dostosuj filtry',
// 'Your custom filter have been created successfully.' => '',
// 'Unable to create your custom filter.' => '',
// 'Custom filter removed successfully.' => '',
@@ -1000,68 +880,272 @@ return array(
// 'Your custom filter have been updated successfully.' => '',
// 'Unable to update custom filter.' => '',
// 'Web' => '',
- // 'New attachment on task #%d: %s' => '',
- // 'New comment on task #%d' => '',
- // 'Comment updated on task #%d' => '',
- // 'New subtask on task #%d' => '',
- // 'Subtask updated on task #%d' => '',
- // 'New task #%d: %s' => '',
- // 'Task updated #%d' => '',
- // 'Task #%d closed' => '',
- // 'Task #%d opened' => '',
- // 'Column changed for task #%d' => '',
- // 'New position for task #%d' => '',
- // 'Swimlane changed for task #%d' => '',
- // 'Assignee changed on task #%d' => '',
+ 'New attachment on task #%d: %s' => 'Nowy załącznik do zadania #%d: %s',
+ 'New comment on task #%d' => 'Nowy komentarz do zadania #%d',
+ 'Comment updated on task #%d' => 'Aktualizacja komentarza do zadania #%d',
+ 'New subtask on task #%d' => 'Nowe podzadanie dla zadania #%d',
+ 'Subtask updated on task #%d' => 'Aktualizacja podzadania w zadaniu #%d',
+ 'New task #%d: %s' => 'Nowe zadanie #%d: %s',
+ 'Task updated #%d' => 'Aktualizacja zadania #%d',
+ 'Task #%d closed' => 'Zamknięto zadanie #%d',
+ 'Task #%d opened' => 'Otwarto zadanie #%d',
+ 'Column changed for task #%d' => 'Zmieniono kolumnę zadania #%d',
+ 'New position for task #%d' => 'Ustalono nową pozycję zadania #%d',
+ 'Swimlane changed for task #%d' => 'Zmieniono swimlane dla zadania #%d',
+ 'Assignee changed on task #%d' => 'Zmieniono osobę odpowiedzialną dla zadania #%d',
// '%d overdue tasks' => '',
// 'Task #%d is overdue' => '',
- // 'No new notifications.' => '',
- // 'Mark all as read' => '',
- // 'Mark as read' => '',
+ 'No new notifications.' => 'Brak nowych powiadomień.',
+ 'Mark all as read' => 'Oznacz wszystkie jako przeczytane',
+ 'Mark as read' => 'Oznacz jako przeczytane',
// 'Total number of tasks in this column across all swimlanes' => '',
- // 'Collapse swimlane' => '',
- // 'Expand swimlane' => '',
- // 'Add a new filter' => '',
- // 'Share with all project members' => '',
+ 'Collapse swimlane' => 'Zwiń swimlane',
+ 'Expand swimlane' => 'Rozwiń swimlane',
+ 'Add a new filter' => 'Dodaj nowy filtr',
+ 'Share with all project members' => 'Udostępnij wszystkim uczestnikom projektu',
// 'Shared' => '',
- // 'Owner' => '',
- // 'Unread notifications' => '',
- // 'My filters' => '',
- // 'Notification methods:' => '',
+ 'Owner' => 'Właściciel',
+ 'Unread notifications' => 'Nieprzeczytane powiadomienia',
+ 'My filters' => 'Moje filtry',
+ 'Notification methods:' => 'Metody powiadomień:',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
// '%d task(s) have been imported successfully.' => '',
// 'Nothing have been imported!' => '',
// 'Import users from CSV file' => '',
// '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
+ 'Comma' => 'Przecinek',
+ 'Semi-colon' => 'Średnik',
+ 'Tab' => 'Tabulacja',
+ 'Vertical bar' => 'Kreska pionowa',
+ 'Double Quote' => 'Cudzysłów',
+ 'Single Quote' => 'Apostrof',
// '%s attached a file to the task #%d' => '',
// 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
+ 'Append filter (instead of replacement)' => 'Dołączaj filtr do zastosowanego filtru(zamiast przełączać)',
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
+ 'change sorting' => 'odwróć sortowanie',
+ 'Tasks Importation' => 'Import zadań',
+ 'Delimiter' => 'Separator pola',
+ 'Enclosure' => 'Separator tekstu',
+ 'CSV File' => 'Plik CSV',
+ 'Instructions' => 'Instrukcje',
+ 'Your file must use the predefined CSV format' => 'Twój plik musi być zgodny z predefiniowanym formatem CSV (pobierz szablon)',
+ 'Your file must be encoded in UTF-8' => 'Twój plik musi być kodowany w UTF-8',
+ 'The first row must be the header' => 'Pierwszy wiersz pliku musi definiować nagłówki',
+ 'Duplicates are not verified for you' => 'Duplikaty nie będą weryfikowane',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'Data musi być w formacie ISO: YYYY-MM-DD',
+ 'Download CSV template' => 'Pobierz szablon pliku CSV',
// 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
+ 'Duplicates are not imported' => 'Duplikaty nie zostaną zaimportowane',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ 'Groups' => 'Grupy',
+ 'Members of %s' => 'Członkowie %s',
+ 'New group' => 'Nowa grupa',
+ 'Group created successfully.' => 'Grupa została utworzona.',
+ 'Unable to create your group.' => 'Nie można utworzyć grupy.',
+ 'Edit group' => 'Edytuj grupę',
+ 'Group updated successfully.' => 'Grupa została zaaktualizowana.',
+ 'Unable to update your group.' => 'Nie można zaaktualizować grupy.',
+ 'Add group member to "%s"' => 'Dodaj członka do grupy "%s"',
+ 'Group member added successfully.' => 'Użytkownik został dodany do grupy.',
+ 'Unable to add group member.' => 'Nie można dodać użytkownika do grupy.',
+ 'Remove user from group "%s"' => 'Usuń użytkownika z grupy "%s"',
+ 'User removed successfully from this group.' => 'Użytkownik został usunięty z grupy.',
+ 'Unable to remove this user from the group.' => 'Nie można usunąć użytkownika z grupy.',
+ 'Remove group' => 'Usuń grupę',
+ 'Group removed successfully.' => 'Grupa została usunięta.',
+ 'Unable to remove this group.' => 'Nie można usunąć grupy.',
+ 'Project Permissions' => 'Prawa dostępowe projektu',
+ 'Manager' => 'Menedżer',
+ 'Project Manager' => 'Menedżer projektu',
+ 'Project Member' => 'Uczestnik projektu',
+ 'Project Viewer' => 'Obserwator projektu',
+ 'Your account is locked for %d minutes' => 'Twoje konto zostało zablokowane na %d minut',
+ 'Invalid captcha' => 'Błędny kod z obrazka (captcha)',
+ 'The name must be unique' => 'Nazwa musi być unikatowa',
+ 'View all groups' => 'Wyświetl wszystkie grupy',
+ 'View group members' => 'Wyświetl wszystkich członków grupy',
+ 'There is no user available.' => 'Żaden użytkownik nie jest dostępny.',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Czy napewno chcesz usunąć użytkownika "%s" z grupy "%s"?',
+ 'There is no group.' => 'Nie utworzono jeszcze żadnej grupy.',
+ 'External Id' => 'Zewnętrzny Id',
+ 'Add group member' => 'Dodaj członka grupy',
+ 'Do you really want to remove this group: "%s"?' => 'Czy napewno chcesz usunąć grupę "%s"?',
+ 'There is no user in this group.' => 'Wybrana grupa nie posiada członków.',
+ 'Remove this user' => 'Usuń użytkownika',
+ 'Permissions' => 'Prawa dostępu',
+ 'Allowed Users' => 'Użytkownicy, którzy mają dostęp',
+ 'No user have been allowed specifically.' => 'Żaden użytkownik nie ma przyznanego dostępu.',
+ 'Role' => 'Rola',
+ 'Enter user name...' => 'Wprowadź nazwę użytkownika...',
+ 'Allowed Groups' => 'Grupy, które mają dostęp',
+ 'No group have been allowed specifically.' => 'Żadna grupa nie ma przyznanego dostępu.',
+ 'Group' => 'Grupa',
+ 'Group Name' => 'Nazwa grupy',
+ 'Enter group name...' => 'Wprowadź nazwę grupy...',
+ 'Role:' => 'Rola:',
+ 'Project members' => 'Uczestnicy projektu',
+ // 'Compare hours for "%s"' => '',
+ '%s mentioned you in the task #%d' => '%s wspomiał o Tobie w zadaniu #%d',
+ '%s mentioned you in a comment on the task #%d' => '%s wspomiał o Tobie w komentarzu do zadania #%d',
+ 'You were mentioned in the task #%d' => 'Wspomiano o Tobie w zadaniu #%d',
+ 'You were mentioned in a comment on the task #%d' => 'Wspomiano o Tobie w komentarzu do zadania #%d',
+ 'Mentioned' => 'Wspomiano',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ 'There is no integration registered at the moment.' => 'W chwili obecnej funkcjonalność ta została wyłączona.',
+ 'Password Reset for Kanboard' => 'Resetuj hasło do Kanboarda',
+ 'Forgot password?' => 'Nie pamiętasz hasła?',
+ 'Enable "Forget Password"' => 'Włącz możliwość odzyskiwania zapomianego hasła przez użytkowników',
+ 'Password Reset' => 'Resetuj hasło',
+ 'New password' => 'Nowe hasło',
+ 'Change Password' => 'Zmień hasło',
+ 'To reset your password click on this link:' => 'Kliknij w poniższy link, aby zresetować hasło:',
+ 'Last Password Reset' => 'Ostatnie odzyskiwanie hasła',
+ 'The password has never been reinitialized.' => 'Hasło nigdy nie było odzyskiwane.',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ 'Password reset history' => 'Historia hasła',
+ // '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' => 'Zamknij wszystkie zadania w tej kolumnie',
+ 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Wtyczki obsługujące dodatkowe powiadomienia nie zostały zainstalowane. Dalej jednak możesz korzystać z standardowych powiadomień (sprawdź w ustawieniach Twojego profilu).',
+ 'My dashboard' => 'Mój dashboard',
+ 'My profile' => 'Mój profil',
+ 'Project owner:' => 'Właściciel:',
+ 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'Identyfikator projektu jest opcjonalny i musi być alfanumeryczny, przykład: MYPROJECT.',
+ 'Project owner' => 'Właściciel projektu',
+ 'Those dates are useful for the project Gantt chart.' => 'Daty te są przydatne dla wykresu Gantta.',
+ 'Private projects do not have users and groups management.' => 'Projekty prywatne nie wspierają obsługi użytkowników i grup.',
+ // 'There is no project member.' => '',
+ 'Priority' => 'Priorytet',
+ 'Task priority' => 'Priorytety zadań',
+ 'General' => 'Ogólne',
+ 'Dates' => 'Czas życia projektu',
+ 'Default priority' => 'Domyślny priorytet',
+ 'Lowest priority' => 'Najniższy priorytet',
+ 'Highest priority' => 'Najwyższy priorytet',
+ 'If you put zero to the low and high priority, this feature will be disabled.' => 'Jeżeli dla najniższego i najwyższego priorytetu ustawisz 0, to priorytety dla tablicy zostaną wyłączone.',
+ // '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',
+ 'Related' => 'Powiązanie',
+ 'Attachment' => 'Załącznik',
+ // 'Title not found' => '',
+ 'Web Link' => 'Link URL',
+ 'External links' => 'Linki zewnętrzne',
+ 'Add external link' => 'Dodaj link zewnętrzny',
+ 'Type' => 'Typ',
+ 'Dependency' => 'Zależność',
+ 'Add internal link' => 'Dodaj link do innego zadania',
+ 'Add a new external link' => 'Dodaj nowy link zewnętrzny',
+ 'Edit external link' => 'Edytuj link zewnętrzny',
+ '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',
+ 'Projects management' => 'Zarządzanie projektami',
+ '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:',
+ // 'Reference:' => '',
+ 'Complexity:' => 'Złożoność:',
+ 'Swimlane:' => 'Proces:',
+ 'Column:' => 'Kolumna:',
+ 'Position:' => 'Pozycja:',
+ 'Creator:' => 'Utworzył:',
+ 'Time estimated:' => 'Szacowany czas:',
+ '%s hours' => '%s godzin',
+ 'Time spent:' => 'Wykorzystany czas:',
+ 'Created:' => 'Utworzone:',
+ 'Modified:' => 'Zmodyfikowane:',
+ 'Completed:' => 'Ukończone:',
+ 'Started:' => 'Rozpoczęte:',
+ 'Moved:' => 'Przesunięcie:',
+ 'Task #%d' => 'Zadanie #%d',
+ 'Sub-tasks' => 'Podzadania',
+ // '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' => 'Przeciągnij i upuść pliki tutaj',
+ 'choose files' => 'wybierz pliki',
+ // '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' => 'Pobierz',
+ 'Uploaded: %s' => 'Data załączenia: %s',
+ 'Size: %s' => 'Rozmiar: %s',
+ 'Uploaded by %s' => 'Załadowany przez %s',
+ 'Filename' => 'Nazwa pliku',
+ 'Size' => 'Rozmiar',
+ // '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' => '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 6ee5f2dd..8f2ebedb 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -68,11 +68,10 @@ return array(
'Disable' => 'Desativar',
'Enable' => 'Ativar',
'New project' => 'Novo projeto',
- 'Do you really want to remove this project: "%s"?' => 'Você realmente deseja remover este projeto: "%s" ?',
+ 'Do you really want to remove this project: "%s"?' => 'Você realmente deseja remover este projeto: "%s"?',
'Remove project' => 'Remover projeto',
'Edit the board for "%s"' => 'Editar o board para "%s"',
'All projects' => 'Todos os projetos',
- 'Change columns' => 'Modificar colunas',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
'Nobody assigned' => 'Ninguém designado',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Abrir uma tarefa',
'Do you really want to open this task: "%s"?' => 'Você realmente deseja abrir esta tarefa: "%s"?',
'Back to the board' => 'Voltar ao board',
- 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M',
'There is nobody assigned' => 'Não há ninguém designado',
'Column on the board:' => 'Coluna no board:',
- 'Status is open' => 'Status está aberto',
- 'Status is closed' => 'Status está finalizado',
'Close this task' => 'Finalizar esta tarefa',
'Open this task' => 'Abrir esta tarefa',
'There is no description.' => 'Não há descrição.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'O ID é obrigatório',
'The project id is required' => 'O ID do projeto é obrigatório',
'The project name is required' => 'O nome do projeto é obrigatório',
- 'This project must be unique' => 'Este projeto deve ser único',
'The title is required' => 'O título é obrigatório',
'Settings saved successfully.' => 'Configurações salvas com sucesso.',
'Unable to save your settings.' => 'Não é possível salvar suas configurações.',
@@ -159,29 +154,19 @@ return array(
'Work in progress' => 'Em andamento',
'Done' => 'Finalizado',
'Application version:' => 'Versão da aplicação:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Finalizado em %d %B %Y às %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M',
- 'Date created' => 'Data de criação',
- 'Date completed' => 'Data da finalização',
'Id' => 'Id',
'%d closed tasks' => '%d tarefas finalizadas',
'No task for this project' => 'Não há tarefa para este projeto',
'Public link' => 'Link público',
- 'Change assignee' => 'Mudar a designação',
- 'Change assignee for the task "%s"' => 'Modificar designação para a tarefa "%s"',
+ 'Change assignee' => 'Alterar designação',
+ 'Change assignee for the task "%s"' => 'Alterar designação para a tarefa "%s"',
'Timezone' => 'Fuso horário',
'Sorry, I didn\'t find this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!',
'Page not found' => 'Página não encontrada',
'Complexity' => 'Complexidade',
'Task limit' => 'Limite de tarefas',
'Task count' => 'Número de tarefas',
- 'Edit project access list' => 'Editar lista de acesso ao projeto',
- 'Allow this user' => 'Permitir este usuário',
- 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.',
- 'Revoke' => 'Revogar',
- 'List of authorized users' => 'Lista de usuários autorizados',
'User' => 'Usuário',
- 'Nobody have access to this project.' => 'Ninguém tem acesso a este projeto.',
'Comments' => 'Comentários',
'Write your text in Markdown' => 'Escreva seu texto em Markdown',
'Leave a comment' => 'Deixe um comentário',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Editar esta tarefa',
'Due Date' => 'Data de vencimento',
'Invalid date' => 'Data inválida',
- 'Must be done before %B %e, %Y' => 'Deve ser finalizado antes de %d %B %Y',
- '%B %e, %Y' => '%d %B %Y',
- '%b %e, %Y' => '%d %B %Y',
'Automatic actions' => 'Ações automáticas',
'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.',
'Unable to create your automatic action.' => 'Não é possível criar sua ação automática.',
@@ -217,7 +199,7 @@ return array(
'Remove an automatic action' => 'Remover uma ação automática',
'Assign the task to a specific user' => 'Designar a tarefa para um usuário específico',
'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a ação',
- 'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projeto',
+ 'Duplicate the task to another project' => 'Duplicar a tarefa para outro projeto',
'Move a task to another column' => 'Mover a tarefa para outra coluna',
'Task modification' => 'Modificação de tarefa',
'Task creation' => 'Criação de tarefa',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Designar uma cor para um usuário específico',
'Column title' => 'Título da coluna',
'Position' => 'Posição',
- 'Move Up' => 'Mover para cima',
- 'Move Down' => 'Mover para baixo',
'Duplicate to another project' => 'Duplicar para outro projeto',
'Duplicate' => 'Duplicar',
'link' => 'link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Comentário removido com sucesso.',
'Unable to remove this comment.' => 'Não é possível remover este comentário.',
'Do you really want to remove this comment?' => 'Você realmente deseja remover este comentário?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Somente os administradores ou o criator deste comentário possuem acesso a esta página.',
'Current password for the user "%s"' => 'Senha atual para o usuário "%s"',
'The current password is required' => 'A senha atual é obrigatória',
'Wrong password' => 'Senha incorreta',
@@ -267,14 +246,10 @@ return array(
'External authentication failed' => 'Autenticação externa falhou',
'Your external account is linked to your profile successfully.' => 'Sua conta externa está agora ligada ao seu perfil.',
'Email' => 'E-mail',
- 'Link my Google Account' => 'Vincular minha Conta do Google',
- 'Unlink my Google Account' => 'Desvincular minha Conta do Google',
- 'Login with my Google Account' => 'Entrar com minha Conta do Google',
- 'Project not found.' => 'Projeto não encontrado.',
'Task removed successfully.' => 'Tarefa removida com sucesso.',
'Unable to remove this task.' => 'Não foi possível remover esta tarefa.',
'Remove a task' => 'Remover uma tarefa',
- 'Do you really want to remove this task: "%s"?' => 'Você realmente deseja remover esta tarefa: "%s"',
+ 'Do you really want to remove this task: "%s"?' => 'Você realmente deseja remover esta tarefa: "%s"?',
'Assign automatically a color based on a category' => 'Atribuir automaticamente uma cor com base em uma categoria',
'Assign automatically a category based on a color' => 'Atribuir automaticamente uma categoria com base em uma cor',
'Task creation or modification' => 'Criação ou modificação de tarefa',
@@ -287,12 +262,12 @@ return array(
'Your category have been updated successfully.' => 'A sua categoria foi atualizada com sucesso.',
'Unable to update your category.' => 'Não foi possível atualizar a sua categoria.',
'Remove a category' => 'Remover uma categoria',
- 'Category removed successfully.' => 'Categoria removido com sucesso.',
+ 'Category removed successfully.' => 'Categoria removida com sucesso.',
'Unable to remove this category.' => 'Não foi possível remover esta categoria.',
'Category modification for the project "%s"' => 'Modificação de categoria para o projeto "%s"',
- 'Category Name' => 'Nome da Categoria',
+ 'Category Name' => 'Nome da categoria',
'Add a new category' => 'Adicionar uma nova categoria',
- 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"',
+ 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"?',
'All categories' => 'Todas as categorias',
'No category' => 'Nenhum categoria',
'The name is required' => 'O nome é obrigatório',
@@ -300,7 +275,7 @@ return array(
'Unable to remove this file.' => 'Não foi possível remover este arquivo.',
'File removed successfully.' => 'Arquivo removido com sucesso.',
'Attach a document' => 'Anexar um documento',
- 'Do you really want to remove this file: "%s"?' => 'Você realmente deseja remover este arquivo: "%s"',
+ 'Do you really want to remove this file: "%s"?' => 'Você realmente deseja remover este arquivo: "%s"?',
'Attachments' => 'Anexos',
'Edit the task' => 'Editar a tarefa',
'Edit the description' => 'Editar a descrição',
@@ -331,14 +306,10 @@ return array(
'Unable to update your sub-task.' => 'Não foi possível atualizar a sua subtarefa.',
'Unable to create your sub-task.' => 'Não é possível criar a sua subtarefa.',
'Sub-task added successfully.' => 'Subtarefa adicionada com sucesso.',
- 'Maximum size: ' => 'Tamanho máximo:',
+ 'Maximum size: ' => 'Tamanho máximo: ',
'Unable to upload the file.' => 'Não foi possível carregar o arquivo.',
'Display another project' => 'Exibir outro projeto',
- 'Login with my Github Account' => 'Entrar com minha Conta do Github',
- 'Link my Github Account' => 'Associar à minha Conta do Github',
- 'Unlink my Github Account' => 'Desassociar a minha Conta do Github',
'Created by %s' => 'Criado por %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p',
'Tasks Export' => 'Exportar Tarefas',
'Tasks exportation for "%s"' => 'As tarefas foram exportadas para "%s"',
'Start Date' => 'Data inicial',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projetos:',
'view the task on Kanboard' => 'ver a tarefa no Kanboard',
'Public access' => 'Acesso público',
- 'User management' => 'Gerenciamento de usuários',
'Active tasks' => 'Tarefas ativas',
'Disable public access' => 'Desabilitar o acesso público',
'Enable public access' => 'Habilitar o acesso público',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'E-mail:',
'Notifications:' => 'Notificações:',
'Notifications' => 'Notificações',
- 'Group:' => 'Grupo:',
- 'Regular user' => 'Usuário comum',
'Account type:' => 'Tipo de conta:',
'Edit profile' => 'Editar perfil',
'Change password' => 'Alterar senha',
'Password modification' => 'Alteração de senha',
'External authentications' => 'Autenticação externa',
- 'Google Account' => 'Conta do Google',
- 'Github Account' => 'Conta do Github',
'Never connected.' => 'Nunca conectado.',
- 'No account linked.' => 'Nenhuma conta associada.',
- 'Account linked.' => 'Conta associada.',
'No external authentication enabled.' => 'Nenhuma autenticação externa habilitada.',
'Password modified successfully.' => 'Senha alterada com sucesso.',
'Unable to change the password.' => 'Não foi possível alterar a senha.',
@@ -446,22 +410,15 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s mudou a designação da tarefa %s para %s',
'New password for the user "%s"' => 'Nova senha para o usuário "%s"',
'Choose an event' => 'Escolher um evento',
- 'Github commit received' => 'Github commit received',
- 'Github issue opened' => 'Github issue opened',
- 'Github issue closed' => 'Github issue closed',
- 'Github issue reopened' => 'Github issue reopened',
- 'Github issue assignee change' => 'Github issue assignee change',
- 'Github issue label change' => 'Github issue label change',
'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo',
'Change the assignee based on an external username' => 'Alterar designação com base em um usuário externo',
'Change the category based on an external label' => 'Alterar categoria com base em um rótulo externo',
'Reference' => 'Referência',
- 'Reference: %s' => 'Referência: %s',
'Label' => 'Rótulo',
'Database' => 'Banco de dados',
'About' => 'Sobre',
'Database driver:' => 'Driver do banco de dados:',
- 'Board settings' => 'Configurações do Board',
+ 'Board settings' => 'Configurações do board',
'URL and token' => 'URL e token',
'Webhook settings' => 'Configurações do Webhook',
'URL for task creation:' => 'URL para a criação da tarefa:',
@@ -474,17 +431,13 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frequência em segundos (60 segundos por padrão)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequência em segundos (0 para desativar este recurso, 10 segundos por padrão)',
'Application URL' => 'URL da Aplicação',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemplo: http://example.kanboard.net/ (utilizado nas notificações por e-mail)',
- 'Token regenerated.' => 'Token ',
+ 'Token regenerated.' => 'Novo token gerado.',
'Date format' => 'Formato de data',
'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',
- 'Estimated time: %s hours' => 'Tempo estimado: %s horas',
- 'Time spent: %s hours' => 'Tempo gasto: %s horas',
- 'Started on %B %e, %Y' => 'Iniciado em %B %e, %Y',
'Start date' => 'Data de início',
'Time estimated' => 'Tempo estimado',
'There is nothing assigned to you.' => 'Não há nada designado à você.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Todos possuem acesso a este projeto.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github webhooks',
- 'Help on Github webhooks' => 'Ajuda sobre os webhooks do GitHub',
'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo',
- 'Github issue comment created' => 'Github issue comment created',
'Project management' => 'Gerenciamento de projetos',
'My projects' => 'Meus projetos',
'Columns' => 'Colunas',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Redistribuição de usuário para "%s"',
'Clone this project' => 'Clonar este projeto',
'Column removed successfully.' => 'Coluna removida com sucesso.',
- 'Github Issue' => 'Github Issue',
'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.',
'Previous' => 'Anterior',
'The id must be an integer' => 'O ID deve ser um número inteiro',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Swimlane padrão',
'Do you really want to remove this swimlane: "%s"?' => 'Você realmente deseja remover esta swimlane: "%s"?',
'Inactive swimlanes' => 'Desativar swimlanes',
- 'Set project manager' => 'Definir gerente do projeto',
- 'Set project member' => 'Definir membro do projeto',
'Remove a swimlane' => 'Remover uma swimlane',
- 'Rename' => 'Renomear',
'Show default swimlane' => 'Exibir swimlane padrão',
'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projeto "%s"',
'Swimlane not found.' => 'Swimlane não encontrada.',
@@ -558,37 +504,26 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane atualizada com sucesso.',
'The default swimlane have been updated successfully.' => 'A swimlane padrão foi atualizada com sucesso.',
- 'Unable to create your swimlane.' => 'Não foi possível criar a sua swimlane.',
'Unable to remove this swimlane.' => 'Não foi possível remover esta swimlane.',
'Unable to update this swimlane.' => 'Não foi possível atualizar esta swimlane.',
'Your swimlane have been created successfully.' => 'Sua swimlane foi criada com sucesso.',
- 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Feature Request, Improvement"',
+ 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Solicitação de Recurso, Melhoria"',
'Default categories for new projects (Comma-separated)' => 'Categorias padrões para novos projetos (separadas por vírgula)',
- 'Gitlab commit received' => 'Gitlab commit received',
- 'Gitlab issue opened' => 'Gitlab issue opened',
- 'Gitlab issue closed' => 'Gitlab issue closed',
- 'Gitlab webhooks' => 'Gitlab webhooks',
- 'Help on Gitlab webhooks' => 'Ajuda sobre os webhooks do GitLab',
'Integrations' => 'Integrações',
'Integration with third-party services' => 'Integração com serviços de terceiros',
- 'Role for this project' => 'Função para este projeto',
- 'Project manager' => 'Gerente do projeto',
- 'Project member' => 'Membro do projeto',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Um gerente do projeto pode alterar as configurações do projeto e ter mais privilégios que um usuário padrão.',
- 'Gitlab Issue' => 'Gitlab Issue',
'Subtask Id' => 'ID da subtarefa',
'Subtasks' => 'Subtarefas',
'Subtasks Export' => 'Exportar subtarefas',
'Subtasks exportation for "%s"' => 'Subtarefas exportadas para "%s"',
'Task Title' => 'Título da Tarefa',
'Untitled' => 'Sem título',
- 'Application default' => 'Aplicação padrão',
+ 'Application default' => 'Padrão da aplicação',
'Language:' => 'Idioma',
'Timezone:' => 'Fuso horário',
'All columns' => 'Todas as colunas',
'Calendar' => 'Calendário',
'Next' => 'Próximo',
- // '#%d' => '',
+ '#%d' => '#%d',
'All swimlanes' => 'Todas as swimlanes',
'All colors' => 'Todas as cores',
'Moved to column %s' => 'Mover para a coluna %s',
@@ -598,23 +533,20 @@ return array(
'Edit column "%s"' => 'Editar a coluna "%s"',
'Select the new status of the subtask: "%s"' => 'Selecionar um novo status para a subtarefa: "%s"',
'Subtask timesheet' => 'Gestão de tempo das subtarefas',
- 'There is nothing to show.' => 'Não há nada para mostrar',
+ 'There is nothing to show.' => 'Não há nada para mostrar.',
'Time Tracking' => 'Gestão de tempo',
'You already have one subtask in progress' => 'Você já tem um subtarefa em andamento',
'Which parts of the project do you want to duplicate?' => 'Quais partes do projeto você deseja duplicar?',
'Disallow login form' => 'Proibir o formulário de login',
- 'Bitbucket commit received' => '"Commit" recebido via Bitbucket',
- 'Bitbucket webhooks' => 'Webhook Bitbucket',
- 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks do Bitbucket',
'Start' => 'Início',
'End' => 'Fim',
'Task age in days' => 'Idade da tarefa em dias',
'Days in this column' => 'Dias nesta coluna',
- // '%dd' => '',
+ '%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 n°%d?',
+ 'Do you really want to remove this link with task #%d?' => 'Você realmente deseja remover esta associação com a tarefa #%d?',
'Field required' => 'Campo requerido',
'Link added successfully.' => 'Associação criada com sucesso.',
'Link updated successfully.' => 'Associação atualizada com sucesso.',
@@ -637,8 +569,8 @@ return array(
'is blocked by' => 'está bloqueada por',
'duplicates' => 'duplica',
'is duplicated by' => 'é duplicada por',
- 'is a child of' => 'é um filho de',
- 'is a parent of' => 'é um parente do',
+ 'is a child of' => 'é filha de',
+ 'is a parent of' => 'é mãe de',
'targets milestone' => 'visa um milestone',
'is a milestone of' => 'é um milestone de',
'fixes' => 'corrige',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Esta tarefa',
'<1h' => '<1h',
'%dh' => '%dh',
- '%b %e' => '%e %b',
'Expand tasks' => 'Expandir tarefas',
'Collapse tasks' => 'Contrair tarefas',
'Expand/collapse tasks' => 'Expandir/Contrair tarefas',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Atalhos de teclado',
'Open board switcher' => 'Abrir o comutador de painel',
'Application' => 'Aplicação',
- 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M',
'Compact view' => 'Vista reduzida',
'Horizontal scrolling' => 'Rolagem horizontal',
'Compact/wide view' => 'Alternar entre a vista compacta e ampliada',
'No results match:' => 'Nenhum resultado:',
'Currency' => 'Moeda',
- 'Files' => 'Arquivos',
- 'Images' => 'Imagens',
'Private project' => 'Projeto privado',
'AUD - Australian Dollar' => 'AUD - Dólar australiano',
'CAD - Canadian Dollar' => 'CAD - Dólar canadense',
@@ -698,40 +626,33 @@ return array(
'%s remove the assignee of the task %s' => '%s removeu a pessoa designada para a tarefa %s',
'Enable Gravatar images' => 'Ativar imagens do Gravatar',
'Information' => 'Informações',
- 'Check two factor authentication code' => 'Verificação do código de autenticação à fator duplo',
- 'The two factor authentication code is not valid.' => 'O código de autenticação à fator duplo não é válido',
- 'The two factor authentication code is valid.' => 'O código de autenticação à fator duplo é válido',
+ 'Check two factor authentication code' => 'Verifique o código de autenticação em duas etapas',
+ 'The two factor authentication code is not valid.' => 'O código de autenticação em duas etapas não é válido.',
+ 'The two factor authentication code is valid.' => 'O código de autenticação em duas etapas é válido.',
'Code' => 'Código',
- 'Two factor authentication' => 'Autenticação à fator duplo',
- 'Enable/disable two factor authentication' => 'Ativar/Desativar autenticação à fator duplo',
+ 'Two factor authentication' => 'Autenticação em duas etapas',
'This QR code contains the key URI: ' => 'Este Código QR contém a chave URI:',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salve esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).',
'Check my code' => 'Verifique o meu código',
'Secret key: ' => 'Chave secreta:',
'Test your device' => 'Teste o seu dispositivo',
'Assign a color when the task is moved to a specific column' => 'Atribuir uma cor quando a tarefa é movida em uma coluna específica',
'%s via Kanboard' => '%s via Kanboard',
- 'uploaded by: %s' => 'carregado por: %s',
- 'uploaded on: %s' => 'carregado em: %s',
- 'size: %s' => 'tamanho: %s',
'Burndown chart for "%s"' => 'Gráfico de Burndown para "%s"',
'Burndown chart' => 'Gráfico de Burndown',
'This chart show the task complexity over the time (Work Remaining).' => 'Este gráfico mostra a complexidade da tarefa ao longo do tempo (Trabalho Restante).',
- 'Screenshot taken %s' => 'Screenshot tomada em %s',
- 'Add a screenshot' => 'Adicionar uma Screenshot',
- 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Tomar um screenshot e pressione CTRL + V ou ⌘ + V para colar aqui.',
- 'Screenshot uploaded successfully.' => 'Screenshot enviada com sucesso.',
+ 'Screenshot taken %s' => 'Captura de tela tirada em %s',
+ 'Add a screenshot' => 'Adicionar uma captura de tela',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Tire uma captura de tela e pressione CTRL + V ou ⌘ + V para colar aqui.',
+ 'Screenshot uploaded successfully.' => 'Captura de tela enviada com sucesso.',
'SEK - Swedish Krona' => 'SEK - Coroa sueca',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'O identificador de projeto é um código alfanumérico opcional utilizado para identificar o seu projeto.',
'Identifier' => 'Identificador',
- 'Disable two factor authentication' => 'Desativar autenticação à dois fatores',
- 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Você deseja realmente desativar a autenticação à dois fatores para esse usuário: "%s"?',
+ 'Disable two factor authentication' => 'Desativar autenticação em duas etapas',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Você realmente deseja desativar a autenticação em duas etapas para este usuário: "%s"?',
'Edit link' => 'Editar um link',
'Start to type task title...' => 'Digite o título do trabalho...',
- 'A task cannot be linked to itself' => 'Uma tarefa não pode ser ligada a si própria',
- 'The exact same link already exists' => 'Um link idêntico jà existe',
+ 'A task cannot be linked to itself' => 'Uma tarefa não pode ser vinculada a si própria',
+ 'The exact same link already exists' => 'Um link idêntico já existe',
'Recurrent task is scheduled to be generated' => 'A tarefa recorrente está programada para ser criada',
- 'Recurring information' => 'Informação sobre a recorrência',
'Score' => 'Complexidade',
'The identifier must be unique' => 'O identificador deve ser único',
'This linked task id doesn\'t exists' => 'O identificador da tarefa associada não existe',
@@ -739,7 +660,7 @@ return array(
'Edit recurrence' => 'Modificar a recorrência',
'Generate recurrent task' => 'Gerar uma tarefa recorrente',
'Trigger to generate recurrent task' => 'Trigger para gerar tarefa recorrente',
- 'Factor to calculate new due date' => 'Fator para o cálculo do nova data limite',
+ 'Factor to calculate new due date' => 'Fator para o cálculo da nova data limite',
'Timeframe to calculate new due date' => 'Escala de tempo para o cálculo da nova data limite',
'Base date to calculate new due date' => 'Data a ser utilizada para calcular a nova data limite',
'Action date' => 'Data da ação',
@@ -770,32 +691,21 @@ return array(
'iCal feed' => 'Subscrição iCal',
'Preferences' => 'Preferências',
'Security' => 'Segurança',
- 'Two factor authentication disabled' => 'Autenticação à fator duplo desativado',
- 'Two factor authentication enabled' => 'Autenticação à fator duplo activado',
+ 'Two factor authentication disabled' => 'Autenticação em duas etapas desativada',
+ 'Two factor authentication enabled' => 'Autenticação em duas etapas ativada',
'Unable to update this user.' => 'Impossível de atualizar esse usuário.',
'There is no user management for private projects.' => 'Não há gerenciamento de usuários para projetos privados.',
'User that will receive the email' => 'O usuário que vai receber o e-mail',
'Email subject' => 'Assunto do e-mail',
'Date' => 'Data',
- 'By @%s on Bitbucket' => 'Por @%s no Bitbucket',
- 'Bitbucket Issue' => 'Bitbucket Issue',
- 'Commit made by @%s on Bitbucket' => 'Commit feito por @%s no Bitbucket',
- 'Commit made by @%s on Github' => 'Commit feito por @%s no Github',
- 'By @%s on Github' => 'Por @%s no Github',
- 'Commit made by @%s on Gitlab' => 'Commit feito por @%s no Gitlab',
'Add a comment log when moving the task between columns' => 'Adicionar um comentário de log quando uma tarefa é movida para uma outra coluna',
'Move the task to another column when the category is changed' => 'Mover uma tarefa para outra coluna quando a categoria mudou',
'Send a task by email to someone' => 'Enviar uma tarefa por e-mail a alguém',
'Reopen a task' => 'Reabrir uma tarefa',
- 'Bitbucket issue opened' => 'Bitbucket issue opened',
- 'Bitbucket issue closed' => 'Bitbucket issue closed',
- 'Bitbucket issue reopened' => 'Bitbucket issue reopened',
- 'Bitbucket issue assignee change' => 'Bitbucket issue assignee change',
- 'Bitbucket issue comment created' => 'Bitbucket issue comment created',
'Column change' => 'Mudança de coluna',
'Position change' => 'Mudança de posição',
'Swimlane change' => 'Mudança de swimlane',
- 'Assignee change' => 'Mudança do designado',
+ 'Assignee change' => 'Mudança de designação',
'[%s] Overdue tasks' => '[%s] Tarefas atrasadas',
'Notification' => 'Notificação',
'%s moved the task #%d to the first swimlane' => '%s moveu a tarefa #%d para a primeira swimlane',
@@ -804,7 +714,7 @@ return array(
'Gravatar' => 'Gravatar',
'%s moved the task %s to the first swimlane' => '%s moveu a tarefa %s para a primeira swimlane',
'%s moved the task %s to the swimlane "%s"' => '%s moveu a tarefa %s para a swimlane "%s"',
- 'This report contains all subtasks information for the given date range.' => 'Este relatório contém informações de todas as sub-tarefas para o período selecionado.',
+ 'This report contains all subtasks information for the given date range.' => 'Este relatório contém informações de todas as subtarefas para o período selecionado.',
'This report contains all tasks information for the given date range.' => 'Este relatório contém informações de todas as tarefas para o período selecionado.',
'Project activities for %s' => 'Atividade do projeto "%s"',
'view the board on Kanboard' => 'ver o painel no Kanboard',
@@ -825,18 +735,12 @@ return array(
'Time estimated changed: %sh' => 'O tempo estimado foi mudado/ %sh',
'The field "%s" have been updated' => 'O campo "%s" foi atualizada',
'The description have been modified' => 'A descrição foi modificada',
- 'Do you really want to close the task "%s" as well as all subtasks?' => 'Você realmente quer fechar a tarefa "%s" e todas as suas sub-tarefas?',
- 'Swimlane: %s' => 'Swimlane: %s',
+ 'Do you really want to close the task "%s" as well as all subtasks?' => 'Você realmente deseja finalizar a tarefa "%s" e todas as suas subtarefas?',
'I want to receive notifications for:' => 'Eu quero receber as notificações para:',
'All tasks' => 'Todas as tarefas',
'Only for tasks assigned to me' => 'Somente as tarefas atribuídas a mim',
'Only for tasks created by me' => 'Apenas as tarefas que eu criei',
'Only for tasks created by me and assigned to me' => 'Apenas as tarefas que eu criei e aquelas atribuídas a mim',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M',
- 'New due date: %B %e, %Y' => 'Nova data limite: %d/%m/%Y',
- 'Start date changed: %B %e, %Y' => 'Data de início alterada: %d/%m/%Y',
- '%k:%M %p' => '%H:%M',
'%%Y-%%m-%%d' => '%%d/%%m/%%Y',
'Total for all columns' => 'Total para todas as colunas',
'You need at least 2 days of data to show the chart.' => 'Você precisa de pelo menos 2 dias de dados para visualizar o gráfico.',
@@ -844,7 +748,7 @@ return array(
'<30m' => '<30m',
'Stop timer' => 'Stop timer',
'Start timer' => 'Start timer',
- 'Add project member' => 'Adicionar um membro ao projeto',
+ 'Add project member' => 'Adicionar membro ao projeto',
'Enable notifications' => 'Ativar as notificações',
'My activity stream' => 'Meu feed de atividades',
'My calendar' => 'Minha agenda',
@@ -861,14 +765,13 @@ return array(
'Not assigned' => 'Não designada',
'View advanced search syntax' => 'Ver a sintaxe para pesquisa avançada',
'Overview' => 'Visão global',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Vista Painel/Calendário/Lista',
'Switch to the board view' => 'Mudar para o modo Painel',
'Switch to the calendar view' => 'Mudar par o modo Calendário',
'Switch to the list view' => 'Mudar par o modo Lista',
'Go to the search/filter box' => 'Ir para o campo de pesquisa',
'There is no activity yet.' => 'Não há nenhuma atividade ainda.',
- 'No tasks found.' => 'Nenhuma tarefa encontrada',
+ 'No tasks found.' => 'Nenhuma tarefa encontrada.',
'Keyboard shortcut: "%s"' => 'Tecla de atalho: "%s"',
'List' => 'Lista',
'Filter' => 'Filtro',
@@ -894,10 +797,6 @@ return array(
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Este gráfico mostra o tempo médio do Lead and cycle time das últimas %d tarefas.',
'Average time into each column' => 'Tempo médio de cada coluna',
'Lead and cycle time' => 'Lead and cycle time',
- 'Google Authentication' => 'Autenticação Google',
- 'Help on Google authentication' => 'Ajuda com a autenticação Google',
- 'Github Authentication' => 'Autenticação Github',
- 'Help on Github authentication' => 'Ajuda com a autenticação Github',
'Lead time: ' => 'Lead time: ',
'Cycle time: ' => 'Cycle time: ',
'Time spent into each column' => 'Tempo gasto em cada coluna',
@@ -906,19 +805,13 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Se a tarefa não está fechada, a hora atual é usada no lugar da data de conclusão.',
'Set automatically the start date' => 'Definir automaticamente a data de início',
'Edit Authentication' => 'Modificar a autenticação',
- 'Google Id' => 'Google ID',
- 'Github Id' => 'Github Id',
'Remote user' => 'Usuário remoto',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Os usuários remotos não conservam as suas senhas no banco de dados Kanboard, exemplos: contas LDAP, Github ou Google.',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se você marcar "Interdir o formulário de autenticação", os identificadores entrados no formulário de login serão ignorado.',
- 'By @%s on Gitlab' => 'Por @%s no Gitlab',
- 'Gitlab issue comment created' => 'Comentário criado em um bilhete Gitlab',
'New remote user' => 'Criar um usuário remoto',
'New local user' => 'Criar um usuário local',
'Default task color' => 'Cor padrão para as tarefas',
- 'Hide sidebar' => 'Esconder a barra lateral',
- 'Expand sidebar' => 'Expandir a barra lateral',
- 'This feature does not work with all browsers.' => 'Esta funcionalidade não é compatível com todos os navegadores',
+ 'This feature does not work with all browsers.' => 'Esta funcionalidade não é compatível com todos os navegadores.',
'There is no destination project available.' => 'Não há nenhum projeto de destino disponível.',
'Trigger automatically subtask time tracking' => 'Ativar automaticamente o monitoramento do tempo para as subtarefas',
'Include closed tasks in the cumulative flow diagram' => 'Incluir as tarefas fechadas no diagrama de fluxo acumulado',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'contribuidores',
'License:' => 'Licença:',
'License' => 'Licença',
- 'Project Administrator' => 'Administrador de Projeto',
'Enter the text below' => 'Entre o texto abaixo',
'Gantt chart for %s' => 'Gráfico de Gantt para %s',
'Sort by position' => 'Ordenar por posição',
@@ -952,38 +844,26 @@ return array(
'open file' => 'abrir um arquivo',
'End date' => 'Data de término',
'Users overview' => 'Visão geral dos usuários',
- 'Managers' => 'Gerentes',
'Members' => 'Membros',
'Shared project' => 'Projeto compartilhado',
'Project managers' => 'Gerentes de projeto',
- 'Project members' => 'Membros de projeto',
'Gantt chart for all projects' => 'Gráfico de Gantt para todos os projetos',
'Projects list' => 'Lista dos projetos',
- 'Gantt chart for this project' => 'Gráfico de Gantt para este projecto',
+ 'Gantt chart for this project' => 'Gráfico de Gantt para este projeto',
'Project board' => 'Painel do projeto',
'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',
- 'Start date: %s' => 'Data de início: %s',
- 'End date: %s' => 'Data de término: %s',
'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',
- 'Login with my Gitlab Account' => 'Login com a minha conta Gitlab',
'Milestone' => 'Milestone',
- 'Gitlab Authentication' => 'Autenticação Gitlab',
- 'Help on Gitlab authentication' => 'Ajuda com a autenticação Gitlab',
- 'Gitlab Id' => 'Gitlab Id',
- 'Gitlab Account' => 'Conta Gitlab',
- 'Link my Gitlab Account' => 'Vincular minha conta Gitlab',
- 'Unlink my Gitlab Account' => 'Desvincular minha conta Gitlab',
'Documentation: %s' => 'Documentação: %s',
'Switch to the Gantt chart view' => 'Mudar para a vista gráfico de Gantt',
'Reset the search/filter box' => 'Reiniciar o campo de pesquisa',
'Documentation' => 'Documentação',
'Table of contents' => 'Índice',
'Gantt' => 'Gantt',
- 'Help with project permissions' => 'Ajuda com as permissões de projetos',
'Author' => 'Autor',
'Version' => 'Versão',
'Plugins' => 'Extensões',
@@ -1028,40 +908,244 @@ return array(
'Unread notifications' => 'Notificações não lidas',
'My filters' => 'Meus filtros',
'Notification methods:' => 'Métodos de notificação:',
- // 'Import tasks from CSV file' => '',
- // 'Unable to read your file' => '',
- // '%d task(s) have been imported successfully.' => '',
- // 'Nothing have been imported!' => '',
- // 'Import users from CSV file' => '',
- // '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
- // '%s attached a file to the task #%d' => '',
- // 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
- // 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
- // 'Usernames must be lowercase and unique' => '',
- // 'Passwords will be encrypted if present' => '',
+ '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',
+ '%d task(s) have been imported successfully.' => '%d tarefa(s) importada(s) com sucesso.',
+ 'Nothing have been imported!' => 'Nada foi importado!',
+ 'Import users from CSV file' => 'Importar usuários a partir de arquivo CSV',
+ '%d user(s) have been imported successfully.' => '%d usuário(s) importado(s) com sucesso.',
+ 'Comma' => 'Vírgula',
+ 'Semi-colon' => 'Ponto e vírgula',
+ 'Tab' => 'Tab',
+ 'Vertical bar' => 'Barra vertical',
+ 'Double Quote' => 'Aspas duplas',
+ 'Single Quote' => 'Aspas simples',
+ '%s attached a file to the task #%d' => '%s anexou um arquivo à tarefa #%d',
+ 'There is no column or swimlane activated in your project!' => 'Não há coluna ou swimlane ativa em seu projeto!',
+ 'Append filter (instead of replacement)' => 'Adicionar filtro (em vez de substituir)',
+ 'Append/Replace' => 'Adicionar/Substituir',
+ 'Append' => 'Adicionar',
+ 'Replace' => 'Substituir',
+ 'Import' => 'Importar',
+ 'change sorting' => 'alterar ordenação',
+ 'Tasks Importation' => 'Importação de Tarefas',
+ 'Delimiter' => 'Separador',
+ 'Enclosure' => 'Delimitador de campos',
+ 'CSV File' => 'Arquivo CSV',
+ 'Instructions' => 'Instruções',
+ 'Your file must use the predefined CSV format' => 'Seu arquivo deve utilizar o formato CSV pré-definido',
+ 'Your file must be encoded in UTF-8' => 'Seu arquivo deve estar codificado em UTF-8',
+ 'The first row must be the header' => 'A primeira linha deve ser o cabeçalho',
+ 'Duplicates are not verified for you' => 'Registros duplicados não são verificados',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'A data de vencimento deve utilizar o formato ISO: YYYY-MM-DD',
+ 'Download CSV template' => 'Baixar modelo de arquivo CSV',
+ 'No external integration registered.' => 'Nenhuma integração externa registrada.',
+ 'Duplicates are not imported' => 'Registros duplicados não são importados',
+ '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',
+ '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',
+ 'Assignee Name' => 'Nome do designado',
+ 'Groups' => 'Grupos',
+ 'Members of %s' => 'Membros do %s',
+ 'New group' => 'Novo grupo',
+ 'Group created successfully.' => 'Grupo criado com sucesso.',
+ 'Unable to create your group.' => 'Não foi possível de criar o seu grupo.',
+ 'Edit group' => 'Editar o grupo',
+ 'Group updated successfully.' => 'Grupo atualizado com sucesso.',
+ 'Unable to update your group.' => 'Não foi possível atualizar o seu grupo.',
+ 'Add group member to "%s"' => 'Adicionar um membro ao grupo "%s"',
+ 'Group member added successfully.' => 'Membro adicionado com sucesso ao o grupo.',
+ 'Unable to add group member.' => 'Não foi possivel adicionar esse membro ao grupo.',
+ 'Remove user from group "%s"' => 'Remover usuário do grupo "%s"',
+ 'User removed successfully from this group.' => 'Usuário removido com sucesso deste grupo.',
+ 'Unable to remove this user from the group.' => 'Não foi possivel remover este Usuário do grupo.',
+ 'Remove group' => 'Remover o grupo',
+ 'Group removed successfully.' => 'Grupo removido com sucesso.',
+ 'Unable to remove this group.' => 'Não foi possível remover este grupo.',
+ 'Project Permissions' => 'Permissões do projeto',
+ 'Manager' => 'Gerente',
+ 'Project Manager' => 'Gerente de projeto',
+ 'Project Member' => 'Membro de projeto',
+ // 'Project Viewer' => '',
+ 'Your account is locked for %d minutes' => 'A sua conta está bloqueada por %d minutos',
+ 'Invalid captcha' => 'Captcha inválido',
+ 'The name must be unique' => 'O nome deve ser único',
+ 'View all groups' => 'Ver todos os grupos',
+ 'View group members' => 'Ver os membros do grupo',
+ 'There is no user available.' => 'Não há nenhum usuário disponível',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Você realmente deseja remover o usuário "%s" do grupo "%s"?',
+ 'There is no group.' => 'Não há nenhum grupo.',
+ 'External Id' => 'Id externo',
+ 'Add group member' => 'Adicionar um membro ao grupo',
+ 'Do you really want to remove this group: "%s"?' => 'Você realmente deseja excluir este grupo: "%s"?',
+ 'There is no user in this group.' => 'Não há usuários neste grupo.',
+ 'Remove this user' => 'Remover este usuário',
+ 'Permissions' => 'Permissões',
+ 'Allowed Users' => 'Usuários autorizados',
+ 'No user have been allowed specifically.' => 'Nenhum usuário foi especificamente autorizado.',
+ 'Role' => 'Função',
+ 'Enter user name...' => 'Digite o nome do usuário...',
+ 'Allowed Groups' => 'Grupos autorizados',
+ 'No group have been allowed specifically.' => 'Nenhum grupo foi especificamente autorizado.',
+ 'Group' => 'Grupo',
+ 'Group Name' => 'Nome do grupo',
+ 'Enter group name...' => 'Digite o nome do grupo',
+ 'Role:' => 'Função:',
+ 'Project members' => 'Membros de projeto',
+ 'Compare hours for "%s"' => 'Comparar as horas para "%s"',
+ '%s mentioned you in the task #%d' => '%s mencionou você na tarefa #%d',
+ '%s mentioned you in a comment on the task #%d' => '%s mencionou você num comentário da tarefa #%d',
+ 'You were mentioned in the task #%d' => 'Você foi mencionado na tarefa #%d',
+ 'You were mentioned in a comment on the task #%d' => 'Você foi mencionado num comentário da tarefa #%d',
+ 'Mentioned' => 'Mencionado',
+ 'Compare Estimated Time vs Actual Time' => 'Comparar o tempo estimado e o tempo atual',
+ 'Estimated hours: ' => 'Horas estimadas: ',
+ 'Actual hours: ' => 'Horas reais: ',
+ 'Hours Spent' => 'Horas gastas',
+ 'Hours Estimated' => 'Horas estimadas',
+ 'Estimated Time' => 'Tempo estimado',
+ 'Actual Time' => 'Tempo real',
+ 'Estimated vs actual time' => 'Tempo estimado vs tempo real',
+ 'RUB - Russian Ruble' => 'RUB - Rublo russo',
+ 'Assign the task to the person who does the action when the column is changed' => 'Atribuir a tarefa a pessoa que faz a ação quando a coluna é alterada',
+ 'Close a task in a specific column' => 'Fechar uma tarefa em uma coluna específica',
+ 'Time-based One-time Password Algorithm' => 'Senha de uso único baseado no tempo',
+ 'Two-Factor Provider: ' => 'Provedor de autenticação a dois fatores: ',
+ 'Disable two-factor authentication' => 'Desativar a autenticação a dois fatores',
+ 'Enable two-factor authentication' => 'Ativar a autenticação a dois fatores',
+ 'There is no integration registered at the moment.' => 'Não há nenhuma integração registrada por enquanto.',
+ 'Password Reset for Kanboard' => 'Redefinir a senha para Kanboard',
+ 'Forgot password?' => 'Esqueceu a senha?',
+ 'Enable "Forget Password"' => 'Activar a funcionalidade "Esqueceu a senha"',
+ 'Password Reset' => 'Redefinir a senha',
+ 'New password' => 'Nova senha',
+ 'Change Password' => 'Alterar a senha',
+ 'To reset your password click on this link:' => 'Para redefinir a sua senha, clique neste link:',
+ 'Last Password Reset' => 'Última redefinição de senha',
+ 'The password has never been reinitialized.' => 'A senha nunca foi redifinida.',
+ 'Creation' => 'Criação',
+ 'Expiration' => 'Expiração',
+ 'Password reset history' => 'Histórico da redefinição da senha',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas as tarefas da coluna "%s" e da Swimlane "%s" foram fechadas com sucesso.',
+ 'Do you really want to close all tasks of this column?' => 'Você realmente deseja fechar todas as tarefas desta coluna?',
+ '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarefa(s) na coluna "%s" e na Swimlane "%s" serão fechadas.',
+ 'Close all tasks of this column' => 'Fechar todas as tarefas desta coluna',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 540a4a56..82412733 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Remover projecto',
'Edit the board for "%s"' => 'Editar o quadro para "%s"',
'All projects' => 'Todos os projectos',
- 'Change columns' => 'Modificar colunas',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
'Nobody assigned' => 'Ninguém assignado',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Abrir uma tarefa',
'Do you really want to open this task: "%s"?' => 'Tem a certeza que quer abrir esta tarefa: "%s"?',
'Back to the board' => 'Voltar ao quadro',
- 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M',
'There is nobody assigned' => 'Não há ninguém assignado',
'Column on the board:' => 'Coluna no quadro:',
- 'Status is open' => 'Estado é aberto',
- 'Status is closed' => 'Estado é finalizado',
'Close this task' => 'Finalizar esta tarefa',
'Open this task' => 'Abrir esta tarefa',
'There is no description.' => 'Não há descrição.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'O ID é obrigatório',
'The project id is required' => 'O ID do projecto é obrigatório',
'The project name is required' => 'O nome do projecto é obrigatório',
- 'This project must be unique' => 'Este projecto deve ser único',
'The title is required' => 'O título é obrigatório',
'Settings saved successfully.' => 'Configurações guardadas com sucesso.',
'Unable to save your settings.' => 'Não é possível guardar as suas configurações.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Em andamento',
'Done' => 'Finalizado',
'Application version:' => 'Versão da aplicação:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Finalizado em %d %B %Y às %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M',
- 'Date created' => 'Data de criação',
- 'Date completed' => 'Data da finalização',
'Id' => 'Id',
'%d closed tasks' => '%d tarefas finalizadas',
'No task for this project' => 'Não há tarefa para este projecto',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Complexidade',
'Task limit' => 'Limite da tarefa',
'Task count' => 'Número de tarefas',
- 'Edit project access list' => 'Editar lista de acesso ao projecto',
- 'Allow this user' => 'Permitir este utilizador',
- 'Don\'t forget that administrators have access to everything.' => 'Não se esqueça que administradores têm acesso a tudo.',
- 'Revoke' => 'Revogar',
- 'List of authorized users' => 'Lista de utilizadores autorizados',
'User' => 'Utilizador',
- 'Nobody have access to this project.' => 'Ninguém tem acesso a este projecto.',
'Comments' => 'Comentários',
'Write your text in Markdown' => 'Escreva o seu texto em Markdown',
'Leave a comment' => 'Deixe um comentário',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Editar esta tarefa',
'Due Date' => 'Data de vencimento',
'Invalid date' => 'Data inválida',
- 'Must be done before %B %e, %Y' => 'Deve ser finalizado antes de %d %B %Y',
- '%B %e, %Y' => '%d %B %Y',
- '%b %e, %Y' => '%d %B %Y',
'Automatic actions' => 'Acções automáticas',
'Your automatic action have been created successfully.' => 'A sua acção automática foi criada com sucesso.',
'Unable to create your automatic action.' => 'Não é possível criar a sua acção automática.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Designar uma cor para um utilizador específico',
'Column title' => 'Título da coluna',
'Position' => 'Posição',
- 'Move Up' => 'Mover para cima',
- 'Move Down' => 'Mover para baixo',
'Duplicate to another project' => 'Duplicar para outro projecto',
'Duplicate' => 'Duplicar',
'link' => 'link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Comentário removido com sucesso.',
'Unable to remove this comment.' => 'Não é possível remover este comentário.',
'Do you really want to remove this comment?' => 'Tem a certeza que quer remover este comentário?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Somente os administradores ou o criator deste comentário possuem acesso a esta página.',
'Current password for the user "%s"' => 'Senha atual para o utilizador "%s"',
'The current password is required' => 'A senha atual é obrigatória',
'Wrong password' => 'Senha incorreta',
@@ -267,10 +246,6 @@ return array(
'External authentication failed' => 'Autenticação externa falhou',
'Your external account is linked to your profile successfully.' => 'A sua conta externa foi vinculada com sucesso ao seu perfil',
'Email' => 'E-mail',
- 'Link my Google Account' => 'Vincular a minha Conta do Google',
- 'Unlink my Google Account' => 'Desvincular a minha Conta do Google',
- 'Login with my Google Account' => 'Entrar com a minha Conta do Google',
- 'Project not found.' => 'Projecto não encontrado.',
'Task removed successfully.' => 'Tarefa removida com sucesso.',
'Unable to remove this task.' => 'Não foi possível remover esta tarefa.',
'Remove a task' => 'Remover uma tarefa',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Tamanho máximo: ',
'Unable to upload the file.' => 'Não foi possível carregar o arquivo.',
'Display another project' => 'Mostrar outro projecto',
- 'Login with my Github Account' => 'Entrar com a minha Conta do Github',
- 'Link my Github Account' => 'Associar à minha Conta do Github',
- 'Unlink my Github Account' => 'Desassociar a minha Conta do Github',
'Created by %s' => 'Criado por %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p',
'Tasks Export' => 'Exportar Tarefas',
'Tasks exportation for "%s"' => 'As tarefas foram exportadas para "%s"',
'Start Date' => 'Data inicial',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projectos:',
'view the task on Kanboard' => 'ver a tarefa no Kanboard',
'Public access' => 'Acesso público',
- 'User management' => 'Gestão de utilizadores',
'Active tasks' => 'Tarefas activas',
'Disable public access' => 'Desactivar o acesso público',
'Enable public access' => 'Activar o acesso público',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'E-mail:',
'Notifications:' => 'Notificações:',
'Notifications' => 'Notificações',
- 'Group:' => 'Grupo:',
- 'Regular user' => 'Utilizador comum',
'Account type:' => 'Tipo de conta:',
'Edit profile' => 'Editar perfil',
'Change password' => 'Alterar senha',
'Password modification' => 'Alteração de senha',
'External authentications' => 'Autenticação externa',
- 'Google Account' => 'Conta do Google',
- 'Github Account' => 'Conta do Github',
'Never connected.' => 'Nunca conectado.',
- 'No account linked.' => 'Nenhuma conta associada.',
- 'Account linked.' => 'Conta associada.',
'No external authentication enabled.' => 'Nenhuma autenticação externa activa.',
'Password modified successfully.' => 'Senha alterada com sucesso.',
'Unable to change the password.' => 'Não foi possível alterar a senha.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s mudou a assignação da tarefa %s para %s',
'New password for the user "%s"' => 'Nova senha para o utilizador "%s"',
'Choose an event' => 'Escolher um evento',
- 'Github commit received' => 'Recebido commit do Github',
- 'Github issue opened' => 'Problema aberto no Github',
- 'Github issue closed' => 'Problema fechado no Github',
- 'Github issue reopened' => 'Problema reaberto no Github',
- 'Github issue assignee change' => 'Alterar assignação ao problema no Githubnge',
- 'Github issue label change' => 'Alterar etiqueta do problema no Github',
'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo',
'Change the assignee based on an external username' => 'Alterar assignação com base num utilizador externo',
'Change the category based on an external label' => 'Alterar categoria com base num rótulo externo',
'Reference' => 'Referência',
- 'Reference: %s' => 'Referência: %s',
'Label' => 'Rótulo',
'Database' => 'Base de dados',
'About' => 'Sobre',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frequência em segundos (60 segundos por defeito)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequência em segundos (0 para desactivar este recurso, 10 segundos por defeito)',
'Application URL' => 'URL da Aplicação',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemplo: http://example.kanboard.net/ (utilizado nas notificações por e-mail)',
'Token regenerated.' => 'Token ',
'Date format' => 'Formato da data',
'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceite, exemplo: "%s" e "%s"',
@@ -482,9 +438,6 @@ return array(
'This project is private' => 'Este projecto é privado',
'Type here to create a new sub-task' => 'Escreva aqui para criar uma nova subtarefa',
'Add' => 'Adicionar',
- 'Estimated time: %s hours' => 'Tempo estimado: %s horas',
- 'Time spent: %s hours' => 'Tempo gasto: %s horas',
- 'Started on %B %e, %Y' => 'Iniciado em %B %e, %Y',
'Start date' => 'Data de início',
'Time estimated' => 'Tempo estimado',
'There is nothing assigned to you.' => 'Não há nada assignado a si.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Todos possuem acesso a este projecto.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github webhooks',
- 'Help on Github webhooks' => 'Ajuda para o Github webhooks',
'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo',
- 'Github issue comment created' => 'Criado comentário ao problema no Github',
'Project management' => 'Gestão de projectos',
'My projects' => 'Os meus projectos',
'Columns' => 'Colunas',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Redistribuição de utilizador para "%s"',
'Clone this project' => 'Clonar este projecto',
'Column removed successfully.' => 'Coluna removida com sucesso.',
- 'Github Issue' => 'Problema no Github',
'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.',
'Previous' => 'Anterior',
'The id must be an integer' => 'O ID deve ser um número inteiro',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Swimlane padrão',
'Do you really want to remove this swimlane: "%s"?' => 'Tem a certeza que quer remover este swimlane: "%s"?',
'Inactive swimlanes' => 'Desactivar swimlanes',
- 'Set project manager' => 'Definir gerente do projecto',
- 'Set project member' => 'Definir membro do projecto',
'Remove a swimlane' => 'Remover um swimlane',
- 'Rename' => 'Renomear',
'Show default swimlane' => 'Mostrar swimlane padrão',
'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projecto "%s"',
'Swimlane not found.' => 'Swimlane não encontrado.',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane atualizado com sucesso.',
'The default swimlane have been updated successfully.' => 'O swimlane padrão foi atualizado com sucesso.',
- 'Unable to create your swimlane.' => 'Não foi possível criar o seu swimlane.',
'Unable to remove this swimlane.' => 'Não foi possível remover este swimlane.',
'Unable to update this swimlane.' => 'Não foi possível atualizar este swimlane.',
'Your swimlane have been created successfully.' => 'Seu swimlane foi criado com sucesso.',
'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Feature Request, Improvement"',
'Default categories for new projects (Comma-separated)' => 'Categorias padrão para novos projectos (Separadas por vírgula)',
- 'Gitlab commit received' => 'Commit recebido do Gitlab',
- 'Gitlab issue opened' => 'Problema aberto no Gitlab',
- 'Gitlab issue closed' => 'Problema fechado no Gitlab',
- 'Gitlab webhooks' => 'Gitlab webhooks',
- 'Help on Gitlab webhooks' => 'Ajuda sobre Gitlab webhooks',
'Integrations' => 'Integrações',
'Integration with third-party services' => 'Integração com serviços de terceiros',
- 'Role for this project' => 'Função para este projecto',
- 'Project manager' => 'Gerente do projecto',
- 'Project member' => 'Membro do projecto',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Um gerente do projecto pode alterar as configurações do projecto e ter mais privilégios que um utilizador padrão.',
- 'Gitlab Issue' => 'Problema Gitlab',
'Subtask Id' => 'ID da subtarefa',
'Subtasks' => 'Subtarefas',
'Subtasks Export' => 'Exportar subtarefas',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'Já tem uma subtarefa em andamento',
'Which parts of the project do you want to duplicate?' => 'Quais as partes do projecto que deseja duplicar?',
'Disallow login form' => 'Desactivar login',
- 'Bitbucket commit received' => '"Commit" recebido via Bitbucket',
- 'Bitbucket webhooks' => 'Webhook Bitbucket',
- 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks Bitbucket',
'Start' => 'Inicio',
'End' => 'Fim',
'Task age in days' => 'Idade da tarefa em dias',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Esta tarefa',
'<1h' => '<1h',
'%dh' => '%dh',
- '%b %e' => '%e %b',
'Expand tasks' => 'Expandir tarefas',
'Collapse tasks' => 'Contrair tarefas',
'Expand/collapse tasks' => 'Expandir/Contrair tarefas',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Atalhos de teclado',
'Open board switcher' => 'Abrir o comutador de painel',
'Application' => 'Aplicação',
- 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M',
'Compact view' => 'Vista reduzida',
'Horizontal scrolling' => 'Deslocamento horizontal',
'Compact/wide view' => 'Alternar entre a vista compacta e ampliada',
'No results match:' => 'Nenhum resultado:',
'Currency' => 'Moeda',
- 'Files' => 'Arquivos',
- 'Images' => 'Imagens',
'Private project' => 'Projecto privado',
'AUD - Australian Dollar' => 'AUD - Dólar australiano',
'CAD - Canadian Dollar' => 'CAD - Dólar canadense',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => 'O código de autenticação com factor duplo é válido',
'Code' => 'Código',
'Two factor authentication' => 'Autenticação com factor duplo',
- 'Enable/disable two factor authentication' => 'Activar/Desactivar autenticação com factor duplo',
'This QR code contains the key URI: ' => 'Este Código QR contém a chave URI: ',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Guarde esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).',
'Check my code' => 'Verificar o meu código',
'Secret key: ' => 'Chave secreta: ',
'Test your device' => 'Teste o seu dispositivo',
'Assign a color when the task is moved to a specific column' => 'Atribuir uma cor quando a tarefa é movida em uma coluna específica',
'%s via Kanboard' => '%s via Kanboard',
- 'uploaded by: %s' => 'carregado por: %s',
- 'uploaded on: %s' => 'carregado em: %s',
- 'size: %s' => 'tamanho: %s',
'Burndown chart for "%s"' => 'Gráfico de Burndown para "%s"',
'Burndown chart' => 'Gráfico de Burndown',
'This chart show the task complexity over the time (Work Remaining).' => 'Este gráfico mostra a complexidade da tarefa ao longo do tempo (Trabalho Restante).',
@@ -722,7 +645,6 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Tire um screenshot e pressione CTRL + V ou ⌘ + V para colar aqui.',
'Screenshot uploaded successfully.' => 'Screenshot enviada com sucesso.',
'SEK - Swedish Krona' => 'SEK - Coroa sueca',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'O identificador de projecto é um código alfanumérico opcional utilizado para identificar o seu projecto.',
'Identifier' => 'Identificador',
'Disable two factor authentication' => 'Desactivar autenticação com dois factores',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Tem a certeza que quer desactivar a autenticação com dois factores para esse utilizador: "%s"?',
@@ -731,7 +653,6 @@ return array(
'A task cannot be linked to itself' => 'Uma tarefa não pode ser ligada a si própria',
'The exact same link already exists' => 'Um link idêntico jà existe',
'Recurrent task is scheduled to be generated' => 'A tarefa recorrente está programada para ser criada',
- 'Recurring information' => 'Informação sobre a recorrência',
'Score' => 'Complexidade',
'The identifier must be unique' => 'O identificador deve ser único',
'This linked task id doesn\'t exists' => 'O identificador da tarefa associada não existe',
@@ -739,20 +660,20 @@ return array(
'Edit recurrence' => 'Modificar a recorrência',
'Generate recurrent task' => 'Gerar uma tarefa recorrente',
'Trigger to generate recurrent task' => 'Activador para gerar tarefa recorrente',
- 'Factor to calculate new due date' => 'Factor para o cálculo do nova data limite',
- 'Timeframe to calculate new due date' => 'Escala de tempo para o cálculo da nova data limite',
- 'Base date to calculate new due date' => 'Data a ser utilizada para calcular a nova data limite',
+ 'Factor to calculate new due date' => 'Factor para o cálculo do nova data de vencimento',
+ 'Timeframe to calculate new due date' => 'Escala de tempo para o cálculo da nova data de vencimento',
+ 'Base date to calculate new due date' => 'Data a ser utilizada para calcular a nova data de vencimento',
'Action date' => 'Data da acção',
- 'Base date to calculate new due date: ' => 'Data a ser utilizada para calcular a nova data limite: ',
+ 'Base date to calculate new due date: ' => 'Data a ser utilizada para calcular a nova data de vencimento: ',
'This task has created this child task: ' => 'Esta tarefa criou a tarefa filha: ',
'Day(s)' => 'Dia(s)',
- 'Existing due date' => 'Data limite existente',
- 'Factor to calculate new due date: ' => 'Factor para calcular a nova data limite: ',
+ 'Existing due date' => 'Data de vencimento existente',
+ 'Factor to calculate new due date: ' => 'Factor para calcular a nova data de vencimento: ',
'Month(s)' => 'Mês(es)',
'Recurrence' => 'Recorrência',
'This task has been created by: ' => 'Esta tarefa foi criada por: ',
'Recurrent task has been generated:' => 'A tarefa recorrente foi gerada:',
- 'Timeframe to calculate new due date: ' => 'Escala de tempo para o cálculo da nova data limite: ',
+ 'Timeframe to calculate new due date: ' => 'Escala de tempo para o cálculo da nova data de vencimento: ',
'Trigger to generate recurrent task: ' => 'Activador para gerar tarefa recorrente: ',
'When task is closed' => 'Quando a tarefa é fechada',
'When task is moved from first column' => 'Quando a tarefa é movida fora da primeira coluna',
@@ -777,21 +698,10 @@ return array(
'User that will receive the email' => 'O utilizador que vai receber o e-mail',
'Email subject' => 'Assunto do e-mail',
'Date' => 'Data',
- 'By @%s on Bitbucket' => 'Por @%s no Bitbucket',
- 'Bitbucket Issue' => 'Problema Bitbucket',
- 'Commit made by @%s on Bitbucket' => 'Commit feito por @%s no Bitbucket',
- 'Commit made by @%s on Github' => 'Commit feito por @%s no Github',
- 'By @%s on Github' => 'Por @%s no Github',
- 'Commit made by @%s on Gitlab' => 'Commit feito por @%s no Gitlab',
'Add a comment log when moving the task between columns' => 'Adicionar um comentário de log quando uma tarefa é movida para uma outra coluna',
'Move the task to another column when the category is changed' => 'Mover uma tarefa para outra coluna quando a categoria mudar',
'Send a task by email to someone' => 'Enviar uma tarefa por e-mail a alguém',
'Reopen a task' => 'Reabrir uma tarefa',
- 'Bitbucket issue opened' => 'Problema aberto no Bitbucket',
- 'Bitbucket issue closed' => 'Problema fechado no Bitbucket',
- 'Bitbucket issue reopened' => 'Problema reaberto no Bitbucket',
- 'Bitbucket issue assignee change' => 'Alterar assignação do problema no Bitbucket',
- 'Bitbucket issue comment created' => 'Comentário ao problema adicionado ao Bitbucket',
'Column change' => 'Mudança de coluna',
'Position change' => 'Mudança de posição',
'Swimlane change' => 'Mudança de swimlane',
@@ -818,7 +728,7 @@ return array(
'New category: %s' => 'Nova categoria: %s',
'New color: %s' => 'Nova cor: %s',
'New complexity: %d' => 'Nova complexidade: %d',
- 'The due date have been removed' => 'A data limite foi retirada',
+ 'The due date have been removed' => 'A data de vencimento foi retirada',
'There is no description anymore' => 'Já não há descrição',
'Recurrence settings have been modified' => 'As configurações da recorrência foram modificadas',
'Time spent changed: %sh' => 'O tempo despendido foi mudado: %sh',
@@ -826,17 +736,11 @@ return array(
'The field "%s" have been updated' => 'O campo "%s" foi actualizada',
'The description have been modified' => 'A descrição foi modificada',
'Do you really want to close the task "%s" as well as all subtasks?' => 'Tem a certeza que quer fechar a tarefa "%s" e todas as suas sub-tarefas?',
- 'Swimlane: %s' => 'Swimlane: %s',
'I want to receive notifications for:' => 'Eu quero receber as notificações para:',
'All tasks' => 'Todas as tarefas',
'Only for tasks assigned to me' => 'Somente as tarefas atribuídas a mim',
'Only for tasks created by me' => 'Apenas as tarefas que eu criei',
'Only for tasks created by me and assigned to me' => 'Apenas as tarefas que eu criei e aquelas atribuídas a mim',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M',
- 'New due date: %B %e, %Y' => 'Nova data limite: %d/%m/%Y',
- 'Start date changed: %B %e, %Y' => 'Data de início alterada: %d/%m/%Y',
- '%k:%M %p' => '%H:%M',
'%%Y-%%m-%%d' => '%%d/%%m/%%Y',
'Total for all columns' => 'Total para todas as colunas',
'You need at least 2 days of data to show the chart.' => 'Precisa de pelo menos 2 dias de dados para visualizar o gráfico.',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'Não assignada',
'View advanced search syntax' => 'Ver sintaxe avançada de pesquisa',
'Overview' => 'Visão global',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Vista Painel/Calendário/Lista',
'Switch to the board view' => 'Mudar para o modo Painel',
'Switch to the calendar view' => 'Mudar para o modo Calendário',
@@ -880,7 +783,7 @@ return array(
'Search by color: ' => 'Pesquisar por cor: ',
'Search by category: ' => 'Pesquisar por categoria: ',
'Search by description: ' => 'Pesquisar por descrição: ',
- 'Search by due date: ' => 'Pesquisar por data de expiração: ',
+ 'Search by due date: ' => 'Pesquisar por data de vencimento: ',
'Lead and Cycle time for "%s"' => 'Tempo de Espera e Ciclo para "%s"',
'Average time spent into each column for "%s"' => 'Tempo médio gasto em cada coluna para "%s"',
'Average time spent into each column' => 'Tempo médio gasto em cada coluna',
@@ -894,10 +797,6 @@ return array(
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Este gráfico mostra o tempo médio de espera e ciclo para as últimas %d tarefas realizadas.',
'Average time into each column' => 'Tempo médio em cada coluna',
'Lead and cycle time' => 'Tempo de Espera e Ciclo',
- 'Google Authentication' => 'Autenticação Google',
- 'Help on Google authentication' => 'Ajuda com autenticação Google',
- 'Github Authentication' => 'Autenticação Github',
- 'Help on Github authentication' => 'Ajuda com autenticação Github',
'Lead time: ' => 'Tempo de Espera: ',
'Cycle time: ' => 'Tempo de Ciclo: ',
'Time spent into each column' => 'Tempo gasto em cada coluna',
@@ -906,18 +805,12 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Se a tarefa não estiver fechada o hora actual será usada em vez da hora de conclusão',
'Set automatically the start date' => 'Definir data de inicio automáticamente',
'Edit Authentication' => 'Editar Autenticação',
- 'Google Id' => 'Id Google',
- 'Github Id' => 'Id Github',
'Remote user' => 'Utilizador remoto',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Utilizadores remotos não guardam a password na base de dados do Kanboard, por exemplo: LDAP, contas do Google e Github.',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se activar a opção "Desactivar login", as credenciais digitadas no login serão ignoradas.',
- 'By @%s on Gitlab' => 'Por @%s no Gitlab',
- 'Gitlab issue comment created' => 'Comentário a problema no Gitlab adicionado',
'New remote user' => 'Novo utilizador remoto',
'New local user' => 'Novo utilizador local',
'Default task color' => 'Cor de tarefa por defeito',
- 'Hide sidebar' => 'Esconder barra lateral',
- 'Expand sidebar' => 'Expandir barra lateral',
'This feature does not work with all browsers.' => 'Esta funcionalidade não funciona em todos os browsers',
'There is no destination project available.' => 'Não há projecto de destino disponivel',
'Trigger automatically subtask time tracking' => 'Activar automáticamente subtarefa de controlo de tempo',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'contribuidores',
'License:' => 'Licença:',
'License' => 'Licença',
- 'Project Administrator' => 'Administrador do Projecto',
'Enter the text below' => 'Escreva o texto em baixo',
'Gantt chart for %s' => 'Gráfico de Gantt para %s',
'Sort by position' => 'Ordenar por posição',
@@ -945,18 +837,16 @@ return array(
'There is no task in your project.' => 'Não existe tarefa no seu projecto.',
'Gantt chart' => 'Gráfico de Gantt',
'People who are project managers' => 'Pessoas que são gestores do projecto',
- 'People who are project members' => 'Pessoas que são membors do projecto',
+ 'People who are project members' => 'Pessoas que são membros do projecto',
'NOK - Norwegian Krone' => 'NOK - Coroa Norueguesa',
'Show this column' => 'Mostrar esta coluna',
'Hide this column' => 'Esconder esta coluna',
'open file' => 'abrir ficheiro',
'End date' => 'Data de fim',
'Users overview' => 'Visão geral de Utilizadores',
- 'Managers' => 'Gestores',
'Members' => 'Membros',
'Shared project' => 'Projecto partilhado',
'Project managers' => 'Gestores do projecto',
- 'Project members' => 'Membros do projecto',
'Gantt chart for all projects' => 'Gráfico de Gantt para todos os projectos',
'Projects list' => 'Lista de projectos',
'Gantt chart for this project' => 'Gráfico de Gantt para este projecto',
@@ -964,26 +854,16 @@ 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',
- 'Start date: %s' => 'Data de inicio: %s',
- 'End date: %s' => 'Data de fim: %s',
'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',
- 'Login with my Gitlab Account' => 'Login com a minha Conta Gitlab',
'Milestone' => 'Objectivo',
- 'Gitlab Authentication' => 'Autenticação Gitlab',
- 'Help on Gitlab authentication' => 'Ajuda com autenticação Gitlab',
- 'Gitlab Id' => 'Id Gitlab',
- 'Gitlab Account' => 'Conta Gitlab',
- 'Link my Gitlab Account' => 'Connectar a minha Conta Gitlab',
- 'Unlink my Gitlab Account' => 'Desconectar a minha Conta Gitlab',
'Documentation: %s' => 'Documentação: %s',
'Switch to the Gantt chart view' => 'Mudar para vista de gráfico de Gantt',
'Reset the search/filter box' => 'Repor caixa de procura/filtro',
'Documentation' => 'Documentação',
'Table of contents' => 'Tabela de conteúdos',
'Gantt' => 'Gantt',
- 'Help with project permissions' => 'Ajuda com permissões de projecto',
'Author' => 'Autor',
'Version' => 'Versão',
'Plugins' => 'Extras',
@@ -1027,41 +907,245 @@ return array(
'Owner' => 'Dono',
'Unread notifications' => 'Notificações por ler',
'My filters' => 'Os meus filtros',
- 'Notification methods:' => 'Metodos de notificação:',
- // 'Import tasks from CSV file' => '',
- // 'Unable to read your file' => '',
- // '%d task(s) have been imported successfully.' => '',
- // 'Nothing have been imported!' => '',
- // 'Import users from CSV file' => '',
- // '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
- // '%s attached a file to the task #%d' => '',
- // 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
- // 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
- // 'Usernames must be lowercase and unique' => '',
- // 'Passwords will be encrypted if present' => '',
+ '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',
+ '%d task(s) have been imported successfully.' => '%d tarefa(s) importada(s) com successo.',
+ 'Nothing have been imported!' => 'Nada foi importado',
+ 'Import users from CSV file' => 'Importar utilizadores de um ficheiro CSV',
+ '%d user(s) have been imported successfully.' => '%d utilizadore(s) importados com successo.',
+ 'Comma' => 'Vírgula',
+ 'Semi-colon' => 'Ponto e Vírgula',
+ 'Tab' => 'Tabulação',
+ 'Vertical bar' => 'Barra vertical',
+ 'Double Quote' => 'Aspas',
+ 'Single Quote' => 'Plica',
+ '%s attached a file to the task #%d' => '%s anexou um ficheiro à tarefa #%d',
+ 'There is no column or swimlane activated in your project!' => 'Não existe nenhuma coluna ou swimlane activado no seu projecto!',
+ 'Append filter (instead of replacement)' => 'Acrescentar filtro (em vez de substituir)',
+ 'Append/Replace' => 'Acrescentar/Substituir',
+ 'Append' => 'Acrescentar',
+ 'Replace' => 'Substituir',
+ 'Import' => 'Importar',
+ 'change sorting' => 'alterar ordernação',
+ 'Tasks Importation' => 'Importação de Tarefas',
+ 'Delimiter' => 'Delimitador',
+ 'Enclosure' => 'Clausura',
+ 'CSV File' => 'Ficheiro CSV',
+ 'Instructions' => 'Instruções',
+ 'Your file must use the predefined CSV format' => 'O seu ficheiro tem de usar um formato CSV pre-definido',
+ 'Your file must be encoded in UTF-8' => 'O seu ficheiro tem de estar codificado como UTF-8',
+ 'The first row must be the header' => 'A primeira linha tem de ser o cabeçalho',
+ 'Duplicates are not verified for you' => 'Duplicados não são verificados por si',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'A data de expiração tem de estar no formato ISO: AAAA-MM-DD',
+ 'Download CSV template' => 'Descarregar template CSV',
+ 'No external integration registered.' => 'Nenhuma integração externa registrada.',
+ 'Duplicates are not imported' => 'Duplicados não são importados',
+ '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',
+ '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',
+ 'Assignee Name' => 'Nome do Assignado',
+ 'Groups' => 'Grupos',
+ 'Members of %s' => 'Membros de %s',
+ 'New group' => 'Novo grupo',
+ 'Group created successfully.' => 'Grupo criado com sucesso.',
+ 'Unable to create your group.' => 'Não foi possivel criar o seu grupo.',
+ 'Edit group' => 'Editar grupo',
+ 'Group updated successfully.' => 'Grupo actualizado com sucesso.',
+ 'Unable to update your group.' => 'Não foi possivel actualizar o seu grupo.',
+ 'Add group member to "%s"' => 'Adicionar membro do grupo a "%s"',
+ 'Group member added successfully.' => 'Membro de grupo adicionado com sucesso.',
+ 'Unable to add group member.' => 'Não foi possivel adicionar membro de grupo.',
+ 'Remove user from group "%s"' => 'Remover utilizador do grupo "%s"',
+ 'User removed successfully from this group.' => 'Utilizador removido com sucesso deste grupo.',
+ 'Unable to remove this user from the group.' => 'Não foi possivel remover este utilizador do grupo.',
+ 'Remove group' => 'Remover grupo.',
+ 'Group removed successfully.' => 'Grupo removido com sucesso.',
+ 'Unable to remove this group.' => 'Não foi possivel remover este grupo.',
+ 'Project Permissions' => 'Permissões de Projecto',
+ 'Manager' => 'Gestor',
+ 'Project Manager' => 'Gestor de Projecto',
+ 'Project Member' => 'Membro de Projecto',
+ 'Project Viewer' => 'Visualizador de Projecto',
+ 'Your account is locked for %d minutes' => 'A sua conta está bloqueada por %d minutos',
+ 'Invalid captcha' => 'Captcha inválido',
+ 'The name must be unique' => 'O nome deve ser único',
+ 'View all groups' => 'Ver todos os grupos',
+ 'View group members' => 'Ver membros do grupo',
+ 'There is no user available.' => 'Não existe utilizador disponivel.',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Tem a certeza que quer remover o utilizador "%s" do grupo "%s"?',
+ 'There is no group.' => 'Não existe grupo.',
+ 'External Id' => 'Id externo',
+ 'Add group member' => 'Adicionar membro de grupo',
+ 'Do you really want to remove this group: "%s"?' => 'Tem a certeza que quer remover este grupo: "%s"?',
+ 'There is no user in this group.' => 'Não existe utilizadores neste grupo.',
+ 'Remove this user' => 'Remover este utilizador',
+ 'Permissions' => 'Permissões',
+ 'Allowed Users' => 'Utilizadores Permitidos',
+ 'No user have been allowed specifically.' => 'Nenhum utilizador foi especificamente permitido.',
+ 'Role' => 'Função',
+ 'Enter user name...' => 'Escreva o nome do utilizador...',
+ 'Allowed Groups' => 'Grupos Permitidos',
+ 'No group have been allowed specifically.' => 'Nenhum grupo foi especificamente permitido.',
+ 'Group' => 'Grupo',
+ 'Group Name' => 'Nome do Grupo',
+ 'Enter group name...' => 'Escreva o nome do Grupo',
+ 'Role:' => 'Função:',
+ 'Project members' => 'Membros do projecto',
+ 'Compare hours for "%s"' => 'Comparar horas para "%s"',
+ '%s mentioned you in the task #%d' => '%s mencionou-te na tarefa #%d',
+ '%s mentioned you in a comment on the task #%d' => '%s mencionou-te num comentário na tarefa #%d',
+ 'You were mentioned in the task #%d' => 'Foi mencionado na tarefa #%d',
+ 'You were mentioned in a comment on the task #%d' => 'Foi mencionado num comentário na tarefa #%d',
+ 'Mentioned' => 'Mencionado',
+ 'Compare Estimated Time vs Actual Time' => 'Comparar Tempo Estimado vs Tempo Real',
+ 'Estimated hours: ' => 'Horas estimadas: ',
+ 'Actual hours: ' => 'Horas reais: ',
+ 'Hours Spent' => 'Horas Gastas',
+ 'Hours Estimated' => 'Horas Estimadas',
+ 'Estimated Time' => 'Tempo Estimado',
+ 'Actual Time' => 'Tempo Real',
+ 'Estimated vs actual time' => 'Tempo estimado vs real',
+ 'RUB - Russian Ruble' => 'RUB - Rublo Russo',
+ 'Assign the task to the person who does the action when the column is changed' => 'Assignar a tarefa à pessoa que realiza a acção quando a coluna é alterada',
+ 'Close a task in a specific column' => 'Fechar tarefa numa coluna especifica',
+ 'Time-based One-time Password Algorithm' => 'Algoritmo de password para uso único baseado em tempo',
+ 'Two-Factor Provider: ' => 'Provedor de Dois Passos: ',
+ 'Disable two-factor authentication' => 'Desactivar autenticação de dois passos',
+ 'Enable two-factor authentication' => 'Activar autenticação de dois passos',
+ 'There is no integration registered at the moment.' => 'Não existe nenhuma integração registrada até ao momento.',
+ 'Password Reset for Kanboard' => 'Redefinir Password para Kanboard',
+ 'Forgot password?' => 'Esqueceu a password?',
+ 'Enable "Forget Password"' => 'Activar "Esqueceu a password"',
+ 'Password Reset' => 'Redefinir a Password',
+ 'New password' => 'Nova Password',
+ 'Change Password' => 'Alterar Password',
+ 'To reset your password click on this link:' => 'Para redefinir a sua password click nesta ligação:',
+ 'Last Password Reset' => 'Última Redefinição da Password',
+ 'The password has never been reinitialized.' => 'A password nunca foi redefinida.',
+ 'Creation' => 'Criação',
+ 'Expiration' => 'Expiração',
+ 'Password reset history' => 'Histórico da redefinição da password',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas as tarefas na coluna "%s" e na swimlane "%s" foram fechadas com successo.',
+ 'Do you really want to close all tasks of this column?' => 'Tem a certeza que quer fechar todas as tarefas nesta coluna?',
+ '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarefa(s) na coluna "%s" e na swimlane "%s" serão fechadas.',
+ 'Close all tasks of this column' => 'Fechar todas as tarefas nesta coluna',
+ 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Nenhum plugin tem registado um método de notificação do projecto. Pode continuar a configurar notificações individuais no seu perfil de utilizador.',
+ 'My dashboard' => 'Meu painel',
+ 'My profile' => 'Meu perfil',
+ 'Project owner: ' => 'Dono do projecto: ',
+ 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'O identificador do projecto é opcional e tem de ser alfa-numerico, exemplo: MEUPROJECTO.',
+ 'Project owner' => 'Dono do projecto',
+ 'Those dates are useful for the project Gantt chart.' => 'Estas datas são uteis para o gráfico de Grantt do projecto.',
+ 'Private projects do not have users and groups management.' => 'Projectos privados não têm gestão de utilizadores nem de grupos.',
+ 'There is no project member.' => 'Não existe membro do projecto.',
+ 'Priority' => 'Prioridade',
+ 'Task priority' => 'Prioridade da Tarefa',
+ 'General' => 'Geral',
+ 'Dates' => 'Datas',
+ 'Default priority' => 'Prioridade por defeito',
+ 'Lowest priority' => 'Prioridade mais baixa',
+ 'Highest priority' => 'Prioridade mais alta',
+ 'If you put zero to the low and high priority, this feature will be disabled.' => 'Se colocar zero na prioridade baixa ou alta, essa funcionalidade será desactivada.',
+ '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',
+ 'Related' => 'Relacionado',
+ 'Attachment' => 'Anexo',
+ 'Title not found' => 'Titulo não encontrado',
+ 'Web Link' => 'Ligação Web',
+ 'External links' => 'Ligações externas',
+ 'Add external link' => 'Adicionar ligação externa',
+ 'Type' => 'Tipo',
+ 'Dependency' => 'Dependencia',
+ 'Add internal link' => 'Adicionar ligação interna',
+ 'Add a new external link' => 'Adicionar nova ligação externa',
+ 'Edit external link' => 'Editar ligação externa',
+ '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',
+ 'Projects management' => 'Gestão de projectos',
+ '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:',
+ '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:' => '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: ',
+ 'End date: ' => 'Data final: ',
+ 'New due date: ' => 'Nova data estimada: ',
+ 'Start date changed: ' => 'Data inicio alterada: ',
+ 'Disable private projects' => 'Desactivar projectos privados',
+ 'Do you really want to remove this custom filter: "%s"?' => 'Tem a certeza que quer remover este filtro personalizado: "%s"?',
+ 'Remove a custom filter' => 'Remover o filtro personalizado',
+ 'User activated successfully.' => 'Utilizador activado com sucesso.',
+ 'Unable to enable this user.' => 'Não foi possivel activar este utilizador.',
+ 'User disabled successfully.' => 'Utilizador desactivado com sucesso.',
+ 'Unable to disable this user.' => 'Não foi possivel desactivar este utilizador.',
+ 'All files have been uploaded successfully.' => 'Todos os ficheiros foram enviados com sucesso.',
+ 'View uploaded files' => 'Ver ficheiros enviados',
+ 'The maximum allowed file size is %sB.' => 'O tamanho máximo permitido é %sB.',
+ 'Choose files again' => 'Escolher ficheiros novamente',
+ 'Drag and drop your files here' => 'Arraste e deixe os ficheiros para aqui',
+ 'choose files' => 'escolher ficheiros',
+ 'View profile' => 'Ver perfil',
+ 'Two Factor' => 'Dois Factores',
+ 'Disable user' => 'Desactivar utilizador',
+ 'Do you really want to disable this user: "%s"?' => 'Tem a certeza que quer desactivar este utilizador: "%s"?',
+ 'Enable user' => 'Activar utilizador',
+ 'Do you really want to enable this user: "%s"?' => 'Tem a certeza que quer activar este utilizador: "%s"?',
+ 'Download' => 'Transferir',
+ 'Uploaded: %s' => 'Enviado: %s',
+ 'Size: %s' => 'Tamanho: %s',
+ 'Uploaded by %s' => 'Enviado por %s',
+ 'Filename' => 'Nome do ficheiro',
+ 'Size' => 'Tamanho',
+ // '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)' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 0e9f2da8..0d73f903 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Удалить проект',
'Edit the board for "%s"' => 'Изменить доску для "%s"',
'All projects' => 'Все проекты',
- 'Change columns' => 'Изменить колонки',
'Add a new column' => 'Добавить новую колонку',
'Title' => 'Название',
'Nobody assigned' => 'Никто не назначен',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Открыть задачу',
'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу: "%s" ?',
'Back to the board' => 'Вернуться на доску',
- 'Created on %B %e, %Y at %k:%M %p' => 'Создано %B /%e /%Y в %k:%M %p',
'There is nobody assigned' => 'Никто не назначен',
'Column on the board:' => 'Колонка на доске: ',
- 'Status is open' => 'Статус - открыт',
- 'Status is closed' => 'Статус - закрыт',
'Close this task' => 'Закрыть задачу',
'Open this task' => 'Открыть задачу',
'There is no description.' => 'Нет описания.',
@@ -120,11 +116,10 @@ return array(
'The user id is required' => 'Необходим ID пользователя',
'Passwords don\'t match' => 'Пароли не совпадают',
'The confirmation is required' => 'Необходимо подтверждение',
- 'The project is required' => 'Необъодимо указать проект',
+ 'The project is required' => 'Необходимо указать проект',
'The id is required' => 'Необходим ID',
'The project id is required' => 'Необходим ID проекта',
'The project name is required' => 'Необходимо имя проекта',
- 'This project must be unique' => 'Проект должен быть уникальным',
'The title is required' => 'Необходим заголовок',
'Settings saved successfully.' => 'Параметры успешно сохранены.',
'Unable to save your settings.' => 'Невозможно сохранить параметры.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'В процессе',
'Done' => 'Выполнено',
'Application version:' => 'Версия приложения:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Завершен %d/%m/%Y в %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d/%m/%Y в %H:%M',
- 'Date created' => 'Дата создания',
- 'Date completed' => 'Дата завершения',
'Id' => 'ID',
'%d closed tasks' => '%d завершенных задач',
'No task for this project' => 'Нет задач для этого проекта',
@@ -175,16 +166,10 @@ return array(
'Complexity' => 'Сложность',
'Task limit' => 'Лимит задач',
'Task count' => 'Количество задач',
- 'Edit project access list' => 'Изменить доступ к проекту',
- 'Allow this user' => 'Разрешить этого пользователя',
- 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет неограниченные права.',
- 'Revoke' => 'Отозвать',
- 'List of authorized users' => 'Список авторизованных пользователей',
'User' => 'Пользователь',
- 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту',
'Comments' => 'Комментарии',
'Write your text in Markdown' => 'Справка по синтаксису Markdown',
- 'Leave a comment' => 'Оставить комментарий 2',
+ 'Leave a comment' => 'Оставить комментарий',
'Comment is required' => 'Нужен комментарий',
'Leave a description' => 'Напишите описание',
'Comment added successfully.' => 'Комментарий успешно добавлен.',
@@ -192,11 +177,8 @@ return array(
'Edit this task' => 'Изменить задачу',
'Due Date' => 'Сделать до',
'Invalid date' => 'Неверная дата',
- 'Must be done before %B %e, %Y' => 'Должно быть сделано до %B %e %Y',
- '%B %e, %Y' => '%B, %e, %Y',
- '%b %e, %Y' => '%b %e, %Y',
'Automatic actions' => 'Автоматические действия',
- 'Your automatic action have been created successfully.' => 'Автоматика успешно настроена.',
+ 'Your automatic action have been created successfully.' => 'Автоматизированное действие успешно настроено.',
'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.',
'Remove an action' => 'Удалить действие',
'Unable to remove this action.' => 'Не удалось удалить действие',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Назначить определенный цвет пользователю',
'Column title' => 'Название колонки',
'Position' => 'Расположение',
- 'Move Up' => 'Сдвинуть вверх',
- 'Move Down' => 'Сдвинуть вниз',
'Duplicate to another project' => 'Клонировать в другой проект',
'Duplicate' => 'Клонировать',
'link' => 'ссылка',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Комментарий удален.',
'Unable to remove this comment.' => 'Не удалось удалить этот комментарий.',
'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор и автор комментария имеют доступ к этой странице.',
'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »',
'The current password is required' => 'Требуется текущий пароль',
'Wrong password' => 'Неверный пароль',
@@ -267,10 +246,6 @@ return array(
'External authentication failed' => 'Внешняя авторизация не удалась',
'Your external account is linked to your profile successfully.' => 'Ваш внешний аккаунт успешно подключен к профилю.',
'Email' => 'E-mail',
- 'Link my Google Account' => 'Привязать мой профиль к Google',
- 'Unlink my Google Account' => 'Отвязать мой профиль от Google',
- 'Login with my Google Account' => 'Аутентификация через Google',
- 'Project not found.' => 'Проект не найден.',
'Task removed successfully.' => 'Задача удалена.',
'Unable to remove this task.' => 'Не удалось удалить эту задачу.',
'Remove a task' => 'Удалить задачу',
@@ -301,7 +276,7 @@ return array(
'File removed successfully.' => 'Файл удален.',
'Attach a document' => 'Прикрепить файл',
'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?',
- 'Attachments' => 'Приложение',
+ 'Attachments' => 'Вложения',
'Edit the task' => 'Изменить задачу',
'Edit the description' => 'Изменить описание',
'Add a comment' => 'Добавить комментарий',
@@ -317,7 +292,7 @@ return array(
'estimated' => 'расчетное',
'Sub-Tasks' => 'Подзадачи',
'Add a sub-task' => 'Добавить подзадачу',
- 'Original estimate' => 'Запланировано',
+ 'Original estimate' => 'Заплан.',
'Create another sub-task' => 'Создать другую подзадачу',
'Time spent' => 'Времени затрачено',
'Edit a sub-task' => 'Изменить подзадачу',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Максимальный размер: ',
'Unable to upload the file.' => 'Не удалось загрузить файл.',
'Display another project' => 'Показать другой проект',
- 'Login with my Github Account' => 'Аутентификация через Github',
- 'Link my Github Account' => 'Привязать мой профиль к Github',
- 'Unlink my Github Account' => 'Отвязать мой профиль от Github',
'Created by %s' => 'Создано %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Последнее изменение %d/%m/%Y в %H:%M',
'Tasks Export' => 'Экспорт задач',
'Tasks exportation for "%s"' => 'Задача экспортирована для « %s »',
'Start Date' => 'Дата начала',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам:',
'view the task on Kanboard' => 'посмотреть задачу на Kanboard',
'Public access' => 'Общий доступ',
- 'User management' => 'Управление пользователями',
'Active tasks' => 'Активные задачи',
'Disable public access' => 'Отключить общий доступ',
'Enable public access' => 'Включить общий доступ',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'E-mail:',
'Notifications:' => 'Уведомления:',
'Notifications' => 'Уведомления',
- 'Group:' => 'Группа:',
- 'Regular user' => 'Обычный пользователь',
'Account type:' => 'Тип профиля:',
'Edit profile' => 'Редактировать профиль',
'Change password' => 'Сменить пароль',
'Password modification' => 'Изменение пароля',
'External authentications' => 'Внешняя аутентификация',
- 'Google Account' => 'Профиль Google',
- 'Github Account' => 'Профиль Github',
'Never connected.' => 'Ранее не соединялось.',
- 'No account linked.' => 'Нет связанных профилей.',
- 'Account linked.' => 'Профиль связан.',
'No external authentication enabled.' => 'Нет активной внешней аутентификации.',
'Password modified successfully.' => 'Пароль изменен.',
'Unable to change the password.' => 'Не удалось сменить пароль.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s сменил назначенного для задачи %s на %s',
'New password for the user "%s"' => 'Новый пароль для пользователя "%s"',
'Choose an event' => 'Выберите событие',
- 'Github commit received' => 'Github: коммит получен',
- 'Github issue opened' => 'Github: новая проблема',
- 'Github issue closed' => 'Github: проблема закрыта',
- 'Github issue reopened' => 'Github: проблема переоткрыта',
- 'Github issue assignee change' => 'Github: сменить ответственного за проблему',
- 'Github issue label change' => 'Github: ярлык проблемы изменен',
'Create a task from an external provider' => 'Создать задачу из внешнего источника',
'Change the assignee based on an external username' => 'Изменить назначенного основываясь на внешнем имени пользователя',
'Change the category based on an external label' => 'Изменить категорию основываясь на внешнем ярлыке',
'Reference' => 'Ссылка',
- 'Reference: %s' => 'Ссылка: %s',
'Label' => 'Ярлык',
'Database' => 'База данных',
'About' => 'Информация',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Частота в секундах (60 секунд по умолчанию)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Частота в секундах (0 для выключения, 10 секунд по умолчанию)',
'Application URL' => 'URL приложения',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Пример: http://example.kanboard.net (используется в email уведомлениях)',
'Token regenerated.' => 'Токен пересоздан',
'Date format' => 'Формат даты',
'ISO format is always accepted, example: "%s" and "%s"' => 'Время должно быть в ISO-формате, например: "%s" или "%s"',
@@ -482,9 +438,6 @@ return array(
'This project is private' => 'Это проект с ограниченным доступом',
'Type here to create a new sub-task' => 'Печатайте сюда чтобы создать подзадачу',
'Add' => 'Добавить',
- 'Estimated time: %s hours' => 'Запланировано: %s часов',
- 'Time spent: %s hours' => 'Потрачено времени: %s часов',
- 'Started on %B %e, %Y' => 'Начато %B %e, %Y',
'Start date' => 'Дата начала',
'Time estimated' => 'Запланировано',
'There is nothing assigned to you.' => 'Вам ничего не назначено',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Любой может получить доступ к этому проекту.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github webhooks',
- 'Help on Github webhooks' => 'Помощь по Github webhooks',
'Create a comment from an external provider' => 'Создать комментарий из внешнего источника',
- 'Github issue comment created' => 'Github issue комментарий создан',
'Project management' => 'Управление проектом',
'My projects' => 'Мои проекты',
'Columns' => 'Колонки',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Перераспределение пользователей для "%s"',
'Clone this project' => 'Клонировать проект',
'Column removed successfully.' => 'Колонка успешно удалена.',
- 'Github Issue' => 'Вопрос на Github',
'Not enough data to show the graph.' => 'Недостаточно данных, чтобы показать график.',
'Previous' => 'Предыдущий',
'The id must be an integer' => 'Этот id должен быть целочисленным',
@@ -541,16 +490,13 @@ return array(
'Nothing to preview...' => 'Нет данных для предпросмотра...',
'Preview' => 'Предпросмотр',
'Write' => 'Написание',
- 'Active swimlanes' => 'Активные ',
+ 'Active swimlanes' => 'Активные дорожки',
'Add a new swimlane' => 'Добавить новую дорожку',
'Change default swimlane' => 'Сменить стандартную дорожку',
'Default swimlane' => 'Стандартная дорожка',
'Do you really want to remove this swimlane: "%s"?' => 'Вы действительно хотите удалить дорожку "%s"?',
'Inactive swimlanes' => 'Неактивные дорожки',
- 'Set project manager' => 'Установить менеджера проекта',
- 'Set project member' => 'Установить участника проекта',
'Remove a swimlane' => 'Удалить дорожку',
- 'Rename' => 'Переименовать',
'Show default swimlane' => 'Показать стандартную дорожку',
'Swimlane modification for the project "%s"' => 'Редактирование дорожки для проекта "%s"',
'Swimlane not found.' => 'Дорожка не найдена.',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Дорожки',
'Swimlane updated successfully.' => 'Дорожка успешно обновлена.',
'The default swimlane have been updated successfully.' => 'Стандартная swimlane был успешно обновлен.',
- 'Unable to create your swimlane.' => 'Невозможно создать дорожку.',
'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)' => 'Стандартные категории для нового проекта (разделяются запятыми)',
- // 'Gitlab commit received' => '',
- 'Gitlab issue opened' => 'Gitlab вопрос открыт',
- 'Gitlab issue closed' => 'Gitlab вопрос закрыт',
- 'Gitlab webhooks' => 'Gitlab webhooks',
- 'Help on Gitlab webhooks' => 'Помощь по Gitlab webhooks',
'Integrations' => 'Интеграции',
'Integration with third-party services' => 'Интеграция со сторонними сервисами',
- 'Role for this project' => 'Роли для этого проекта',
- 'Project manager' => 'Менеджер проекта',
- 'Project member' => 'Участник проекта',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Менеджер проекта может изменять настройки проекта и имеет больше привелегий чем стандартный пользователь.',
- 'Gitlab Issue' => 'Gitlab вопросы',
'Subtask Id' => 'Id подзадачи',
'Subtasks' => 'Подзадачи',
'Subtasks Export' => 'Экспортировать подзадачи',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'У вас уже есть одна задача в разработке',
'Which parts of the project do you want to duplicate?' => 'Какие части проекта должны быть дублированы?',
'Disallow login form' => 'Запретить форму входа',
- // 'Bitbucket commit received' => '',
- 'Bitbucket webhooks' => 'BitBucket webhooks',
- 'Help on Bitbucket webhooks' => 'Помощь по BitBucket webhooks',
'Start' => 'Начало',
'End' => 'Конец',
'Task age in days' => 'Возраст задачи в днях',
@@ -628,7 +560,7 @@ return array(
'Task\'s links' => 'Ссылки задачи',
'The labels must be different' => 'Ярлыки должны быть разными',
'There is no link.' => 'Это не ссылка',
- 'This label must be unique' => 'Этот ярлык должна быть уникальной ',
+ 'This label must be unique' => 'Этот ярлык должен быть уникальным ',
'Unable to create your link.' => 'Не удается создать эту ссылку.',
'Unable to update your link.' => 'Не удается обновить эту ссылку.',
'Unable to remove this link.' => 'Не удается удалить эту ссылку.',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Эта задача',
'<1h' => '<1ч',
'%dh' => '%dh',
- '%b %e' => '%b %e',
'Expand tasks' => 'Развернуть задачи',
'Collapse tasks' => 'Свернуть задачи',
'Expand/collapse tasks' => 'Развернуть/свернуть задачи',
@@ -656,18 +587,15 @@ return array(
'Keyboard shortcuts' => 'Горячие клавиши',
'Open board switcher' => 'Открыть переключатель доски',
'Application' => 'Приложение',
- // 'since %B %e, %Y at %k:%M %p' => '',
'Compact view' => 'Компактный вид',
'Horizontal scrolling' => 'Широкий вид',
'Compact/wide view' => 'Компактный/широкий вид',
'No results match:' => 'Отсутствуют результаты:',
'Currency' => 'Валюта',
- 'Files' => 'Файлы',
- 'Images' => 'Изображения',
'Private project' => 'Приватный проект',
'AUD - Australian Dollar' => 'AUD - Австралийский доллар',
'CAD - Canadian Dollar' => 'CAD - Канадский доллар',
- 'CHF - Swiss Francs' => 'CHF - Швейцарский Франк',
+ 'CHF - Swiss Francs' => 'CHF - Швейцарский франк',
'Custom Stylesheet' => 'Пользовательский стиль',
'download' => 'загрузить',
'EUR - Euro' => 'EUR - Евро',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => 'Код двухфакторной авторизации валиден',
'Code' => 'Код',
'Two factor authentication' => 'Двухфакторная авторизация',
- 'Enable/disable two factor authentication' => 'Включить/выключить двухфакторную авторизацию',
'This QR code contains the key URI: ' => 'Это QR-код содержит ключевую URI:',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Сохраните Ваш секретный ключ в TOTP программе (например Google Autentificator или FreeOTP).',
'Check my code' => 'Проверить мой код',
'Secret key: ' => 'Секретный ключ: ',
'Test your device' => 'Проверьте свое устройство',
'Assign a color when the task is moved to a specific column' => 'Назначить цвет, когда задача перемещается в определенную колонку',
'%s via Kanboard' => '%s через Канборд',
- 'uploaded by: %s' => 'загружено: %s',
- 'uploaded on: %s' => 'загружено в: %s',
- 'size: %s' => 'размер: %s',
'Burndown chart for "%s"' => 'Диаграмма сгорания для « %s »',
'Burndown chart' => 'Диаграмма сгорания',
'This chart show the task complexity over the time (Work Remaining).' => 'Эта диаграмма показывают сложность задачи по времени (оставшейся работы).',
@@ -722,7 +645,6 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Сделайте скриншот и нажмите CTRL+V или ⌘+V для вложения',
'Screenshot uploaded successfully.' => 'Скриншет успешно загружен',
'SEK - Swedish Krona' => 'SEK - Шведская крона',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Идентификатор проекта - это опциональный буквенно-цифровой код использующийся для идентификации проекта',
'Identifier' => 'Идентификатор',
'Disable two factor authentication' => 'Выключить двухфакторную авторизацию',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Вы действительно хотите выключить двухфакторную авторизацию для пользователя "%s"?',
@@ -731,7 +653,6 @@ return array(
'A task cannot be linked to itself' => 'Задача не может быть связана с собой же',
'The exact same link already exists' => 'Такая ссылка уже существует',
'Recurrent task is scheduled to be generated' => 'Периодическая задача запланирована к созданию',
- 'Recurring information' => 'Информация о периодичности',
'Score' => 'Оценка',
'The identifier must be unique' => 'Идентификатор должен быть уникальным',
'This linked task id doesn\'t exists' => 'Этот ID звязанной задачи не существует',
@@ -769,7 +690,7 @@ return array(
'Automatically update the start date' => 'Автоматическое обновление даты начала',
'iCal feed' => 'iCal данные',
'Preferences' => 'Предпочтения',
- 'Security' => 'Безопастность',
+ 'Security' => 'Безопасность',
'Two factor authentication disabled' => 'Двухфакторная аутентификация отключена',
'Two factor authentication enabled' => 'Включена двухфакторная аутентификация',
'Unable to update this user.' => 'Не удается обновить этого пользователя.',
@@ -777,43 +698,32 @@ return array(
'User that will receive the email' => 'Пользователь, который будет получать e-mail',
'Email subject' => 'Тема e-mail',
'Date' => 'Дата',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
- // 'Add a comment log when moving the task between columns' => '',
- // 'Move the task to another column when the category is changed' => '',
- // 'Send a task by email to someone' => '',
- // 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
+ 'Add a comment log when moving the task between columns' => 'Добавлять запись при перемещении задачи между колонками',
+ 'Move the task to another column when the category is changed' => 'Переносить задачи в другую колонку при изменении категории',
+ 'Send a task by email to someone' => 'Отправить задачу по email',
+ 'Reopen a task' => 'Переоткрыть задачу',
'Column change' => 'Изменение колонки',
'Position change' => 'Позиция изменена',
'Swimlane change' => 'Дорожка изменена',
- // 'Assignee change' => '',
+ 'Assignee change' => 'Назначенный пользователь изменен',
'[%s] Overdue tasks' => '[%s] просроченные задачи',
'Notification' => 'Уведомления',
'%s moved the task #%d to the first swimlane' => '%s задач перемещено #%d в первой дорожке',
'%s moved the task #%d to the swimlane "%s"' => '%s задач перемещено #%d в дорожке "%s"',
'Swimlane' => 'Дорожки',
'Gravatar' => 'Граватар',
- // '%s moved the task %s to the first swimlane' => '',
- // '%s moved the task %s to the swimlane "%s"' => '',
+ '%s moved the task %s to the first swimlane' => '%s переместил задачу %s на первую дорожку',
+ '%s moved the task %s to the swimlane "%s"' => '%s переместил задачу %s на дорожку "%s"',
'This report contains all subtasks information for the given date range.' => 'Этот отчет содержит всю информацию подзадач в заданном диапазоне дат.',
'This report contains all tasks information for the given date range.' => 'Этот отчет содержит всю информацию для задачи в заданном диапазоне дат.',
'Project activities for %s' => 'Активность проекта для %s',
- // 'view the board on Kanboard' => '',
+ 'view the board on Kanboard' => 'посмотреть доску на Kanboard',
'The task have been moved to the first swimlane' => 'Эта задача была перемещена в первую дорожку',
'The task have been moved to another swimlane:' => 'Эта задача была перемещена в другую дорожку:',
'Overdue tasks for the project "%s"' => 'Просроченные задачи для проекта "%s"',
'New title: %s' => 'Новый заголовок: %s',
'The task is not assigned anymore' => 'Задача больше не назначена',
- // 'New assignee: %s' => '',
+ 'New assignee: %s' => 'Новый назначенный: %s',
'There is no category now' => 'В настоящее время здесь нет категорий',
'New category: %s' => 'Новая категория: %s',
'New color: %s' => 'Новый цвет: %s',
@@ -826,17 +736,11 @@ return array(
'The field "%s" have been updated' => 'Поле "%s" ,было изменено',
'The description have been modified' => 'Описание было изменено',
'Do you really want to close the task "%s" as well as all subtasks?' => 'Вы действительно хотите закрыть задачу "%s", а также все подзадачи?',
- 'Swimlane: %s' => 'Дорожка: %s',
'I want to receive notifications for:' => 'Я хочу получать уведомления для:',
'All tasks' => 'Все задачи',
'Only for tasks assigned to me' => 'Только для задач, назначенных на меня',
'Only for tasks created by me' => 'Только для задач, созданных мной',
'Only for tasks created by me and assigned to me' => 'Только для задач, созданных мной и назначенных мной',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p',
- 'New due date: %B %e, %Y' => 'Новая дата завершения: %B %e, %Y',
- 'Start date changed: %B %e, %Y' => 'Изменить дату начала: %B %e, %Y',
- '%k:%M %p' => '%k:%M %p',
'%%Y-%%m-%%d' => '%%Y-%%m-%%d',
'Total for all columns' => 'Суммарно для всех колонок',
'You need at least 2 days of data to show the chart.' => 'Для отображения диаграммы нужно по крайней мере 2 дня.',
@@ -845,7 +749,7 @@ return array(
'Stop timer' => 'Остановить таймер',
'Start timer' => 'Запустить таймер',
'Add project member' => 'Добавить номер проекта',
- 'Enable notifications' => 'Отключить уведомления',
+ 'Enable notifications' => 'Включить уведомления',
'My activity stream' => 'Лента моей активности',
'My calendar' => 'Мой календарь',
'Search tasks' => 'Поиск задачи',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'Не назначенные',
'View advanced search syntax' => 'Просмотр расширенного синтаксиса поиска',
'Overview' => 'Обзор',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Просмотр Доска/Календарь/Список',
'Switch to the board view' => 'Переключиться в режим доски',
'Switch to the calendar view' => 'Переключиться в режим календаря',
@@ -887,37 +790,27 @@ return array(
'Average time spent' => 'Затрачено времени в среднем',
'This chart show the average time spent into each column for the last %d tasks.' => 'Эта диаграмма показывает среднее время, проведенное задачами в каждой колонке за последний %d.',
'Average Lead and Cycle time' => 'Среднее время выполнения и цикла',
- 'Average lead time: ' => 'Среднее время выполнения',
- 'Average 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.' => 'Эта диаграма показывает среднее время выполнения и цикла задачь в последние %d.',
'Average time into each column' => 'Среднее время в каждом столбце',
'Lead and cycle time' => 'Время выполнения и цикла',
- 'Google Authentication' => 'Авторизация Google',
- 'Help on Google authentication' => 'Помощь в авторизации Google',
- 'Github Authentication' => 'Авторизация Github',
- 'Help on Github authentication' => 'Помощь в авторизации Github',
'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.' => '',
+ 'If the task is not closed the current time is used instead of the completion date.' => 'Если задача не закрыта, то текущая дата будет указана в дате завершения задачи.',
'Set automatically the start date' => 'Установить автоматическую дату начала',
'Edit Authentication' => 'Редактировать авторизацию',
- 'Google Id' => 'Google I',
- 'Github Id' => 'Github Id',
'Remote user' => 'Удаленный пользователь',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Учетные данные для входа через LDAP, Google и Github не будут сохранены в Kanboard.',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Если вы установите флажок "Запретить форму входа", учетные данные, введенные в форму входа будет игнорироваться.',
- 'By @%s on Gitlab' => 'От @%s на Gitlab',
- 'Gitlab issue comment created' => 'Был создан комментарий к задаче на Gitlab',
'New remote user' => 'Новый удаленный пользователь',
'New local user' => 'Новый локальный пользователь',
'Default task color' => 'Стандартные цвета задач',
- 'Hide sidebar' => 'Свернуть сайдбар',
- 'Expand sidebar' => 'Показать сайдбар',
'This feature does not work with all browsers.' => 'Эта функция доступна не во всех браузерах.',
'There is no destination project available.' => 'Нет доступного для назначения проекта.',
'Trigger automatically subtask time tracking' => 'Триггер автоматического отслеживания времени подзадач',
@@ -926,15 +819,14 @@ return array(
'Current column: %s' => 'Текущая колонка: %s',
'Current category: %s' => 'Текущая категория: %s',
'no category' => 'без категории',
- 'Current assignee: %s' => 'Current assignee: %s',
+ 'Current assignee: %s' => 'Текущее назначенное лицо: %s',
'not assigned' => 'не назначен',
'Author:' => 'Автор:',
'contributors' => 'соавторы',
'License:' => 'Лицензия:',
'License' => 'Лицензия',
- 'Project Administrator' => 'Администратор проекта',
'Enter the text below' => 'Введите текст ниже',
- 'Gantt chart for %s' => 'Диаграмма Гантта для %s',
+ 'Gantt chart for %s' => 'Диаграмма Ганта для %s',
'Sort by position' => 'Сортировать по позиции',
'Sort by date' => 'Сортировать по дате',
'Add task' => 'Добавить задачу',
@@ -943,7 +835,7 @@ return array(
'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' => 'Диаграмма Гантта',
+ 'Gantt chart' => 'Диаграмма Ганта',
'People who are project managers' => 'Люди, которые менеджеры проекта',
'People who are project members' => 'Люди, которые участники проекта',
'NOK - Norwegian Krone' => 'НК - Норвежская крона',
@@ -952,116 +844,308 @@ return array(
'open file' => 'открыть файл',
'End date' => 'Дата завершения',
'Users overview' => 'Обзор пользователей',
- 'Managers' => 'Менеджеры',
'Members' => 'Участники',
'Shared project' => 'Общие/публичные проекты',
'Project managers' => 'Менеджер проекта',
- 'Project members' => 'Участники проекта',
- 'Gantt chart for all projects' => 'Диаграмма Гантта для всех проектов',
+ 'Gantt chart for all projects' => 'Диаграмма Ганта для всех проектов',
'Projects list' => 'Список проектов',
- 'Gantt chart for this project' => 'Диаграмма Гантта для этого проекта',
+ 'Gantt chart for this project' => 'Диаграмма Ганта для этого проекта',
'Project board' => 'Доска проекта',
'End date:' => 'Дата завершения:',
'There is no start date or end date for this project.' => 'В проекте не указаны дата начала или завершения.',
- 'Projects Gantt chart' => 'Диаграмма Гантта проектов',
- 'Start date: %s' => 'Дата начала: %s',
- 'End date: %s' => 'Дата завершения: %s',
+ 'Projects Gantt chart' => 'Диаграмма Ганта проектов',
'Link type' => 'Тип ссылки',
'Change task color when using a specific task link' => 'Изменение цвета задач при использовании ссылки на определенные задачи',
'Task link creation or modification' => 'Ссылка на создание или модификацию задачи',
- 'Login with my Gitlab Account' => 'Авторизоваться через аккаунт Gitlab',
'Milestone' => 'Веха',
- 'Gitlab Authentication' => 'Авторизация через Gitlab',
- 'Help on Gitlab authentication' => 'Помощь а авторизации через Gitlab',
- 'Gitlab Id' => 'Gitlab Id',
- 'Gitlab Account' => 'Аккаунт Gitlab',
- 'Link my Gitlab Account' => 'Привязать аккаунт Gitlab',
- 'Unlink my Gitlab Account' => 'Отвязать аккаунт Gitlab',
'Documentation: %s' => 'Документация: %s',
- 'Switch to the Gantt chart view' => 'Переключиться в режим диаграммы Гантта',
+ 'Switch to the Gantt chart view' => 'Переключиться в режим диаграммы Ганта',
'Reset the search/filter box' => 'Сбросить поиск/фильтр',
'Documentation' => 'Документация',
- 'Table of contents' => 'Сожержание',
- 'Gantt' => 'Гантт',
- 'Help with project permissions' => 'Помощь с правами доступа по проекту',
- // 'Author' => '',
- // 'Version' => '',
- // 'Plugins' => '',
- // 'There is no plugin loaded.' => '',
- // 'Set maximum column height' => '',
- // 'Remove maximum column height' => '',
- // 'My notifications' => '',
- // 'Custom filters' => '',
- // 'Your custom filter have been created successfully.' => '',
- // 'Unable to create your custom filter.' => '',
- // 'Custom filter removed successfully.' => '',
- // 'Unable to remove this custom filter.' => '',
- // 'Edit custom filter' => '',
- // 'Your custom filter have been updated successfully.' => '',
- // 'Unable to update custom filter.' => '',
- // 'Web' => '',
- // 'New attachment on task #%d: %s' => '',
- // 'New comment on task #%d' => '',
- // 'Comment updated on task #%d' => '',
- // 'New subtask on task #%d' => '',
- // 'Subtask updated on task #%d' => '',
- // 'New task #%d: %s' => '',
- // 'Task updated #%d' => '',
- // 'Task #%d closed' => '',
- // 'Task #%d opened' => '',
- // 'Column changed for task #%d' => '',
- // 'New position for task #%d' => '',
- // 'Swimlane changed for task #%d' => '',
- // 'Assignee changed on task #%d' => '',
- // '%d overdue tasks' => '',
- // 'Task #%d is overdue' => '',
- // 'No new notifications.' => '',
- // 'Mark all as read' => '',
- // 'Mark as read' => '',
- // 'Total number of tasks in this column across all swimlanes' => '',
- // 'Collapse swimlane' => '',
- // 'Expand swimlane' => '',
- // 'Add a new filter' => '',
- // 'Share with all project members' => '',
- // 'Shared' => '',
- // 'Owner' => '',
- // 'Unread notifications' => '',
- // 'My filters' => '',
- // 'Notification methods:' => '',
- // 'Import tasks from CSV file' => '',
- // 'Unable to read your file' => '',
- // '%d task(s) have been imported successfully.' => '',
- // 'Nothing have been imported!' => '',
- // 'Import users from CSV file' => '',
- // '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
- // '%s attached a file to the task #%d' => '',
- // 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
- // 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
- // 'Usernames must be lowercase and unique' => '',
- // 'Passwords will be encrypted if present' => '',
+ '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' => 'Новое вложение для задачи #%d: %s',
+ 'New comment on task #%d' => 'Новый комментарий для задачи #%d',
+ 'Comment updated on task #%d' => 'Обновлен комментарий у задачи #%d',
+ 'New subtask on task #%d' => 'Новая подзадача у задачи #%d',
+ 'Subtask updated on task #%d' => 'Подзадача обновлена у задачи #%d',
+ 'New task #%d: %s' => 'Новая задача #%d: %s',
+ 'Task updated #%d' => 'Обновлена задача #%d',
+ 'Task #%d closed' => 'Задача #%d закрыта',
+ 'Task #%d opened' => 'Задача #%d открыта',
+ 'Column changed for task #%d' => 'Обновлена колонка у задачи #%d',
+ 'New position for task #%d' => 'Новая позиция для задачи #%d',
+ 'Swimlane changed for task #%d' => 'Изменена дорожка у задачи #%d',
+ 'Assignee changed on task #%d' => 'Изменен назначенный у задачи #%d',
+ '%d overdue tasks' => '%d просроченных задач',
+ 'Task #%d is overdue' => 'Задача #%d просрочена',
+ 'No new notifications.' => 'Нет новых уведомлений.',
+ 'Mark all as read' => 'Пометить все прочитанными',
+ 'Mark as read' => 'Пометить прочитанным',
+ 'Total number of tasks in this column across all swimlanes' => 'Общее число задач в этой колонке на всех дорожках',
+ 'Collapse swimlane' => 'Свернуть дорожку',
+ 'Expand swimlane' => 'Развернуть дорожку',
+ 'Add a new filter' => 'Добавить новый фильтр',
+ 'Share with all project members' => 'Сделать общим для всех участников проекта',
+ 'Shared' => 'Общие',
+ 'Owner' => 'Владелец',
+ 'Unread notifications' => 'Непрочитанные уведомления',
+ 'My filters' => 'Мои фильтры',
+ 'Notification methods:' => 'Способы уведомления:',
+ 'Import tasks from CSV file' => 'Импорт задач из CSV-файла',
+ 'Unable to read your file' => 'Невозможно прочитать файл',
+ '%d task(s) have been imported successfully.' => '%d задач было успешно импортировано.',
+ 'Nothing have been imported!' => 'Ничего не было импортировано!',
+ 'Import users from CSV file' => 'Импорт пользователей из CSV-файла',
+ '%d user(s) have been imported successfully.' => '%d пользователей было успешно импортировано.',
+ 'Comma' => 'Запятая',
+ 'Semi-colon' => 'Точка с запятой',
+ 'Tab' => 'Пробел (Tab)',
+ 'Vertical bar' => 'Вертикальная черта (|)',
+ 'Double Quote' => 'Одинарные кавычки',
+ 'Single Quote' => 'Двойные кавычки',
+ '%s attached a file to the task #%d' => '%s добавил файл к задаче #%d',
+ 'There is no column or swimlane activated in your project!' => 'В вашей задаче нет активных колонок или дорожек!',
+ 'Append filter (instead of replacement)' => 'Добавляющий фильтр (не заменяющий)',
+ 'Append/Replace' => 'Добавление/Замена',
+ 'Append' => 'Добавление',
+ 'Replace' => 'Замена',
+ 'Import' => 'Импорт',
+ 'change sorting' => 'изменить сортировку',
+ 'Tasks Importation' => 'Импортирование задач',
+ 'Delimiter' => 'Разделитель',
+ 'Enclosure' => 'Тип кавычек',
+ 'CSV File' => 'CSV-файл',
+ 'Instructions' => 'Инструкции',
+ 'Your file must use the predefined CSV format' => 'Ваш файл должен использовать структуру формата CSV',
+ 'Your file must be encoded in UTF-8' => 'Ваш файл должен иметь кодировку UTF-8',
+ 'The first row must be the header' => 'В первой строке должны быть заголовки столбцов',
+ 'Duplicates are not verified for you' => 'Проверка на дубликаты не осуществляется',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'Дата просрочки должна быть в формате ISO: ГГГГ-ММ-ДД',
+ 'Download CSV template' => 'Скачать шаблон CSV-файла',
+ 'No external integration registered.' => 'Нет зарегистрированных внешних интеграций.',
+ 'Duplicates are not imported' => 'Дубликаты не импортируются',
+ 'Usernames must be lowercase and unique' => 'Логины пользователей должны быть строчными и уникальными',
+ 'Passwords will be encrypted if present' => 'Пароли будут зашифрованы (если указаны)',
+ '%s attached a new file to the task %s' => '%s добавил новый файл к задаче %s',
+ 'Assign automatically a category based on a link' => 'Автоматически назначать категории на основе ссылки',
+ 'BAM - Konvertible Mark' => 'BAM - Конвертируемая марка',
+ 'Assignee Username' => 'Логин назначенного',
+ 'Assignee Name' => 'Имя назначенного',
+ 'Groups' => 'Группы',
+ 'Members of %s' => 'Участник группы %s',
+ 'New group' => 'Новая группа',
+ 'Group created successfully.' => 'Группа успешно создана.',
+ 'Unable to create your group.' => 'Невозможно создать группу.',
+ 'Edit group' => 'Именить группу',
+ 'Group updated successfully.' => 'Группы успешно обновлена.',
+ 'Unable to update your group.' => 'Невозможно обновить группу.',
+ 'Add group member to "%s"' => 'Добавить участника в "%s"',
+ 'Group member added successfully.' => 'Участник группы успешно добавлен.',
+ 'Unable to add group member.' => 'Невозможно добавить участника.',
+ 'Remove user from group "%s"' => 'Удалить пользователя из группы "%s"',
+ 'User removed successfully from this group.' => 'Пользователь успешно удален из группы.',
+ 'Unable to remove this user from the group.' => 'Невозможно удалить пользователя из группы.',
+ 'Remove group' => 'Удалить группу',
+ 'Group removed successfully.' => 'Группа успешно удалена.',
+ 'Unable to remove this group.' => 'Невозможно удалить группу.',
+ 'Project Permissions' => 'Разрешения проекта',
+ 'Manager' => 'Менеджер',
+ 'Project Manager' => 'Менеджер проекта',
+ 'Project Member' => 'Участник проекта',
+ 'Project Viewer' => 'Наблюдатель проекта',
+ 'Your account is locked for %d minutes' => 'Ваш аккаунт заблокирован на %d минут',
+ 'Invalid captcha' => 'Неверный код подтверждения',
+ 'The name must be unique' => 'Имя должно быть уникальным',
+ 'View all groups' => 'Просмотр всех групп',
+ 'View group members' => 'Просмотр участников группы',
+ 'There is no user available.' => 'Нет доступных пользователей.',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'Вы действительно хотите удалить пользователя "%s" из группы "%s"?',
+ 'There is no group.' => 'Нет созданных групп.',
+ 'External Id' => 'Внешний Id',
+ 'Add group member' => 'Добавить участника в группу',
+ 'Do you really want to remove this group: "%s"?' => 'Вы действительно хотите удалить группу "%s"?',
+ 'There is no user in this group.' => 'В этой группе нет участников.',
+ 'Remove this user' => 'Удалить пользователя.',
+ 'Permissions' => 'Разрешения',
+ 'Allowed Users' => 'Разрешенные пользователи',
+ 'No user have been allowed specifically.' => 'Нет заданных разрешений для пользователей.',
+ 'Role' => 'Роль',
+ 'Enter user name...' => 'Введите имя пользователя...',
+ 'Allowed Groups' => 'Разрешенные группы',
+ 'No group have been allowed specifically.' => 'Нет заданных разрешений для групп.',
+ 'Group' => 'Группа',
+ 'Group Name' => 'Имя группы',
+ 'Enter group name...' => 'Введите имя группы...',
+ 'Role:' => 'Роль:',
+ 'Project members' => 'Участники проекта',
+ 'Compare hours for "%s"' => 'Сравнить часы для "%s"',
+ '%s mentioned you in the task #%d' => '%s упомянул вас в задаче #%d',
+ '%s mentioned you in a comment on the task #%d' => '%s упомянул вас в комментарии к задаче #%d',
+ 'You were mentioned in the task #%d' => 'Вы упомянуты в задаче #%d',
+ 'You were mentioned in a comment on the task #%d' => 'Вы упомянуты в комментарии к задаче #%d',
+ 'Mentioned' => 'Упоминания',
+ 'Compare Estimated Time vs Actual Time' => 'Сравнить запланированное время и реальное',
+ 'Estimated hours: ' => 'Запланировано часов: ',
+ 'Actual hours: ' => 'Реально затрачено часов: ',
+ 'Hours Spent' => 'Затрачено часов',
+ 'Hours Estimated' => 'Запланировано часов',
+ 'Estimated Time' => 'Запланировано времени',
+ 'Actual Time' => 'Затрачено времени',
+ 'Estimated vs actual time' => 'Запланировано и реально затрачено времени',
+ 'RUB - Russian Ruble' => 'Руб - Российский рубль',
+ 'Assign the task to the person who does the action when the column is changed' => 'Назначить задачу пользователю, который произвел изменение в колонке',
+ 'Close a task in a specific column' => 'Закрыть задачу в выбранной колонке',
+ 'Time-based One-time Password Algorithm' => 'Зависимый от времени, одноразовый алгоритм пароля',
+ 'Two-Factor Provider: ' => 'Провайдер двух-факторной авторизации: ',
+ 'Disable two-factor authentication' => 'Отключить двух-факторную авторизацию',
+ 'Enable two-factor authentication' => 'Включить двух-факторную авторизацию',
+ 'There is no integration registered at the moment.' => 'Интеграции в данный момент не зарегистрированы.',
+ 'Password Reset for Kanboard' => 'Сброс пароля для 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.' => 'Все задачи для колонки "%s" и дорожки "%s" были успешно закрыты.',
+ '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.' => '%d задач в колонке "%s" и дорожке "%s" будут закрыты.',
+ '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.' => 'Идентификатор проекта не обязателен и должен содержать буквенно-цифровые символы, пример: 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.' => 'Если Вы введете 0 для наименьшего и наивысшего приоритета, этот функционал будет отключен.',
+ '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' => 'Авто',
+ 'Related' => 'Связано',
+ 'Attachment' => 'Вложение',
+ 'Title not found' => 'Заголовок не найден',
+ 'Web Link' => 'Web-ссылка',
+ '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' => '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' => '%s часов',
+ 'Time spent:' => 'Времени потрачено:',
+ 'Created:' => 'Создана:',
+ 'Modified:' => 'Изменена:',
+ 'Completed:' => 'Завершена:',
+ 'Started:' => 'Начата:',
+ 'Moved:' => 'Перемещена:',
+ 'Task #%d' => 'Задача #%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"?' => 'Вы точно ходите удалить этот пользовательский фильтр: "%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.' => 'Максимально допустимый размер файла: %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"?' => 'Вы точно хотите выключить этого пользователя: "%s"?',
+ 'Enable user' => 'Включить пользователя',
+ 'Do you really want to enable this user: "%s"?' => 'Вы точно хотите включить этого пользователя: "%s"?',
+ 'Download' => 'Загрузка',
+ 'Uploaded: %s' => 'Загружено: %s',
+ 'Size: %s' => 'Размер: %s',
+ 'Uploaded by %s' => 'Загружено пользователем: %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)' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index 0e25033d..3825ecde 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Ukloni projekat',
'Edit the board for "%s"' => 'Izmeni tablu za "%s"',
'All projects' => 'Svi projekti',
- 'Change columns' => 'Zameni kolonu',
'Add a new column' => 'Dodaj novu kolonu',
'Title' => 'Naslov',
'Nobody assigned' => 'Niko nije dodeljen',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Otvori zadatak',
'Do you really want to open this task: "%s"?' => 'Da li zaista želiš da otvoriš zadatak: "%s"?',
'Back to the board' => 'Nazad na tablu',
- 'Created on %B %e, %Y at %k:%M %p' => 'Kreiran %e %B %Y o %k:%M',
'There is nobody assigned' => 'Niko nije dodeljen!',
'Column on the board:' => 'Kolona na tabli:',
- 'Status is open' => 'Status otvoren',
- 'Status is closed' => 'Status zatvoren',
'Close this task' => 'Zatvori ovaj zadatak',
'Open this task' => 'Otvori ovaj zadatak',
'There is no description.' => 'Bez opisa.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'ID je obavezan',
'The project id is required' => 'ID projekta je obavezan',
'The project name is required' => 'Naziv projekta je obavezan',
- 'This project must be unique' => 'Projekat mora biti jedinstven',
'The title is required' => 'Naslov je obavezan',
'Settings saved successfully.' => 'Podešavanja uspešno snimljena.',
'Unable to save your settings.' => 'Nemoguće snimanje podešavanja.',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'U radu',
'Done' => 'Gotovo',
'Application version:' => 'Verzija aplikacije:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Završeno u %e %B %Y o %k:%M',
- '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M',
- 'Date created' => 'Kreiran dana',
- 'Date completed' => 'Završen dana',
'Id' => 'Id',
'%d closed tasks' => '%d zatvorenih zadataka',
'No task for this project' => 'Nema dodeljenih zadataka ovom projektu',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Složenost',
'Task limit' => 'Ograničenje zadatka',
'Task count' => 'Broj zadataka',
- 'Edit project access list' => 'Izmeni prava pristupa projektu',
- 'Allow this user' => 'Dozvoli ovog korisnika',
- 'Don\'t forget that administrators have access to everything.' => 'Zapamti: Administrator može pristupiti svemu!',
- 'Revoke' => 'Povuci',
- 'List of authorized users' => 'Spisak odobrenih korisnika',
'User' => 'Korisnik',
- 'Nobody have access to this project.' => 'Niko nema pristup ovom projektu',
'Comments' => 'Komentari',
'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown',
'Leave a comment' => 'Ostavi komentar',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Izmeni ovaj zadatak',
'Due Date' => 'Termin',
'Invalid date' => 'Loš datum',
- 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y',
- '%B %e, %Y' => '%e %B %Y',
- // '%b %e, %Y' => '',
'Automatic actions' => 'Automatske akcije',
'Your automatic action have been created successfully.' => 'Uspešno kreirana automatska akcija',
'Unable to create your automatic action.' => 'Nemoguće kreiranje automatske akcije',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Dodeli boju korisniku',
'Column title' => 'Naslov kolone',
'Position' => 'Pozicija',
- 'Move Up' => 'Podigni',
- 'Move Down' => 'Spusti',
'Duplicate to another project' => 'Kopiraj u drugi projekat',
'Duplicate' => 'Napravi kopiju',
'link' => 'link',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Komentar je uspešno obrisan.',
'Unable to remove this comment.' => 'Neuspešno brisanje komentara.',
'Do you really want to remove this comment?' => 'Da li da obrišem ovaj komentar?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Samo administrator i kreator komentara mogu ga obrisati.',
'Current password for the user "%s"' => 'Trenutna lozinka za korisnika "%s"',
'The current password is required' => 'Trenutna lozinka je obavezna',
'Wrong password' => 'Pogrešna lozinka',
@@ -267,10 +246,6 @@ return array(
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'E-mail',
- 'Link my Google Account' => 'Poveži sa Google nalogom',
- 'Unlink my Google Account' => 'Ukini vezu sa Google nalogom',
- 'Login with my Google Account' => 'Prijavi se preko Google naloga',
- 'Project not found.' => 'Projekat nije pronađen.',
'Task removed successfully.' => 'Zadatak uspešno uklonjen.',
'Unable to remove this task.' => 'Nemoguće uklanjanje zadatka.',
'Remove a task' => 'Ukloni zadatak',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maksimalna veličina: ',
'Unable to upload the file.' => 'Nije moguće snimiti fajl.',
'Display another project' => 'Prikaži drugi projekat',
- 'Login with my Github Account' => 'Zaloguj przy użyciu konta Github',
- 'Link my Github Account' => 'Podłącz konto Github',
- 'Unlink my Github Account' => 'Odłącz konto Github',
'Created by %s' => 'Kreirao %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Poslednja izmena %e %B %Y o %k:%M',
'Tasks Export' => 'Izvoz zadataka',
'Tasks exportation for "%s"' => 'Izvoz zadataka za "%s"',
'Start Date' => 'Početni datum',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Želim obaveštenja samo za ovaj projekat:',
'view the task on Kanboard' => 'Pregledaj zadatke',
'Public access' => 'Javni pristup',
- 'User management' => 'Uređivanje korisnika',
'Active tasks' => 'Aktivni zadaci',
'Disable public access' => 'Zabrani javni pristup',
'Enable public access' => 'Dozvoli javni pristup',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'Email: ',
'Notifications:' => 'Obaveštenja: ',
'Notifications' => 'Obaveštenja',
- 'Group:' => 'Grupa:',
- 'Regular user' => 'Standardni korisnik',
'Account type:' => 'Vrsta naloga:',
'Edit profile' => 'Izmeni profil',
'Change password' => 'Izmeni lozinku',
'Password modification' => 'Izmena lozinke',
'External authentications' => 'Spoljne akcije',
- 'Google Account' => 'Google nalog',
- 'Github Account' => 'Github nalog',
'Never connected.' => 'Bez konekcija.',
- 'No account linked.' => 'Bez povezanih naloga.',
- 'Account linked.' => 'Nalog povezan.',
'No external authentication enabled.' => 'Bez omogućenih spoljnih autentikacija.',
'Password modified successfully.' => 'Uspešna izmena lozinke.',
'Unable to change the password.' => 'Nije moguće izmeniti lozinku.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s zamena dodele za zadatak %s na %s',
'New password for the user "%s"' => 'Nova lozinka za korisnika "%s"',
'Choose an event' => 'Izaberi događaj',
- // 'Github commit received' => '',
- // 'Github issue opened' => '',
- // 'Github issue closed' => '',
- // 'Github issue reopened' => '',
- // 'Github issue assignee change' => '',
- // 'Github issue label change' => '',
'Create a task from an external provider' => 'Kreiraj zadatak preko posrednika',
'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika',
'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrzenj etykiety',
// 'Reference' => '',
- // 'Reference: %s' => '',
'Label' => 'Etikieta',
'Database' => 'Baza',
'About' => 'Informacje',
@@ -474,7 +431,6 @@ return array(
// 'Frequency in second (60 seconds by default)' => '',
// 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
'Application URL' => 'Adres URL aplikacji',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Primer: http://example.kanboard.net/ (koristi se u obaveštenjima putem mail-a)',
'Token regenerated.' => 'Token wygenerowany ponownie.',
'Date format' => 'Format daty',
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek prihvatljiv, primer: "%s", "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Procenjeno vreme: %s godzin',
- 'Time spent: %s hours' => 'Utrošeno vreme: %s godzin',
- 'Started on %B %e, %Y' => 'Započeto dana %e %B %Y',
'Start date' => 'Datum početka',
'Time estimated' => 'Procenjeno vreme',
'There is nothing assigned to you.' => 'Ništa vam nije dodeljeno',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Svima je dozvoljen pristup.',
// 'Webhooks' => '',
// 'API' => '',
- // 'Github webhooks' => '',
- // 'Help on Github webhooks' => '',
// 'Create a comment from an external provider' => '',
- // 'Github issue comment created' => '',
'Project management' => 'Uređivanje projekata',
'My projects' => 'Moji projekti',
'Columns' => 'Kolone',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Zaduženja korisnika za "%s"',
'Clone this project' => 'Kopiraj projekat',
'Column removed successfully.' => 'Kolumna usunięta pomyslnie.',
- // 'Github Issue' => '',
'Not enough data to show the graph.' => 'Nedovoljno podataka za grafikon.',
'Previous' => 'Prethodni',
'The id must be an integer' => 'ID musi być liczbą całkowitą',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Osnovni razdelnik',
'Do you really want to remove this swimlane: "%s"?' => 'Da li da uklonim razdelnik: "%s"?',
'Inactive swimlanes' => 'Neaktivni razdelniki',
- 'Set project manager' => 'Podesi menadžera projekta',
- 'Set project member' => 'Podesi učesnika projekat',
'Remove a swimlane' => 'Ukloni razdelnik',
- 'Rename' => 'Preimenuj',
'Show default swimlane' => 'Prikaži osnovni razdelnik',
'Swimlane modification for the project "%s"' => 'Izmena razdelnika za projekat "%s"',
'Swimlane not found.' => 'Razdelnik nije pronađen.',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Razdelnici',
'Swimlane updated successfully.' => 'Razdelnik zaktualizowany pomyślnie.',
// 'The default swimlane have been updated successfully.' => '',
- // 'Unable to create your swimlane.' => '',
// 'Unable to remove this swimlane.' => '',
// 'Unable to update this swimlane.' => '',
'Your swimlane have been created successfully.' => 'Razdelnik je uspešno kreiran.',
'Example: "Bug, Feature Request, Improvement"' => 'Npr: "Greška, Zahtev za izmenama, Poboljšanje"',
'Default categories for new projects (Comma-separated)' => 'Osnovne kategorije za projekat',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
'Integrations' => 'Integracje',
'Integration with third-party services' => 'Integracja sa uslugama spoljnih servisa',
- 'Role for this project' => 'Uloga u ovom projektu',
- 'Project manager' => 'Manadžer projekta',
- 'Project member' => 'Učesnik projekta',
- // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '',
- // 'Gitlab Issue' => '',
'Subtask Id' => 'ID pod-zadania',
'Subtasks' => 'Pod-zadataka',
'Subtasks Export' => 'Eksport pod-zadań',
@@ -603,9 +538,6 @@ return array(
// 'You already have one subtask in progress' => '',
'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želite da kopirate',
// 'Disallow login form' => '',
- // 'Bitbucket commit received' => '',
- // 'Bitbucket webhooks' => '',
- // 'Help on Bitbucket webhooks' => '',
// 'Start' => '',
// 'End' => '',
// 'Task age in days' => '',
@@ -646,7 +578,6 @@ return array(
// 'This task' => '',
// '<1h' => '',
// '%dh' => '',
- // '%b %e' => '',
// 'Expand tasks' => '',
// 'Collapse tasks' => '',
// 'Expand/collapse tasks' => '',
@@ -656,14 +587,11 @@ return array(
// 'Keyboard shortcuts' => '',
// 'Open board switcher' => '',
// 'Application' => '',
- // 'since %B %e, %Y at %k:%M %p' => '',
// 'Compact view' => '',
// 'Horizontal scrolling' => '',
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
- // 'Files' => '',
- // 'Images' => '',
// 'Private project' => '',
// 'AUD - Australian Dollar' => '',
// 'CAD - Canadian Dollar' => '',
@@ -703,17 +631,12 @@ return array(
// 'The two factor authentication code is valid.' => '',
// 'Code' => '',
// 'Two factor authentication' => '',
- // 'Enable/disable two factor authentication' => '',
// 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
// 'Check my code' => '',
// 'Secret key: ' => '',
// 'Test your device' => '',
// 'Assign a color when the task is moved to a specific column' => '',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -722,7 +645,6 @@ return array(
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
@@ -731,7 +653,6 @@ return array(
// 'A task cannot be linked to itself' => '',
// 'The exact same link already exists' => '',
// 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
// 'Score' => '',
// 'The identifier must be unique' => '',
// 'This linked task id doesn\'t exists' => '',
@@ -777,21 +698,10 @@ return array(
// 'User that will receive the email' => '',
// 'Email subject' => '',
// 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
// 'Add a comment log when moving the task between columns' => '',
// 'Move the task to another column when the category is changed' => '',
// 'Send a task by email to someone' => '',
// 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
// 'Column change' => '',
// 'Position change' => '',
// 'Swimlane change' => '',
@@ -826,17 +736,11 @@ return array(
// 'The field "%s" have been updated' => '',
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
// 'I want to receive notifications for:' => '',
// 'All tasks' => '',
// 'Only for tasks assigned to me' => '',
// 'Only for tasks created by me' => '',
// 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
// 'Total for all columns' => '',
// 'You need at least 2 days of data to show the chart.' => '',
@@ -861,7 +765,6 @@ return array(
// 'Not assigned' => '',
// 'View advanced search syntax' => '',
// 'Overview' => '',
- // '%b %e %Y' => '',
// 'Board/Calendar/List view' => '',
// 'Switch to the board view' => '',
// 'Switch to the calendar view' => '',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -906,18 +805,12 @@ return array(
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
// 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
// 'Remote user' => '',
// 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
// 'New remote user' => '',
// 'New local user' => '',
// 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
// 'This feature does not work with all browsers.' => '',
// 'There is no destination project available.' => '',
// 'Trigger automatically subtask time tracking' => '',
@@ -932,7 +825,6 @@ return array(
// 'contributors' => '',
// 'License:' => '',
// 'License' => '',
- // 'Project Administrator' => '',
// 'Enter the text below' => '',
// 'Gantt chart for %s' => '',
// 'Sort by position' => '',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 8663d600..96a28b2c 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Ta bort projekt',
'Edit the board for "%s"' => 'Ändra tavlan för "%s"',
'All projects' => 'Alla projekt',
- 'Change columns' => 'Ändra kolumner',
'Add a new column' => 'Lägg till ny kolumn',
'Title' => 'Titel',
'Nobody assigned' => 'Ingen tilldelad',
@@ -102,11 +101,8 @@ return array(
'Open a task' => 'Öppna en uppgift',
'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?',
'Back to the board' => 'Tillbaka till tavlan',
- 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %Y-%m-%d kl %H:%M',
'There is nobody assigned' => 'Det finns ingen tilldelad',
'Column on the board:' => 'Kolumn på tavlan:',
- 'Status is open' => 'Statusen är öppen',
- 'Status is closed' => 'Statusen är stängd',
'Close this task' => 'Stäng uppgiften',
'Open this task' => 'Öppna uppgiften',
'There is no description.' => 'Det finns ingen beskrivning.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Aktuellt ID måste anges',
'The project id is required' => 'Projekt-ID måste anges',
'The project name is required' => 'Ett projektnamn måste anges',
- 'This project must be unique' => 'Detta projekt måste vara unikt',
'The title is required' => 'En titel måste anges.',
'Settings saved successfully.' => 'Inställningarna har sparats.',
'Unable to save your settings.' => 'Kunde inte spara dina ändringar',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => 'Pågående',
'Done' => 'Slutfört',
'Application version:' => 'Version:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %Y-%m-%d kl %H:%M',
- '%B %e, %Y at %k:%M %p' => '%Y-%m-%d kl %H:%M',
- 'Date created' => 'Skapat datum',
- 'Date completed' => 'Slutfört datum',
'Id' => 'ID',
'%d closed tasks' => '%d stängda uppgifter',
'No task for this project' => 'Inga uppgifter i detta projekt',
@@ -175,13 +166,7 @@ return array(
'Complexity' => 'Komplexitet',
'Task limit' => 'Uppgiftsbegränsning',
'Task count' => 'Antal uppgifter',
- 'Edit project access list' => 'Ändra projektåtkomst lista',
- 'Allow this user' => 'Tillåt användare',
- 'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.',
- 'Revoke' => 'Dra tillbaka behörighet',
- 'List of authorized users' => 'Lista med behöriga användare',
'User' => 'Användare',
- 'Nobody have access to this project.' => 'Ingen har tillgång till detta projekt.',
'Comments' => 'Kommentarer',
'Write your text in Markdown' => 'Exempelsyntax för text',
'Leave a comment' => 'Lämna en kommentar',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => 'Ändra denna uppgift',
'Due Date' => 'Måldatum',
'Invalid date' => 'Ej tillåtet datum',
- 'Must be done before %B %e, %Y' => 'Måste vara klart innan %Y-%m-%d',
- '%B %e, %Y' => '%Y-%m-%d',
- '%b %e, %Y' => '%Y-%m-%d',
'Automatic actions' => 'Automatiska åtgärder',
'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.',
'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => 'Tilldela en färg till en specifik användare',
'Column title' => 'Kolumnens titel',
'Position' => 'Position',
- 'Move Up' => 'Flytta upp',
- 'Move Down' => 'Flytta ned',
'Duplicate to another project' => 'Kopiera till ett annat projekt',
'Duplicate' => 'Kopiera uppgiften',
'link' => 'länk',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => 'Kommentaren har tagits bort.',
'Unable to remove this comment.' => 'Kunde inte ta bort denna kommentar.',
'Do you really want to remove this comment?' => 'Är du säker på att du vill ta bort denna kommentar?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Bara administratörer eller skaparen av kommentaren har tillgång till denna sida.',
'Current password for the user "%s"' => 'Nuvarande lösenord för användaren %s"',
'The current password is required' => 'Det nuvarande lösenordet måste anges',
'Wrong password' => 'Fel lösenord',
@@ -267,10 +246,6 @@ return array(
'External authentication failed' => 'Extern autentisering misslyckades',
'Your external account is linked to your profile successfully.' => 'Ditt externa konto länkades till din profil.',
'Email' => 'Epost',
- 'Link my Google Account' => 'Länka till mitt Google-konto',
- 'Unlink my Google Account' => 'Ta bort länken till mitt Google-konto',
- 'Login with my Google Account' => 'Logga in med mitt Google-konto',
- 'Project not found.' => 'Projektet kunde inte hittas',
'Task removed successfully.' => 'Uppgiften har tagits bort',
'Unable to remove this task.' => 'Kunde inte ta bort denna uppgift',
'Remove a task' => 'Ta bort en uppgift',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => 'Maxstorlek: ',
'Unable to upload the file.' => 'Kunde inte ladda upp filen.',
'Display another project' => 'Visa ett annat projekt',
- 'Login with my Github Account' => 'Logga in med mitt Github-konto',
- 'Link my Github Account' => 'Anslut mitt Github-konto',
- 'Unlink my Github Account' => 'Koppla ifrån mitt Github-konto',
'Created by %s' => 'Skapad av %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %Y-%m-%d kl %H:%M',
'Tasks Export' => 'Exportera uppgifter',
'Tasks exportation for "%s"' => 'Exportera uppgifter för "%s"',
'Start Date' => 'Startdatum',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => 'Jag vill endast få notiser för dessa projekt:',
'view the task on Kanboard' => 'Visa uppgiften på Kanboard',
'Public access' => 'Publik åtkomst',
- 'User management' => 'Hantera användare',
'Active tasks' => 'Aktiva uppgifter',
'Disable public access' => 'Inaktivera publik åtkomst',
'Enable public access' => 'Aktivera publik åtkomst',
@@ -397,18 +367,12 @@ return array(
'Email:' => 'E-post:',
'Notifications:' => 'Notiser:',
'Notifications' => 'Notiser',
- 'Group:' => 'Grupp:',
- 'Regular user' => 'Normal användare',
'Account type:' => 'Kontotyp:',
'Edit profile' => 'Ändra profil',
'Change password' => 'Byt lösenord',
'Password modification' => 'Ändra lösenord',
'External authentications' => 'Extern autentisering',
- 'Google Account' => 'Googlekonto',
- 'Github Account' => 'Githubkonto',
'Never connected.' => 'Inte ansluten.',
- 'No account linked.' => 'Inget konto länkat.',
- 'Account linked.' => 'Konto länkat.',
'No external authentication enabled.' => 'Ingen extern autentisering aktiverad.',
'Password modified successfully.' => 'Lösenordet har ändrats.',
'Unable to change the password.' => 'Kunde inte byta lösenord.',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s byt tilldelning av uppgiften %s till %s',
'New password for the user "%s"' => 'Nytt lösenord för användaren "%s"',
'Choose an event' => 'Välj en händelse',
- 'Github commit received' => 'Github-bidrag mottaget',
- 'Github issue opened' => 'Github-fråga öppnad',
- 'Github issue closed' => 'Github-fråga stängd',
- 'Github issue reopened' => 'Github-fråga öppnad på nytt',
- 'Github issue assignee change' => 'Github-fråga ny tilldelning',
- 'Github issue label change' => 'Github-fråga etikettförändring',
'Create a task from an external provider' => 'Skapa en uppgift från en extern leverantör',
'Change the assignee based on an external username' => 'Ändra tilldelning baserat på ett externt användarnamn',
'Change the category based on an external label' => 'Ändra kategori baserat på en extern etikett',
'Reference' => 'Referens',
- 'Reference: %s' => 'Referens: %s',
'Label' => 'Etikett',
'Database' => 'Databas',
'About' => 'Om',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => 'Frekvens i sekunder (60 sekunder är standard)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvens i sekunder (ange 0 för att inaktivera, 10 sekunder är standard)',
'Application URL' => 'Applikations-URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exempel: http://example.kanboard.net/ (används för e-postnotiser)',
'Token regenerated.' => 'Token nyskapad.',
'Date format' => 'Datumformat',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-format är alltid tillåtet, exempel: "%s" och "%s"',
@@ -482,9 +438,6 @@ return array(
'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',
- 'Estimated time: %s hours' => 'Uppskattad tid: %s timmar',
- 'Time spent: %s hours' => 'Nedlaggd tid: %s timmar',
- 'Started on %B %e, %Y' => 'Startad %Y-%m-%d',
'Start date' => 'Startdatum',
'Time estimated' => 'Uppskattad tid',
'There is nothing assigned to you.' => 'Du har inget tilldelat till dig.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Alla har tillgång till projektet',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github webhooks',
- 'Help on Github webhooks' => 'Hjälp för Github webhooks',
'Create a comment from an external provider' => 'Skapa en kommentar från en extern leverantör',
- 'Github issue comment created' => 'Github frågekommentar skapad',
'Project management' => 'Projekthantering',
'My projects' => 'Mina projekt',
'Columns' => 'Kolumner',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => 'Användardeltagande för "%s"',
'Clone this project' => 'Klona projektet',
'Column removed successfully.' => 'Kolumnen togs bort',
- 'Github Issue' => 'Github fråga',
'Not enough data to show the graph.' => 'Inte tillräckligt med data för att visa graf',
'Previous' => 'Föregående',
'The id must be an integer' => 'ID måste vara ett heltal',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Standard swimlane',
'Do you really want to remove this swimlane: "%s"?' => 'Vill du verkligen ta bort denna swimlane: "%s"?',
'Inactive swimlanes' => 'Inaktiv swimlane',
- 'Set project manager' => 'Sätt Projektadministratör',
- 'Set project member' => 'Sätt projektmedlem',
'Remove a swimlane' => 'Ta bort en swimlane',
- 'Rename' => 'Byt namn',
'Show default swimlane' => 'Visa standard swimlane',
'Swimlane modification for the project "%s"' => 'Ändra swimlane för projektet "%s"',
'Swimlane not found.' => 'Swimlane kunde inte hittas',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Swimlanes',
'Swimlane updated successfully.' => 'Swimlane uppdaterad',
'The default swimlane have been updated successfully.' => 'Standardswimlane har uppdaterats',
- 'Unable to create your swimlane.' => 'Kunde inte skapa din swimlane',
'Unable to remove this swimlane.' => 'Kunde inte ta bort swimlane',
'Unable to update this swimlane.' => 'Kunde inte uppdatera swimlane',
'Your swimlane have been created successfully.' => 'Din swimlane har skapats',
'Example: "Bug, Feature Request, Improvement"' => 'Exempel: "Bug, ny funktionalitet, förbättringar"',
'Default categories for new projects (Comma-separated)' => 'Standardkategorier för nya projekt (komma-separerade)',
- 'Gitlab commit received' => 'Gitlab bidrag mottaget',
- 'Gitlab issue opened' => 'Gitlab fråga öppnad',
- 'Gitlab issue closed' => 'Gitlab fråga stängd',
- 'Gitlab webhooks' => 'Gitlab webhooks',
- 'Help on Gitlab webhooks' => 'Hjälp för Gitlab webhooks',
'Integrations' => 'Integrationer',
'Integration with third-party services' => 'Integration med tjänst från tredjepart',
- 'Role for this project' => 'Roll för detta projekt',
- 'Project manager' => 'Projektadministratör',
- 'Project member' => 'Projektmedlem',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'En projektadministratör kan ändra inställningar för projektet och har mer rättigheter än en standardanvändare.',
- 'Gitlab Issue' => 'Gitlab fråga',
'Subtask Id' => 'Deluppgifts-ID',
'Subtasks' => 'Deluppgift',
'Subtasks Export' => 'Export av deluppgifter',
@@ -603,9 +538,6 @@ return array(
'You already have one subtask in progress' => 'Du har redan en deluppgift igång',
'Which parts of the project do you want to duplicate?' => 'Vilka delar av projektet vill du duplicera?',
// 'Disallow login form' => '',
- 'Bitbucket commit received' => 'Bitbucket bidrag mottaget',
- 'Bitbucket webhooks' => 'Bitbucket webhooks',
- 'Help on Bitbucket webhooks' => 'Hjälp för Bitbucket webhooks',
'Start' => 'Start',
'End' => 'Slut',
'Task age in days' => 'Uppgiftsålder i dagar',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Denna uppgift',
'<1h' => '<1h',
'%dh' => '%dh',
- '%b %e' => '%b %e',
'Expand tasks' => 'Expandera uppgifter',
'Collapse tasks' => 'Minimera uppgifter',
'Expand/collapse tasks' => 'Expandera/minimera uppgifter',
@@ -656,14 +587,11 @@ return array(
'Keyboard shortcuts' => 'Tangentbordsgenvägar',
'Open board switcher' => 'Växling av öppen tavla',
'Application' => 'Applikation',
- 'since %B %e, %Y at %k:%M %p' => 'sedan %B %e, %Y at %k:%M %p',
'Compact view' => 'Kompakt vy',
'Horizontal scrolling' => 'Horisontell scroll',
'Compact/wide view' => 'Kompakt/bred vy',
'No results match:' => 'Inga matchande resultat',
'Currency' => 'Valuta',
- 'Files' => 'Filer',
- 'Images' => 'Bilder',
'Private project' => 'Privat projekt',
'AUD - Australian Dollar' => 'AUD - Australiska dollar',
'CAD - Canadian Dollar' => 'CAD - Kanadensiska dollar',
@@ -703,17 +631,12 @@ return array(
'The two factor authentication code is valid.' => 'Tvåfaktorsverifieringskoden är giltig.',
'Code' => 'Kod',
'Two factor authentication' => 'Tvåfaktorsverifiering',
- 'Enable/disable two factor authentication' => 'Aktivera/avaktivera tvåfaktorsverifiering',
'This QR code contains the key URI: ' => 'Denna QR-kod innehåller nyckel-URI:n',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Spara säkerhetsnyckeln i din TOTP mjukvara (med exempelvis Google Authenticator eller FreeOTP).',
'Check my code' => 'Kolla min kod',
'Secret key: ' => 'Säkerhetsnyckel:',
'Test your device' => 'Testa din enhet',
'Assign a color when the task is moved to a specific column' => 'Tilldela en färg när uppgiften flyttas till en specifik kolumn',
'%s via Kanboard' => '%s via Kanboard',
- 'uploaded by: %s' => 'uppladdad av: %s',
- 'uploaded on: %s' => 'uppladdad på: %s',
- 'size: %s' => 'storlek: %s',
'Burndown chart for "%s"' => 'Burndown diagram för "%s"',
'Burndown chart' => 'Burndown diagram',
'This chart show the task complexity over the time (Work Remaining).' => 'Diagrammet visar uppgiftens svårighet över tid (återstående arbete).',
@@ -722,7 +645,6 @@ return array(
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Ta en skärmdump och tryck CTRL+V för att klistra in här.',
'Screenshot uploaded successfully.' => 'Skärmdumpen laddades upp.',
'SEK - Swedish Krona' => 'SEK - Svensk Krona',
- 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Projektidentifieraren är en valbar alfanumerisk kod som används för att identifiera ditt projekt.',
'Identifier' => 'Identifierare',
'Disable two factor authentication' => 'Inaktivera två-faktors autentisering',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Vill du verkligen inaktivera två-faktors autentisering för denna användare: "%s"?',
@@ -731,7 +653,6 @@ return array(
'A task cannot be linked to itself' => 'En uppgift kan inte länkas till sig själv',
'The exact same link already exists' => 'Länken existerar redan',
'Recurrent task is scheduled to be generated' => 'Återkommande uppgift är schemalagd att genereras',
- 'Recurring information' => 'Återkommande information',
'Score' => 'Poäng',
'The identifier must be unique' => 'Identifieraren måste vara unik',
'This linked task id doesn\'t exists' => 'Denna länkade uppgifts id existerar inte',
@@ -777,21 +698,10 @@ return array(
'User that will receive the email' => 'Användare som kommer att ta emot mailet',
'Email subject' => 'E-post ämne',
'Date' => 'Datum',
- 'By @%s on Bitbucket' => 'Av @%s på Bitbucket',
- 'Bitbucket Issue' => 'Bitbucket fråga',
- 'Commit made by @%s on Bitbucket' => 'Bidrag gjort av @%s på Bitbucket',
- 'Commit made by @%s on Github' => 'Bidrag gjort av @%s på Github',
- 'By @%s on Github' => 'Av @%s på Github',
- 'Commit made by @%s on Gitlab' => 'Bidrag gjort av @%s på Gitlab',
'Add a comment log when moving the task between columns' => 'Lägg till en kommentarslogg när en uppgift flyttas mellan kolumner',
'Move the task to another column when the category is changed' => 'Flyttas uppgiften till en annan kolumn när kategorin ändras',
'Send a task by email to someone' => 'Skicka en uppgift med e-post till någon',
'Reopen a task' => 'Återöppna en uppgift',
- 'Bitbucket issue opened' => 'Bitbucketfråga öppnad',
- 'Bitbucket issue closed' => 'Bitbucketfråga stängd',
- 'Bitbucket issue reopened' => 'Bitbucketfråga återöppnad',
- 'Bitbucket issue assignee change' => 'Bitbucketfråga tilldelningsändring',
- 'Bitbucket issue comment created' => 'Bitbucketfråga kommentar skapad',
'Column change' => 'Kolumnändring',
'Position change' => 'Positionsändring',
'Swimlane change' => 'Swimlaneändring',
@@ -826,17 +736,11 @@ return array(
'The field "%s" have been updated' => 'Fältet "%s" har uppdaterats',
'The description have been modified' => 'Beskrivningen har modifierats',
'Do you really want to close the task "%s" as well as all subtasks?' => 'Vill du verkligen stänga uppgiften "%s" och alla deluppgifter?',
- 'Swimlane: %s' => 'Swimlane: %s',
'I want to receive notifications for:' => 'Jag vill få notiser för:',
'All tasks' => 'Alla uppgifter',
'Only for tasks assigned to me' => 'Bara för uppgifter tilldelade mig',
'Only for tasks created by me' => 'Bara för uppgifter skapade av mig',
'Only for tasks created by me and assigned to me' => 'Bara för uppgifter skapade av mig och tilldelade till mig',
- '%A' => '%A',
- '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p',
- 'New due date: %B %e, %Y' => 'Nytt förfallodatum: %B %e, %Y',
- 'Start date changed: %B %e, %Y' => 'Startdatum ändrat: %B %e, %Y',
- '%k:%M %p' => '%k:%M %p',
'%%Y-%%m-%%d' => '%%Y-%%m-%%d',
'Total for all columns' => 'Totalt för alla kolumner',
'You need at least 2 days of data to show the chart.' => 'Du behöver minst två dagars data för att visa diagrammet.',
@@ -861,7 +765,6 @@ return array(
'Not assigned' => 'Inte tilldelad',
'View advanced search syntax' => 'Visa avancerad söksyntax',
'Overview' => 'Översikts',
- '%b %e %Y' => '%b %e %Y',
'Board/Calendar/List view' => 'Tavla/Kalender/Listvy',
'Switch to the board view' => 'Växla till tavelvy',
'Switch to the calendar view' => 'Växla till kalendervy',
@@ -894,10 +797,6 @@ return array(
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Diagramet visar medel av led och cykeltid för de senaste %d uppgifterna över tiden.',
'Average time into each column' => 'Medeltidsåtgång i varje kolumn',
'Lead and cycle time' => 'Led och cykeltid',
- 'Google Authentication' => 'Google autentisering',
- 'Help on Google authentication' => 'Hjälp för Google autentisering',
- 'Github Authentication' => 'Github autentisering',
- 'Help on Github authentication' => 'Hjälp för Github autentisering',
'Lead time: ' => 'Ledtid',
'Cycle time: ' => 'Cykeltid',
'Time spent into each column' => 'Tidsåtgång per kolumn',
@@ -906,18 +805,12 @@ return array(
'If the task is not closed the current time is used instead of the completion date.' => 'Om uppgiften inte är stängd används nuvarande tid istället för slutförandedatum.',
'Set automatically the start date' => 'Sätt startdatum automatiskt',
'Edit Authentication' => 'Ändra autentisering',
- 'Google Id' => 'Google Id',
- 'Github Id' => 'Github Id',
'Remote user' => 'Extern användare',
'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Externa användares lösenord lagras inte i Kanboard-databasen, exempel: LDAP, Google och Github-konton.',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Om du aktiverar boxen "Tillåt inte loginformulär" kommer inloggningsuppgifter i formuläret att ignoreras.',
- 'By @%s on Gitlab' => 'Av @%s på Gitlab',
- 'Gitlab issue comment created' => 'Gitlab frågekommentar skapad',
'New remote user' => 'Ny extern användare',
'New local user' => 'Ny lokal användare',
'Default task color' => 'Standardfärg för uppgifter',
- 'Hide sidebar' => 'Göm sidokolumn',
- 'Expand sidebar' => 'Expandera sidokolumn',
'This feature does not work with all browsers.' => 'Denna funktion fungerar inte i alla webbläsare.',
'There is no destination project available.' => 'Det finns inget destinationsprojekt tillgängligt.',
'Trigger automatically subtask time tracking' => 'Aktivera automatisk tidsbevakning av deluppgifter',
@@ -932,7 +825,6 @@ return array(
'contributors' => 'bidragare:',
'License:' => 'Licens:',
'License' => 'Licens',
- 'Project Administrator' => 'Projektadministratör',
'Enter the text below' => 'Fyll i texten nedan',
'Gantt chart for %s' => 'Gantt-schema för %s',
'Sort by position' => 'Sortera efter position',
@@ -952,11 +844,9 @@ return array(
// 'open file' => '',
// 'End date' => '',
// 'Users overview' => '',
- // 'Managers' => '',
// 'Members' => '',
// 'Shared project' => '',
// 'Project managers' => '',
- // 'Project members' => '',
// 'Gantt chart for all projects' => '',
// 'Projects list' => '',
// 'Gantt chart for this project' => '',
@@ -964,26 +854,16 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
// 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
// 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
// 'Documentation: %s' => '',
// 'Switch to the Gantt chart view' => '',
// 'Reset the search/filter box' => '',
// 'Documentation' => '',
// 'Table of contents' => '',
// 'Gantt' => '',
- // 'Help with project permissions' => '',
// 'Author' => '',
// 'Version' => '',
// 'Plugins' => '',
@@ -1046,7 +926,6 @@ return array(
// 'Append/Replace' => '',
// 'Append' => '',
// 'Replace' => '',
- // 'There is no notification method registered.' => '',
// 'Import' => '',
// 'change sorting' => '',
// 'Tasks Importation' => '',
@@ -1064,4 +943,209 @@ return array(
// 'Duplicates are not imported' => '',
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
+ // '%s attached a new file to the task %s' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ // 'Manager' => '',
+ // 'Project Manager' => '',
+ // 'Project Member' => '',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ // 'View all groups' => '',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ // 'Add group member' => '',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ // 'Permissions' => '',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ // 'Role' => '',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ // 'Role:' => '',
+ // 'Project members' => '',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ // 'You were mentioned in a comment on the task #%d' => '',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ // 'Close all tasks of this column' => '',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 50181c79..f26ee30d 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -20,15 +20,15 @@ return array(
'Red' => 'สีแดง',
'Orange' => 'สีส้ม',
'Grey' => 'สีเทา',
- // 'Brown' => '',
- // 'Deep Orange' => '',
- // 'Dark Grey' => '',
- // 'Pink' => '',
- // 'Teal' => '',
- // 'Cyan' => '',
- // 'Lime' => '',
- // 'Light Green' => '',
- // 'Amber' => '',
+ 'Brown' => 'สีน้ำตาล',
+ 'Deep Orange' => 'สีส้มเข้ม',
+ 'Dark Grey' => 'สีเทาเข้ม',
+ 'Pink' => 'สีชมพู',
+ 'Teal' => 'สีเขียวหัวเป็ด',
+ 'Cyan' => 'สีฟ้า',
+ 'Lime' => 'สีมะนาว',
+ 'Light Green' => 'สีเขียวสว่าง',
+ 'Amber' => 'สีเหลืองอำพัน',
'Save' => 'บันทึก',
'Login' => 'เข้าสู่ระบบ',
'Official website:' => 'เวบไซต์อย่างเป็นทางการ:',
@@ -65,8 +65,8 @@ return array(
'%d tasks in total' => '%d งานทั้งหมด',
'Unable to update this board.' => 'ไม่สามารถปรับปรุงบอร์ดได้.',
'Edit board' => 'แก้ไขบอร์ด',
- 'Disable' => 'ปิด',
- 'Enable' => 'เปิด',
+ 'Disable' => 'ปิดการทำงาน',
+ 'Enable' => 'เปิดการทำงาน',
'New project' => 'โปรเจคใหม่',
'Do you really want to remove this project: "%s"?' => 'คุณต้องการเอาโปรเจค « %s » ออกใช่หรือไม่?',
'Remove project' => 'ลบโปรเจค',
@@ -102,11 +102,8 @@ return array(
'Open a task' => 'เปิดงาน',
'Do you really want to open this task: "%s"?' => 'คุณต้องการเปิดงาน: « %s » ใช่หรือไม่?',
'Back to the board' => 'กลับไปที่บอร์ด',
- 'Created on %B %e, %Y at %k:%M %p' => 'สร้างวันที่ %d/%m/%Y เวลา %H:%M',
'There is nobody assigned' => 'ไม่มีใครถูกกำหนด',
'Column on the board:' => 'คอลัมน์บนบอร์ด:',
- 'Status is open' => 'สถานะเปิด',
- 'Status is closed' => 'สถานะปิด',
'Close this task' => 'ปิดงานนี้',
'Open this task' => 'เปิดงานนี้',
'There is no description.' => 'ไม่มีคำอธิบาย',
@@ -124,7 +121,6 @@ return array(
'The id is required' => 'ต้องการไอดี',
'The project id is required' => 'ต้องการไอดีโปรเจค',
'The project name is required' => 'ต้องการชื่อโปรเจค',
- 'This project must be unique' => 'ชื่อโปรเจคต้องไม่ซ้ำ',
'The title is required' => 'ต้องการหัวเรื่อง',
'Settings saved successfully.' => 'บันทึกการตั้งค่าเรียบร้อยแล้ว',
'Unable to save your settings.' => 'ไม่สามารถบันทึกการตั้งค่าได้',
@@ -159,10 +155,6 @@ return array(
'Work in progress' => 'กำลังทำ',
'Done' => 'เสร็จ',
'Application version:' => 'แอพเวอร์ชัน:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'เรียบร้อยวันที่ %d/%m/%Y เวลา %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d/%m/%Y เวลา %H:%M',
- 'Date created' => 'สร้างวันที่',
- 'Date completed' => 'เรียบร้อยวันที่',
'Id' => 'ไอดี',
'%d closed tasks' => '%d งานที่ปิด',
'No task for this project' => 'ไม่มีงานสำหรับโปรเจคนี้',
@@ -175,13 +167,7 @@ return array(
'Complexity' => 'ความซับซ้อน',
'Task limit' => 'จำกัดงาน',
'Task count' => 'นับงาน',
- 'Edit project access list' => 'แก้ไขการเข้าถึงรายชื่อโปรเจค',
- 'Allow this user' => 'อนุญาตผู้ใช้นี้',
- 'Don\'t forget that administrators have access to everything.' => 'อย่าลืมผู้ดูแลระบบสามารถเข้าถึงได้ทุกอย่าง',
- 'Revoke' => 'ยกเลิก',
- 'List of authorized users' => 'รายชื่อผู้ใช้ที่ได้รับการยืนยัน',
'User' => 'ผู้ใช้',
- // 'Nobody have access to this project.' => '',
'Comments' => 'ความคิดเห็น',
'Write your text in Markdown' => 'เขียนข้อความในรูปแบบ Markdown',
'Leave a comment' => 'ออกความคิดเห็น',
@@ -192,9 +178,6 @@ return array(
'Edit this task' => 'แก้ไขงาน',
'Due Date' => 'วันที่ครบกำหนด',
'Invalid date' => 'วันที่ผิด',
- 'Must be done before %B %e, %Y' => 'ต้องทำให้เสร็จก่อน %d/%m/%Y',
- '%B %e, %Y' => '%d/%m/%Y',
- // '%b %e, %Y' => '',
'Automatic actions' => 'การกระทำอัตโนมัติ',
'Your automatic action have been created successfully.' => 'การกระทำอัตโนมัติสร้างเรียบร้อยแล้ว',
'Unable to create your automatic action.' => 'ไม่สามารถสร้างการกระทำอัตโนมัติได้',
@@ -236,7 +219,6 @@ return array(
'Comment removed successfully.' => 'ลบความคิดเห็นเรียบร้อยแล้ว',
'Unable to remove this comment.' => 'ไม่สามารถลบความคิดเห็นได้',
'Do you really want to remove this comment?' => 'คุณต้องการลบความคิดเห็น',
- 'Only administrators or the creator of the comment can access to this page.' => 'เฉพาะผู้ดูแลระบบหรือผู้สร้างความคิดเห็นเข้าถึงหน้านี้',
'Current password for the user "%s"' => 'รหัสผ่านปัจจุบันของผู้ใช้ « %s »',
'The current password is required' => 'ต้องการรหัสผ่านปัจจุบัน',
'Wrong password' => 'รหัสผ่านผิด',
@@ -262,39 +244,35 @@ return array(
'%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.' => '',
+ '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' => 'อีเมล',
- 'Link my Google Account' => 'เชื่อมต่อกับกูเกิลแอคเคาท์',
- 'Unlink my Google Account' => 'ไม่เชื่อมต่อกับกูเกิลแอคเคาท์',
- 'Login with my Google Account' => 'เข้าใช้ด้วยกูเกิลแอคเคาท์',
- 'Project not found.' => 'หาโปรเจคไม่พบ',
'Task removed successfully.' => 'ลบงานเรียบร้อยแล้ว',
'Unable to remove this task.' => 'ไม่สามารถลบงานนี้',
'Remove a task' => 'ลบงาาน',
'Do you really want to remove this task: "%s"?' => 'คุณต้องการลบงาน "%s" ออกใช่หรือไม่?',
- 'Assign automatically a color based on a category' => 'กำหนดสีอัตโนมัติขึ้นอยู่กับกลุ่ม',
- 'Assign automatically a category based on a color' => 'กำหนดกลุ่มอัตโนมัติขึ้นอยู่กับสี',
+ '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"' => 'แก้ไขกลุ่มสำหรับโปรเจค "%s"',
- 'Category Name' => 'ชื่อกลุ่ม',
- 'Add a new category' => 'เพิ่มกลุ่มใหม่',
- 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบกลุ่ม "%s" ใช่หรือไม่?',
- 'All categories' => 'กลุ่มทั้งหมด',
- 'No category' => 'ไม่มีกลุ่ม',
+ '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"' => 'แก้ไขหมวดสำหรับโปรเจค "%s"',
+ 'Category Name' => 'ชื่อหมวด',
+ 'Add a new category' => 'เพิ่มหมวดใหม่',
+ 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบหมวด "%s" ใช่หรือไม่?',
+ 'All categories' => 'หมวดทั้งหมด',
+ 'No category' => 'ไม่มีหมวด',
'The name is required' => 'ต้องการชื่อ',
'Remove a file' => 'ลบไฟล์',
'Unable to remove this file.' => 'ไม่สามารถลบไฟล์ได้',
@@ -317,7 +295,7 @@ return array(
'estimated' => 'ประมาณ',
'Sub-Tasks' => 'งานย่อย',
'Add a sub-task' => 'เพิ่มงานย่อย',
- // 'Original estimate' => '',
+ 'Original estimate' => 'ประมาณการเดิม',
'Create another sub-task' => 'สร้างงานย่อยอื่น',
'Time spent' => 'ใช้เวลา',
'Edit a sub-task' => 'แก้ไขงานย่อย',
@@ -334,11 +312,7 @@ return array(
'Maximum size: ' => 'ขนาดสูงสุด:',
'Unable to upload the file.' => 'ไม่สามารถอัพโหลดไฟล์ได้',
'Display another project' => 'แสดงโปรเจคอื่น',
- 'Login with my Github Account' => 'เข้าใช้ด้วยกิทฮับแอคเคาท์',
- 'Link my Github Account' => 'เชื่อมกับกิทฮับแอคเคาท์',
- 'Unlink my Github Account' => 'ยกเลิกการเชื่อมกับกิทอับแอคเคาท์',
'Created by %s' => 'สร้างโดย %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'แก้ไขล่าสุดวันที่ %B %e, %Y เวลา %k:%M %p',
'Tasks Export' => 'ส่งออกงาน',
'Tasks exportation for "%s"' => 'ส่งออกงานสำหรับ "%s"',
'Start Date' => 'เริ่มวันที่',
@@ -374,7 +348,6 @@ return array(
'I want to receive notifications only for those projects:' => 'ฉันต้องการรับการแจ้งเตือนสำหรับโปรเจค:',
'view the task on Kanboard' => 'แสดงงานบน Kanboard',
'Public access' => 'การเข้าถึงสาธารณะ',
- 'User management' => 'การจัดการผู้ใช้',
'Active tasks' => 'งานที่กำลังใช้งาน',
'Disable public access' => 'ปิดการเข้าถึงสาธารณะ',
'Enable public access' => 'เปิดการเข้าถึงสาธารณะ',
@@ -397,23 +370,17 @@ return array(
'Email:' => 'อีเมล:',
'Notifications:' => 'แจ้งเตือน:',
'Notifications' => 'การแจ้งเตือน',
- 'Group:' => 'กลุ่ม:',
- 'Regular user' => 'ผู้ใช้ปกติ:',
- 'Account type:' => 'ชนิดบัญชี:',
+ 'Account type:' => 'ประเภทบัญชี:',
'Edit profile' => 'แก้ไขประวัติ',
'Change password' => 'เปลี่ยนรหัสผ่าน',
'Password modification' => 'แก้ไขรหัสผ่าน',
'External authentications' => 'การยืนยันภายนอก',
- 'Google Account' => 'กูเกิลแอคเคาท์',
- 'Github Account' => 'กิทฮับแอคเคาท์',
'Never connected.' => 'ไม่เชื่อมต่อ',
- 'No account linked.' => 'แอคเคาท์ไม่มีการเชื่อม',
- 'Account linked.' => 'แอคเคาท์เชื่อมต่อแล้ว',
'No external authentication enabled.' => 'ไม่เปิดการใช้งานการยืนยันภายนอก',
'Password modified successfully.' => 'แก้ไขรหัสผ่านเรียบร้อยแล้ว',
'Unable to change the password.' => 'ไม่สามารถเปลี่ยนรหัสผ่านได้',
- 'Change category for the task "%s"' => 'เปลี่ยนกลุ่มสำหรับงาน "%s"',
- 'Change category' => 'เปลี่ยนกลุ่ม',
+ 'Change category for the task "%s"' => 'เปลี่ยนหมวดสำหรับงาน "%s"',
+ 'Change category' => 'เปลี่ยนหมวด',
'%s updated the task %s' => '%s ปรับปรุงงานแล้ว %s',
'%s opened the task %s' => '%s เปิดงานแล้ว %s',
'%s moved the task %s to the position #%d in the column "%s"' => '%s ย้ายงานแล้ว %s ไปตำแหน่ง #%d ในคอลัมน์ "%s"',
@@ -442,21 +409,14 @@ return array(
'Default values are "%s"' => 'ค่าเริ่มต้น "%s"',
'Default columns for new projects (Comma-separated)' => 'คอลัมน์เริ่มต้นสำหรับโปรเจคใหม่ (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 change the assignee of the task #%d to %s' => '%s เปลี่ยนผู้รับผิดชอบของงาน #%d เป็น %s',
+ '%s changed the assignee of the task %s to %s' => '%s เปลี่ยนผู้รับผิดชอบของงาน %s เป็น %s',
'New password for the user "%s"' => 'รหัสผ่านใหม่สำหรับผู้ใช้ "%s"',
'Choose an event' => 'เลือกเหตุการณ์',
- // 'Github commit received' => '',
- // 'Github issue opened' => '',
- // 'Github issue closed' => '',
- // 'Github issue reopened' => '',
- // 'Github issue assignee change' => '',
- // 'Github issue label change' => '',
- // 'Create a task from an external provider' => '',
- // 'Change the assignee based on an external username' => '',
- // 'Change the category based on an external label' => '',
- // 'Reference' => '',
- // 'Reference: %s' => '',
+ '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' => 'เกี่ยวกับ',
@@ -474,17 +434,13 @@ return array(
'Frequency in second (60 seconds by default)' => 'ความถี่ (ค่าเริ่มต้นทุก 60 วินาที) ',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'ความถี่ (0 ไม่ใช้คุณลักษณะนี้, ค่าเริ่มต้นทุก 10 วินาที)',
// 'Application URL' => '',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'ตัวอย่าง: http://example.kanboard.net/ (ถูกใช้ในการแจ้งเตือนทางอีเมล์)',
// 'Token regenerated.' => '',
'Date format' => 'รูปแบบวันที่',
- // 'ISO format is always accepted, example: "%s" and "%s"' => '',
+ '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' => '',
+ 'Type here to create a new sub-task' => 'พิมพ์ที่นี้เพื่อสร้างงานย่อยใหม่',
'Add' => 'เพิ่ม',
- 'Estimated time: %s hours' => 'เวลาเฉลี่ย: %s ชั่วโมง',
- 'Time spent: %s hours' => 'ใช้เวลาไป %s ชม.',
- 'Started on %B %e, %Y' => 'เริ่ม %B %e, %Y',
'Start date' => 'เริ่มวันที่',
'Time estimated' => 'เวลาโดยประมาณ',
'There is nothing assigned to you.' => 'ไม่มีอะไรกำหนดให้คุณ',
@@ -496,10 +452,7 @@ return array(
'Everybody have access to this project.' => 'ทุกคนสามารถเข้าถึงโปรเจคนี้',
// 'Webhooks' => '',
// 'API' => '',
- // 'Github webhooks' => '',
- // 'Help on Github webhooks' => '',
- // 'Create a comment from an external provider' => '',
- // 'Github issue comment created' => '',
+ 'Create a comment from an external provider' => 'สร้างความคิดเห็นจากบริการภายนอก',
'Project management' => 'การจัดการโปรเจค',
'My projects' => 'โปรเจคของฉัน',
'Columns' => 'คอลัมน์',
@@ -517,7 +470,6 @@ return array(
'User repartition for "%s"' => 'การแบ่งงานของผู้ใช้ "%s"',
'Clone this project' => 'เลียนแบบโปรเจคนี้',
'Column removed successfully.' => 'ลบคอลัมน์สำเร็จ',
- // 'Github Issue' => '',
'Not enough data to show the graph.' => 'ไม่มีข้อมูลแสดงเป็นกราฟ',
'Previous' => 'ก่อนหน้า',
'The id must be an integer' => 'ไอดีต้องเป็นตัวเลขจำนวนเต็ม',
@@ -547,8 +499,6 @@ return array(
'Default swimlane' => 'สวิมเลนเริ่มต้น',
'Do you really want to remove this swimlane: "%s"?' => 'คุณต้องการลบสวิมเลนนี้ : "%s"?',
'Inactive swimlanes' => 'สวิมเลนไม่ทำงาน',
- 'Set project manager' => 'กำหนดผู้จัดการโปรเจค',
- 'Set project member' => 'กำหนดสมาชิกโปรเจค',
'Remove a swimlane' => 'ลบสวิมเลน',
'Rename' => 'เปลี่ยนชื่อ',
'Show default swimlane' => 'แสดงสวิมเลนเริ่มต้น',
@@ -563,19 +513,9 @@ return array(
'Unable to update this swimlane.' => 'ไม่สามารถปรับปรุงสวิมเลนนี้',
'Your swimlane have been created successfully.' => 'สวิมเลนของคุณถูกสร้างเรียบร้อยแล้ว',
'Example: "Bug, Feature Request, Improvement"' => 'ตัวอย่าง: "Bug, Feature Request, Improvement"',
- 'Default categories for new projects (Comma-separated)' => 'ค่าเริ่มต้นกลุ่มสำหรับโปรเจคใหม่ (Comma-separated)',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
+ 'Default categories for new projects (Comma-separated)' => 'ค่าเริ่มต้นหมวดสำหรับโปรเจคใหม่ (Comma-separated)',
'Integrations' => 'การใช้ร่วมกัน',
'Integration with third-party services' => 'การใช้งานร่วมกับบริการ third-party',
- // 'Role for this project' => '',
- 'Project manager' => 'ผู้จัดการโปรเจค',
- 'Project member' => 'สมาชิกโปรเจค',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'ผู้จัดการโปรเจคสามารถตั้งค่าของโปรเจคและมีสิทธิ์มากกว่าผู้ใช้ทั่วไป',
- // 'Gitlab Issue' => '',
'Subtask Id' => 'รหัสงานย่อย',
'Subtasks' => 'งานย่อย',
'Subtasks Export' => 'ส่งออก งานย่อย',
@@ -588,7 +528,7 @@ return array(
'All columns' => 'คอลัมน์ทั้งหมด',
'Calendar' => 'ปฏิทิน',
'Next' => 'ต่อไป',
- // '#%d' => '',
+ '#%d' => '#%d',
'All swimlanes' => 'สวิมเลนทั้งหมด',
'All colors' => 'สีทั้งหมด',
'Moved to column %s' => 'เคลื่อนไปคอลัมน์ %s',
@@ -601,20 +541,17 @@ return array(
'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' => '',
- // 'Bitbucket commit received' => '',
- // 'Bitbucket webhooks' => '',
- // 'Help on Bitbucket webhooks' => '',
+ 'Which parts of the project do you want to duplicate?' => 'เลือกส่วนของโปรเจคที่คุณต้องการทำซ้ำ?',
+ 'Disallow login form' => 'ไม่อนุญาตให้ใช้แบบฟอร์มการเข้าสู่ระบบ',
'Start' => 'เริ่ม',
'End' => 'จบ',
'Task age in days' => 'อายุงาน',
'Days in this column' => 'วันในคอลัมน์นี้',
- // '%dd' => '',
+ '%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?' => '',
+ 'Do you really want to remove this link with task #%d?' => 'คุณต้องการลบลิงค์นี้ของงาน #%d?',
'Field required' => 'ต้องใส่',
'Link added successfully.' => 'เพิ่มลิงค์เรียบร้อยแล้ว',
'Link updated successfully.' => 'ปรับปรุงลิงค์เรียบร้อยแล้ว',
@@ -625,7 +562,7 @@ return array(
'Link settings' => 'ตั้งค่าลิงค์',
'Opposite label' => 'ป้ายชื่อตรงข้าม',
'Remove a link' => 'ลบลิงค์',
- 'Task\'s links' => 'ลิงค',
+ 'Task\'s links' => 'ลิงค์',
'The labels must be different' => 'ป้ายชื่อต้องต่างกัน',
'There is no link.' => 'ไม่มีลิงค์',
'This label must be unique' => 'ป้ายชื่อต้องไม่ซ้ำกัน',
@@ -644,9 +581,8 @@ return array(
'fixes' => 'เจาะจง',
'is fixed by' => 'ถูกเจาะจงด้วย',
'This task' => 'งานนี้',
- // '<1h' => '',
- // '%dh' => '',
- // '%b %e' => '',
+ '<1h' => '<1 ชม.',
+ '%dh' => '%d ชม.',
'Expand tasks' => 'ขยายงาน',
'Collapse tasks' => 'ย่องาน',
'Expand/collapse tasks' => 'ขยาย/ย่อ งาน',
@@ -656,7 +592,6 @@ return array(
'Keyboard shortcuts' => 'คีย์ลัด',
'Open board switcher' => 'เปิดการสลับบอร์ด',
'Application' => 'แอพพลิเคชัน',
- 'since %B %e, %Y at %k:%M %p' => 'เริ่ม %B %e, %Y เวลา %k:%M %p',
'Compact view' => 'มุมมองพอดี',
'Horizontal scrolling' => 'เลื่อนตามแนวนอน',
'Compact/wide view' => 'พอดี/กว้าง มุมมอง',
@@ -665,18 +600,18 @@ return array(
'Files' => 'ไฟล์',
'Images' => 'รูปภาพ',
'Private project' => 'โปรเจคส่วนตัว',
- // 'AUD - Australian Dollar' => '',
- // 'CAD - Canadian Dollar' => '',
- // 'CHF - Swiss Francs' => '',
- // 'Custom Stylesheet' => '',
+ 'AUD - Australian Dollar' => 'AUD - ดอลลาร์ออสเตรเลีย',
+ 'CAD - Canadian Dollar' => 'CAD - ดอลลาร์แคนาดา',
+ 'CHF - Swiss Francs' => 'CHF - ฟรังก์สวิส',
+ 'Custom Stylesheet' => 'สไตล์ที่กำหนดเอง',
'download' => 'ดาวน์โหลด',
- // 'EUR - Euro' => '',
- // 'GBP - British Pound' => '',
- // 'INR - Indian Rupee' => '',
- // 'JPY - Japanese Yen' => '',
- // 'NZD - New Zealand Dollar' => '',
- // 'RSD - Serbian dinar' => '',
- // 'USD - US Dollar' => '',
+ 'EUR - Euro' => 'EUR - ยูโร',
+ 'GBP - British Pound' => 'GBP - ปอนด์อังกฤษ',
+ 'INR - Indian Rupee' => 'INR - รูปี',
+ 'JPY - Japanese Yen' => 'JPY - เยน',
+ 'NZD - New Zealand Dollar' => 'NZD - ดอลลาร์นิวซีแลนด์',
+ 'RSD - Serbian dinar' => 'RSD - ดีนาร์เซอร์เบีย',
+ 'USD - US Dollar' => 'USD - ดอลลาร์สหรัฐ',
'Destination column' => 'คอลัมน์เป้าหมาย',
'Move the task to another column when assigned to a user' => 'ย้ายงานไปคอลัมน์อื่นเมื่อกำหนดบุคคลรับผิดชอบ',
'Move the task to another column when assignee is cleared' => 'ย้ายงานไปคอลัมน์อื่นเมื่อไม่กำหนดบุคคลรับผิดชอบ',
@@ -686,16 +621,16 @@ return array(
'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' => 'อัตราแลกเปลี่ยน',
+ '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.' => '',
+ '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' => '',
- // '%s remove the assignee of the task %s' => '',
+ '%s remove the assignee of the task %s' => '%s เอาผู้รับผิดชอบออกจากงาน %s',
'Enable Gravatar images' => 'สามารถใช้งานภาพ Gravatar',
'Information' => 'ข้อมูลสารสนเทศ',
// 'Check two factor authentication code' => '',
@@ -703,56 +638,52 @@ return array(
// 'The two factor authentication code is valid.' => '',
'Code' => 'รหัส',
// 'Two factor authentication' => '',
- 'Enable/disable two factor authentication' => 'เปิด/ปิด การยืนยันตัวตนสองชั้น',
// 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
'Check my code' => 'ตรวจสอบรหัสของฉัน',
'Secret key: ' => 'กุญแจลับ',
'Test your device' => 'ทดสอบอุปกรณ์ของคุณ',
'Assign a color when the task is moved to a specific column' => 'กำหนดสีเมื่องานถูกย้ายไปคอลัมน์ที่กำหนดไว้',
// '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
+ 'uploaded by: %s' => 'อัพโหลดโดย: %s',
+ 'uploaded on: %s' => 'อัพโหลดบน: %s',
'size: %s' => 'ขนาด: %s',
'Burndown chart for "%s"' => 'แผนภูมิงานกับเวลา "%s"',
'Burndown chart' => 'แผนภูมิงานกับเวลา',
- // 'This chart show the task complexity over the time (Work Remaining).' => '',
- // 'Screenshot taken %s' => '',
+ 'This chart show the task complexity over the time (Work Remaining).' => 'แผนภูมิแสดงความซับซ้อนของงานตามเวลา (งานที่เหลือ)',
+ 'Screenshot taken %s' => 'จับภาพหน้าจอ %s',
'Add a screenshot' => 'เพิ่ม screenshot',
- // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'จับภาพหน้าจอ (screenshot) และกด CTRL+V หรือ ⌘+V เพื่อวางที่นี้',
'Screenshot uploaded successfully.' => 'อัพโหลด screenshot เรียบร้อยแล้ว',
- // 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
- // 'Identifier' => '',
+ 'SEK - Swedish Krona' => 'SEK - โครนสวีเดน',
+ 'Identifier' => 'ตัวบ่งชี้',
// 'Disable two factor authentication' => '',
- // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'คุณต้องการยกเลิก the two factor authentication สำหรับผู้ใช้นีั: "%s"?',
'Edit link' => 'แก้ไขลิงค์',
'Start to type task title...' => 'พิมพ์ชื่องาน',
'A task cannot be linked to itself' => 'งานไม่สามารถลิงค์ตัวเอง',
// 'The exact same link already exists' => '',
'Recurrent task is scheduled to be generated' => 'งานแบบวนลูปถูกสร้างตามที่กำหนดไว้',
- 'Recurring information' => 'รายละเอียดการวนลูป',
'Score' => 'คะแนน',
- // 'The identifier must be unique' => '',
+ '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: ' => '',
+ '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: ' => '',
+ '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: ' => '',
+ 'Timeframe to calculate new due date: ' => 'ระยะเวลาการคำนวณวันครบกำหนดใหม่: ',
'Trigger to generate recurrent task: ' => 'จะสร้างงานแบบวนลูป',
'When task is closed' => 'เมื่อปิดงาน',
'When task is moved from first column' => 'เมื่องานถูกย้ายจากคอลัมน์แรก',
@@ -774,294 +705,416 @@ return array(
// 'Two factor authentication enabled' => '',
'Unable to update this user.' => 'ไม่สามารถปรับปรุงผู้ใช้นี้',
'There is no user management for private projects.' => 'ไม่มีการจัดการผู้ใช้สำหรับโปรเจคส่วนตัว',
- // 'User that will receive the email' => '',
- // 'Email subject' => '',
- // 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
- // 'Add a comment log when moving the task between columns' => '',
- // 'Move the task to another column when the category is changed' => '',
- // 'Send a task by email to someone' => '',
- // 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
- // 'Column change' => '',
- // 'Position change' => '',
- // 'Swimlane change' => '',
- // 'Assignee change' => '',
- // '[%s] Overdue tasks' => '',
- // 'Notification' => '',
- // '%s moved the task #%d to the first swimlane' => '',
- // '%s moved the task #%d to the swimlane "%s"' => '',
- // 'Swimlane' => '',
- // 'Gravatar' => '',
- // '%s moved the task %s to the first swimlane' => '',
- // '%s moved the task %s to the swimlane "%s"' => '',
- // 'This report contains all subtasks information for the given date range.' => '',
- // 'This report contains all tasks information for the given date range.' => '',
- // 'Project activities for %s' => '',
- // 'view the board on Kanboard' => '',
- // 'The task have been moved to the first swimlane' => '',
- // 'The task have been moved to another swimlane:' => '',
- // 'Overdue tasks for the project "%s"' => '',
- // 'New title: %s' => '',
- // 'The task is not assigned anymore' => '',
- // 'New assignee: %s' => '',
- // 'There is no category now' => '',
- // 'New category: %s' => '',
- // 'New color: %s' => '',
- // 'New complexity: %d' => '',
- // 'The due date have been removed' => '',
- // 'There is no description anymore' => '',
- // 'Recurrence settings have been modified' => '',
- // 'Time spent changed: %sh' => '',
- // 'Time estimated changed: %sh' => '',
- // 'The field "%s" have been updated' => '',
- // 'The description have been modified' => '',
- // 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
- // 'I want to receive notifications for:' => '',
- // 'All tasks' => '',
- // 'Only for tasks assigned to me' => '',
- // 'Only for tasks created by me' => '',
- // 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
+ '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 ย้ายงาน #%d ไปสวินเลนแรก',
+ '%s moved the task #%d to the swimlane "%s"' => '%s ย้ายงาน #%d ไปสวินเลน "%s"',
+ 'Swimlane' => 'สวิมเลน',
+ 'Gravatar' => 'รูปแทนตัว',
+ '%s moved the task %s to the first swimlane' => '%s ย้ายงาน %s ไปสวินเลนแรก',
+ '%s moved the task %s to the swimlane "%s"' => '%s ย้ายงาน %s ไปสวินเลนไปสวินเลน "%s"',
+ 'This report contains all subtasks information for the given date range.' => 'รายงานนี้มีข้อมูลงานย่อยทั้งหมดในช่วงวันที่กำหนด',
+ 'This report contains all tasks information for the given date range.' => 'รายงานนี้มีข้อมูลงานทั้งหมดสำหรับช่วงวันที่ที่กำหนด',
+ 'Project activities for %s' => 'กิจกรรมโปรเจคสำหรับ %s',
+ 'view the board on Kanboard' => 'แสดงบอร์ดบนคังบอร์ด',
+ 'The task have been moved to the first swimlane' => 'งานถูกย้านไปสวิมเลนแรก',
+ 'The task have been moved to another swimlane:' => 'งานถูกย้านไปสวิมเลนอื่น:',
+ 'Overdue tasks for the project "%s"' => 'งานที่เกินกำหนดสำหรับโปรเจค "%s"',
+ 'New title: %s' => 'ชื่อเรื่องใหม่: %s',
+ 'The task is not assigned anymore' => 'ไม่กำหนดผู้รับผิดชอบ',
+ 'New assignee: %s' => 'ผู้รับผิดชอบใหม่: %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' => 'ฟิลด์ "%s" ถูกปรับปรุง',
+ 'The description have 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' => '',
- // '<30m' => '',
- // 'Stop timer' => '',
- // 'Start timer' => '',
- // 'Add project member' => '',
- // 'Enable notifications' => '',
- // 'My activity stream' => '',
- // 'My calendar' => '',
- // 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
- // 'Reset filters' => '',
- // 'My tasks due tomorrow' => '',
- // 'Tasks due today' => '',
- // 'Tasks due tomorrow' => '',
- // 'Tasks due yesterday' => '',
- // 'Closed tasks' => '',
- // 'Open tasks' => '',
- // 'Not assigned' => '',
- // 'View advanced search syntax' => '',
- // 'Overview' => '',
- // '%b %e %Y' => '',
- // 'Board/Calendar/List view' => '',
- // 'Switch to the board view' => '',
- // 'Switch to the calendar view' => '',
- // 'Switch to the list view' => '',
- // 'Go to the search/filter box' => '',
- // 'There is no activity yet.' => '',
- // 'No tasks found.' => '',
- // 'Keyboard shortcut: "%s"' => '',
- // 'List' => '',
- // 'Filter' => '',
- // 'Advanced search' => '',
- // 'Example of query: ' => '',
- // 'Search by project: ' => '',
- // 'Search by column: ' => '',
- // 'Search by assignee: ' => '',
- // 'Search by color: ' => '',
- // 'Search by category: ' => '',
- // 'Search by description: ' => '',
- // 'Search by due date: ' => '',
- // 'Lead and Cycle time for "%s"' => '',
- // 'Average time spent into each column for "%s"' => '',
- // 'Average time spent into each column' => '',
- // 'Average time spent' => '',
- // 'This chart show the average time spent into each column for the last %d tasks.' => '',
- // 'Average Lead and Cycle time' => '',
- // 'Average lead time: ' => '',
- // 'Average cycle time: ' => '',
- // 'Cycle Time' => '',
- // 'Lead Time' => '',
- // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
- // 'Average time into each column' => '',
- // 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
- // 'Lead time: ' => '',
- // 'Cycle time: ' => '',
- // 'Time spent into each column' => '',
- // 'The lead time is the duration between the task creation and the completion.' => '',
- // 'The cycle time is the duration between the start date and the completion.' => '',
+ 'Total for all columns' => 'จำนวนคอลัมน์ทั้งหมด',
+ 'You need at least 2 days of data to show the chart.' => 'คุณต้องการอย่างน้อย 2 วันในการแสดงแผนภูมิ',
+ '<15m' => '<15นาที',
+ '<30m' => '<30นาที',
+ 'Stop timer' => 'หยุดจับเวลา',
+ 'Start timer' => 'เริ่มจับเวลา',
+ 'Add project member' => 'เพิ่มสมาชิกโปรเจค',
+ 'Enable notifications' => 'เปิดการแจ้งเตือน',
+ 'My activity stream' => 'กิจกรรมที่เกิดขึ้นของฉัน',
+ 'My calendar' => 'ปฎิทินของฉัน',
+ 'Search tasks' => 'ค้นหางาน',
+ 'Back to the calendar' => 'กลับไปที่ปฎิทิน',
+ 'Filters' => 'ตัวกรอง',
+ 'Reset filters' => 'ล้างตัวกรอง',
+ 'My tasks due tomorrow' => 'งานถึงกำหนดของฉันวันพรุ่งนี้',
+ 'Tasks due today' => 'งานถึงกำหนดวันนี้',
+ 'Tasks due tomorrow' => 'งานถึงกำหนดพรุ่งนี้',
+ 'Tasks due yesterday' => 'งานถึงกำหนดเมื่อวาน',
+ 'Closed tasks' => 'งานปิด',
+ 'Open tasks' => 'งานเปิด',
+ 'Not assigned' => 'ไม่กำหนดใคร',
+ 'View advanced search syntax' => 'แสดงรูปแบบการค้นหาขั้นสูง',
+ 'Overview' => 'ภาพรวม',
+ '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"' => 'คีย์ลัด: %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"' => 'เวลานำและรอบเวลาสำหรับ "%s"',
+ 'Average time spent into each column for "%s"' => 'ค่าเฉลี่ยเวลาที่ใช้แต่ละคอลัมน์สำหรับ "%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.' => 'แผนภูมิแสดงค่าเฉลี่ยเวลาที่ใช้แต่ละคอลัมน์สำหรับ %d งานล่าสุด',
+ '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.' => 'แผนภูมิแสดงค่าเฉลี่ยเวลานำและรอบเวลาสำหรับ %d งานล่าสุดเมื่อเวลาผ่านไป',
+ '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' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
- // 'Remote user' => '',
+ '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.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
- // 'New remote user' => '',
- // 'New local user' => '',
- // 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
- // 'This feature does not work with all browsers.' => '',
+ 'New remote user' => 'เพิ่มผู้ใช้รีโมทใหม่',
+ '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' => '',
+ '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' => '',
- // 'Project Administrator' => '',
- // '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' => '',
- // 'Managers' => '',
- // 'Members' => '',
- // 'Shared project' => '',
- // 'Project managers' => '',
- // 'Project members' => '',
- // 'Gantt chart for all projects' => '',
- // 'Projects list' => '',
- // 'Gantt chart for this project' => '',
- // 'Project board' => '',
- // 'End date:' => '',
- // 'There is no start date or end date for this project.' => '',
- // 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
- // 'Link type' => '',
- // 'Change task color when using a specific task link' => '',
- // 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
- // 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
- // 'Documentation: %s' => '',
- // 'Switch to the Gantt chart view' => '',
- // 'Reset the search/filter box' => '',
- // 'Documentation' => '',
- // 'Table of contents' => '',
- // 'Gantt' => '',
- // 'Help with project permissions' => '',
- // 'Author' => '',
- // 'Version' => '',
- // 'Plugins' => '',
- // 'There is no plugin loaded.' => '',
- // 'Set maximum column height' => '',
- // 'Remove maximum column height' => '',
- // 'My notifications' => '',
- // 'Custom filters' => '',
- // 'Your custom filter have been created successfully.' => '',
- // 'Unable to create your custom filter.' => '',
- // 'Custom filter removed successfully.' => '',
- // 'Unable to remove this custom filter.' => '',
- // 'Edit custom filter' => '',
- // 'Your custom filter have been updated successfully.' => '',
- // 'Unable to update custom filter.' => '',
- // 'Web' => '',
- // 'New attachment on task #%d: %s' => '',
- // 'New comment on task #%d' => '',
- // 'Comment updated on task #%d' => '',
- // 'New subtask on task #%d' => '',
- // 'Subtask updated on task #%d' => '',
- // 'New task #%d: %s' => '',
- // 'Task updated #%d' => '',
- // 'Task #%d closed' => '',
- // 'Task #%d opened' => '',
- // 'Column changed for task #%d' => '',
- // 'New position for task #%d' => '',
- // 'Swimlane changed for task #%d' => '',
- // 'Assignee changed on task #%d' => '',
- // '%d overdue tasks' => '',
- // 'Task #%d is overdue' => '',
- // 'No new notifications.' => '',
- // 'Mark all as read' => '',
- // 'Mark as read' => '',
+ 'Current swimlane: %s' => 'สวิมเลนปัจจุบัน: %s',
+ 'Current column: %s' => 'คอลัมน์ปัจจุบัน: %s',
+ 'Current category: %s' => 'หมวดปัจจุบัน: %s',
+ 'no category' => 'ไม่มีหมวด',
+ 'Current assignee: %s' => 'ผู้รับผิดชอบปัจจุบัน: %s',
+ 'not assigned' => 'ไม่กำหนด',
+ 'Author:' => 'ผู้แต่ง:',
+ 'contributors' => 'ผู้ให้กำเนิด',
+ 'License:' => 'สัญญาอนุญาต:',
+ 'License' => 'สัญญาอนุญาต',
+ 'Enter the text below' => 'พิมพ์ข้อความด้านล่าง',
+ 'Gantt chart for %s' => 'แผนภูมิแกรนท์สำหรับ %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' => 'NOK - โครนนอร์เวย์',
+ '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' => 'แผนภูมิแกรน์ของโปรเจค',
+ 'Link type' => 'ประเภทลิงค์',
+ 'Change task color when using a specific task link' => 'เปลี่ยนสีงานเมื่อมีการใช้การเชื่อมโยงงาน',
+ 'Task link creation or modification' => 'การสร้างการเชื่อมโยงงานหรือการปรับเปลี่ยน',
+ 'Milestone' => 'ขั้น',
+ 'Documentation: %s' => 'เอกสาร: %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' => 'แนบใหม่ของงาน #%d: %s',
+ 'New comment on task #%d' => 'ความคิดเห็นใหม่ของงาน #%d',
+ 'Comment updated on task #%d' => 'แก้ไขความคิดเห็นของงาน #%d',
+ 'New subtask on task #%d' => 'งานย่อยใหม่ของงาน #%d',
+ 'Subtask updated on task #%d' => 'แก้ไขงานย่อยของงาน #%d',
+ 'New task #%d: %s' => 'งานใหม่ #%d: %s',
+ 'Task updated #%d' => 'แก้ไขงาน #%d',
+ 'Task #%d closed' => 'ปิดงาน #%d',
+ 'Task #%d opened' => 'เปิดงาน #%d',
+ 'Column changed for task #%d' => 'เปลี่ยนคอลัมน์สำหรับงาน #%d',
+ 'New position for task #%d' => 'ตำแหน่งใหม่ของงาน #%d',
+ 'Swimlane changed for task #%d' => 'เปลี่ยนสวิมเลนสำหรับงาน #%d',
+ 'Assignee changed on task #%d' => 'เปลี่ยนผู้รับผิดชอบงาน #%d',
+ '%d overdue tasks' => '%d งานเกินกำหนด',
+ 'Task #%d is overdue' => 'งาน #%d เกินกำหนด',
+ 'No new notifications.' => 'ไม่มีการแจ้งเตือนใหม่',
+ 'Mark all as read' => 'มาร์คทั้งหมดว่าอ่านแล้ว',
+ 'Mark as read' => 'มาร์คว่าอ่านแล้ว',
// 'Total number of tasks in this column across all swimlanes' => '',
- // 'Collapse swimlane' => '',
- // 'Expand swimlane' => '',
- // 'Add a new filter' => '',
- // 'Share with all project members' => '',
- // 'Shared' => '',
- // 'Owner' => '',
- // 'Unread notifications' => '',
- // 'My filters' => '',
- // 'Notification methods:' => '',
- // 'Import tasks from CSV file' => '',
- // '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!' => '',
+ 'Collapse swimlane' => 'ย่อสวิมเลน',
+ 'Expand swimlane' => 'ขยายสวิมเลน',
+ 'Add a new filter' => 'เพิ่มตัวกรองใหม่',
+ 'Share with all project members' => 'แชร์ให้สมาชิกทุกคนของโปรเจค',
+ 'Shared' => 'แชร์',
+ 'Owner' => 'เจ้าของ',
+ 'Unread notifications' => 'การแจ้งเตือนยังไม่ได้อ่าน',
+ 'My filters' => 'ตัวกรองของฉัน',
+ 'Notification methods:' => 'ลักษณะการแจ้งเตือน:',
+ 'Import tasks from CSV file' => 'นำเข้างานจากไฟล์ CSV',
+ 'Unable to read your file' => 'ไม่สามารถอ่านไฟล์ของคุณ',
+ '%d task(s) have been imported successfully.' => '%d งานนำเข้าเรียบร้อย',
+ 'Nothing have been imported!' => 'ไม่มีอะไรถูกนำเข้า',
+ 'Import users from CSV file' => 'นำเข้าผู้ใช้จากไฟล์ CSV',
+ '%d user(s) have been imported successfully.' => '%d ผู้ใช้นำเข้าเรียบร้อย',
+ 'Comma' => ', - Comma',
+ 'Semi-colon' => '; - Semi-colon',
+ 'Tab' => 'Tab - Tab',
+ 'Vertical bar' => '| - Vertical bar',
+ 'Double Quote' => '" " - Double Quote',
+ 'Single Quote' => '\' \' - Single Quote',
+ '%s attached a file to the task #%d' => '%s แนบไฟล์ในงาน #%d',
+ 'There is no column or swimlane activated in your project!' => 'ไม่มีคอลัมน์หรือสวิมเลนเปิดใช้งานในโปรเจคของคุณ!',
// 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
- // 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
- // 'Usernames must be lowercase and unique' => '',
+ 'Append/Replace' => 'เพิ่มเติม/แทนที่',
+ 'Append' => 'เพิ่มเติม',
+ 'Replace' => 'แทนที่',
+ 'Import' => 'นำเข้า',
+ 'change sorting' => 'เปลี่ยนการเรียง',
+ 'Tasks Importation' => 'การนำเข้างาน',
+ 'Delimiter' => 'คั่น',
+ 'Enclosure' => 'กำหนดข้อความ',
+ 'CSV File' => 'ไฟล์ CSV',
+ 'Instructions' => 'คำสั่ง',
+ 'Your file must use the predefined CSV format' => 'ไฟล์ของคุณจะต้องใช้รูปแบบ CSV ที่กำหนดไว้ล่วงหน้า',
+ 'Your file must be encoded in UTF-8' => 'ไฟล์ของคุณต้องเอนโค้ดด้วย UTF-8',
+ 'The first row must be the header' => 'แถวแรกต้องเป็นหัวข้อ',
+ 'Duplicates are not verified for you' => 'รายการที่ซ้ำกันจะไม่ได้รับการตรวจสอบสำหรับคุณ',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'วันที่ต้องอยู่ในรูปแบบ ISO: YYYY-MM-DD',
+ 'Download CSV template' => 'ดาวน์โหลด CSV ต้นฉบับ',
+ 'No external integration registered.' => 'ไม่มีการรวมภายนอกถูกลงทะเบียนไว้',
+ 'Duplicates are not imported' => 'ซ้ำกันไม่สามารถนำเข้าได้',
+ 'Usernames must be lowercase and unique' => 'ชื่อผู้ใช้ต้องเป็นตัวพิมพ์เล็กและไม่ซ้ำ',
// 'Passwords will be encrypted if present' => '',
+ '%s attached a new file to the task %s' => '%s แนบไฟล์ใหม่ในงาน %s',
+ 'Assign automatically a category based on a link' => 'กำหนดหมวดอัตโนมัติตามลิงค์',
+ // 'BAM - Konvertible Mark' => '',
+ 'Assignee Username' => 'กำหนดชื่อผู้ใช้',
+ 'Assignee Name' => 'กำหนดชื่อ',
+ 'Groups' => 'กลุ่ม',
+ 'Members of %s' => 'สมาชิกของ %s',
+ 'New group' => 'กลุ่มใหม่',
+ 'Group created successfully.' => 'สร้างกลุ่มสำเร็จ',
+ 'Unable to create your group.' => 'ไม่สามารถสร้างกลุ่มของคุณ',
+ 'Edit group' => 'แก้ไขกลุ่ม',
+ 'Group updated successfully.' => 'แก้ไขกลุ่มเรียบร้อย',
+ 'Unable to update your group.' => 'ไม่สามารถแก้ไขกลุ่มของคุณ',
+ 'Add group member to "%s"' => 'เพิ่มสมาชิกในกลุ่ม %s',
+ 'Group member added successfully.' => 'เพิ่มสมาชิกกลุ่มเรียบร้อย',
+ 'Unable to add group member.' => 'ไม่สามารถเพิ่มสมาชิกกลุ่ม',
+ 'Remove user from group "%s"' => 'เอาผู้ใช้ออกจากกลุ่ม %s',
+ 'User removed successfully from this group.' => 'เอาผู้ใช้ออกจากกลุ่มนี้เรียบร้อย',
+ 'Unable to remove this user from the group.' => 'ไม่สามารถลบผู้ใช้ออกจากกลุ่มนี้',
+ 'Remove group' => 'ลบกลุ่ม',
+ 'Group removed successfully.' => 'ลบกลุ่มเรียบร้อย',
+ 'Unable to remove this group.' => 'ไม่สามารถลบกลุ่มนี้',
+ 'Project Permissions' => 'การอนุญาตใช้งานโปรเจค',
+ 'Manager' => 'ผู้จัดการ',
+ 'Project Manager' => 'ผู้จัดการโปรเจค',
+ 'Project Member' => 'สมาชิกโปรเจค',
+ 'Project Viewer' => 'ผู้ดูโปรเจค',
+ 'Your account is locked for %d minutes' => 'บัญชีของคุณถูกล็อก %d นาที',
+ 'Invalid captcha' => 'captcha ไม่ถูกต้อง',
+ 'The name must be unique' => 'ชื่อต้องไม่ซ้ำ',
+ 'View all groups' => 'แสดงกลุ่มทั้งหมด',
+ 'View group members' => 'แสดงสมาชิกกลุ่ม',
+ // 'There is no user available.' => '',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => 'คุณต้องการลบผู้ใช้ "%s" ออกจากกลุ่ม "%s"?',
+ 'There is no group.' => 'ไม่มีกลุ่ม',
+ 'External Id' => 'ไอดีภายนอก',
+ 'Add group member' => 'เพิ่มสมาชิกกลุ่ม',
+ 'Do you really want to remove this group: "%s"?' => 'คุณต้องการลบกลุ่มนี้: "%s"?',
+ 'There is no user in this group.' => 'ไม่มีผู้ใช้ในกลุ่มนี้',
+ 'Remove this user' => 'เอาผู้ใช้คนนี้ออก',
+ 'Permissions' => 'การอนุญาตใช้งาน',
+ 'Allowed Users' => 'การอนุญาตผู้ใช้',
+ 'No user have been allowed specifically.' => 'ไม่มีผู้ใช้ได้รับอนุญาติเป็นพิเศษ',
+ 'Role' => 'บทบาท',
+ 'Enter user name...' => 'พิมพ์ชื่อผู้ใช้...',
+ 'Allowed Groups' => 'อนุญาตกลุ่ม',
+ 'No group have been allowed specifically.' => 'ไม่มีกลุ่มได้รับอนุญาติเป็นพิเศษ',
+ 'Group' => 'กลุ่ม',
+ 'Group Name' => 'ชื่อกลุ่ม',
+ 'Enter group name...' => 'พิมพ์ชื่อกลุ่ม...',
+ 'Role:' => 'บทบาท:',
+ 'Project members' => 'สมาชิกโปรเจค',
+ 'Compare hours for "%s"' => 'เปรียบเทียบรายชั่วโมงสำหรับ %s',
+ '%s mentioned you in the task #%d' => '%s กล่าวถึงคุณในงาน #%d',
+ '%s mentioned you in a comment on the task #%d' => '%s กล่าวถึงคุณในความคิดเห็นของงาน #%d',
+ 'You were mentioned in the task #%d' => 'คุณได้รับการกล่าวถึงในงาน #%d',
+ 'You were mentioned in a comment on the task #%d' => 'คุณได้รับการกล่าวถึงในความคิดเห็นของงาน #%d',
+ 'Mentioned' => 'กล่าวถึง',
+ 'Compare Estimated Time vs Actual Time' => 'เปรียบเทียบเวลาโดยประมาณกับเวลาที่เกิดขึ้นจริง',
+ 'Estimated hours: ' => 'เวลาโดยประมาณ:',
+ 'Actual hours: ' => 'เวลาที่เกิดขึ้นจริง:',
+ 'Hours Spent' => 'เวลาที่ใช้',
+ 'Hours Estimated' => 'เวลาโดยประมาณ',
+ 'Estimated Time' => 'เวลาโดยประมาณ',
+ 'Actual Time' => 'เวลาที่ใช้',
+ 'Estimated vs actual time' => 'เวลาโดยประมาณกับเวลาจริง',
+ 'RUB - Russian Ruble' => 'RUB - รูเบิลรัสเซีย',
+ 'Assign the task to the person who does the action when the column is changed' => 'กำหนดผู้รับผิดชอบงานเมื่อเปลี่ยนคอลัมน์',
+ 'Close a task in a specific column' => 'ปิดงานในคอลัมน์ที่เฉพาะเจาะจง',
+ // 'Time-based One-time Password Algorithm' => '',
+ // 'Two-Factor Provider: ' => '',
+ // 'Disable two-factor authentication' => '',
+ // 'Enable two-factor authentication' => '',
+ // 'There is no integration registered at the moment.' => '',
+ 'Password Reset for Kanboard' => 'รีเซตรหัสผ่านสำหรับคังบอร์ด',
+ 'Forgot password?' => 'ลืมรหัสผ่าน?',
+ 'Enable "Forget Password"' => 'เปิดการใช้งาน "ลืมรหัสผ่าน"',
+ 'Password Reset' => 'รีเซตรหัสผ่าน',
+ 'New password' => 'รหัสผ่านใหม่',
+ 'Change Password' => 'เปลี่ยนรหัสผ่าน',
+ 'To reset your password click on this link:' => 'ในการรีเซตรหัสผ่านของคุณคลิ๊กที่ลิงค์นี้:',
+ 'Last Password Reset' => 'รีเซตรหัสผ่านครั้งล่าสุด',
+ 'The password has never been reinitialized.' => 'รหัสผ่านไม่เคยเริ่มใหม่อีกครั้ง',
+ 'Creation' => 'สร้าง',
+ 'Expiration' => 'สิ้นสุด',
+ 'Password reset history' => 'ประวัติการรีเซตรหัสผ่าน',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'ทุกงานของคอลัมน์ "%s" และสวิมเลน "%s" ถูกปิดเรียบร้อย',
+ '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.' => '%d งานในคอลัมน์ "%s" และสวิมเลน "%s" จะปิด',
+ '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.' => 'ตัวบ่งชี้โปรโจคเป็นตัวเลือกเสริมและต้องเป็นตัวอักษรหรือตัวเลข ตัวอย่าง: 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' => 'ส่งอีเมลเมื่อไม่มีกิจกรรมเกิดขึ้นในงาน',
+ 'List of external links' => 'รายการเชื่อมโยงภายนอก',
+ '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' => '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' => '%s ชั่วโมง',
+ 'Time spent:' => 'ใช้เวลา:',
+ 'Created:' => 'สร้าง:',
+ 'Modified:' => 'แก้ไข:',
+ 'Completed:' => 'เสร็จสิ้น:',
+ 'Started:' => 'เริ่ม:',
+ 'Moved:' => 'ย้าย:',
+ 'Task #%d' => 'งานที่ #%d',
+ 'Sub-tasks' => 'งานย่อย',
+ 'Date and time format' => 'รูปแบบของวันเวลา',
+ 'Time format' => 'รูปแบบของเวลา',
+ 'Start date: ' => 'เริ่มวันที่:',
+ 'End date: ' => 'จบวันที่:',
+ 'New due date: ' => 'วันครบกำหนดใหม่',
+ 'Start date changed: ' => 'เปลี่ยนวันที่เริ่ม',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 05277797..aa73c1c6 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -20,15 +20,15 @@ return array(
'Red' => 'Kırmızı',
'Orange' => 'Turuncu',
'Grey' => 'Gri',
- // 'Brown' => '',
- // 'Deep Orange' => '',
- // 'Dark Grey' => '',
- // 'Pink' => '',
- // 'Teal' => '',
- // 'Cyan' => '',
- // 'Lime' => '',
- // 'Light Green' => '',
- // 'Amber' => '',
+ 'Brown' => 'Kahverengi',
+ 'Deep Orange' => 'Koyu Turuncu',
+ 'Dark Grey' => 'Koyu Gri',
+ 'Pink' => 'Pembe',
+ 'Teal' => 'Turkuaz',
+ 'Cyan' => 'Cam Göbeği',
+ 'Lime' => 'Limon rengi',
+ 'Light Green' => 'Açık Yeşil',
+ 'Amber' => 'Koyu sarı',
'Save' => 'Kaydet',
'Login' => 'Giriş',
'Official website:' => 'Resmi internet sitesi:',
@@ -72,7 +72,6 @@ return array(
'Remove project' => 'Projeyi sil',
'Edit the board for "%s"' => 'Tabloyu "%s" için güncelle',
'All projects' => 'Tüm projeler',
- 'Change columns' => 'Sütunları değiştir',
'Add a new column' => 'Yeni sütun ekle',
'Title' => 'Başlık',
'Nobody assigned' => 'Kullanıcı atanmamış',
@@ -86,7 +85,7 @@ return array(
'Application settings' => 'Uygulama ayarları',
'Language' => 'Dil',
// 'Webhook token:' => '',
- 'API token:' => 'API Token:',
+ 'API token:' => 'API belirteci:',
'Database size:' => 'Veritabanı boyutu :',
'Download the database' => 'Veritabanını indir',
'Optimize the database' => 'Veritabanını optimize et',
@@ -98,15 +97,12 @@ return array(
'Color' => 'Renk',
'Assignee' => 'Atanan',
'Create another task' => 'Başka bir görev oluştur',
- 'New task' => 'Nouvelle tâche',
+ 'New task' => 'Yeni görev',
'Open a task' => 'Bir görevi aç',
'Do you really want to open this task: "%s"?' => 'Bu görevi gerçekten açmak istiyor musunuz: "%s"?',
'Back to the board' => 'Tabloya dön',
- // 'Created on %B %e, %Y at %k:%M %p' => '',
'There is nobody assigned' => 'Kimse atanmamış',
'Column on the board:' => 'Tablodaki sütun:',
- 'Status is open' => 'Açık durumda',
- 'Status is closed' => 'Kapalı durumda',
'Close this task' => 'Görevi kapat',
'Open this task' => 'Görevi aç',
'There is no description.' => 'Açıklama yok.',
@@ -124,7 +120,6 @@ return array(
'The id is required' => 'Kod gerekli',
'The project id is required' => 'Proje kodu gerekli',
'The project name is required' => 'Proje adı gerekli',
- 'This project must be unique' => 'Bu projenin tekil olması gerekli',
'The title is required' => 'Başlık gerekli',
'Settings saved successfully.' => 'Ayarlar başarıyla kaydedildi.',
'Unable to save your settings.' => 'Ayarlarınız kaydedilemedi.',
@@ -159,29 +154,19 @@ return array(
'Work in progress' => 'İşlemde',
'Done' => 'Tamamlandı',
'Application version:' => 'Uygulama versiyonu:',
- // 'Completed on %B %e, %Y at %k:%M %p' => '',
- // '%B %e, %Y at %k:%M %p' => '',
- 'Date created' => 'Oluşturulma tarihi',
- 'Date completed' => 'Tamamlanma tarihi',
'Id' => 'Kod',
'%d closed tasks' => '%d kapatılmış görevler',
- // 'No task for this project' => '',
+ 'No task for this project' => 'Bu proje için görev yok',
'Public link' => 'Dışa açık link',
'Change assignee' => 'Atanmış Kullanıcıyı değiştir',
'Change assignee for the task "%s"' => '"%s" görevi için atanmış kullanıcıyı değiştir',
'Timezone' => 'Saat dilimi',
- // 'Sorry, I didn\'t find this information in my database!' => '',
+ 'Sorry, I didn\'t find this information in my database!' => 'Üzgünüm, bu bilgiyi veri tabanımda bulamadım.',
'Page not found' => 'Sayfa bulunamadı',
'Complexity' => 'Zorluk seviyesi',
'Task limit' => 'Görev limiti',
'Task count' => 'Görev sayısı',
- 'Edit project access list' => 'Proje erişim listesini düzenle',
- 'Allow this user' => 'Bu kullanıcıya izin ver',
- 'Don\'t forget that administrators have access to everything.' => 'Dikkat: Yöneticilerin herşeye erişimi olduğunu unutmayın!',
- 'Revoke' => 'Iptal et',
- 'List of authorized users' => 'Yetkili kullanıcıların listesi',
'User' => 'Kullanıcı',
- 'Nobody have access to this project.' => 'Bu projeye kimsenin erişimi yok.',
'Comments' => 'Yorumlar',
'Write your text in Markdown' => 'Yazınızı Markdown ile yazın',
'Leave a comment' => 'Bir yorum ekle',
@@ -190,11 +175,8 @@ return array(
'Comment added successfully.' => 'Yorum eklendi',
'Unable to create your comment.' => 'Yorumunuz oluşturulamadı',
'Edit this task' => 'Bu görevi değiştir',
- 'Due Date' => 'Termin',
+ 'Due Date' => 'Bitiş Tarihi',
'Invalid date' => 'Geçersiz tarihi',
- // 'Must be done before %B %e, %Y' => '',
- '%B %e, %Y' => '%d %B %Y',
- '%b %e, %Y' => '%d/%m/%Y',
'Automatic actions' => 'Otomatik işlemler',
'Your automatic action have been created successfully.' => 'Otomatik işlem başarıyla oluşturuldu',
'Unable to create your automatic action.' => 'Otomatik işleminiz oluşturulamadı',
@@ -225,21 +207,18 @@ return array(
'Assign a color to a specific user' => 'Bir kullanıcıya renk tanımla',
'Column title' => 'Sütun başlığı',
'Position' => 'Pozisyon',
- 'Move Up' => 'Yukarı taşı',
- 'Move Down' => 'Aşağı taşı',
'Duplicate to another project' => 'Başka bir projeye kopyala',
'Duplicate' => 'Kopya oluştur',
- 'link' => 'link',
+ 'link' => 'bağlantı',
'Comment updated successfully.' => 'Yorum güncellendi.',
'Unable to update your comment.' => 'Yorum güncellenemedi.',
'Remove a comment' => 'Bir yorumu sil',
'Comment removed successfully.' => 'Yorum silindi.',
'Unable to remove this comment.' => 'Bu yorum silinemiyor.',
'Do you really want to remove this comment?' => 'Bu yorumu silmek istediğinize emin misiniz?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Bu sayfaya yalnızca yorum sahibi ve yöneticiler erişebilir.',
'Current password for the user "%s"' => 'Kullanıcı için mevcut şifre "%s"',
'The current password is required' => 'Mevcut şifre gerekli',
- 'Wrong password' => 'Yanlış Şifre',
+ 'Wrong password' => 'Yanlış şifre',
'Unknown' => 'Bilinmeyen',
'Last logins' => 'Son kullanıcı girişleri',
'Login date' => 'Giriş tarihi',
@@ -247,7 +226,7 @@ return array(
'IP address' => 'IP adresi',
'User agent' => 'Kullanıcı sistemi',
'Persistent connections' => 'Kalıcı bağlantılar',
- // 'No session.' => '',
+ 'No session.' => 'Oturum yok.',
'Expiration date' => 'Geçerlilik sonu',
'Remember Me' => 'Beni hatırla',
'Creation date' => 'Oluşturulma tarihi',
@@ -255,41 +234,37 @@ return array(
'Open' => 'Açık',
'Closed' => 'Kapalı',
'Search' => 'Ara',
- 'Nothing found.' => 'Hiçbir şey bulunamadı',
- 'Due date' => 'Termin',
+ 'Nothing found.' => 'Hiçbir şey bulunamadı.',
+ 'Due date' => 'Bitiş tarihi',
'Others formats accepted: %s and %s' => 'Diğer kabul edilen formatlar: %s ve %s',
'Description' => 'Açıklama',
- '%d comments' => '%d yorumlar',
+ '%d comments' => '%d yorum',
'%d comment' => '%d yorum',
- 'Email address invalid' => 'E-Posta adresi geçersiz',
- // 'Your external account is not linked anymore to your profile.' => '',
- // 'Unable to unlink your external account.' => '',
- // 'External authentication failed' => '',
- // 'Your external account is linked to your profile successfully.' => '',
- 'Email' => 'E-Posta',
- 'Link my Google Account' => 'Google hesabımla bağ oluştur',
- 'Unlink my Google Account' => 'Google hesabımla bağı kaldır',
- 'Login with my Google Account' => 'Google hesabımla giriş yap',
- 'Project not found.' => 'Proje bulunamadı',
- 'Task removed successfully.' => 'Görev silindi',
- 'Unable to remove this task.' => 'Görev silinemiyor',
+ 'Email address invalid' => 'E-posta adresi geçersiz',
+ 'Your external account is not linked anymore to your profile.' => 'Harici hesabınız artık profilinize bağlı değil',
+ 'Unable to unlink your external account.' => 'Harici hesabınızla bağlantı koparılamadı',
+ 'External authentication failed' => 'Harici hesap doğrulaması başarısız',
+ 'Your external account is linked to your profile successfully.' => 'Harici hesabınız profilinizle başarıyla bağlandı.',
+ 'Email' => 'E-posta',
+ 'Task removed successfully.' => 'Görev başarıyla silindi.',
+ 'Unable to remove this task.' => 'Görev silinemiyor.',
'Remove a task' => 'Bir görevi sil',
'Do you really want to remove this task: "%s"?' => 'Bu görevi silmek istediğinize emin misiniz: "%s"?',
'Assign automatically a color based on a category' => 'Kategoriye göre otomatik renk ata',
'Assign automatically a category based on a color' => 'Rengine göre otomatik kategori ata',
- 'Task creation or modification' => 'Görev oluşturma veya değiştirme',
+ 'Task creation or modification' => 'Görev oluşturma veya düzenleme',
'Category' => 'Kategori',
'Category:' => 'Kategori:',
'Categories' => 'Kategoriler',
- 'Category not found.' => 'Kategori bulunamadı',
- 'Your category have been created successfully.' => 'Kategori oluşturuldu',
- 'Unable to create your category.' => 'Kategori oluşturulamadı',
- 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi',
- 'Unable to update your category.' => 'Kategori güncellenemedi',
+ 'Category not found.' => 'Kategori bulunamadı.',
+ 'Your category have been created successfully.' => 'Kategori başarıyla oluşturuldu.',
+ 'Unable to create your category.' => 'Kategori oluşturulamadı.',
+ 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi.',
+ 'Unable to update your category.' => 'Kategori güncellenemedi.',
'Remove a category' => 'Bir kategoriyi sil',
- 'Category removed successfully.' => 'Kategori silindi',
- 'Unable to remove this category.' => 'Bu kategori silinemedi',
- 'Category modification for the project "%s"' => '"%s" projesi için kategori değiştirme',
+ 'Category removed successfully.' => 'Kategori başarıyla silindi.',
+ 'Unable to remove this category.' => 'Bu kategori silinemedi.',
+ 'Category modification for the project "%s"' => '"%s" projesi için kategori düzenleme',
'Category Name' => 'Kategori adı',
'Add a new category' => 'Yeni kategori ekle',
'Do you really want to remove this category: "%s"?' => 'Bu kategoriyi silmek istediğinize emin misiniz: "%s"?',
@@ -317,41 +292,37 @@ return array(
'estimated' => 'tahmini',
'Sub-Tasks' => 'Alt Görev',
'Add a sub-task' => 'Alt görev ekle',
- // 'Original estimate' => '',
+ 'Original estimate' => 'Orjinal tahmin',
'Create another sub-task' => 'Başka bir alt görev daha oluştur',
- // 'Time spent' => '',
+ 'Time spent' => 'Harcanan zaman',
'Edit a sub-task' => 'Alt görev düzenle',
'Remove a sub-task' => 'Alt görev sil',
- 'The time must be a numeric value' => 'Zaman alfanumerik bir değer olmalı',
+ 'The time must be a numeric value' => 'Zaman alfanümerik bir değer olmalı',
'Todo' => 'Yapılacaklar',
- 'In progress' => 'İşlemde',
- 'Sub-task removed successfully.' => 'Alt görev silindi',
- 'Unable to remove this sub-task.' => 'Alt görev silinemedi',
- 'Sub-task updated successfully.' => 'Alt görev güncellendi',
- 'Unable to update your sub-task.' => 'Alt görev güncellenemiyor',
- 'Unable to create your sub-task.' => 'Alt görev oluşturulamadı',
- 'Sub-task added successfully.' => 'Alt görev başarıyla eklendii',
+ 'In progress' => 'Devam etmekte',
+ 'Sub-task removed successfully.' => 'Alt görev başarıyla silindi.',
+ 'Unable to remove this sub-task.' => 'Alt görev silinemedi.',
+ 'Sub-task updated successfully.' => 'Alt görev güncellendi.',
+ 'Unable to update your sub-task.' => 'Alt görev güncellenemiyor.',
+ 'Unable to create your sub-task.' => 'Alt görev oluşturulamadı.',
+ 'Sub-task added successfully.' => 'Alt görev başarıyla eklendi.',
'Maximum size: ' => 'Maksimum boyutu',
- 'Unable to upload the file.' => 'Karşıya yükleme başarısız',
+ 'Unable to upload the file.' => 'Dosya yüklenemiyor.',
'Display another project' => 'Başka bir proje göster',
- // 'Login with my Github Account' => '',
- // 'Link my Github Account' => '',
- // 'Unlink my Github Account' => '',
'Created by %s' => '%s tarafından oluşturuldu',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Son değişiklik tarihi %d.%m.%Y, saati %H:%M',
'Tasks Export' => 'Görevleri dışa aktar',
'Tasks exportation for "%s"' => '"%s" için görevleri dışa aktar',
'Start Date' => 'Başlangıç tarihi',
'End Date' => 'Bitiş tarihi',
'Execute' => 'Gerçekleştir',
- 'Task Id' => 'Görev No',
+ 'Task Id' => 'Görev Kimliği',
'Creator' => 'Oluşturan',
'Modification date' => 'Değişiklik tarihi',
'Completion date' => 'Tamamlanma tarihi',
'Clone' => 'Kopya oluştur',
'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.',
- 'Unable to clone this project.' => 'Proje kopyası oluşturulamadı.',
- 'Enable email notifications' => 'E-Posta bilgilendirmesini aç',
+ 'Unable to clone this project.' => 'Proje kopyası oluşturulamıyor.',
+ 'Enable email notifications' => 'E-posta bilgilendirmesini aç',
'Task position:' => 'Görev pozisyonu',
'The task #%d have been opened.' => '#%d numaralı görev açıldı.',
'The task #%d have been closed.' => '#%d numaralı görev kapatıldı.',
@@ -372,9 +343,8 @@ return array(
'Task closed' => 'Görev kapatıldı',
'Task opened' => 'Görev açıldı',
'I want to receive notifications only for those projects:' => 'Yalnızca bu projelerle ilgili bildirim almak istiyorum:',
- 'view the task on Kanboard' => 'bu görevi Kanboard\'da göster',
+ 'view the task on Kanboard' => 'bu görevi Kanboard üzerinde göster',
'Public access' => 'Dışa açık erişim',
- 'User management' => 'Kullanıcı yönetimi',
'Active tasks' => 'Aktif görevler',
'Disable public access' => 'Dışa açık erişimi kapat',
'Enable public access' => 'Dışa açık erişimi aç',
@@ -394,24 +364,18 @@ return array(
'Disabled' => 'Devre dışı bırakıldı',
'Username:' => 'Kullanıcı adı',
'Name:' => 'Ad',
- 'Email:' => 'E-Posta',
+ 'Email:' => 'E-posta',
'Notifications:' => 'Bildirimler:',
'Notifications' => 'Bildirimler',
- 'Group:' => 'Grup',
- 'Regular user' => 'Varsayılan kullanıcı',
'Account type:' => 'Hesap türü:',
'Edit profile' => 'Profili değiştir',
'Change password' => 'Şifre değiştir',
'Password modification' => 'Şifre değişimi',
'External authentications' => 'Dış kimlik doğrulamaları',
- 'Google Account' => 'Google hesabı',
- 'Github Account' => 'Github hesabı',
'Never connected.' => 'Hiç bağlanmamış.',
- 'No account linked.' => 'Bağlanmış hesap yok.',
- 'Account linked.' => 'Hesap bağlandı',
'No external authentication enabled.' => 'Dış kimlik doğrulama kapalı.',
'Password modified successfully.' => 'Şifre başarıyla değiştirildi.',
- 'Unable to change the password.' => 'Şifre değiştirilemedi.',
+ 'Unable to change the password.' => 'Şifre değiştirilemiyor.',
'Change category for the task "%s"' => '"%s" görevi için kategori değiştirme',
'Change category' => 'Kategori değiştirme',
'%s updated the task %s' => '%s kullanıcısı %s görevini güncelledi',
@@ -446,27 +410,20 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s kullanıcısı %s görevinin sorumlusunu %s olarak değiştirdi',
'New password for the user "%s"' => '"%s" kullanıcısı için yeni şifre',
'Choose an event' => 'Bir durum seçin',
- // 'Github commit received' => '',
- // 'Github issue opened' => '',
- // 'Github issue closed' => '',
- // 'Github issue reopened' => '',
- // 'Github issue assignee change' => '',
- // 'Github issue label change' => '',
'Create a task from an external provider' => 'Dış sağlayıcı ile bir görev oluştur',
'Change the assignee based on an external username' => 'Dış kaynaklı kullanıcı adı ile göreve atananı değiştir',
'Change the category based on an external label' => 'Dış kaynaklı bir etiket ile kategori değiştir',
'Reference' => 'Referans',
- 'Reference: %s' => 'Referans: %s',
'Label' => 'Etiket',
- 'Database' => 'Veri bankası',
+ 'Database' => 'Veritabanı',
'About' => 'Hakkında',
- 'Database driver:' => 'Veri bankası sürücüsü',
+ 'Database driver:' => 'Veritabanı sürücüsü:',
'Board settings' => 'Tablo ayarları',
- 'URL and token' => 'URL veya Token',
+ 'URL and token' => 'URL veya Belirteç',
'Webhook settings' => 'Webhook ayarları',
'URL for task creation:' => 'Görev oluşturma için URL',
- 'Reset token' => 'Reset Token',
- 'API endpoint:' => 'API endpoint',
+ 'Reset token' => 'Belirteci sıfırla',
+ 'API endpoint:' => 'API bitiş noktası:',
'Refresh interval for private board' => 'Özel tablolar için yenileme sıklığı',
'Refresh interval for public board' => 'Dışa açık tablolar için yenileme sıklığı',
'Task highlight period' => 'Görevi öne çıkarma süresi',
@@ -474,17 +431,13 @@ return array(
'Frequency in second (60 seconds by default)' => 'Saniye olarak frekans (varsayılan 60 saniye)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Saniye olarak frekans (Bu özelliği iptal etmek için 0, varsayılan değer 10 saniye)',
'Application URL' => 'Uygulama URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Örneğin: http://example.kanboard.net/ (E-posta bildirimleri için kullanılıyor)',
- 'Token regenerated.' => 'Token yeniden oluşturuldu.',
+ 'Token regenerated.' => 'Beliteç yeniden oluşturuldu.',
'Date format' => 'Tarih formatı',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"',
'New private project' => 'Yeni özel proje',
'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',
- 'Estimated time: %s hours' => 'Tahmini süre: %s Saat',
- 'Time spent: %s hours' => 'Kullanılan süre: %s Saat',
- 'Started on %B %e, %Y' => '%B %e %Y tarihinde başlatıldı',
'Start date' => 'Başlangıç tarihi',
'Time estimated' => 'Tahmini süre',
'There is nothing assigned to you.' => 'Size atanan hiçbir şey yok.',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => 'Bu projeye herkesin erişimi var.',
'Webhooks' => 'Webhooks',
'API' => 'API',
- 'Github webhooks' => 'Github Webhook',
- 'Help on Github webhooks' => 'Github Webhooks hakkında yardım',
'Create a comment from an external provider' => 'Dış sağlayıcı ile bir yorum oluştur',
- 'Github issue comment created' => 'Github hata yorumu oluşturuldu',
'Project management' => 'Proje yönetimi',
'My projects' => 'Projelerim',
'Columns' => 'Sütunlar',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => '"%s" için kullanıcı dağılımı',
'Clone this project' => 'Projenin kopyasını oluştur',
'Column removed successfully.' => 'Sütun başarıyla kaldırıldı.',
- 'Github Issue' => 'Github Issue',
'Not enough data to show the graph.' => 'Grafik gösterimi için yeterli veri yok.',
'Previous' => 'Önceki',
'The id must be an integer' => 'ID bir tamsayı olmalı',
@@ -547,10 +496,7 @@ return array(
'Default swimlane' => 'Varsayılan Kulvar',
'Do you really want to remove this swimlane: "%s"?' => 'Bu Kulvarı silmek istediğinize emin misiniz?: "%s"?',
'Inactive swimlanes' => 'Pasif Kulvarlar',
- 'Set project manager' => 'Proje yöneticisi olarak ata',
- 'Set project member' => 'Proje üyesi olarak ata',
'Remove a swimlane' => 'Kulvarı sil',
- 'Rename' => 'Yeniden adlandır',
'Show default swimlane' => 'Varsayılan Kulvarı göster',
'Swimlane modification for the project "%s"' => '"%s" Projesi için Kulvar değişikliği',
'Swimlane not found.' => 'Kulvar bulunamadı',
@@ -558,24 +504,13 @@ return array(
'Swimlanes' => 'Kulvarlar',
'Swimlane updated successfully.' => 'Kulvar başarıyla güncellendi.',
'The default swimlane have been updated successfully.' => 'Varsayılan Kulvarlar başarıyla güncellendi.',
- 'Unable to create your swimlane.' => 'Bu Kulvarı oluşturmak mümkün değil.',
'Unable to remove this swimlane.' => 'Bu Kulvarı silmek mümkün değil.',
'Unable to update this swimlane.' => 'Bu Kulvarı değiştirmek mümkün değil.',
'Your swimlane have been created successfully.' => 'Kulvar başarıyla oluşturuldu.',
'Example: "Bug, Feature Request, Improvement"' => 'Örnek: "Sorun, Özellik talebi, İyileştirme"',
'Default categories for new projects (Comma-separated)' => 'Yeni projeler için varsayılan kategoriler (Virgül ile ayrılmış)',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
'Integrations' => 'Entegrasyon',
'Integration with third-party services' => 'Dış servislerle entegrasyon',
- 'Role for this project' => 'Bu proje için rol',
- 'Project manager' => 'Proje Yöneticisi',
- 'Project member' => 'Proje Üyesi',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Bir Proje Yöneticisi proje ayarlarını değiştirebilir ve bir üyeden daha fazla yetkiye sahiptir.',
- // 'Gitlab Issue' => '',
'Subtask Id' => 'Alt görev No:',
'Subtasks' => 'Alt görevler',
'Subtasks Export' => 'Alt görevleri dışa aktar',
@@ -602,10 +537,7 @@ return array(
'Time Tracking' => 'Zaman takibi',
'You already have one subtask in progress' => 'Zaten işlemde olan bir alt görev var',
'Which parts of the project do you want to duplicate?' => 'Projenin hangi kısımlarının kopyasını oluşturmak istiyorsunuz?',
- // 'Disallow login form' => '',
- 'Bitbucket commit received' => 'Bitbucket commit alındı',
- 'Bitbucket webhooks' => 'Bitbucket webhooks',
- 'Help on Bitbucket webhooks' => 'Bitbucket webhooks için yardım',
+ 'Disallow login form' => 'Giriş formu erişimini engelle',
'Start' => 'Başlangıç',
'End' => 'Son',
'Task age in days' => 'Görev yaşı gün olarak',
@@ -646,7 +578,6 @@ return array(
'This task' => 'Bu görev',
'<1h' => '<1s',
'%dh' => '%ds',
- // '%b %e' => '',
'Expand tasks' => 'Görevleri genişlet',
'Collapse tasks' => 'Görevleri daralt',
'Expand/collapse tasks' => 'Görevleri genişlet/daralt',
@@ -656,20 +587,17 @@ return array(
'Keyboard shortcuts' => 'Klavye kısayolları',
'Open board switcher' => 'Tablo seçim listesini aç',
'Application' => 'Uygulama',
- 'since %B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p\'den beri',
'Compact view' => 'Ekrana sığdır',
'Horizontal scrolling' => 'Geniş görünüm',
'Compact/wide view' => 'Ekrana sığdır / Geniş görünüm',
- // 'No results match:' => '',
- // 'Currency' => '',
- // 'Files' => '',
- // 'Images' => '',
- // 'Private project' => '',
+ 'No results match:' => 'Uygun sonuç bulunamadı',
+ 'Currency' => 'Para birimi',
+ 'Private project' => 'Özel proje',
// 'AUD - Australian Dollar' => '',
// 'CAD - Canadian Dollar' => '',
// 'CHF - Swiss Francs' => '',
// 'Custom Stylesheet' => '',
- // 'download' => '',
+ 'download' => 'indir',
// 'EUR - Euro' => '',
// 'GBP - British Pound' => '',
// 'INR - Indian Rupee' => '',
@@ -677,391 +605,547 @@ return array(
// 'NZD - New Zealand Dollar' => '',
// 'RSD - Serbian dinar' => '',
// 'USD - US Dollar' => '',
- // 'Destination column' => '',
- // 'Move the task to another column when assigned to a user' => '',
- // 'Move the task to another column when assignee is cleared' => '',
- // 'Source column' => '',
- // 'Transitions' => '',
- // 'Executer' => '',
- // 'Time spent in the column' => '',
- // 'Task transitions' => '',
- // 'Task transitions export' => '',
- // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '',
- // 'Currency rates' => '',
- // 'Rate' => '',
- // 'Change reference currency' => '',
- // 'Add a new currency rate' => '',
- // 'Reference currency' => '',
- // 'The currency rate have been added successfully.' => '',
- // 'Unable to add this currency rate.' => '',
+ 'Destination column' => 'Hedef Sütun',
+ 'Move the task to another column when assigned to a user' => 'Bir kullanıcıya atandığında görevi başka bir sütuna taşı',
+ 'Move the task to another column when assignee is cleared' => 'Atanmış kullanıcı kaldırıldığında görevi başka bir sütuna taşı',
+ 'Source column' => 'Kaynak sütun',
+ 'Transitions' => 'Geçişler',
+ 'Executer' => 'Uygulayıcı',
+ 'Time spent in the column' => 'Sütunda harcanan süre',
+ 'Task transitions' => 'Görev geçişleri',
+ 'Task transitions export' => 'Görev geçişlerini dışa aktar',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Bu rapor her bir görevin sütunlar arası geçişlerini tarih, kullanıcı ve sütunda harcanan zaman detaylarıyla içerir.',
+ 'Currency rates' => 'Döviz kurları',
+ 'Rate' => 'Kurlar',
+ 'Change reference currency' => 'Referans kur değiştir',
+ 'Add a new currency rate' => 'Yeni bir kur ekle',
+ 'Reference currency' => 'Referans kur',
+ 'The currency rate have been added successfully.' => 'Kur başarıyla eklendi',
+ 'Unable to add this currency rate.' => 'Bu kur eklenemedi',
// 'Webhook URL' => '',
- // '%s remove the assignee of the task %s' => '',
- // 'Enable Gravatar images' => '',
- // 'Information' => '',
- // 'Check two factor authentication code' => '',
- // 'The two factor authentication code is not valid.' => '',
- // 'The two factor authentication code is valid.' => '',
- // 'Code' => '',
- // 'Two factor authentication' => '',
- // 'Enable/disable two factor authentication' => '',
- // 'This QR code contains the key URI: ' => '',
- // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
- // 'Check my code' => '',
- // 'Secret key: ' => '',
- // 'Test your device' => '',
- // 'Assign a color when the task is moved to a specific column' => '',
- // '%s via Kanboard' => '',
- // 'uploaded by: %s' => '',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
- // 'Burndown chart for "%s"' => '',
- // 'Burndown chart' => '',
- // 'This chart show the task complexity over the time (Work Remaining).' => '',
- // 'Screenshot taken %s' => '',
- // 'Add a screenshot' => '',
- // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
- // 'Screenshot uploaded successfully.' => '',
+ '%s remove the assignee of the task %s' => '%s, %s görevinin atanan bilgisini kaldırdı',
+ 'Enable Gravatar images' => 'Gravatar resimlerini kullanıma aç',
+ 'Information' => 'Bilgi',
+ 'Check two factor authentication code' => 'İki kademeli doğrulama kodunu kontrol et',
+ 'The two factor authentication code is not valid.' => 'İki kademeli doğrulama kodu geçersiz',
+ 'The two factor authentication code is valid.' => 'İki kademeli doğrulama kodu onaylandı',
+ 'Code' => 'Kod',
+ 'Two factor authentication' => 'İki kademeli doğrulama',
+ 'This QR code contains the key URI: ' => 'Bu QR kodu anahtar URI içerir',
+ 'Check my code' => 'Kodu kontrol et',
+ 'Secret key: ' => 'Gizli anahtar',
+ 'Test your device' => 'Cihazınızı test edin',
+ 'Assign a color when the task is moved to a specific column' => 'Görev belirli bir sütuna taşındığında rengini değiştir',
+ '%s via Kanboard' => '%s Kanboard ile',
+ 'Burndown chart for "%s"' => '%s icin kalan iş grafiği',
+ 'Burndown chart' => 'Kalan iş grafiği',
+ 'This chart show the task complexity over the time (Work Remaining).' => 'Bu grafik zorluk seviyesini zamana göre gösterir (kalan iş)',
+ 'Screenshot taken %s' => 'Ekran görüntüsü alındı %s',
+ 'Add a screenshot' => 'Bir ekran görüntüsü ekle',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Bir ekran görüntüsü alın ve buraya yapıştırmak için CTRL+V veya ⌘+V tuşlarına basın.',
+ 'Screenshot uploaded successfully.' => 'Ekran görüntüsü başarıyla yüklendi',
// 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
- // 'Identifier' => '',
- // 'Disable two factor authentication' => '',
- // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
- // 'Edit link' => '',
- // 'Start to type task title...' => '',
- // 'A task cannot be linked to itself' => '',
- // 'The exact same link already exists' => '',
- // 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
- // 'Score' => '',
- // 'The identifier must be unique' => '',
- // 'This linked task id doesn\'t exists' => '',
- // 'This value must be alphanumeric' => '',
- // 'Edit recurrence' => '',
- // 'Generate recurrent task' => '',
- // 'Trigger to generate recurrent task' => '',
- // 'Factor to calculate new due date' => '',
- // 'Timeframe to calculate new due date' => '',
- // 'Base date to calculate new due date' => '',
- // 'Action date' => '',
- // 'Base date to calculate new due date: ' => '',
- // 'This task has created this child task: ' => '',
- // 'Day(s)' => '',
- // 'Existing due date' => '',
- // 'Factor to calculate new due date: ' => '',
- // 'Month(s)' => '',
- // 'Recurrence' => '',
- // 'This task has been created by: ' => '',
- // 'Recurrent task has been generated:' => '',
- // 'Timeframe to calculate new due date: ' => '',
- // 'Trigger to generate recurrent task: ' => '',
- // 'When task is closed' => '',
- // 'When task is moved from first column' => '',
- // 'When task is moved to last column' => '',
- // 'Year(s)' => '',
- // 'Calendar settings' => '',
- // 'Project calendar view' => '',
- // 'Project settings' => '',
- // 'Show subtasks based on the time tracking' => '',
- // 'Show tasks based on the creation date' => '',
- // 'Show tasks based on the start date' => '',
- // 'Subtasks time tracking' => '',
- // 'User calendar view' => '',
- // 'Automatically update the start date' => '',
- // 'iCal feed' => '',
- // 'Preferences' => '',
- // 'Security' => '',
- // 'Two factor authentication disabled' => '',
- // 'Two factor authentication enabled' => '',
- // 'Unable to update this user.' => '',
- // 'There is no user management for private projects.' => '',
- // 'User that will receive the email' => '',
- // 'Email subject' => '',
- // 'Date' => '',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
- // 'Add a comment log when moving the task between columns' => '',
- // 'Move the task to another column when the category is changed' => '',
- // 'Send a task by email to someone' => '',
- // 'Reopen a task' => '',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
- // 'Column change' => '',
- // 'Position change' => '',
- // 'Swimlane change' => '',
- // 'Assignee change' => '',
- // '[%s] Overdue tasks' => '',
- // 'Notification' => '',
- // '%s moved the task #%d to the first swimlane' => '',
- // '%s moved the task #%d to the swimlane "%s"' => '',
- // 'Swimlane' => '',
- // 'Gravatar' => '',
- // '%s moved the task %s to the first swimlane' => '',
- // '%s moved the task %s to the swimlane "%s"' => '',
- // 'This report contains all subtasks information for the given date range.' => '',
- // 'This report contains all tasks information for the given date range.' => '',
- // 'Project activities for %s' => '',
- // 'view the board on Kanboard' => '',
- // 'The task have been moved to the first swimlane' => '',
- // 'The task have been moved to another swimlane:' => '',
- // 'Overdue tasks for the project "%s"' => '',
- // 'New title: %s' => '',
- // 'The task is not assigned anymore' => '',
- // 'New assignee: %s' => '',
- // 'There is no category now' => '',
- // 'New category: %s' => '',
- // 'New color: %s' => '',
- // 'New complexity: %d' => '',
- // 'The due date have been removed' => '',
- // 'There is no description anymore' => '',
- // 'Recurrence settings have been modified' => '',
- // 'Time spent changed: %sh' => '',
- // 'Time estimated changed: %sh' => '',
- // 'The field "%s" have been updated' => '',
- // 'The description have been modified' => '',
- // 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
- // 'I want to receive notifications for:' => '',
- // 'All tasks' => '',
- // 'Only for tasks assigned to me' => '',
- // 'Only for tasks created by me' => '',
- // 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
- // '%%Y-%%m-%%d' => '',
- // 'Total for all columns' => '',
- // 'You need at least 2 days of data to show the chart.' => '',
- // '<15m' => '',
- // '<30m' => '',
- // 'Stop timer' => '',
- // 'Start timer' => '',
- // 'Add project member' => '',
- // 'Enable notifications' => '',
- // 'My activity stream' => '',
- // 'My calendar' => '',
- // 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
- // 'Reset filters' => '',
- // 'My tasks due tomorrow' => '',
- // 'Tasks due today' => '',
- // 'Tasks due tomorrow' => '',
- // 'Tasks due yesterday' => '',
- // 'Closed tasks' => '',
- // 'Open tasks' => '',
- // 'Not assigned' => '',
- // 'View advanced search syntax' => '',
- // 'Overview' => '',
- // '%b %e %Y' => '',
- // 'Board/Calendar/List view' => '',
- // 'Switch to the board view' => '',
- // 'Switch to the calendar view' => '',
- // 'Switch to the list view' => '',
- // 'Go to the search/filter box' => '',
- // 'There is no activity yet.' => '',
- // 'No tasks found.' => '',
- // 'Keyboard shortcut: "%s"' => '',
- // 'List' => '',
- // 'Filter' => '',
- // 'Advanced search' => '',
- // 'Example of query: ' => '',
- // 'Search by project: ' => '',
- // 'Search by column: ' => '',
- // 'Search by assignee: ' => '',
- // 'Search by color: ' => '',
- // 'Search by category: ' => '',
- // 'Search by description: ' => '',
- // 'Search by due date: ' => '',
- // 'Lead and Cycle time for "%s"' => '',
- // 'Average time spent into each column for "%s"' => '',
- // 'Average time spent into each column' => '',
- // 'Average time spent' => '',
- // 'This chart show the average time spent into each column for the last %d tasks.' => '',
- // 'Average Lead and Cycle time' => '',
- // 'Average lead time: ' => '',
- // 'Average cycle time: ' => '',
- // 'Cycle Time' => '',
- // 'Lead Time' => '',
- // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
- // 'Average time into each column' => '',
- // 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
- // 'Lead time: ' => '',
- // 'Cycle time: ' => '',
- // 'Time spent into each column' => '',
- // 'The lead time is the duration between the task creation and the completion.' => '',
- // 'The cycle time is the duration between the start date and the completion.' => '',
- // 'If the task is not closed the current time is used instead of the completion date.' => '',
- // 'Set automatically the start date' => '',
- // 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
- // 'Remote user' => '',
- // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
- // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
- // 'New remote user' => '',
- // 'New local user' => '',
- // 'Default task color' => '',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
- // 'This feature does not work with all browsers.' => '',
- // 'There is no destination project available.' => '',
- // 'Trigger automatically subtask time tracking' => '',
- // 'Include closed tasks in the cumulative flow diagram' => '',
- // 'Current swimlane: %s' => '',
- // 'Current column: %s' => '',
- // 'Current category: %s' => '',
- // 'no category' => '',
- // 'Current assignee: %s' => '',
- // 'not assigned' => '',
- // 'Author:' => '',
- // 'contributors' => '',
- // 'License:' => '',
- // 'License' => '',
- // 'Project Administrator' => '',
- // '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' => '',
+ 'Identifier' => 'Kimlik',
+ 'Disable two factor authentication' => 'İki kademeli doğrulamayı iptal et',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Bu kullanıcı için iki kademeli doğrulamayı iptal etmek istediğinize emin misiniz: "%s"?',
+ 'Edit link' => 'Linki düzenle',
+ 'Start to type task title...' => 'Görev başlığını yazmaya başlayın...',
+ 'A task cannot be linked to itself' => 'Bir görevden kendine link atanamaz',
+ 'The exact same link already exists' => 'Birebir aynı link zaten var',
+ 'Recurrent task is scheduled to be generated' => 'Tekrarlanan görev oluşturulması zamanlandı',
+ 'Score' => 'Skor',
+ 'The identifier must be unique' => 'Kimlik özgün olmalı',
+ 'This linked task id doesn\'t exists' => 'Link oluşturulan görec kodu mevcut değil',
+ 'This value must be alphanumeric' => 'Bu değer alfanumerik olmalı',
+ 'Edit recurrence' => 'Tekrarlanmayı düzenle',
+ 'Generate recurrent task' => 'Tekrarlanan görev oluştur',
+ 'Trigger to generate recurrent task' => 'Tekrarlanan görev oluşturmak için tetikleyici',
+ 'Factor to calculate new due date' => 'Yeni tamamlanma tarihi için hesaplama faktörü',
+ 'Timeframe to calculate new due date' => 'Yeni tamamlanma tarihinin hesaplanması için zaman dilimi',
+ 'Base date to calculate new due date' => 'Yeni tamamlanma tarihinin hesaplanması için baz alınacak tarih',
+ 'Action date' => 'Hareket tarihi',
+ 'Base date to calculate new due date: ' => 'Yeni tamamlanma tarihinin hesaplanması için baz alınacak tarih',
+ 'This task has created this child task: ' => 'Bu görev şu alt görevi oluşturdu:',
+ 'Day(s)' => 'Gün(ler)',
+ 'Existing due date' => 'Mevcut tamamlanma tarihi',
+ 'Factor to calculate new due date: ' => 'Yeni tamamlanma tarihi için hesaplama faktörü',
+ 'Month(s)' => 'Ay(lar)',
+ 'Recurrence' => 'Tekrar',
+ 'This task has been created by: ' => 'Bu görev şunun tarafından oluşturuldu:',
+ 'Recurrent task has been generated:' => 'Tekrarlanan görev oluşturuldu:',
+ 'Timeframe to calculate new due date: ' => 'Yeni tamamlanma tarihinin hesaplanması için zaman dilimi',
+ 'Trigger to generate recurrent task: ' => 'Tekrarlanan görev oluşturmak için tetikleyici',
+ 'When task is closed' => 'Görev kapatıldığı zaman',
+ 'When task is moved from first column' => 'Görev ilk sütundan taşındığı zaman',
+ 'When task is moved to last column' => 'Görev son sütuna taşındığı zaman',
+ 'Year(s)' => 'Yıl(lar)',
+ 'Calendar settings' => 'Takvim ayarları',
+ 'Project calendar view' => 'Proje takvim görünümü',
+ 'Project settings' => 'Proje ayarları',
+ 'Show subtasks based on the time tracking' => 'Zaman takibi bazında alt görevleri göster',
+ 'Show tasks based on the creation date' => 'Oluşturulma zamanına göre görevleri göster',
+ 'Show tasks based on the start date' => 'Başlangıç zamanına göre görevleri göster',
+ 'Subtasks time tracking' => 'Alt görevler zaman takibi',
+ 'User calendar view' => 'Kullanıcı takvim görünümü',
+ 'Automatically update the start date' => 'Başlangıç tarihini otomatik olarak güncelle',
+ 'iCal feed' => 'iCal akışı',
+ 'Preferences' => 'Ayarlar',
+ 'Security' => 'Güvenlik',
+ 'Two factor authentication disabled' => 'İki kademeli doğrulamayı devre dışı bırak',
+ 'Two factor authentication enabled' => 'İki kademeli doğrulamayı etkinleştir',
+ 'Unable to update this user.' => 'Bu kullanıcı güncellenemiyor',
+ 'There is no user management for private projects.' => 'Özel projeler için kullanıcı yönetimi yoktur.',
+ 'User that will receive the email' => 'Email alacak kullanıcı',
+ 'Email subject' => 'Email başlığı',
+ 'Date' => 'Tarih',
+ 'Add a comment log when moving the task between columns' => 'Görevi sütunlar arasında taşırken yorum kaydı ekle',
+ 'Move the task to another column when the category is changed' => 'Kategori değiştirildiğinde görevi başka sütuna taşı',
+ 'Send a task by email to someone' => 'Bir görevi email ile birine gönder',
+ 'Reopen a task' => 'Bir görevi tekrar aç',
+ 'Column change' => 'Sütun değişikliği',
+ 'Position change' => 'Konum değişikliği',
+ 'Swimlane change' => 'Kulvar değişikliği',
+ 'Assignee change' => 'Atanan değişikliği',
+ '[%s] Overdue tasks' => '[%s] Gecikmiş görevler',
+ 'Notification' => 'Uyarılar',
+ '%s moved the task #%d to the first swimlane' => '%s, #%d görevini birinci kulvara taşıdı',
+ '%s moved the task #%d to the swimlane "%s"' => '%s, #%d görevini "%s" kulvarına taşıdı',
+ 'Swimlane' => 'Kulvar',
+ 'Gravatar' => 'Gravatar',
+ '%s moved the task %s to the first swimlane' => '%s, %s görevini ilk kulvara taşıdı',
+ '%s moved the task %s to the swimlane "%s"' => '%s, %s görevini "%s" kulvarına taşıdı',
+ 'This report contains all subtasks information for the given date range.' => 'Bu rapor belirtilen tarih aralığında tüm alt görev bilgilerini içerir.',
+ 'This report contains all tasks information for the given date range.' => 'Bu rapor belirtilen tarih aralığında tüm görev bilgilerini içerir.',
+ 'Project activities for %s' => '%s için proje aktiviteleri',
+ 'view the board on Kanboard' => 'Tabloyu Kanboard\'da görüntüle',
+ 'The task have been moved to the first swimlane' => 'Görev birinci kulvara taşındı',
+ 'The task have been moved to another swimlane:' => 'Görev başka bir kulvara taşındı:',
+ 'Overdue tasks for the project "%s"' => '"%s" projesi için gecikmiş görevler',
+ 'New title: %s' => 'Yeni başlık: %s',
+ 'The task is not assigned anymore' => 'Görev artık atanmamış',
+ 'New assignee: %s' => 'Yeni atanan: %s',
+ 'There is no category now' => 'Şu anda kategori yok',
+ 'New category: %s' => 'Yeni kategori: %s',
+ 'New color: %s' => 'Yeni renk: %s',
+ 'New complexity: %d' => 'Yeni zorluk seviyesi: %d',
+ 'The due date have been removed' => 'Tamamlanma tarihi silindi',
+ 'There is no description anymore' => 'Artık açıklama yok',
+ 'Recurrence settings have been modified' => 'Tekrarlanma ayarları değiştirildi',
+ 'Time spent changed: %sh' => 'Harcanan zaman değiştirildi: %sh',
+ 'Time estimated changed: %sh' => 'Tahmini süre değiştirildi: %sh',
+ 'The field "%s" have been updated' => '"%s" hanesi değiştirildi',
+ 'The description have been modified' => 'Açıklama değiştirildi',
+ 'Do you really want to close the task "%s" as well as all subtasks?' => '"%s" görevini ve tüm alt görevlerini kapatmak istediğinize emin misiniz?',
+ 'I want to receive notifications for:' => 'Bununla ilgili bildirimler almak istiyorum:',
+ 'All tasks' => 'Tüm görevler',
+ 'Only for tasks assigned to me' => 'Yalnızca bana atanmış görevler için',
+ 'Only for tasks created by me' => 'Yalnızca benim oluşturduğum görevler için',
+ 'Only for tasks created by me and assigned to me' => 'Yalnızca benim oluşturduğum ve bana atanmış görevler için',
+ '%%Y-%%m-%%d' => '%%Y-%%m-%%d',
+ 'Total for all columns' => 'Tüm sütunlar için toplam',
+ 'You need at least 2 days of data to show the chart.' => 'Grafiği göstermek için en az iki günlük veriye ihtiyaç var.',
+ '<15m' => '<15dk',
+ '<30m' => '<30dk',
+ 'Stop timer' => 'Zamanlayıcıyı durdur',
+ 'Start timer' => 'Zamanlayıcıyı başlat',
+ 'Add project member' => 'Proje üyesi ekle',
+ 'Enable notifications' => 'Bildirimleri etkinleştir',
+ 'My activity stream' => 'Olay akışım',
+ 'My calendar' => 'Takvimim',
+ 'Search tasks' => 'Görevleri ara',
+ 'Back to the calendar' => 'Takvime geri dön',
+ 'Filters' => 'Filtreler',
+ 'Reset filters' => 'Filtreleri sıfırla',
+ 'My tasks due tomorrow' => 'Yarına tamamlanması gereken görevlerim',
+ 'Tasks due today' => 'Bugün tamamlanması gereken görevler',
+ 'Tasks due tomorrow' => 'Yarına tamamlanması gereken görevler',
+ 'Tasks due yesterday' => 'Dün tamamlanmış olması gereken görevler',
+ 'Closed tasks' => 'Kapatılmış görevler',
+ 'Open tasks' => 'Açık görevler',
+ 'Not assigned' => 'Atanmamış',
+ 'View advanced search syntax' => 'Gelişmiş arama kodlarını göster',
+ 'Overview' => 'Genel bakış',
+ 'Board/Calendar/List view' => 'Tablo/Takvim/Liste görünümü',
+ 'Switch to the board view' => 'Tablo görünümüne geç',
+ 'Switch to the calendar view' => 'Takvim görünümüne geç',
+ 'Switch to the list view' => 'Liste görünümüne geç',
+ 'Go to the search/filter box' => 'Arama/Filtreleme kutusuna git',
+ 'There is no activity yet.' => 'Henüz bir aktivite yok.',
+ 'No tasks found.' => 'Hiç görev bulunamadı.',
+ 'Keyboard shortcut: "%s"' => 'Klavye kısayolu: "%s"',
+ 'List' => 'Liste',
+ 'Filter' => 'Filtre',
+ 'Advanced search' => 'Gelişmiş arama',
+ 'Example of query: ' => 'Sorgu örneği',
+ 'Search by project: ' => 'Projeye göre ara',
+ 'Search by column: ' => 'Sütuna göre ara',
+ 'Search by assignee: ' => 'Atanana göre ara',
+ 'Search by color: ' => 'Renge göre ara',
+ 'Search by category: ' => 'Kategoriye göre ara',
+ 'Search by description: ' => 'Açıklamaya göre ara',
+ 'Search by due date: ' => 'Tamamlanma tarihine göre ara',
+ 'Lead and Cycle time for "%s"' => '"%s" için teslim ve çevrim süresi',
+ 'Average time spent into each column for "%s"' => '"%s" için her bir sütunda geçirilen ortalama zaman',
+ 'Average time spent into each column' => 'Her bir sütunda geçirilen ortalama zaman',
+ 'Average time spent' => 'Harcanan ortalama zaman',
+ 'This chart show the average time spent into each column for the last %d tasks.' => 'Bu grafik son %d görev için her bir sütunda geçirilen ortalama zamanı gösterir.',
+ 'Average Lead and Cycle time' => 'Ortalama teslim ve çevrim süresi',
+ 'Average lead time: ' => 'Ortalama teslim süresi',
+ 'Average cycle time: ' => 'Ortalama çevrim süresi',
+ 'Cycle Time' => 'Çevrim süresi',
+ 'Lead Time' => 'Teslim süresi',
+ 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Bu grafik son %d görev için zaman içinde gerçekleşen ortalama teslim ve çevrim sürelerini gösterir.',
+ 'Average time into each column' => 'Her bir sütunda ortalama zaman',
+ 'Lead and cycle time' => 'Teslim ve çevrim süresi',
+ 'Lead time: ' => 'Teslim süresi: ',
+ 'Cycle time: ' => 'Çevrim süresi: ',
+ 'Time spent into each column' => 'Her sütunda harcanan zaman',
+ 'The lead time is the duration between the task creation and the completion.' => 'Teslim süresi, görevin oluşturulması ile tamamlanması arasında geçen süredir.',
+ 'The cycle time is the duration between the start date and the completion.' => 'Çevrim süresi, görevin başlangıç tarihi ile tamamlanması arasında geçen süredir.',
+ 'If the task is not closed the current time is used instead of the completion date.' => 'Eğer görev henüz kapatılmamışsa, tamamlanma tarihi yerine şu anki tarih kullanılır.',
+ 'Set automatically the start date' => 'Başlangıç tarihini otomatik olarak belirle',
+ 'Edit Authentication' => 'Doğrulamayı düzenle',
+ 'Remote user' => 'Uzak kullanıcı',
+ 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Uzak kullanıcıların şifreleri Kanboard veritabanında saklanmaz, örnek: LDAP, Google ve Github hesapları',
+ 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Eğer giriş formuna erişimi engelleyi seçerseniz, giriş formuna girilen bilgiler gözardı edilir.',
+ 'New remote user' => 'Yeni uzak kullanıcı',
+ 'New local user' => 'Yeni yerel kullanıcı',
+ 'Default task color' => 'Varsayılan görev rengi',
+ 'This feature does not work with all browsers.' => 'Bu özellik tüm tarayıcılarla çalışmaz',
+ 'There is no destination project available.' => 'Seçilebilecek hedef proje yok.',
+ 'Trigger automatically subtask time tracking' => 'Altgörev zamanlayıcıyı otomatik olarak başlat',
+ 'Include closed tasks in the cumulative flow diagram' => 'Kümülatif akış diyagramında kapatılmış görevleri de dahil et',
+ 'Current swimlane: %s' => 'Geçerli kulvar: %s',
+ 'Current column: %s' => 'Geçerli sütun: %s',
+ 'Current category: %s' => 'Geçerli kategori: %s',
+ 'no category' => 'kategori yok',
+ 'Current assignee: %s' => 'Geçerli atanan: %s',
+ 'not assigned' => 'atanmamış',
+ 'Author:' => 'Yazar',
+ 'contributors' => 'Katkı sağlayanlar',
+ 'License:' => 'Lisans:',
+ 'License' => 'Lisans',
+ 'Enter the text below' => 'Aşağıdaki metni girin',
+ 'Gantt chart for %s' => '%s için Gantt diyagramı',
+ 'Sort by position' => 'Pozisyona göre sırala',
+ 'Sort by date' => 'Tarihe göre sırala',
+ 'Add task' => 'Görev ekle',
+ 'Start date:' => 'Başlangıç tarihi:',
+ 'Due date:' => 'Tamamlanması gereken tarih:',
+ 'There is no start date or due date for this task.' => 'Bu görev için başlangıç veya tamamlanması gereken tarih yok.',
+ 'Moving or resizing a task will change the start and due date of the task.' => 'Bir görevin boyutunu değiştirmek, görevin başlangıç ve tamamlanması gereken tarihlerini değiştirir.',
+ 'There is no task in your project.' => 'Projenizde hiç görev yok.',
+ 'Gantt chart' => 'Gantt diyagramı',
+ 'People who are project managers' => 'Proje yöneticisi olan kişiler',
+ 'People who are project members' => 'Proje üyesi olan kişiler',
// 'NOK - Norwegian Krone' => '',
- // 'Show this column' => '',
- // 'Hide this column' => '',
- // 'open file' => '',
- // 'End date' => '',
- // 'Users overview' => '',
- // 'Managers' => '',
- // 'Members' => '',
- // 'Shared project' => '',
- // 'Project managers' => '',
- // 'Project members' => '',
- // 'Gantt chart for all projects' => '',
- // 'Projects list' => '',
- // 'Gantt chart for this project' => '',
- // 'Project board' => '',
- // 'End date:' => '',
- // 'There is no start date or end date for this project.' => '',
- // 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
- // 'Link type' => '',
- // 'Change task color when using a specific task link' => '',
- // 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
- // 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
- // 'Documentation: %s' => '',
- // 'Switch to the Gantt chart view' => '',
- // 'Reset the search/filter box' => '',
- // 'Documentation' => '',
- // 'Table of contents' => '',
- // 'Gantt' => '',
- // 'Help with project permissions' => '',
- // 'Author' => '',
- // 'Version' => '',
- // 'Plugins' => '',
- // 'There is no plugin loaded.' => '',
- // 'Set maximum column height' => '',
- // 'Remove maximum column height' => '',
- // 'My notifications' => '',
- // 'Custom filters' => '',
- // 'Your custom filter have been created successfully.' => '',
- // 'Unable to create your custom filter.' => '',
- // 'Custom filter removed successfully.' => '',
- // 'Unable to remove this custom filter.' => '',
- // 'Edit custom filter' => '',
- // 'Your custom filter have been updated successfully.' => '',
- // 'Unable to update custom filter.' => '',
- // 'Web' => '',
- // 'New attachment on task #%d: %s' => '',
- // 'New comment on task #%d' => '',
- // 'Comment updated on task #%d' => '',
- // 'New subtask on task #%d' => '',
- // 'Subtask updated on task #%d' => '',
- // 'New task #%d: %s' => '',
- // 'Task updated #%d' => '',
- // 'Task #%d closed' => '',
- // 'Task #%d opened' => '',
- // 'Column changed for task #%d' => '',
- // 'New position for task #%d' => '',
- // 'Swimlane changed for task #%d' => '',
- // 'Assignee changed on task #%d' => '',
- // '%d overdue tasks' => '',
- // 'Task #%d is overdue' => '',
- // 'No new notifications.' => '',
- // 'Mark all as read' => '',
- // 'Mark as read' => '',
- // 'Total number of tasks in this column across all swimlanes' => '',
- // 'Collapse swimlane' => '',
- // 'Expand swimlane' => '',
- // 'Add a new filter' => '',
- // 'Share with all project members' => '',
- // 'Shared' => '',
- // 'Owner' => '',
- // 'Unread notifications' => '',
- // 'My filters' => '',
- // 'Notification methods:' => '',
- // 'Import tasks from CSV file' => '',
- // 'Unable to read your file' => '',
- // '%d task(s) have been imported successfully.' => '',
- // 'Nothing have been imported!' => '',
- // 'Import users from CSV file' => '',
- // '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
- // '%s attached a file to the task #%d' => '',
- // 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
- // 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
- // 'Usernames must be lowercase and unique' => '',
- // 'Passwords will be encrypted if present' => '',
+ 'Show this column' => 'Bu sütunu göster',
+ 'Hide this column' => 'Bu sütunu gizle',
+ 'open file' => 'dosyayı aç',
+ 'End date' => 'Bitiş tarihi',
+ 'Users overview' => 'Kullanıcılara genel bakış',
+ 'Members' => 'Üyeler',
+ 'Shared project' => 'Paylaşılan proje',
+ 'Project managers' => 'Proje yöneticileri',
+ 'Gantt chart for all projects' => 'Tüm projeler için Gantt diyagramı',
+ 'Projects list' => 'Proje listesi',
+ 'Gantt chart for this project' => 'Bu proje için Gantt diyagramı',
+ 'Project board' => 'Proje tablosu',
+ 'End date:' => 'Bitiş tarihi:',
+ 'There is no start date or end date for this project.' => 'Bu proje için başlangıç veya bitiş tarihi yok.',
+ 'Projects Gantt chart' => 'Projeler Gantt diyagramı',
+ '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şı',
+ 'Documentation: %s' => 'Dokümantasyon: %s',
+ 'Switch to the Gantt chart view' => 'Gantt diyagramı görünümüne geç',
+ 'Reset the search/filter box' => 'Arama/Filtre kutusunu sıfırla',
+ 'Documentation' => 'Dokümantasyon',
+ 'Table of contents' => 'İçindekiler',
+ 'Gantt' => 'Gantt',
+ 'Author' => 'Yazar',
+ 'Version' => 'Versiyon',
+ 'Plugins' => 'Eklentiler',
+ 'There is no plugin loaded.' => 'Yüklenmiş bir eklendi yok',
+ 'Set maximum column height' => 'Maksimum sütun yüksekliğini belirle',
+ 'Remove maximum column height' => 'Maksimum sütun yüksekliğini iptal et',
+ 'My notifications' => 'Bildirimlerim',
+ 'Custom filters' => 'Özel filtreler',
+ 'Your custom filter have been created successfully.' => 'Özel filtreleriniz başarıyla oluşturuldu.',
+ 'Unable to create your custom filter.' => 'Özel filtreniz oluşturulamadı.',
+ 'Custom filter removed successfully.' => 'Özel filtreniz başarıyla silindi.',
+ 'Unable to remove this custom filter.' => 'Bu özel filtre silinemiyor.',
+ 'Edit custom filter' => 'Özel filtreyi düzenle',
+ 'Your custom filter have been updated successfully.' => 'Özel filtreleriniz başarıyla güncellendi.',
+ 'Unable to update custom filter.' => 'Özel filtre güncellenemiyor.',
+ 'Web' => 'İnternet',
+ 'New attachment on task #%d: %s' => '#%d görevinde yeni dosya: %s',
+ 'New comment on task #%d' => '#%d görevinde yeni yorum',
+ 'Comment updated on task #%d' => '#%d görevinde yorum güncellendi',
+ 'New subtask on task #%d' => '#%d görevinde yeni alt görev',
+ 'Subtask updated on task #%d' => '#%d görevinde alt görev güncellendi',
+ 'New task #%d: %s' => 'Yeni #%d görevi: %s',
+ 'Task updated #%d' => '#%d görevi güncellendi',
+ 'Task #%d closed' => '#%d görevi kapatıldı',
+ 'Task #%d opened' => '#%d görevi oluşturuldu',
+ 'Column changed for task #%d' => '#%d görevinin sütunu değişti',
+ 'New position for task #%d' => '#%d görevinin konumu değişti',
+ 'Swimlane changed for task #%d' => '#%d görevinin kulvarı değişti',
+ 'Assignee changed on task #%d' => '#%d görevine atanan değişti',
+ '%d overdue tasks' => '%d gecikmiş görev',
+ 'Task #%d is overdue' => '#%d görevi gecikti',
+ 'No new notifications.' => 'Yeni bildirim yok.',
+ 'Mark all as read' => 'Tümünü okunmuş olarak işaretle',
+ 'Mark as read' => 'Okunmuş olarak işaretle',
+ 'Total number of tasks in this column across all swimlanes' => 'Bu sutündaki görev sayısının tüm kulvarlardaki toplamı',
+ 'Collapse swimlane' => 'Kulvarı daralt',
+ 'Expand swimlane' => 'Kulvarı genişlet',
+ 'Add a new filter' => 'Yeni bir filtre ekle',
+ 'Share with all project members' => 'Tüm proje üyeleriyle paylaş',
+ 'Shared' => 'Paylaşılan',
+ 'Owner' => 'Sahibi',
+ 'Unread notifications' => 'Okunmamış bildirimler',
+ 'My filters' => 'Filtrelerim',
+ 'Notification methods:' => 'Bildirim yöntemleri:',
+ 'Import tasks from CSV file' => 'CSV dosyasından görevleri içeri aktar',
+ 'Unable to read your file' => 'Dosya okunamıyor',
+ '%d task(s) have been imported successfully.' => '%d görev başarıyla içeri aktarıldı.',
+ 'Nothing have been imported!' => 'Hiçbir şey içeri aktarılamadı!',
+ 'Import users from CSV file' => 'CSV dosyasından kullanıcıları içeri aktar',
+ '%d user(s) have been imported successfully.' => '%d kullanıcı başarıyla içeri aktarıldı.',
+ 'Comma' => 'Virgül',
+ 'Semi-colon' => 'Noktalı virgül',
+ 'Tab' => 'Tab',
+ 'Vertical bar' => 'Dikey çizgi',
+ 'Double Quote' => 'Tırnak işareti',
+ 'Single Quote' => 'Kesme işareti',
+ '%s attached a file to the task #%d' => '%s, #%d görevine bir dosya ekledi.',
+ 'There is no column or swimlane activated in your project!' => 'Projenizde etkinleştirilmiş hiç bir sütun veya kulvar yok!',
+ 'Append filter (instead of replacement)' => 'Fıltreye ekle (üzerine yazmak yerine)',
+ 'Append/Replace' => 'Ekle/Üzerine yaz',
+ 'Append' => 'Ekle',
+ 'Replace' => 'Üzerine yaz',
+ 'Import' => 'İçeri aktar',
+ 'change sorting' => 'sıralamayı değiştir',
+ 'Tasks Importation' => 'Görevleri içeri aktar',
+ 'Delimiter' => 'Ayırıcı',
+ 'Enclosure' => 'Enclosure',
+ 'CSV File' => 'CSV Dosyası',
+ 'Instructions' => 'Yönergeler',
+ 'Your file must use the predefined CSV format' => 'Dosyanız önceden belirlenmiş CSV formatını kullanmalı',
+ 'Your file must be encoded in UTF-8' => 'Dosyanız UTF-8 kodlamasında olmalı',
+ 'The first row must be the header' => 'İlk satır başlık olmalı',
+ 'Duplicates are not verified for you' => 'Çift girişler sizin için onaylanmamış',
+ 'The due date must use the ISO format: YYYY-MM-DD' => 'Tamamlanma tarihi ISO formatını kullanmalı: YYYY-MM-DD',
+ 'Download CSV template' => 'CSV taslağını indir',
+ 'No external integration registered.' => 'Hiç dış entegrasyon kaydedilmemiş.',
+ 'Duplicates are not imported' => 'Çift girişler içeri aktarılmaz',
+ 'Usernames must be lowercase and unique' => 'Kullanıcı adları küçük harf ve tekil olmalı',
+ 'Passwords will be encrypted if present' => 'Şifreler (eğer varsa) kriptolanır',
+ '%s attached a new file to the task %s' => '%s, %s görevine yeni dosya ekledi',
+ 'Assign automatically a category based on a link' => 'Bir bağlantıya göre otomatik olarak kategori ata',
+ 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
+ 'Assignee Username' => 'Atanan kullanıcı adı',
+ 'Assignee Name' => 'Atanan İsmi',
+ 'Groups' => 'Gruplar',
+ 'Members of %s' => '%s in üyeleri',
+ 'New group' => 'Yeni grup',
+ 'Group created successfully.' => 'Grup başarıyla oluşturuldu.',
+ 'Unable to create your group.' => 'Grup oluşturulamadı.',
+ 'Edit group' => 'Grubu düzenle',
+ 'Group updated successfully.' => 'Grup başarıyla güncellendi.',
+ 'Unable to update your group.' => 'Grup güncellenemedi.',
+ 'Add group member to "%s"' => '"%s" e grup üyesi ekle',
+ 'Group member added successfully.' => 'Grup üyesi başarıyla eklendi.',
+ 'Unable to add group member.' => 'Grup üyesi eklenemedi.',
+ 'Remove user from group "%s"' => '"%s" grubundan kullanıcı çıkar',
+ 'User removed successfully from this group.' => 'Kullanıcı bu gruptan başarıyla çıkarıldı.',
+ 'Unable to remove this user from the group.' => 'Bu kullanıcı bu grubtan çıkarılamadı',
+ 'Remove group' => 'Grubu sil',
+ 'Group removed successfully.' => 'Grup başarıyla silindi.',
+ 'Unable to remove this group.' => 'Grup silinemedi.',
+ 'Project Permissions' => 'Proje izimleri',
+ 'Manager' => 'Yönetici',
+ 'Project Manager' => 'Proje yöneticisi',
+ 'Project Member' => 'Proje üyesi',
+ 'Project Viewer' => 'Proje izleyicisi',
+ 'Your account is locked for %d minutes' => 'Hesabınız %d dakika boyunca kilitlendi',
+ 'Invalid captcha' => 'Geçersiz captcha',
+ 'The name must be unique' => 'İsim tekil olmalı',
+ 'View all groups' => 'Tüm grupları görüntüle',
+ 'View group members' => 'Grup üyelerini görüntüle',
+ 'There is no user available.' => 'Uygun üye yok',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => '"%s" kullanıcısını "%s" grubundan çıkarmak istediğinize emin misiniz?',
+ 'There is no group.' => 'Hiç grup yok.',
+ 'External Id' => 'Harici Kimlik',
+ 'Add group member' => 'Grup üyesi ekle',
+ 'Do you really want to remove this group: "%s"?' => '"%s" grubunu silmek istediğinize emin misiniz?',
+ 'There is no user in this group.' => 'Bu grupta hiç kullanıcı yok.',
+ 'Remove this user' => 'Bu kullanıcıyı sil',
+ 'Permissions' => 'İzinler',
+ 'Allowed Users' => 'İzin verilen kullanıcı',
+ 'No user have been allowed specifically.' => 'Hiç bir kullanıcıya özel olarak izin verilmemiş.',
+ 'Role' => 'Rol',
+ 'Enter user name...' => 'Kullanıcı adını girin...',
+ 'Allowed Groups' => 'İzinli gruplar',
+ 'No group have been allowed specifically.' => 'Hiç bir gruba özel olarak izin verilmemiş',
+ 'Group' => 'Grup',
+ 'Group Name' => 'Grup adı',
+ 'Enter group name...' => 'Grup adını girin...',
+ 'Role:' => 'Rol:',
+ 'Project members' => 'Proje üyeleri',
+ 'Compare hours for "%s"' => '"%s" için saatleri karşılaştır',
+ '%s mentioned you in the task #%d' => '%s sizden #%d görevinde bahsetti',
+ '%s mentioned you in a comment on the task #%d' => '%s sizden #%d görevindeki bir yorumda bahsetti',
+ 'You were mentioned in the task #%d' => '#%d görevinde sizden bahsedildi',
+ 'You were mentioned in a comment on the task #%d' => '#%d görevindeki bir yorumda sizden bahsedildi',
+ 'Mentioned' => 'Bahsedilmiş',
+ 'Compare Estimated Time vs Actual Time' => 'Tahmini süre ile gerçekleşen süreyi karşılaştır',
+ 'Estimated hours: ' => 'Tahmini saat:',
+ 'Actual hours: ' => 'Gerçekleşen saat:',
+ 'Hours Spent' => 'Harcanan saat',
+ 'Hours Estimated' => 'Tahmini saat',
+ 'Estimated Time' => 'Tahmini süre',
+ 'Actual Time' => 'Gerçekleşen süre',
+ 'Estimated vs actual time' => 'Tahmini vs gerçekleşen süre',
+ // 'RUB - Russian Ruble' => '',
+ 'Assign the task to the person who does the action when the column is changed' => 'Sütun değiştirildiği zaman görevi eylemi gerçekleştiren kişiye ata',
+ 'Close a task in a specific column' => 'Belirli bir sütundaki görevi kapat',
+ 'Time-based One-time Password Algorithm' => 'Zamana bağlı tek kullanımlık şifre algoritması',
+ 'Two-Factor Provider: ' => 'Çift kademeli doğrulama sağlayıcısı',
+ 'Disable two-factor authentication' => 'Çift kademeli doğrulamayı devre dışı bırak',
+ 'Enable two-factor authentication' => 'Çift kademeli doğrulamayı etkinleştir',
+ 'There is no integration registered at the moment.' => 'Şu anda kayıtlı bir entegrasyon bulunmuyor.',
+ 'Password Reset for Kanboard' => 'Kanboard için şifre sıfırlama',
+ 'Forgot password?' => 'Şifrenizi mi unuttunuz?',
+ 'Enable "Forget Password"' => '"Şifremi unuttum" komutunu etkinleştir',
+ 'Password Reset' => 'Şifre sıfırlama',
+ 'New password' => 'Yeni şifre',
+ 'Change Password' => 'Şifreyi değiştir',
+ 'To reset your password click on this link:' => 'Şifrenizi sıfırlamak için bu linke tıklayın:',
+ 'Last Password Reset' => 'Son şifre sıfırlama',
+ 'The password has never been reinitialized.' => 'Şifre hiç bir zaman tekrar başlatılmamış.',
+ 'Creation' => 'Oluşturulma',
+ 'Expiration' => 'Sona erme',
+ 'Password reset history' => 'Şifre sıfırlama geçmişi',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '"%s" sütunu ve "%s" kulvarındaki tüm görevler başarıyla kapatıldı.',
+ 'Do you really want to close all tasks of this column?' => 'Bu sütundaki tüm görevleri kapatmak istediğinize emin misiniz?',
+ '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '"%s" sütunu ve "%s" kulvarındaki %d görev kapatılacak.',
+ 'Close all tasks of this column' => 'Bu sütundaki tüm görevleri kapat',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ // 'My dashboard' => '',
+ // 'My profile' => '',
+ // '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' => '',
+ // 'List of external links' => '',
+ // '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' => '',
+ // '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)' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index cc7de564..55d5463d 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -20,15 +20,15 @@ return array(
'Red' => '红色',
'Orange' => '橘色',
'Grey' => '灰色',
- // 'Brown' => '',
- // 'Deep Orange' => '',
- // 'Dark Grey' => '',
- // 'Pink' => '',
- // 'Teal' => '',
- // 'Cyan' => '',
- // 'Lime' => '',
- // 'Light Green' => '',
- // 'Amber' => '',
+ 'Brown' => '褐色',
+ 'Deep Orange' => '橘红色',
+ 'Dark Grey' => '深灰色',
+ 'Pink' => '粉红色',
+ 'Teal' => '青色',
+ 'Cyan' => '蓝绿色',
+ 'Lime' => '绿黄色',
+ 'Light Green' => '浅绿色',
+ 'Amber' => '黄褐色',
'Save' => '保存',
'Login' => '登录',
'Official website:' => '官方网站:',
@@ -40,7 +40,7 @@ return array(
'All users' => '所有用户',
'Username' => '用户名',
'Password' => '密码',
- 'Administrator' => '管理员',
+ 'Administrator' => '超级管理员',
'Sign in' => '登录',
'Users' => '用户',
'No user' => '没有用户',
@@ -72,7 +72,6 @@ return array(
'Remove project' => '移除项目',
'Edit the board for "%s"' => '为"%s"修改看板',
'All projects' => '所有项目',
- 'Change columns' => '更改栏目',
'Add a new column' => '添加新栏目',
'Title' => '标题',
'Nobody assigned' => '无人被指派',
@@ -102,14 +101,11 @@ return array(
'Open a task' => '开一个任务',
'Do you really want to open this task: "%s"?' => '你确定要开这个任务吗?"%s"',
'Back to the board' => '回到看板',
- 'Created on %B %e, %Y at %k:%M %p' => '创建时间:%Y/%m/%d %H:%M',
- 'There is nobody assigned' => '无人负责',
+ 'There is nobody assigned' => '当前无人被指派',
'Column on the board:' => '看板上的栏目:',
- 'Status is open' => '开启状态',
- 'Status is closed' => '关闭状态',
'Close this task' => '关闭该任务',
'Open this task' => '开启该任务',
- 'There is no description.' => '无描述。',
+ 'There is no description.' => '当前没有描述。',
'Add a new task' => '添加新任务',
'The username is required' => '需要用户名',
'The maximum length is %d characters' => '最长%d个英文字符',
@@ -124,7 +120,6 @@ return array(
'The id is required' => '需要指定id',
'The project id is required' => '需要指定项目id',
'The project name is required' => '需要指定项目名称',
- 'This project must be unique' => '项目名称必须唯一',
'The title is required' => '需要指定标题',
'Settings saved successfully.' => '设置成功保存。',
'Unable to save your settings.' => '无法保存你的设置。',
@@ -159,10 +154,6 @@ return array(
'Work in progress' => '工作进行中',
'Done' => '完成',
'Application version:' => '应用程序版本:',
- 'Completed on %B %e, %Y at %k:%M %p' => '于%Y/%m/%d %H:%M 完成',
- '%B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M',
- 'Date created' => '创建时间',
- 'Date completed' => '完成时间',
'Id' => '编号',
'%d closed tasks' => '%d个已关闭任务',
'No task for this project' => '该项目尚无任务',
@@ -175,13 +166,7 @@ return array(
'Complexity' => '复杂度',
'Task limit' => '任务限制',
'Task count' => '任务数',
- 'Edit project access list' => '编辑项目存取列表',
- 'Allow this user' => '允许该用户',
- 'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。',
- 'Revoke' => '撤销',
- 'List of authorized users' => '已授权的用户列表',
'User' => '用户',
- 'Nobody have access to this project.' => '无用户可以访问此项目.',
'Comments' => '评论',
'Write your text in Markdown' => '用Markdown格式编写',
'Leave a comment' => '留言',
@@ -192,9 +177,6 @@ return array(
'Edit this task' => '编辑该任务',
'Due Date' => '到期时间',
'Invalid date' => '无效日期',
- 'Must be done before %B %e, %Y' => '必须在%Y/%m/%d前完成',
- '%B %e, %Y' => '%Y/%m/%d',
- // '%b %e, %Y' => '',
'Automatic actions' => '自动动作',
'Your automatic action have been created successfully.' => '您的自动动作已成功创建',
'Unable to create your automatic action.' => '无法为您创建自动动作。',
@@ -225,8 +207,6 @@ return array(
'Assign a color to a specific user' => '为特定用户指派颜色',
'Column title' => '栏目名称',
'Position' => '位置',
- 'Move Up' => '往上移',
- 'Move Down' => '往下移',
'Duplicate to another project' => '复制到另一项目',
'Duplicate' => '复制',
'link' => '连接',
@@ -236,7 +216,6 @@ return array(
'Comment removed successfully.' => '评论成功移除。',
'Unable to remove this comment.' => '无法移除该评论。',
'Do you really want to remove this comment?' => '确定要移除评论吗?',
- 'Only administrators or the creator of the comment can access to this page.' => '只有管理员或评论创建者可以进入该页面。',
'Current password for the user "%s"' => '用户"%s"的当前密码',
'The current password is required' => '需要输入当前密码',
'Wrong password' => '密码错误',
@@ -248,7 +227,7 @@ return array(
'User agent' => '浏览器标识',
'Persistent connections' => '持续连接',
'No session.' => '无会话',
- 'Expiration date' => '过期',
+ 'Expiration date' => '过期日期',
'Remember Me' => '记住我',
'Creation date' => '创建日期',
'Everybody' => '所有人',
@@ -262,15 +241,11 @@ return array(
'%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.' => '',
+ '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' => '电子邮件',
- 'Link my Google Account' => '关联我的google帐号',
- 'Unlink my Google Account' => '去除我的google帐号关联',
- 'Login with my Google Account' => '用我的google帐号登录',
- 'Project not found.' => '未发现项目',
'Task removed successfully.' => '任务成功去除',
'Unable to remove this task.' => '无法移除该任务。',
'Remove a task' => '移除一个任务',
@@ -334,11 +309,7 @@ return array(
'Maximum size: ' => '大小上限:',
'Unable to upload the file.' => '无法上传文件',
'Display another project' => '显示其它项目',
- 'Login with my Github Account' => '用Github账号登录',
- 'Link my Github Account' => '链接Github账号',
- 'Unlink my Github Account' => '取消Github账号链接',
'Created by %s' => '创建者:%s',
- 'Last modified on %B %e, %Y at %k:%M %p' => '最后修改:%Y/%m/%d/ %H:%M',
'Tasks Export' => '任务导出',
'Tasks exportation for "%s"' => '导出"%s"的任务',
'Start Date' => '开始时间',
@@ -374,7 +345,6 @@ return array(
'I want to receive notifications only for those projects:' => '我仅需要收到下面项目的通知:',
'view the task on Kanboard' => '在看板中查看此任务',
'Public access' => '公开访问',
- 'User management' => '用户管理',
'Active tasks' => '活动任务',
'Disable public access' => '停止公开访问',
'Enable public access' => '开启公开访问',
@@ -397,18 +367,12 @@ return array(
'Email:' => '电子邮件:',
'Notifications:' => '通知:',
'Notifications' => '通知',
- 'Group:' => '群组:',
- 'Regular user' => '常规用户',
'Account type:' => '账户类型:',
'Edit profile' => '编辑属性',
'Change password' => '修改密码',
'Password modification' => '修改密码',
'External authentications' => '外部认证',
- 'Google Account' => '谷歌账号',
- 'Github Account' => 'Github 账号',
'Never connected.' => '从未连接。',
- 'No account linked.' => '未链接账号。',
- 'Account linked.' => '已经链接账号。',
'No external authentication enabled.' => '未启用外部认证。',
'Password modified successfully.' => '已经成功修改密码。',
'Unable to change the password.' => '无法修改密码。',
@@ -446,17 +410,10 @@ return array(
'%s changed the assignee of the task %s to %s' => '%s 将任务 %s 分配给 %s',
'New password for the user "%s"' => '用户"%s"的新密码',
'Choose an event' => '选择一个事件',
- 'Github commit received' => '收到了Github提交',
- 'Github issue opened' => '开启了Github问题报告',
- 'Github issue closed' => '关闭了Github问题报告',
- 'Github issue reopened' => '重新开启Github问题',
- 'Github issue assignee change' => 'Github问题负责人已经变更',
- 'Github issue label change' => 'Github任务标签修改',
'Create a task from an external provider' => '从外部创建任务',
'Change the assignee based on an external username' => '根据外部用户名修改任务分配',
'Change the category based on an external label' => '根据外部标签修改分类',
'Reference' => '参考',
- 'Reference: %s' => '参考:%s',
'Label' => '标签',
'Database' => '数据库',
'About' => '关于',
@@ -474,7 +431,6 @@ return array(
'Frequency in second (60 seconds by default)' => '频率,单位为秒(默认是60秒)',
'Frequency in second (0 to disable this feature, 10 seconds by default)' => '频率,单位为秒(设置为0停用此功能,默认是10秒)',
'Application URL' => '应用URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => '例如:http://example.kanboard.net/ (用于电子邮件通知)',
'Token regenerated.' => '重新生成令牌',
'Date format' => '日期格式',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO 格式总是允许的,例如:"%s" 和 "%s"',
@@ -482,12 +438,9 @@ return array(
'This project is private' => '此项目为私有项目',
'Type here to create a new sub-task' => '要创建新的子任务,请在此输入',
'Add' => '添加',
- 'Estimated time: %s hours' => '预计时间:%s小时',
- 'Time spent: %s hours' => '花费时间:%s小时',
- 'Started on %B %e, %Y' => '开始时间: %Y/%m/%d %H:%M ',
'Start date' => '启动日期',
'Time estimated' => '预计时间',
- 'There is nothing assigned to you.' => '无任务指派给你。',
+ 'There is nothing assigned to you.' => '当前无任务指派给你。',
'My tasks' => '我的任务',
'Activity stream' => '动态记录',
'Dashboard' => '面板',
@@ -496,10 +449,7 @@ return array(
'Everybody have access to this project.' => '所有人都可以访问此项目',
'Webhooks' => '网络钩子',
'API' => '应用程序接口',
- 'Github webhooks' => 'Github 网络钩子',
- 'Help on Github webhooks' => 'Github 网络钩子帮助',
'Create a comment from an external provider' => '从外部创建一个评论',
- 'Github issue comment created' => '已经创建了Github问题评论',
'Project management' => '项目管理',
'My projects' => '我的项目',
'Columns' => '栏目',
@@ -517,7 +467,6 @@ return array(
'User repartition for "%s"' => '"%s"的用户分析',
'Clone this project' => '复制此项目',
'Column removed successfully.' => '成功删除了栏目。',
- 'Github Issue' => 'Github 任务报告',
'Not enough data to show the graph.' => '数据不足,无法绘图。',
'Previous' => '后退',
'The id must be an integer' => '编号必须为整数',
@@ -541,41 +490,27 @@ return array(
'Nothing to preview...' => '没有需要预览的内容',
'Preview' => '预览',
'Write' => '书写',
- 'Active swimlanes' => '活动泳道',
- 'Add a new swimlane' => '添加新泳道',
- 'Change default swimlane' => '修改默认泳道',
- 'Default swimlane' => '默认泳道',
- 'Do you really want to remove this swimlane: "%s"?' => '确定要删除泳道:"%s"?',
- 'Inactive swimlanes' => '非活动泳道',
- 'Set project manager' => '设为项目经理',
- 'Set project member' => '设为项目成员',
- 'Remove a swimlane' => '删除泳道',
- 'Rename' => '重命名',
- 'Show default swimlane' => '显示默认泳道',
- 'Swimlane modification for the project "%s"' => '项目"%s"的泳道变更',
- 'Swimlane not found.' => '未找到泳道。',
- 'Swimlane removed successfully.' => '成功删除泳道',
- 'Swimlanes' => '泳道',
- 'Swimlane updated successfully.' => '成功更新了泳道。',
- 'The default swimlane have been updated successfully.' => '成功更新了默认泳道。',
- 'Unable to create your swimlane.' => '无法创建泳道。',
- 'Unable to remove this swimlane.' => '无法删除此泳道',
- 'Unable to update this swimlane.' => '无法更新此泳道',
- 'Your swimlane have been created successfully.' => '已经成功创建泳道。',
+ 'Active swimlanes' => '活动里程碑',
+ 'Add a new swimlane' => '添加新里程碑',
+ 'Change default swimlane' => '修改默认里程碑',
+ 'Default swimlane' => '默认里程碑',
+ 'Do you really want to remove this swimlane: "%s"?' => '确定要删除里程碑:"%s"?',
+ 'Inactive swimlanes' => '非活动里程碑',
+ 'Remove a swimlane' => '删除里程碑',
+ 'Show default swimlane' => '显示默认里程碑',
+ 'Swimlane modification for the project "%s"' => '项目"%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)' => '新项目的默认分类(用逗号分隔)',
- 'Gitlab commit received' => '收到 Gitlab 提交',
- 'Gitlab issue opened' => '开启 Gitlab 问题',
- 'Gitlab issue closed' => '关闭 Gitlab 问题',
- 'Gitlab webhooks' => 'Gitlab 网络钩子',
- 'Help on Gitlab webhooks' => 'Gitlab 网络钩子帮助',
'Integrations' => '整合',
'Integration with third-party services' => '与第三方服务进行整合',
- 'Role for this project' => '项目角色',
- 'Project manager' => '项目管理员',
- 'Project member' => '项目成员',
- 'A project manager can change the settings of the project and have more privileges than a standard user.' => '项目经理可以修改项目的设置,比标准用户多了一些权限',
- 'Gitlab Issue' => 'Gitlab 问题',
'Subtask Id' => '子任务 Id',
'Subtasks' => '子任务',
'Subtasks Export' => '子任务导出',
@@ -589,7 +524,7 @@ return array(
'Calendar' => '日程表',
'Next' => '前进',
'#%d' => '#%d',
- 'All swimlanes' => '全部泳道',
+ 'All swimlanes' => '全部里程碑',
'All colors' => '全部颜色',
'Moved to column %s' => '移动到栏目 %s',
'Change description' => '修改描述',
@@ -598,14 +533,11 @@ return array(
'Edit column "%s"' => '编辑栏目"%s"',
'Select the new status of the subtask: "%s"' => '选择子任务的新状态:"%s"',
'Subtask timesheet' => '子任务时间',
- 'There is nothing to show.' => '无内容。',
+ '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' => '',
- 'Bitbucket commit received' => '收到Bitbucket提交',
- 'Bitbucket webhooks' => 'Bitbucket网络钩子',
- 'Help on Bitbucket webhooks' => 'Bitbucket网络钩子帮助',
+ 'Disallow login form' => '禁止登陆',
'Start' => '开始',
'End' => '结束',
'Task age in days' => '任务存在天数',
@@ -627,26 +559,25 @@ return array(
'Remove a link' => '删除关联',
'Task\'s links' => '任务的关联',
'The labels must be different' => '标签不能一样',
- 'There is no link.' => '没有关联',
+ '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' => '',
+ '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' => '<1h',
'%dh' => '%dh',
- // '%b %e' => '',
'Expand tasks' => '展开任务',
'Collapse tasks' => '收缩任务',
'Expand/collapse tasks' => '展开/收缩任务',
@@ -656,27 +587,24 @@ return array(
'Keyboard shortcuts' => '键盘快捷方式',
'Open board switcher' => '打开面板切换器',
'Application' => '应用程序',
- // 'since %B %e, %Y at %k:%M %p' => '',
'Compact view' => '紧凑视图',
'Horizontal scrolling' => '水平滚动',
'Compact/wide view' => '紧凑/宽视图',
'No results match:' => '无匹配结果:',
'Currency' => '货币',
- 'Files' => '文件',
- 'Images' => '图片',
'Private project' => '私人项目',
- // 'AUD - Australian Dollar' => '',
- // 'CAD - Canadian Dollar' => '',
- // 'CHF - Swiss Francs' => '',
+ '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' => '',
+ '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' => '移除负责人时移动到其它栏目',
@@ -703,147 +631,123 @@ return array(
'The two factor authentication code is valid.' => '双重认证码正确。',
'Code' => '认证码',
'Two factor authentication' => '双重认证',
- 'Enable/disable two factor authentication' => '启用/禁用双重认证',
'This QR code contains the key URI: ' => '此二维码包含密码 URI:',
- 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '将密码保存到 TOTP 软件(例如Google 认证或 FreeOTP)',
'Check my code' => '检查我的认证码',
'Secret key: ' => '密码:',
'Test your device' => '测试设备',
'Assign a color when the task is moved to a specific column' => '任务移动到指定栏目时设置颜色',
'%s via Kanboard' => '%s 通过KanBoard',
- 'uploaded by: %s' => '由: %s 上传',
- // 'uploaded on: %s' => '',
- // 'size: %s' => '',
- // 'Burndown chart for "%s"' => '',
- // 'Burndown chart' => '',
- // 'This chart show the task complexity over the time (Work Remaining).' => '',
+ 'Burndown chart for "%s"' => '燃尽图:"%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.' => '',
- // 'Screenshot uploaded successfully.' => '',
- // 'SEK - Swedish Krona' => '',
- // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
- // 'Identifier' => '',
- // 'Disable two factor authentication' => '',
- // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
+ '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"?' => '你真的要禁用 "%s" 的双重认证吗?',
'Edit link' => '编辑链接',
- // 'Start to type task title...' => '',
- // 'A task cannot be linked to itself' => '',
- // 'The exact same link already exists' => '',
- // 'Recurrent task is scheduled to be generated' => '',
- // 'Recurring information' => '',
- // 'Score' => '',
- // 'The identifier must be unique' => '',
- // 'This linked task id doesn\'t exists' => '',
- // 'This value must be alphanumeric' => '',
- // 'Edit recurrence' => '',
- // 'Generate recurrent task' => '',
- // 'Trigger to generate recurrent task' => '',
- // 'Factor to calculate new due date' => '',
- // 'Timeframe to calculate new due date' => '',
- // 'Base date to calculate new due date' => '',
- // 'Action date' => '',
- // 'Base date to calculate new due date: ' => '',
- // 'This task has created this child task: ' => '',
- // 'Day(s)' => '',
- // 'Existing due date' => '',
- // 'Factor to calculate new due date: ' => '',
- // 'Month(s)' => '',
- // 'Recurrence' => '',
- // 'This task has been created by: ' => '',
- // 'Recurrent task has been generated:' => '',
- // 'Timeframe to calculate new due date: ' => '',
- // 'Trigger to generate recurrent task: ' => '',
- // 'When task is closed' => '',
- // 'When task is moved from first column' => '',
- // 'When task is moved to last column' => '',
- // 'Year(s)' => '',
+ '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 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' => '',
+ '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' => '',
+ 'iCal feed' => '日历订阅',
'Preferences' => '偏好',
'Security' => '安全',
- // 'Two factor authentication disabled' => '',
- // 'Two factor authentication enabled' => '',
- // 'Unable to update this user.' => '',
- // 'There is no user management for private projects.' => '',
- // 'User that will receive the email' => '',
+ 'Two factor authentication disabled' => '双重认证已禁用',
+ 'Two factor authentication enabled' => '双重认证已启用',
+ 'Unable to update this user.' => '无法更新此用户',
+ 'There is no user management for private projects.' => '私有项目下无用户可管理',
+ 'User that will receive the email' => '用户将收到邮件',
'Email subject' => '邮件主题',
'Date' => '日期',
- // 'By @%s on Bitbucket' => '',
- // 'Bitbucket Issue' => '',
- // 'Commit made by @%s on Bitbucket' => '',
- // 'Commit made by @%s on Github' => '',
- // 'By @%s on Github' => '',
- // 'Commit made by @%s on Gitlab' => '',
- // 'Add a comment log when moving the task between columns' => '',
- // 'Move the task to another column when the category is changed' => '',
- // 'Send a task by email to someone' => '',
+ 'Add a comment log when moving the task between columns' => '当任务移动到任务栏时添加评论日志',
+ 'Move the task to another column when the category is changed' => '当任务分类改变时移动到任务栏',
+ 'Send a task by email to someone' => '发送任务邮件到用户',
'Reopen a task' => '重新开始一个任务',
- // 'Bitbucket issue opened' => '',
- // 'Bitbucket issue closed' => '',
- // 'Bitbucket issue reopened' => '',
- // 'Bitbucket issue assignee change' => '',
- // 'Bitbucket issue comment created' => '',
- // 'Column change' => '',
- // 'Position change' => '',
- // 'Swimlane change' => '',
- // 'Assignee change' => '',
- // '[%s] Overdue tasks' => '',
+ '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' => '',
- // 'The task is not assigned anymore' => '',
- // 'New assignee: %s' => '',
- // 'There is no category now' => '',
- // 'New category: %s' => '',
- // 'New color: %s' => '',
- // 'New complexity: %d' => '',
- // 'The due date have been removed' => '',
- // 'There is no description anymore' => '',
- // 'Recurrence settings have been modified' => '',
- // 'Time spent changed: %sh' => '',
- // 'Time estimated changed: %sh' => '',
- // 'The field "%s" have been updated' => '',
- // 'The description have been modified' => '',
- // 'Do you really want to close the task "%s" as well as all subtasks?' => '',
- // 'Swimlane: %s' => '',
- // 'I want to receive notifications for:' => '',
- // 'All tasks' => '',
- // 'Only for tasks assigned to me' => '',
- // 'Only for tasks created by me' => '',
- // 'Only for tasks created by me and assigned to me' => '',
- // '%A' => '',
- // '%b %e, %Y, %k:%M %p' => '',
- // 'New due date: %B %e, %Y' => '',
- // 'Start date changed: %B %e, %Y' => '',
- // '%k:%M %p' => '',
+ '%s moved the task #%d to the first swimlane' => '%s将任务#%d移动到了首个里程碑',
+ '%s moved the task #%d to the swimlane "%s"' => '%s将任务#%d移动到了里程碑"%s"下',
+ 'Swimlane' => '里程碑',
+ 'Gravatar' => 'Gravatar头像',
+ '%s moved the task %s to the first swimlane' => '%s将任务%s移动到了首个里程碑',
+ '%s moved the task %s to the swimlane "%s"' => '%s将任务%s移动到了里程碑"%s"下',
+ 'This report contains all subtasks information for the given date range.' => '该报告包含了指定日期范围内的所有子任务信息。',
+ 'This report contains all tasks information for the given date range.' => '该报告包含了指定日期范围内的所有任务信息。',
+ 'Project activities for %s' => '%s的项目活动记录',
+ 'view the board on Kanboard' => '在看板上查看面板',
+ 'The task have been moved to the first swimlane' => '该任务已被移动到首个里程碑',
+ 'The task have been moved to another swimlane:' => '该任务已被移动到别的里程碑:',
+ 'Overdue tasks for the project "%s"' => '"%s"项目下的超期任务',
+ 'New title: %s' => '新标题:%s',
+ 'The task is not assigned anymore' => '该任务没有指派人',
+ 'New assignee: %s' => '新指派到:%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' => '时间花费已变更:%sh',
+ 'Time estimated changed: %sh' => '时间预估已变更:%sh',
+ 'The field "%s" have been updated' => '"%s"字段已更新',
+ 'The description have 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' => '',
- // '<30m' => '',
- 'Stop timer' => '停止定时器',
- 'Start timer' => '开启定时器',
+ 'Total for all columns' => '所有栏目下的',
+ 'You need at least 2 days of data to show the chart.' => '当前柱状图至少需要2天的数据。',
+ '<15m' => '小于15分钟',
+ '<30m' => '小于30分钟',
+ 'Stop timer' => '停止计时器',
+ 'Start timer' => '开启计时器',
'Add project member' => '添加项目成员',
'Enable notifications' => '打开通知',
'My activity stream' => '我的活动流',
@@ -861,31 +765,30 @@ return array(
'Not assigned' => '未指派',
'View advanced search syntax' => '查看高级搜索语法',
'Overview' => '概览',
- // '%b %e %Y' => '',
'Board/Calendar/List view' => '看板/日程/列表视图',
'Switch to the board view' => '切换到看板视图',
'Switch to the calendar view' => '切换到日程视图',
'Switch to the list view' => '切换到列表视图',
'Go to the search/filter box' => '前往搜索/过滤箱',
- 'There is no activity yet.' => '目前无任何活动.',
+ 'There is no activity yet.' => '当前无任何活动.',
'No tasks found.' => '没有找人任何任务.',
'Keyboard shortcut: "%s"' => '快捷键: "%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: ' => '',
+ '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 time spent into each column for "%s"' => '"%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.' => '当前柱状图表示最新%d条任务在每个任务栏下的平均花费时间',
// 'Average Lead and Cycle time' => '',
// 'Average lead time: ' => '',
// 'Average cycle time: ' => '',
@@ -894,10 +797,6 @@ return array(
// 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
// 'Average time into each column' => '',
// 'Lead and cycle time' => '',
- // 'Google Authentication' => '',
- // 'Help on Google authentication' => '',
- // 'Github Authentication' => '',
- // 'Help on Github authentication' => '',
// 'Lead time: ' => '',
// 'Cycle time: ' => '',
// 'Time spent into each column' => '',
@@ -905,26 +804,20 @@ return array(
// 'The cycle time is the duration between the start date and the completion.' => '',
// 'If the task is not closed the current time is used instead of the completion date.' => '',
// 'Set automatically the start date' => '',
- // 'Edit Authentication' => '',
- // 'Google Id' => '',
- // 'Github Id' => '',
- // 'Remote user' => '',
- // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
- // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
- // 'By @%s on Gitlab' => '',
- // 'Gitlab issue comment created' => '',
- // 'New remote user' => '',
- // 'New local user' => '',
+ 'Edit Authentication' => '编辑认证信息',
+ 'Remote user' => '远程用户',
+ 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '远程用户不会在看板数据库保存密码,例如:LDAP,GOOGLE,GitHub。',
+ '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' => '默认任务颜色',
- // 'Hide sidebar' => '',
- // 'Expand sidebar' => '',
- // 'This feature does not work with all browsers.' => '',
- // 'There is no destination project available.' => '',
+ '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' => '',
+ 'Current swimlane: %s' => '当前里程碑:%s',
+ 'Current column: %s' => '当前任务栏:%s',
+ 'Current category: %s' => '当前分类:%s',
'no category' => '无分类',
'Current assignee: %s' => '当前被指派人: %s',
'not assigned' => '未指派',
@@ -932,7 +825,6 @@ return array(
'contributors' => '贡献者',
'License:' => '授权许可:',
'License' => '授权许可',
- 'Project Administrator' => '项目管理者',
'Enter the text below' => '输入下方的文本',
'Gantt chart for %s' => '%s的甘特图',
'Sort by position' => '按位置排序',
@@ -940,128 +832,320 @@ return array(
'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.' => '',
+ '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' => '',
+ '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' => '用户概览',
- // 'Managers' => '',
'Members' => '成员',
- // 'Shared project' => '',
- // 'Project managers' => '',
- // 'Project members' => '',
- // 'Gantt chart for all projects' => '',
- // 'Projects list' => '',
- // 'Gantt chart for this project' => '',
- // 'Project board' => '',
- // 'End date:' => '',
- // 'There is no start date or end date for this project.' => '',
- // 'Projects Gantt chart' => '',
- // 'Start date: %s' => '',
- // 'End date: %s' => '',
- // 'Link type' => '',
- // 'Change task color when using a specific task link' => '',
- // 'Task link creation or modification' => '',
- // 'Login with my Gitlab Account' => '',
- // 'Milestone' => '',
- // 'Gitlab Authentication' => '',
- // 'Help on Gitlab authentication' => '',
- // 'Gitlab Id' => '',
- // 'Gitlab Account' => '',
- // 'Link my Gitlab Account' => '',
- // 'Unlink my Gitlab Account' => '',
- // 'Documentation: %s' => '',
- // 'Switch to the Gantt chart view' => '',
- // 'Reset the search/filter box' => '',
- // 'Documentation' => '',
- // 'Table of contents' => '',
- // 'Gantt' => '',
- // 'Help with project permissions' => '',
- // 'Author' => '',
- // 'Version' => '',
- // 'Plugins' => '',
- // 'There is no plugin loaded.' => '',
- // 'Set maximum column height' => '',
- // 'Remove maximum column height' => '',
- // 'My notifications' => '',
- // 'Custom filters' => '',
- // 'Your custom filter have been created successfully.' => '',
- // 'Unable to create your custom filter.' => '',
- // 'Custom filter removed successfully.' => '',
- // 'Unable to remove this custom filter.' => '',
- // 'Edit custom filter' => '',
- // 'Your custom filter have been updated successfully.' => '',
- // 'Unable to update custom filter.' => '',
- // 'Web' => '',
- // 'New attachment on task #%d: %s' => '',
- // 'New comment on task #%d' => '',
- // 'Comment updated on task #%d' => '',
- // 'New subtask on task #%d' => '',
- // 'Subtask updated on task #%d' => '',
- // 'New task #%d: %s' => '',
- // 'Task updated #%d' => '',
- // 'Task #%d closed' => '',
- // 'Task #%d opened' => '',
- // 'Column changed for task #%d' => '',
- // 'New position for task #%d' => '',
- // 'Swimlane changed for task #%d' => '',
- // 'Assignee changed on task #%d' => '',
- // '%d overdue tasks' => '',
- // 'Task #%d is overdue' => '',
- // 'No new notifications.' => '',
- // 'Mark all as read' => '',
- // 'Mark as read' => '',
- // 'Total number of tasks in this column across all swimlanes' => '',
- // 'Collapse swimlane' => '',
- // 'Expand swimlane' => '',
- // 'Add a new filter' => '',
- // 'Share with all project members' => '',
- // 'Shared' => '',
- // 'Owner' => '',
- // 'Unread notifications' => '',
- // 'My filters' => '',
- // 'Notification methods:' => '',
- // 'Import tasks from CSV file' => '',
- // 'Unable to read your file' => '',
- // '%d task(s) have been imported successfully.' => '',
- // 'Nothing have been imported!' => '',
- // 'Import users from CSV file' => '',
- // '%d user(s) have been imported successfully.' => '',
- // 'Comma' => '',
- // 'Semi-colon' => '',
- // 'Tab' => '',
- // 'Vertical bar' => '',
- // 'Double Quote' => '',
- // 'Single Quote' => '',
- // '%s attached a file to the task #%d' => '',
- // 'There is no column or swimlane activated in your project!' => '',
- // 'Append filter (instead of replacement)' => '',
- // 'Append/Replace' => '',
- // 'Append' => '',
- // 'Replace' => '',
- // 'There is no notification method registered.' => '',
- // 'Import' => '',
- // 'change sorting' => '',
- // 'Tasks Importation' => '',
- // 'Delimiter' => '',
- // 'Enclosure' => '',
- // 'CSV File' => '',
- // 'Instructions' => '',
- // 'Your file must use the predefined CSV format' => '',
- // 'Your file must be encoded in UTF-8' => '',
- // 'The first row must be the header' => '',
- // 'Duplicates are not verified for you' => '',
- // 'The due date must use the ISO format: YYYY-MM-DD' => '',
- // 'Download CSV template' => '',
- // 'No external integration registered.' => '',
- // 'Duplicates are not imported' => '',
- // 'Usernames must be lowercase and unique' => '',
- // 'Passwords will be encrypted if present' => '',
+ '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' => '项目甘特图',
+ 'Link type' => '关联类型',
+ 'Change task color when using a specific task link' => '当任务关联到指定任务时改变颜色',
+ 'Task link creation or modification' => '任务链接创建或更新时间',
+ 'Milestone' => '里程碑',
+ 'Documentation: %s' => '文档:%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' => 'web',
+ 'New attachment on task #%d: %s' => '任务#%d下的新附件:%s',
+ 'New comment on task #%d' => '任务#%d下的新评论',
+ 'Comment updated on task #%d' => '任务#%d的评论已更新',
+ 'New subtask on task #%d' => '任务#%d下新的子任务',
+ 'Subtask updated on task #%d' => '任务#%d下的子任务已更新',
+ 'New task #%d: %s' => '新任务#%d:%s',
+ 'Task updated #%d' => '任务#%d已更新',
+ 'Task #%d closed' => '任务#%d已关闭',
+ 'Task #%d opened' => '任务#%d已打开',
+ 'Column changed for task #%d' => '任务#%d的任务栏已改变',
+ 'New position for task #%d' => '任务#%d的新状态',
+ 'Swimlane changed for task #%d' => '任务#%d的里程碑已改变',
+ 'Assignee changed on task #%d' => '任务#%d的指派人已改变',
+ '%d overdue tasks' => '%d条超期任务',
+ 'Task #%d is overdue' => '任务#%d已超期',
+ 'No new notifications.' => '没有新通知',
+ 'Mark all as read' => '标记所有为已读',
+ 'Mark as read' => '标记为已读',
+ 'Total number of tasks in this column across all swimlanes' => '此任务栏下的任务数(跨里程碑)',
+ 'Collapse swimlane' => '收起里程碑',
+ 'Expand swimlane' => '展开里程碑',
+ 'Add a new filter' => '添加新过滤器',
+ 'Share with all project members' => '对项目所有成员共享',
+ 'Shared' => '共享',
+ 'Owner' => '所有人',
+ 'Unread notifications' => '未读通知',
+ 'My filters' => '我的过滤器',
+ 'Notification methods:' => '通知提醒方式:',
+ 'Import tasks from CSV file' => '从CSV文件导入任务',
+ 'Unable to read your file' => '无法读取文件',
+ '%d task(s) have been imported successfully.' => '成功导入%d条任务。',
+ 'Nothing have been imported!' => '没有信息被导入!',
+ 'Import users from CSV file' => '从CSV文件导入用户',
+ '%d user(s) have been imported successfully.' => '成功导入%d个用户。',
+ 'Comma' => '逗号',
+ 'Semi-colon' => '分号',
+ 'Tab' => '制表符',
+ 'Vertical bar' => '竖线',
+ 'Double Quote' => '双引号',
+ 'Single Quote' => '单引号',
+ '%s attached a file to the task #%d' => '%s添加文件到任务#%d',
+ 'There is no column or swimlane activated in your project!' => '当前项目没有活动任务栏或里程碑',
+ 'Append filter (instead of replacement)' => '追加过滤',
+ 'Append/Replace' => '追加/替换',
+ 'Append' => '追加',
+ 'Replace' => '替换',
+ 'Import' => '导入',
+ 'change sorting' => '改变排序',
+ 'Tasks Importation' => '任务重要性',
+ 'Delimiter' => '分隔符',
+ 'Enclosure' => '附件',
+ 'CSV File' => 'CSV文件',
+ 'Instructions' => '操作指南',
+ 'Your file must use the predefined CSV format' => '文件必须为预定格式的CSV文件',
+ 'Your file must be encoded in UTF-8' => '文件编码必须为UTF-8',
+ 'The first row must be the header' => '第一行必须为表头',
+ 'Duplicates are not verified for you' => '无法验证重复信息',
+ 'The due date must use the ISO format: YYYY-MM-DD' => '超期日期必须为ISO格式:YYYY-MM-DD',
+ 'Download CSV template' => '下载CSV模板',
+ 'No external integration registered.' => '没有外部注册信息',
+ 'Duplicates are not imported' => '重复信息未导入',
+ 'Usernames must be lowercase and unique' => '用户名必须小写且唯一',
+ 'Passwords will be encrypted if present' => '密码将被加密',
+ '%s attached a new file to the task %s' => '"%s"添加了附件到任务"%s"',
+ 'Assign automatically a category based on a link' => '基于链接自动关联分类',
+ 'BAM - Konvertible Mark' => '波斯尼亚马克',
+ 'Assignee Username' => '指派用户名',
+ 'Assignee Name' => '指派名称',
+ 'Groups' => '用户组',
+ 'Members of %s' => '“%s”组成员',
+ 'New group' => '新加用户组',
+ 'Group created successfully.' => '用户组创建成功',
+ 'Unable to create your group.' => '无法创建你的用户组',
+ 'Edit group' => '编辑用户组',
+ 'Group updated successfully.' => '用户组更新成功',
+ 'Unable to update your group.' => '无法更新你的用户组',
+ 'Add group member to "%s"' => '添加到用户组"%s"',
+ 'Group member added successfully.' => '成功添加用户组成员',
+ 'Unable to add group member.' => '无法添加用户组成员',
+ 'Remove user from group "%s"' => '从"%s"组中移除用户',
+ 'User removed successfully from this group.' => '用户已从该用户组中删除',
+ 'Unable to remove this user from the group.' => '无法从该用户组中删除用户',
+ 'Remove group' => '删除用户组',
+ 'Group removed successfully.' => '用户组已删除',
+ 'Unable to remove this group.' => '无法删除该用户组',
+ 'Project Permissions' => '项目权限',
+ 'Manager' => '管理员',
+ 'Project Manager' => '项目管理员',
+ 'Project Member' => '项目成员',
+ 'Project Viewer' => '项目观察员',
+ 'Your account is locked for %d minutes' => '你的账户被锁定%d分钟',
+ 'Invalid captcha' => '验证码无效',
+ 'The name must be unique' => '请确保用户名唯一',
+ 'View all groups' => '查看所有用户组',
+ 'View group members' => '查看该组所有用户',
+ 'There is no user available.' => '当前没有有效用户',
+ 'Do you really want to remove the user "%s" from the group "%s"?' => '你确定把用户"%s"从"%s"中移除?',
+ 'There is no group.' => '当前没有用户组',
+ 'External Id' => '关联ID',
+ 'Add group member' => '添加用户组成员',
+ 'Do you really want to remove this group: "%s"?' => '你真的想要移除用户组:"%s"?',
+ 'There is no user in this group.' => '当前用户组下没有成员',
+ 'Remove this user' => '移除用户',
+ 'Permissions' => '权限',
+ 'Allowed Users' => '被允许的用户',
+ 'No user have been allowed specifically.' => '没有被允许的用户。',
+ 'Role' => '角色',
+ 'Enter user name...' => '输入用户名...',
+ 'Allowed Groups' => '被允许的用户组',
+ 'No group have been allowed specifically.' => '没有被允许的用户组。',
+ 'Group' => '用户组',
+ 'Group Name' => '用户组名称',
+ 'Enter group name...' => '输入用户组名称...',
+ 'Role:' => '角色:',
+ 'Project members' => '项目成员',
+ 'Compare hours for "%s"' => '比较"%s"的时间',
+ '%s mentioned you in the task #%d' => '%s在任务#%d里提及你',
+ '%s mentioned you in a comment on the task #%d' => '%s在任务#%d的评论里提及你',
+ 'You were mentioned in the task #%d' => '你在任务#%d里被提及',
+ 'You were mentioned in a comment on the task #%d' => '你在任务#%d的评论里被提及',
+ 'Mentioned' => '被提及',
+ 'Compare Estimated Time vs Actual Time' => '对比预估时间VS实际时间',
+ 'Estimated hours: ' => '预估小时数',
+ 'Actual hours: ' => '实际小时数',
+ 'Hours Spent' => '花费小时数',
+ 'Hours Estimated' => '预估小时数',
+ 'Estimated Time' => '预估时间',
+ 'Actual Time' => '实际时间',
+ 'Estimated vs actual time' => '预估时间 VS 实际时间',
+ 'RUB - Russian Ruble' => '卢布',
+ 'Assign the task to the person who does the action when the column is changed' => '当任务所属任务栏改变时分配到指定用户',
+ 'Close a task in a specific column' => '关闭指定任务栏下的任务',
+ 'Time-based One-time Password Algorithm' => '基于时间的一次性密码算法',
+ 'Two-Factor Provider: ' => '双重认证提供商',
+ 'Disable two-factor authentication' => '禁用双重认证',
+ 'Enable two-factor authentication' => '启用双重认证',
+ 'There is no integration registered at the moment.' => '当前没有注册关联',
+ 'Password Reset for Kanboard' => '重置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.' => '任务栏 "%s" 和 里程碑 "%s" 下的所有任务已关闭',
+ '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.' => '任务栏 "%s" 和 里程碑 "%s" 下 %d 条任务将被关闭。',
+ '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.' => '项目标识符是可选的,且只能是数字或字母组成,例如: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.' => '如果你为优先级填0,将禁用本功能',
+ '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' => '自动',
+ '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' => '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' => '%s 小时',
+ 'Time spent:' => '时间消耗:',
+ 'Created:' => '已创建:',
+ 'Modified:' => '已修改:',
+ 'Completed:' => '已完成:',
+ 'Started:' => '已开始:',
+ 'Moved:' => '已移走',
+ 'Task #%d' => '任务#%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"?' => '你确定要移除这个自定义过滤器:"%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.' => '最大上传尺寸 %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"?' => '你确定要禁用该用户:"%s"?',
+ 'Enable user' => '启用用户',
+ 'Do you really want to enable this user: "%s"?' => '你确定要启用该用户:"%s"?',
+ 'Download' => '下载',
+ 'Uploaded: %s' => '上传:%s',
+ 'Size: %s' => '大小:%s',
+ 'Uploaded by %s' => '由%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)' => '',
);
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
deleted file mode 100644
index 62f850cb..00000000
--- a/app/Model/Acl.php
+++ /dev/null
@@ -1,289 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Access List
- *
- * @package model
- * @author Frederic Guillot
- */
-class Acl extends Base
-{
- /**
- * Controllers and actions allowed from outside
- *
- * @access private
- * @var array
- */
- private $public_acl = array(
- 'auth' => array('login', 'check', 'captcha'),
- 'task' => array('readonly'),
- 'board' => array('readonly'),
- 'webhook' => '*',
- 'ical' => '*',
- 'feed' => '*',
- 'oauth' => array('google', 'github', 'gitlab'),
- );
-
- /**
- * Controllers and actions for project members
- *
- * @access private
- * @var array
- */
- private $project_member_acl = array(
- 'board' => '*',
- 'comment' => '*',
- 'file' => '*',
- 'project' => array('show'),
- 'listing' => '*',
- 'activity' => '*',
- 'subtask' => '*',
- 'task' => '*',
- 'taskduplication' => '*',
- 'taskcreation' => '*',
- 'taskmodification' => '*',
- 'taskstatus' => '*',
- 'tasklink' => '*',
- 'timer' => '*',
- 'customfilter' => '*',
- 'calendar' => array('show', 'project'),
- );
-
- /**
- * Controllers and actions for project managers
- *
- * @access private
- * @var array
- */
- private $project_manager_acl = array(
- 'action' => '*',
- 'analytic' => '*',
- 'category' => '*',
- 'column' => '*',
- 'export' => '*',
- 'taskimport' => '*',
- 'project' => array('edit', 'update', 'share', 'integrations', 'notifications', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'),
- 'swimlane' => '*',
- 'gantt' => array('project', 'savetaskdate', 'task', 'savetask'),
- );
-
- /**
- * Controllers and actions for project admins
- *
- * @access private
- * @var array
- */
- private $project_admin_acl = array(
- 'project' => array('remove'),
- 'projectuser' => '*',
- 'gantt' => array('projects', 'saveprojectdate'),
- );
-
- /**
- * Controllers and actions for admins
- *
- * @access private
- * @var array
- */
- private $admin_acl = array(
- 'user' => array('index', 'create', 'save', 'remove', 'authentication'),
- 'userimport' => '*',
- 'config' => '*',
- 'link' => '*',
- 'currency' => '*',
- 'twofactor' => array('disable'),
- );
-
- /**
- * Extend ACL rules
- *
- * @access public
- * @param string $acl_name
- * @param aray $rules
- */
- public function extend($acl_name, array $rules)
- {
- $this->$acl_name = array_merge($this->$acl_name, $rules);
- }
-
- /**
- * Return true if the specified controller/action match the given acl
- *
- * @access public
- * @param array $acl Acl list
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function matchAcl(array $acl, $controller, $action)
- {
- $controller = strtolower($controller);
- $action = strtolower($action);
- return isset($acl[$controller]) && $this->hasAction($action, $acl[$controller]);
- }
-
- /**
- * Return true if the specified action is inside the list of actions
- *
- * @access public
- * @param string $action Action name
- * @param mixed $action Actions list
- * @return bool
- */
- public function hasAction($action, $actions)
- {
- if (is_array($actions)) {
- return in_array($action, $actions);
- }
-
- return $actions === '*';
- }
-
- /**
- * Return true if the given action is public
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isPublicAction($controller, $action)
- {
- return $this->matchAcl($this->public_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for admins
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isAdminAction($controller, $action)
- {
- return $this->matchAcl($this->admin_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for project managers
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectManagerAction($controller, $action)
- {
- return $this->matchAcl($this->project_manager_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for application managers
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectAdminAction($controller, $action)
- {
- return $this->matchAcl($this->project_admin_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for project members
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectMemberAction($controller, $action)
- {
- return $this->matchAcl($this->project_member_acl, $controller, $action);
- }
-
- /**
- * Return true if the visitor is allowed to access to the given page
- * We suppose the user already authenticated
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @param integer $project_id Project id
- * @return bool
- */
- public function isAllowed($controller, $action, $project_id = 0)
- {
- // If you are admin you have access to everything
- if ($this->userSession->isAdmin()) {
- return true;
- }
-
- // If you access to an admin action, your are not allowed
- if ($this->isAdminAction($controller, $action)) {
- return false;
- }
-
- // Check project admin permissions
- if ($this->isProjectAdminAction($controller, $action)) {
- return $this->handleProjectAdminPermissions($project_id);
- }
-
- // Check project manager permissions
- if ($this->isProjectManagerAction($controller, $action)) {
- return $this->handleProjectManagerPermissions($project_id);
- }
-
- // Check project member permissions
- if ($this->isProjectMemberAction($controller, $action)) {
- return $project_id > 0 && $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- // Other applications actions are allowed
- return true;
- }
-
- /**
- * Handle permission for project manager
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function handleProjectManagerPermissions($project_id)
- {
- if ($project_id > 0) {
- if ($this->userSession->isProjectAdmin()) {
- return $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- return $this->projectPermission->isManager($project_id, $this->userSession->getId());
- }
-
- return false;
- }
-
- /**
- * Handle permission for project admins
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function handleProjectAdminPermissions($project_id)
- {
- if (! $this->userSession->isProjectAdmin()) {
- return false;
- }
-
- if ($project_id > 0) {
- return $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- return true;
- }
-}
diff --git a/app/Model/Action.php b/app/Model/Action.php
index ba74218f..4da2fb8f 100644
--- a/app/Model/Action.php
+++ b/app/Model/Action.php
@@ -2,14 +2,8 @@
namespace Kanboard\Model;
-use Kanboard\Integration\GitlabWebhook;
-use Kanboard\Integration\GithubWebhook;
-use Kanboard\Integration\BitbucketWebhook;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
- * Action model
+ * Action Model
*
* @package model
* @author Frederic Guillot
@@ -24,152 +18,38 @@ class Action extends Base
const TABLE = 'actions';
/**
- * SQL table name for action parameters
- *
- * @var string
- */
- const TABLE_PARAMS = 'action_has_params';
-
- /**
- * Extended actions
- *
- * @access private
- * @var array
- */
- private $actions = array();
-
- /**
- * Extend the list of default actions
- *
- * @access public
- * @param string $className
- * @param string $description
- * @return Action
- */
- public function extendActions($className, $description)
- {
- $this->actions[$className] = $description;
- return $this;
- }
-
- /**
- * Return the name and description of available actions
- *
- * @access public
- * @return array
- */
- public function getAvailableActions()
- {
- $values = array(
- 'TaskClose' => t('Close a task'),
- 'TaskOpen' => t('Open a task'),
- 'TaskAssignSpecificUser' => t('Assign the task to a specific user'),
- 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'),
- 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'),
- 'TaskMoveAnotherProject' => t('Move the task to another project'),
- 'TaskMoveColumnAssigned' => t('Move the task to another column when assigned to a user'),
- 'TaskMoveColumnUnAssigned' => t('Move the task to another column when assignee is cleared'),
- 'TaskAssignColorColumn' => t('Assign a color when the task is moved to a specific column'),
- 'TaskAssignColorUser' => t('Assign a color to a specific user'),
- 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'),
- 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'),
- 'CommentCreation' => t('Create a comment from an external provider'),
- 'TaskCreation' => t('Create a task from an external provider'),
- 'TaskLogMoveAnotherColumn' => t('Add a comment log when moving the task between columns'),
- 'TaskAssignUser' => t('Change the assignee based on an external username'),
- 'TaskAssignCategoryLabel' => t('Change the category based on an external label'),
- 'TaskUpdateStartDate' => t('Automatically update the start date'),
- 'TaskMoveColumnCategoryChange' => t('Move the task to another column when the category is changed'),
- 'TaskEmail' => t('Send a task by email to someone'),
- 'TaskAssignColorLink' => t('Change task color when using a specific task link'),
- );
-
- $values = array_merge($values, $this->actions);
-
- asort($values);
-
- return $values;
- }
-
- /**
- * Return the name and description of available actions
- *
- * @access public
- * @return array
- */
- public function getAvailableEvents()
- {
- $values = array(
- TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'),
- Task::EVENT_MOVE_COLUMN => t('Move a task to another column'),
- Task::EVENT_UPDATE => t('Task modification'),
- Task::EVENT_CREATE => t('Task creation'),
- Task::EVENT_OPEN => t('Reopen a task'),
- Task::EVENT_CLOSE => t('Closing a task'),
- Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
- Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
- GithubWebhook::EVENT_COMMIT => t('Github commit received'),
- GithubWebhook::EVENT_ISSUE_OPENED => t('Github issue opened'),
- GithubWebhook::EVENT_ISSUE_CLOSED => t('Github issue closed'),
- GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'),
- GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'),
- GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'),
- GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'),
- GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'),
- GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'),
- GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'),
- GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'),
- BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'),
- BitbucketWebhook::EVENT_ISSUE_OPENED => t('Bitbucket issue opened'),
- BitbucketWebhook::EVENT_ISSUE_CLOSED => t('Bitbucket issue closed'),
- BitbucketWebhook::EVENT_ISSUE_REOPENED => t('Bitbucket issue reopened'),
- BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Bitbucket issue assignee change'),
- BitbucketWebhook::EVENT_ISSUE_COMMENT => t('Bitbucket issue comment created'),
- );
-
- asort($values);
-
- return $values;
- }
-
- /**
- * Return the name and description of compatible actions
+ * Return actions and parameters for a given user
*
* @access public
- * @param string $action_name Action name
+ * @param integer $user_id
* @return array
*/
- public function getCompatibleEvents($action_name)
+ public function getAllByUser($user_id)
{
- $action = $this->load($action_name, 0, '');
- $compatible_events = $action->getCompatibleEvents();
- $events = array();
+ $project_ids = $this->projectPermission->getActiveProjectIds($user_id);
+ $actions = array();
- foreach ($this->getAvailableEvents() as $event_name => $event_description) {
- if (in_array($event_name, $compatible_events)) {
- $events[$event_name] = $event_description;
- }
+ if (! empty($project_ids)) {
+ $actions = $this->db->table(self::TABLE)->in('project_id', $project_ids)->findAll();
+ $params = $this->actionParameter->getAllByActions(array_column($actions, 'id'));
+ $this->attachParamsToActions($actions, $params);
}
- return $events;
+ return $actions;
}
/**
* Return actions and parameters for a given project
*
* @access public
- * @param $project_id
+ * @param integer $project_id
* @return array
*/
public function getAllByProject($project_id)
{
$actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll();
-
- foreach ($actions as &$action) {
- $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll();
- }
-
- return $actions;
+ $params = $this->actionParameter->getAllByActions(array_column($actions, 'id'));
+ return $this->attachParamsToActions($actions, $params);
}
/**
@@ -181,63 +61,51 @@ class Action extends Base
public function getAll()
{
$actions = $this->db->table(self::TABLE)->findAll();
- $params = $this->db->table(self::TABLE_PARAMS)->findAll();
-
- foreach ($actions as &$action) {
- $action['params'] = array();
-
- foreach ($params as $param) {
- if ($param['action_id'] === $action['id']) {
- $action['params'][] = $param;
- }
- }
- }
-
- return $actions;
+ $params = $this->actionParameter->getAll();
+ return $this->attachParamsToActions($actions, $params);
}
/**
- * Get all required action parameters for all registered actions
+ * Fetch an action
*
* @access public
- * @return array All required parameters for all actions
+ * @param integer $action_id
+ * @return array
*/
- public function getAllActionParameters()
+ public function getById($action_id)
{
- $params = array();
+ $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne();
- foreach ($this->getAll() as $action) {
- $action = $this->load($action['action_name'], $action['project_id'], $action['event_name']);
- $params += $action->getActionRequiredParameters();
+ if (! empty($action)) {
+ $action['params'] = $this->actionParameter->getAllByAction($action_id);
}
- return $params;
+ return $action;
}
/**
- * Fetch an action
+ * Attach parameters to actions
*
- * @access public
- * @param integer $action_id Action id
- * @return array Action data
+ * @access private
+ * @param array &$actions
+ * @param array &$params
+ * @return array
*/
- public function getById($action_id)
+ private function attachParamsToActions(array &$actions, array &$params)
{
- $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne();
-
- if (! empty($action)) {
- $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll();
+ foreach ($actions as &$action) {
+ $action['params'] = isset($params[$action['id']]) ? $params[$action['id']] : array();
}
- return $action;
+ return $actions;
}
/**
* Remove an action
*
* @access public
- * @param integer $action_id Action id
- * @return bool Success or not
+ * @param integer $action_id
+ * @return bool
*/
public function remove($action_id)
{
@@ -261,24 +129,16 @@ class Action extends Base
'action_name' => $values['action_name'],
);
- if (! $this->db->table(self::TABLE)->save($action)) {
+ if (! $this->db->table(self::TABLE)->insert($action)) {
$this->db->cancelTransaction();
return false;
}
$action_id = $this->db->getLastId();
- foreach ($values['params'] as $param_name => $param_value) {
- $action_param = array(
- 'action_id' => $action_id,
- 'name' => $param_name,
- 'value' => $param_value,
- );
-
- if (! $this->db->table(self::TABLE_PARAMS)->save($action_param)) {
- $this->db->cancelTransaction();
- return false;
- }
+ if (! $this->actionParameter->create($action_id, $values)) {
+ $this->db->cancelTransaction();
+ return false;
}
$this->db->closeTransaction();
@@ -287,42 +147,6 @@ class Action extends Base
}
/**
- * Load all actions and attach events
- *
- * @access public
- */
- public function attachEvents()
- {
- $actions = $this->getAll();
-
- foreach ($actions as $action) {
- $listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']);
-
- foreach ($action['params'] as $param) {
- $listener->setParam($param['name'], $param['value']);
- }
-
- $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute'));
- }
- }
-
- /**
- * Load an action
- *
- * @access public
- * @param string $name Action class name
- * @param integer $project_id Project id
- * @param string $event Event name
- * @return \Action\Base
- */
- public function load($name, $project_id, $event)
- {
- $className = $name{0}
- !== '\\' ? '\Kanboard\Action\\'.$name : $name;
- return new $className($this->container, $project_id, $event);
- }
-
- /**
* Copy actions from a project to another one (skip actions that cannot resolve parameters)
*
* @author Antonio Rabelo
@@ -344,15 +168,14 @@ class Action extends Base
);
if (! $this->db->table(self::TABLE)->insert($values)) {
- $this->container['logger']->debug('Action::duplicate => unable to create '.$action['action_name']);
$this->db->cancelTransaction();
continue;
}
$action_id = $this->db->getLastId();
- if (! $this->duplicateParameters($dst_project_id, $action_id, $action['params'])) {
- $this->container['logger']->debug('Action::duplicate => unable to copy parameters for '.$action['action_name']);
+ if (! $this->actionParameter->duplicateParameters($dst_project_id, $action_id, $action['params'])) {
+ $this->logger->error('Action::duplicate => skip action '.$action['action_name'].' '.$action['id']);
$this->db->cancelTransaction();
continue;
}
@@ -362,95 +185,4 @@ class Action extends Base
return true;
}
-
- /**
- * Duplicate action parameters
- *
- * @access public
- * @param integer $project_id
- * @param integer $action_id
- * @param array $params
- * @return boolean
- */
- public function duplicateParameters($project_id, $action_id, array $params)
- {
- foreach ($params as $param) {
- $value = $this->resolveParameters($param, $project_id);
-
- if ($value === false) {
- $this->container['logger']->debug('Action::duplicateParameters => unable to resolve '.$param['name'].'='.$param['value']);
- return false;
- }
-
- $values = array(
- 'action_id' => $action_id,
- 'name' => $param['name'],
- 'value' => $value,
- );
-
- if (! $this->db->table(self::TABLE_PARAMS)->insert($values)) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Resolve action parameter values according to another project
- *
- * @author Antonio Rabelo
- * @access public
- * @param array $param Action parameter
- * @param integer $project_id Project to find the corresponding values
- * @return mixed
- */
- public function resolveParameters(array $param, $project_id)
- {
- switch ($param['name']) {
- case 'project_id':
- return $project_id;
- case 'category_id':
- return $this->category->getIdByName($project_id, $this->category->getNameById($param['value'])) ?: false;
- case 'src_column_id':
- case 'dest_column_id':
- case 'dst_column_id':
- case 'column_id':
- $column = $this->board->getColumn($param['value']);
-
- if (empty($column)) {
- return false;
- }
-
- return $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false;
- case 'user_id':
- case 'owner_id':
- return $this->projectPermission->isMember($project_id, $param['value']) ? $param['value'] : false;
- default:
- return $param['value'];
- }
- }
-
- /**
- * Validate action creation
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Required('event_name', t('This value is required')),
- new Validators\Required('action_name', t('This value is required')),
- new Validators\Required('params', t('This value is required')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/ActionParameter.php b/app/Model/ActionParameter.php
new file mode 100644
index 00000000..53edcbc8
--- /dev/null
+++ b/app/Model/ActionParameter.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Action Parameter Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ActionParameter extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'action_has_params';
+
+ /**
+ * Get all action params
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ $params = $this->db->table(self::TABLE)->findAll();
+ return $this->toDictionary($params);
+ }
+
+ /**
+ * Get all params for a list of actions
+ *
+ * @access public
+ * @param array $action_ids
+ * @return array
+ */
+ public function getAllByActions(array $action_ids)
+ {
+ $params = $this->db->table(self::TABLE)->in('action_id', $action_ids)->findAll();
+ return $this->toDictionary($params);
+ }
+
+ /**
+ * Build params dictionary
+ *
+ * @access private
+ * @param array $params
+ * @return array
+ */
+ private function toDictionary(array $params)
+ {
+ $result = array();
+
+ foreach ($params as $param) {
+ $result[$param['action_id']][$param['name']] = $param['value'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get all action params for a given action
+ *
+ * @access public
+ * @param integer $action_id
+ * @return array
+ */
+ public function getAllByAction($action_id)
+ {
+ return $this->db->hashtable(self::TABLE)->eq('action_id', $action_id)->getAll('name', 'value');
+ }
+
+ /**
+ * Insert new parameters for an action
+ *
+ * @access public
+ * @param integer $action_id
+ * @param array $values
+ * @return boolean
+ */
+ public function create($action_id, array $values)
+ {
+ foreach ($values['params'] as $name => $value) {
+ $param = array(
+ 'action_id' => $action_id,
+ 'name' => $name,
+ 'value' => $value,
+ );
+
+ if (! $this->db->table(self::TABLE)->save($param)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Duplicate action parameters
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $action_id
+ * @param array $params
+ * @return boolean
+ */
+ public function duplicateParameters($project_id, $action_id, array $params)
+ {
+ foreach ($params as $name => $value) {
+ $value = $this->resolveParameter($project_id, $name, $value);
+
+ if ($value === false) {
+ $this->logger->error('ActionParameter::duplicateParameters => unable to resolve '.$name.'='.$value);
+ return false;
+ }
+
+ $values = array(
+ 'action_id' => $action_id,
+ 'name' => $name,
+ 'value' => $value,
+ );
+
+ if (! $this->db->table(self::TABLE)->insert($values)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Resolve action parameter values according to another project
+ *
+ * @access private
+ * @param integer $project_id
+ * @param string $name
+ * @param string $value
+ * @return mixed
+ */
+ private function resolveParameter($project_id, $name, $value)
+ {
+ switch ($name) {
+ case 'project_id':
+ return $value != $project_id ? $value : false;
+ case 'category_id':
+ return $this->category->getIdByName($project_id, $this->category->getNameById($value)) ?: false;
+ case 'src_column_id':
+ case 'dest_column_id':
+ case 'dst_column_id':
+ case 'column_id':
+ $column = $this->column->getById($value);
+ return empty($column) ? false : $this->column->getColumnIdByTitle($project_id, $column['title']) ?: false;
+ case 'user_id':
+ case 'owner_id':
+ return $this->projectPermission->isAssignable($project_id, $value) ? $value : false;
+ default:
+ return $value;
+ }
+ }
+}
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
deleted file mode 100644
index 580c1e14..00000000
--- a/app/Model/Authentication.php
+++ /dev/null
@@ -1,202 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-use Kanboard\Core\Request;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use Gregwar\Captcha\CaptchaBuilder;
-
-/**
- * Authentication model
- *
- * @package model
- * @author Frederic Guillot
- */
-class Authentication extends Base
-{
- /**
- * Load automatically an authentication backend
- *
- * @access public
- * @param string $name Backend class name
- * @return mixed
- */
- public function backend($name)
- {
- if (! isset($this->container[$name])) {
- $class = '\Kanboard\Auth\\'.ucfirst($name);
- $this->container[$name] = new $class($this->container);
- }
-
- return $this->container[$name];
- }
-
- /**
- * Check if the current user is authenticated
- *
- * @access public
- * @return bool
- */
- public function isAuthenticated()
- {
- // If the user is already logged it's ok
- if ($this->userSession->isLogged()) {
-
- // Check if the user session match an existing user
- $userNotFound = ! $this->user->exists($this->userSession->getId());
- $reverseProxyWrongUser = REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->getUsername() !== $_SESSION['user']['username'];
-
- if ($userNotFound || $reverseProxyWrongUser) {
- $this->backend('rememberMe')->destroy($this->userSession->getId());
- $this->session->close();
- return false;
- }
-
- return true;
- }
-
- // We try first with the RememberMe cookie
- if (REMEMBER_ME_AUTH && $this->backend('rememberMe')->authenticate()) {
- return true;
- }
-
- // Then with the ReverseProxy authentication
- if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Authenticate a user by different methods
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean
- */
- public function authenticate($username, $password)
- {
- if ($this->user->isLocked($username)) {
- $this->container['logger']->error('Account locked: '.$username);
- return false;
- } elseif ($this->backend('database')->authenticate($username, $password)) {
- $this->user->resetFailedLogin($username);
- return true;
- } elseif (LDAP_AUTH && $this->backend('ldap')->authenticate($username, $password)) {
- $this->user->resetFailedLogin($username);
- return true;
- }
-
- $this->handleFailedLogin($username);
- return false;
- }
-
- /**
- * Return true if the captcha must be shown
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function hasCaptcha($username)
- {
- return $this->user->getFailedLogin($username) >= BRUTEFORCE_CAPTCHA;
- }
-
- /**
- * Handle failed login
- *
- * @access public
- * @param string $username
- */
- public function handleFailedLogin($username)
- {
- $this->user->incrementFailedLogin($username);
-
- if ($this->user->getFailedLogin($username) >= BRUTEFORCE_LOCKDOWN) {
- $this->container['logger']->critical('Locking account: '.$username);
- $this->user->lock($username, BRUTEFORCE_LOCKDOWN_DURATION);
- }
- }
-
- /**
- * Validate user login form
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateForm(array $values)
- {
- list($result, $errors) = $this->validateFormCredentials($values);
-
- if ($result) {
- if ($this->validateFormCaptcha($values) && $this->authenticate($values['username'], $values['password'])) {
- $this->createRememberMeSession($values);
- } else {
- $result = false;
- $errors['login'] = t('Bad username or password');
- }
- }
-
- return array($result, $errors);
- }
-
- /**
- * Validate credentials syntax
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateFormCredentials(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('username', t('The username is required')),
- new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
- new Validators\Required('password', t('The password is required')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors(),
- );
- }
-
- /**
- * Validate captcha
- *
- * @access public
- * @param array $values Form values
- * @return boolean
- */
- public function validateFormCaptcha(array $values)
- {
- if ($this->hasCaptcha($values['username'])) {
- $builder = new CaptchaBuilder;
- $builder->setPhrase($this->session['captcha']);
- return $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
- }
-
- return true;
- }
-
- /**
- * Create remember me session if necessary
- *
- * @access private
- * @param array $values Form values
- */
- private function createRememberMeSession(array $values)
- {
- if (REMEMBER_ME_AUTH && ! empty($values['remember_me'])) {
- $credentials = $this->backend('rememberMe')
- ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent());
-
- $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
- }
- }
-}
diff --git a/app/Model/Board.php b/app/Model/Board.php
index 79a1a92d..c10be19f 100644
--- a/app/Model/Board.php
+++ b/app/Model/Board.php
@@ -2,10 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use PicoDb\Database;
-
/**
* Board model
*
@@ -15,13 +11,6 @@ use PicoDb\Database;
class Board extends Base
{
/**
- * SQL table name
- *
- * @var string
- */
- const TABLE = 'columns';
-
- /**
* Get Kanboard default columns
*
* @access public
@@ -75,7 +64,7 @@ class Board extends Base
'description' => $column['description'],
);
- if (! $this->db->table(self::TABLE)->save($values)) {
+ if (! $this->db->table(Column::TABLE)->save($values)) {
return false;
}
}
@@ -93,7 +82,7 @@ class Board extends Base
*/
public function duplicate($project_from, $project_to)
{
- $columns = $this->db->table(Board::TABLE)
+ $columns = $this->db->table(Column::TABLE)
->columns('title', 'task_limit', 'description')
->eq('project_id', $project_from)
->asc('position')
@@ -103,134 +92,6 @@ class Board extends Base
}
/**
- * Add a new column to the board
- *
- * @access public
- * @param integer $project_id Project id
- * @param string $title Column title
- * @param integer $task_limit Task limit
- * @param string $description Column description
- * @return boolean|integer
- */
- public function addColumn($project_id, $title, $task_limit = 0, $description = '')
- {
- $values = array(
- 'project_id' => $project_id,
- 'title' => $title,
- 'task_limit' => intval($task_limit),
- 'position' => $this->getLastColumnPosition($project_id) + 1,
- 'description' => $description,
- );
-
- return $this->persist(self::TABLE, $values);
- }
-
- /**
- * Update a column
- *
- * @access public
- * @param integer $column_id Column id
- * @param string $title Column title
- * @param integer $task_limit Task limit
- * @param string $description Optional description
- * @return boolean
- */
- public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
- 'title' => $title,
- 'task_limit' => intval($task_limit),
- 'description' => $description,
- ));
- }
-
- /**
- * Get columns with consecutive positions
- *
- * If you remove a column, the positions are not anymore consecutives
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getNormalizedColumnPositions($project_id)
- {
- $columns = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'position');
- $position = 1;
-
- foreach ($columns as $column_id => $column_position) {
- $columns[$column_id] = $position++;
- }
-
- return $columns;
- }
-
- /**
- * Save the new positions for a set of columns
- *
- * @access public
- * @param array $columns Hashmap of column_id/column_position
- * @return boolean
- */
- public function saveColumnPositions(array $columns)
- {
- return $this->db->transaction(function (Database $db) use ($columns) {
-
- foreach ($columns as $column_id => $position) {
- if (! $db->table(Board::TABLE)->eq('id', $column_id)->update(array('position' => $position))) {
- return false;
- }
- }
- });
- }
-
- /**
- * Move a column down, increment the column position value
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $column_id Column id
- * @return boolean
- */
- public function moveDown($project_id, $column_id)
- {
- $columns = $this->getNormalizedColumnPositions($project_id);
- $positions = array_flip($columns);
-
- if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) {
- $position = ++$columns[$column_id];
- $columns[$positions[$position]]--;
-
- return $this->saveColumnPositions($columns);
- }
-
- return false;
- }
-
- /**
- * Move a column up, decrement the column position value
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $column_id Column id
- * @return boolean
- */
- public function moveUp($project_id, $column_id)
- {
- $columns = $this->getNormalizedColumnPositions($project_id);
- $positions = array_flip($columns);
-
- if (isset($columns[$column_id]) && $columns[$column_id] > 1) {
- $position = --$columns[$column_id];
- $columns[$positions[$position]]++;
-
- return $this->saveColumnPositions($columns);
- }
-
- return false;
- }
-
- /**
* Get all tasks sorted by columns and swimlanes
*
* @access public
@@ -241,7 +102,7 @@ class Board extends Base
public function getBoard($project_id, $callback = null)
{
$swimlanes = $this->swimlane->getSwimlanes($project_id);
- $columns = $this->getColumns($project_id);
+ $columns = $this->column->getAll($project_id);
$nb_columns = count($columns);
for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) {
@@ -309,174 +170,4 @@ class Board extends Base
return $prepend ? array(-1 => t('All columns')) + $listing : $listing;
}
-
- /**
- * Get the first column id for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function getFirstColumn($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id');
- }
-
- /**
- * Get the last column id for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function getLastColumn($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id');
- }
-
- /**
- * Get the list of columns sorted by position [ column_id => title ]
- *
- * @access public
- * @param integer $project_id Project id
- * @param boolean $prepend Prepend a default value
- * @return array
- */
- public function getColumnsList($project_id, $prepend = false)
- {
- $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title');
- return $prepend ? array(-1 => t('All columns')) + $listing : $listing;
- }
-
- /**
- * Get all columns sorted by position for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getColumns($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll();
- }
-
- /**
- * Get the number of columns for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function countColumns($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->count();
- }
-
- /**
- * Get a column by the id
- *
- * @access public
- * @param integer $column_id Column id
- * @return array
- */
- public function getColumn($column_id)
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne();
- }
-
- /**
- * Get a column id by the name
- *
- * @access public
- * @param integer $project_id
- * @param string $title
- * @return integer
- */
- public function getColumnIdByTitle($project_id, $title)
- {
- return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id');
- }
-
- /**
- * Get a column title by the id
- *
- * @access public
- * @param integer $column_id
- * @return integer
- */
- public function getColumnTitleById($column_id)
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title');
- }
-
- /**
- * Get the position of the last column for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function getLastColumnPosition($project_id)
- {
- return (int) $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->desc('position')
- ->findOneColumn('position');
- }
-
- /**
- * Remove a column and all tasks associated to this column
- *
- * @access public
- * @param integer $column_id Column id
- * @return boolean
- */
- public function removeColumn($column_id)
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->remove();
- }
-
- /**
- * Validate column modification
- *
- * @access public
- * @param array $values Required parameters to update a column
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Integer('task_limit', t('This value must be an integer')),
- new Validators\Required('title', t('The title is required')),
- new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate column creation
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Required('title', t('The title is required')),
- new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/Category.php b/app/Model/Category.php
index bf40c60a..883fc282 100644
--- a/app/Model/Category.php
+++ b/app/Model/Category.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Category model
*
@@ -196,11 +193,12 @@ class Category extends Base
*/
public function duplicate($src_project_id, $dst_project_id)
{
- $categories = $this->db->table(self::TABLE)
- ->columns('name')
- ->eq('project_id', $src_project_id)
- ->asc('name')
- ->findAll();
+ $categories = $this->db
+ ->table(self::TABLE)
+ ->columns('name')
+ ->eq('project_id', $src_project_id)
+ ->asc('name')
+ ->findAll();
foreach ($categories as $category) {
$category['project_id'] = $dst_project_id;
@@ -212,63 +210,4 @@ class Category extends Base
return true;
}
-
- /**
- * Validate category creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $rules = array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate category modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('The id must be an integer')),
- new Validators\Integer('project_id', t('The project id must be an integer')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
- );
- }
}
diff --git a/app/Model/Column.php b/app/Model/Column.php
new file mode 100644
index 00000000..ccdcb049
--- /dev/null
+++ b/app/Model/Column.php
@@ -0,0 +1,209 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Column Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Column extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'columns';
+
+ /**
+ * Get a column by the id
+ *
+ * @access public
+ * @param integer $column_id Column id
+ * @return array
+ */
+ public function getById($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne();
+ }
+
+ /**
+ * Get the first column id for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return integer
+ */
+ public function getFirstColumnId($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id');
+ }
+
+ /**
+ * Get the last column id for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return integer
+ */
+ public function getLastColumnId($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id');
+ }
+
+ /**
+ * Get the position of the last column for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return integer
+ */
+ public function getLastColumnPosition($project_id)
+ {
+ return (int) $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->desc('position')
+ ->findOneColumn('position');
+ }
+
+ /**
+ * Get a column id by the name
+ *
+ * @access public
+ * @param integer $project_id
+ * @param string $title
+ * @return integer
+ */
+ public function getColumnIdByTitle($project_id, $title)
+ {
+ return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id');
+ }
+
+ /**
+ * Get a column title by the id
+ *
+ * @access public
+ * @param integer $column_id
+ * @return integer
+ */
+ public function getColumnTitleById($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title');
+ }
+
+ /**
+ * Get all columns sorted by position for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getAll($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll();
+ }
+
+ /**
+ * Get the list of columns sorted by position [ column_id => title ]
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param boolean $prepend Prepend a default value
+ * @return array
+ */
+ public function getList($project_id, $prepend = false)
+ {
+ $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title');
+ return $prepend ? array(-1 => t('All columns')) + $listing : $listing;
+ }
+
+ /**
+ * Add a new column to the board
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $title Column title
+ * @param integer $task_limit Task limit
+ * @param string $description Column description
+ * @return boolean|integer
+ */
+ public function create($project_id, $title, $task_limit = 0, $description = '')
+ {
+ $values = array(
+ 'project_id' => $project_id,
+ 'title' => $title,
+ 'task_limit' => intval($task_limit),
+ 'position' => $this->getLastColumnPosition($project_id) + 1,
+ 'description' => $description,
+ );
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Update a column
+ *
+ * @access public
+ * @param integer $column_id Column id
+ * @param string $title Column title
+ * @param integer $task_limit Task limit
+ * @param string $description Optional description
+ * @return boolean
+ */
+ public function update($column_id, $title, $task_limit = 0, $description = '')
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
+ 'title' => $title,
+ 'task_limit' => intval($task_limit),
+ 'description' => $description,
+ ));
+ }
+
+ /**
+ * Remove a column and all tasks associated to this column
+ *
+ * @access public
+ * @param integer $column_id Column id
+ * @return boolean
+ */
+ public function remove($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->remove();
+ }
+
+ /**
+ * Change column position
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $column_id
+ * @param integer $position
+ * @return boolean
+ */
+ public function changePosition($project_id, $column_id, $position)
+ {
+ if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) {
+ return false;
+ }
+
+ $column_ids = $this->db->table(self::TABLE)->eq('project_id', $project_id)->neq('id', $column_id)->asc('position')->findAllByColumn('id');
+ $offset = 1;
+ $results = array();
+
+ foreach ($column_ids as $current_column_id) {
+ if ($offset == $position) {
+ $offset++;
+ }
+
+ $results[] = $this->db->table(self::TABLE)->eq('id', $current_column_id)->update(array('position' => $offset));
+ $offset++;
+ }
+
+ $results[] = $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position));
+
+ return !in_array(false, $results, true);
+ }
+}
diff --git a/app/Model/Comment.php b/app/Model/Comment.php
index c7125a25..6eb4a1e5 100644
--- a/app/Model/Comment.php
+++ b/app/Model/Comment.php
@@ -3,8 +3,6 @@
namespace Kanboard\Model;
use Kanboard\Event\CommentEvent;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
/**
* Comment model
@@ -26,8 +24,9 @@ class Comment extends Base
*
* @var string
*/
- const EVENT_UPDATE = 'comment.update';
- const EVENT_CREATE = 'comment.create';
+ const EVENT_UPDATE = 'comment.update';
+ const EVENT_CREATE = 'comment.create';
+ const EVENT_USER_MENTION = 'comment.user.mention';
/**
* Get all comments for a given task
@@ -74,6 +73,7 @@ class Comment extends Base
self::TABLE.'.user_id',
self::TABLE.'.date_creation',
self::TABLE.'.comment',
+ self::TABLE.'.reference',
User::TABLE.'.username',
User::TABLE.'.name'
)
@@ -110,7 +110,9 @@ class Comment extends Base
$comment_id = $this->persist(self::TABLE, $values);
if ($comment_id) {
- $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values));
+ $event = new CommentEvent(array('id' => $comment_id) + $values);
+ $this->dispatcher->dispatch(self::EVENT_CREATE, $event);
+ $this->userMention->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event);
}
return $comment_id;
@@ -148,62 +150,4 @@ class Comment extends Base
{
return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove();
}
-
- /**
- * Validate comment creation
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $rules = array(
- new Validators\Required('task_id', t('This value is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate comment modification
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('This value is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('task_id', t('This value must be an integer')),
- new Validators\Integer('user_id', t('This value must be an integer')),
- new Validators\Required('comment', t('Comment is required'))
- );
- }
}
diff --git a/app/Model/Config.php b/app/Model/Config.php
index cf634f80..6f009175 100644
--- a/app/Model/Config.php
+++ b/app/Model/Config.php
@@ -3,8 +3,7 @@
namespace Kanboard\Model;
use Kanboard\Core\Translator;
-use Kanboard\Core\Security;
-use Kanboard\Core\Session;
+use Kanboard\Core\Security\Token;
/**
* Config model
@@ -15,30 +14,6 @@ use Kanboard\Core\Session;
class Config extends Setting
{
/**
- * Get available currencies
- *
- * @access public
- * @return array
- */
- public function getCurrencies()
- {
- return array(
- 'USD' => t('USD - US Dollar'),
- 'EUR' => t('EUR - Euro'),
- 'GBP' => t('GBP - British Pound'),
- 'CHF' => t('CHF - Swiss Francs'),
- 'CAD' => t('CAD - Canadian Dollar'),
- 'AUD' => t('AUD - Australian Dollar'),
- 'NZD' => t('NZD - New Zealand Dollar'),
- 'INR' => t('INR - Indian Rupee'),
- 'JPY' => t('JPY - Japanese Yen'),
- 'RSD' => t('RSD - Serbian dinar'),
- 'SEK' => t('SEK - Swedish Krona'),
- 'NOK' => t('NOK - Norwegian Krone'),
- );
- }
-
- /**
* Get available timezones
*
* @access public
@@ -58,6 +33,31 @@ class Config extends Setting
}
/**
+ * Get current timezone
+ *
+ * @access public
+ * @return string
+ */
+ public function getCurrentTimezone()
+ {
+ if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) {
+ return $this->sessionStorage->user['timezone'];
+ }
+
+ return $this->get('application_timezone', 'UTC');
+ }
+
+ /**
+ * Set timezone
+ *
+ * @access public
+ */
+ public function setupTimezone()
+ {
+ date_default_timezone_set($this->getCurrentTimezone());
+ }
+
+ /**
* Get available languages
*
* @access public
@@ -69,14 +69,17 @@ class Config extends Setting
// Sorted by value
$languages = array(
'id_ID' => 'Bahasa Indonesia',
+ 'bs_BA' => 'Bosanski',
'cs_CZ' => 'Čeština',
'da_DK' => 'Dansk',
'de_DE' => 'Deutsch',
'en_US' => 'English',
'es_ES' => 'Español',
'fr_FR' => 'Français',
+ 'el_GR' => 'Grec',
'it_IT' => 'Italiano',
'hu_HU' => 'Magyar',
+ 'my_MY' => 'Melayu',
'nl_NL' => 'Nederlands',
'nb_NO' => 'Norsk',
'pl_PL' => 'Polski',
@@ -108,7 +111,7 @@ class Config extends Setting
public function getJsLanguageCode()
{
$languages = array(
- 'cs_CZ' => 'cz',
+ 'cs_CZ' => 'cs',
'da_DK' => 'da',
'de_DE' => 'de',
'en_US' => 'en',
@@ -129,7 +132,8 @@ class Config extends Setting
'zh_CN' => 'zh-cn',
'ja_JP' => 'ja',
'th_TH' => 'th',
- 'id_ID' => 'id'
+ 'id_ID' => 'id',
+ 'el_GR' => 'el',
);
$lang = $this->getCurrentLanguage();
@@ -145,51 +149,14 @@ class Config extends Setting
*/
public function getCurrentLanguage()
{
- if ($this->userSession->isLogged() && ! empty($this->session['user']['language'])) {
- return $this->session['user']['language'];
+ if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['language'])) {
+ return $this->sessionStorage->user['language'];
}
return $this->get('application_language', 'en_US');
}
/**
- * Get a config variable from the session or the database
- *
- * @access public
- * @param string $name Parameter name
- * @param string $default_value Default value of the parameter
- * @return string
- */
- public function get($name, $default_value = '')
- {
- if (! Session::isOpen()) {
- return $this->getOption($name, $default_value);
- }
-
- // Cache config in session
- if (! isset($this->session['config'][$name])) {
- $this->session['config'] = $this->getAll();
- }
-
- if (! empty($this->session['config'][$name])) {
- return $this->session['config'][$name];
- }
-
- return $default_value;
- }
-
- /**
- * Reload settings in the session and the translations
- *
- * @access public
- */
- public function reload()
- {
- $this->session['config'] = $this->getAll();
- $this->setupTranslations();
- }
-
- /**
* Load translations
*
* @access public
@@ -200,28 +167,27 @@ class Config extends Setting
}
/**
- * Get current timezone
+ * Get a config variable from the session or the database
*
* @access public
+ * @param string $name Parameter name
+ * @param string $default_value Default value of the parameter
* @return string
*/
- public function getCurrentTimezone()
+ public function get($name, $default_value = '')
{
- if ($this->userSession->isLogged() && ! empty($this->session['user']['timezone'])) {
- return $this->session['user']['timezone'];
- }
-
- return $this->get('application_timezone', 'UTC');
+ $options = $this->memoryCache->proxy($this, 'getAll');
+ return isset($options[$name]) && $options[$name] !== '' ? $options[$name] : $default_value;
}
/**
- * Set timezone
+ * Reload settings in the session and the translations
*
* @access public
*/
- public function setupTimezone()
+ public function reload()
{
- date_default_timezone_set($this->getCurrentTimezone());
+ $this->setupTranslations();
}
/**
@@ -232,7 +198,7 @@ class Config extends Setting
*/
public function optimizeDatabase()
{
- return $this->db->getconnection()->exec("VACUUM");
+ return $this->db->getconnection()->exec('VACUUM');
}
/**
@@ -262,10 +228,11 @@ class Config extends Setting
*
* @access public
* @param string $option Parameter name
+ * @return boolean
*/
public function regenerateToken($option)
{
- $this->save(array($option => Security::generateToken()));
+ return $this->save(array($option => Token::getToken()));
}
/**
diff --git a/app/Model/Currency.php b/app/Model/Currency.php
index c1156610..abcce2f0 100644
--- a/app/Model/Currency.php
+++ b/app/Model/Currency.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Currency
*
@@ -21,6 +18,32 @@ class Currency extends Base
const TABLE = 'currencies';
/**
+ * Get available application currencies
+ *
+ * @access public
+ * @return array
+ */
+ public function getCurrencies()
+ {
+ return array(
+ 'USD' => t('USD - US Dollar'),
+ 'EUR' => t('EUR - Euro'),
+ 'GBP' => t('GBP - British Pound'),
+ 'CHF' => t('CHF - Swiss Francs'),
+ 'CAD' => t('CAD - Canadian Dollar'),
+ 'AUD' => t('AUD - Australian Dollar'),
+ 'NZD' => t('NZD - New Zealand Dollar'),
+ 'INR' => t('INR - Indian Rupee'),
+ 'JPY' => t('JPY - Japanese Yen'),
+ 'RSD' => t('RSD - Serbian dinar'),
+ 'SEK' => t('SEK - Swedish Krona'),
+ 'NOK' => t('NOK - Norwegian Krone'),
+ 'BAM' => t('BAM - Konvertible Mark'),
+ 'RUB' => t('RUB - Russian Ruble'),
+ );
+ }
+
+ /**
* Get all currency rates
*
* @access public
@@ -45,7 +68,7 @@ class Currency extends Base
$reference = $this->config->get('application_currency', 'USD');
if ($reference !== $currency) {
- $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array();
+ $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : $rates;
$rate = isset($rates[$currency]) ? $rates[$currency] : 1;
return $rate * $price;
@@ -68,7 +91,7 @@ class Currency extends Base
return $this->update($currency, $rate);
}
- return $this->persist(self::TABLE, compact('currency', 'rate'));
+ return $this->db->table(self::TABLE)->insert(array('currency' => $currency, 'rate' => $rate));
}
/**
@@ -83,24 +106,4 @@ class Currency extends Base
{
return $this->db->table(self::TABLE)->eq('currency', $currency)->update(array('rate' => $rate));
}
-
- /**
- * Validate
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validate(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('currency', t('Field required')),
- new Validators\Required('rate', t('Field required')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/CustomFilter.php b/app/Model/CustomFilter.php
index 6550b4a7..3a6a1a3a 100644
--- a/app/Model/CustomFilter.php
+++ b/app/Model/CustomFilter.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Custom Filter model
*
@@ -102,63 +99,4 @@ class CustomFilter extends Base
{
return $this->db->table(self::TABLE)->eq('id', $filter_id)->remove();
}
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Required('project_id', t('Field required')),
- new Validators\Required('user_id', t('Field required')),
- new Validators\Required('name', t('Field required')),
- new Validators\Required('filter', t('Field required')),
- new Validators\Integer('user_id', t('This value must be an integer')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100),
- new Validators\MaxLength('filter', t('The maximum length is %d characters', 100), 100)
- );
- }
-
- /**
- * Validate filter creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $v = new Validator($values, $this->commonValidationRules());
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate filter modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('Field required')),
- new Validators\Integer('id', t('This value must be an integer')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/File.php b/app/Model/File.php
index daade517..03ea691d 100644
--- a/app/Model/File.php
+++ b/app/Model/File.php
@@ -2,31 +2,44 @@
namespace Kanboard\Model;
+use Exception;
use Kanboard\Event\FileEvent;
use Kanboard\Core\Tool;
use Kanboard\Core\ObjectStorage\ObjectStorageException;
/**
- * File model
+ * Base File Model
*
* @package model
* @author Frederic Guillot
*/
-class File extends Base
+abstract class File extends Base
{
/**
- * SQL table name
- *
- * @var string
- */
- const TABLE = 'files';
-
- /**
- * Events
+ * Get PicoDb query to get all files
*
- * @var string
+ * @access protected
+ * @return \PicoDb\Table
*/
- const EVENT_CREATE = 'file.create';
+ protected function getQuery()
+ {
+ return $this->db
+ ->table(static::TABLE)
+ ->columns(
+ static::TABLE.'.id',
+ static::TABLE.'.name',
+ static::TABLE.'.path',
+ static::TABLE.'.is_image',
+ static::TABLE.'.'.static::FOREIGN_KEY,
+ static::TABLE.'.date',
+ static::TABLE.'.user_id',
+ static::TABLE.'.size',
+ User::TABLE.'.username',
+ User::TABLE.'.name as user_name'
+ )
+ ->join(User::TABLE, 'id', 'user_id')
+ ->asc(static::TABLE.'.name');
+ }
/**
* Get a file by the id
@@ -37,146 +50,120 @@ class File extends Base
*/
public function getById($file_id)
{
- return $this->db->table(self::TABLE)->eq('id', $file_id)->findOne();
+ return $this->db->table(static::TABLE)->eq('id', $file_id)->findOne();
}
/**
- * Remove a file
+ * Get all files
*
* @access public
- * @param integer $file_id File id
- * @return bool
+ * @param integer $id
+ * @return array
*/
- public function remove($file_id)
+ public function getAll($id)
{
- try {
- $file = $this->getbyId($file_id);
- $this->objectStorage->remove($file['path']);
-
- if ($file['is_image'] == 1) {
- $this->objectStorage->remove($this->getThumbnailPath($file['path']));
- }
-
- return $this->db->table(self::TABLE)->eq('id', $file['id'])->remove();
- } catch (ObjectStorageException $e) {
- $this->logger->error($e->getMessage());
- return false;
- }
+ return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->findAll();
}
/**
- * Remove all files for a given task
+ * Get all images
*
* @access public
- * @param integer $task_id Task id
- * @return bool
+ * @param integer $id
+ * @return array
*/
- public function removeAll($task_id)
+ public function getAllImages($id)
{
- $file_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->asc('id')->findAllByColumn('id');
- $results = array();
-
- foreach ($file_ids as $file_id) {
- $results[] = $this->remove($file_id);
- }
+ return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 1)->findAll();
+ }
- return ! in_array(false, $results, true);
+ /**
+ * Get all files without images
+ *
+ * @access public
+ * @param integer $id
+ * @return array
+ */
+ public function getAllDocuments($id)
+ {
+ return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 0)->findAll();
}
/**
* Create a file entry in the database
*
* @access public
- * @param integer $task_id Task id
+ * @param integer $id Foreign key
* @param string $name Filename
* @param string $path Path on the disk
* @param integer $size File size
* @return bool|integer
*/
- public function create($task_id, $name, $path, $size)
+ public function create($id, $name, $path, $size)
{
- $result = $this->db->table(self::TABLE)->save(array(
- 'task_id' => $task_id,
+ $values = array(
+ static::FOREIGN_KEY => $id,
'name' => substr($name, 0, 255),
'path' => $path,
'is_image' => $this->isImage($name) ? 1 : 0,
'size' => $size,
'user_id' => $this->userSession->getId() ?: 0,
'date' => time(),
- ));
+ );
- if ($result) {
- $this->container['dispatcher']->dispatch(
- self::EVENT_CREATE,
- new FileEvent(array('task_id' => $task_id, 'name' => $name))
- );
+ $result = $this->db->table(static::TABLE)->insert($values);
- return (int) $this->db->getLastId();
+ if ($result) {
+ $file_id = (int) $this->db->getLastId();
+ $event = new FileEvent($values + array('file_id' => $file_id));
+ $this->dispatcher->dispatch(static::EVENT_CREATE, $event);
+ return $file_id;
}
return false;
}
/**
- * Get PicoDb query to get all files
+ * Remove all files
*
* @access public
- * @return \PicoDb\Table
+ * @param integer $id
+ * @return bool
*/
- public function getQuery()
+ public function removeAll($id)
{
- return $this->db
- ->table(self::TABLE)
- ->columns(
- self::TABLE.'.id',
- self::TABLE.'.name',
- self::TABLE.'.path',
- self::TABLE.'.is_image',
- self::TABLE.'.task_id',
- self::TABLE.'.date',
- self::TABLE.'.user_id',
- self::TABLE.'.size',
- User::TABLE.'.username',
- User::TABLE.'.name as user_name'
- )
- ->join(User::TABLE, 'id', 'user_id')
- ->asc(self::TABLE.'.name');
- }
+ $file_ids = $this->db->table(static::TABLE)->eq(static::FOREIGN_KEY, $id)->asc('id')->findAllByColumn('id');
+ $results = array();
- /**
- * Get all files for a given task
- *
- * @access public
- * @param integer $task_id Task id
- * @return array
- */
- public function getAll($task_id)
- {
- return $this->getQuery()->eq('task_id', $task_id)->findAll();
- }
+ foreach ($file_ids as $file_id) {
+ $results[] = $this->remove($file_id);
+ }
- /**
- * Get all images for a given task
- *
- * @access public
- * @param integer $task_id Task id
- * @return array
- */
- public function getAllImages($task_id)
- {
- return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 1)->findAll();
+ return ! in_array(false, $results, true);
}
/**
- * Get all files without images for a given task
+ * Remove a file
*
* @access public
- * @param integer $task_id Task id
- * @return array
+ * @param integer $file_id File id
+ * @return bool
*/
- public function getAllDocuments($task_id)
+ public function remove($file_id)
{
- return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 0)->findAll();
+ try {
+ $file = $this->getById($file_id);
+ $this->objectStorage->remove($file['path']);
+
+ if ($file['is_image'] == 1) {
+ $this->objectStorage->remove($this->getThumbnailPath($file['path']));
+ }
+
+ return $this->db->table(static::TABLE)->eq('id', $file['id'])->remove();
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
}
/**
@@ -202,125 +189,96 @@ class File extends Base
}
/**
- * Return the image mimetype based on the file extension
+ * Generate the path for a thumbnails
*
* @access public
- * @param $filename
+ * @param string $key Storage key
* @return string
*/
- public function getImageMimeType($filename)
+ public function getThumbnailPath($key)
{
- $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
-
- switch ($extension) {
- case 'jpeg':
- case 'jpg':
- return 'image/jpeg';
- case 'png':
- return 'image/png';
- case 'gif':
- return 'image/gif';
- default:
- return 'image/jpeg';
- }
+ return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
}
/**
* Generate the path for a new filename
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
+ * @param integer $id Foreign key
* @param string $filename Filename
* @return string
*/
- public function generatePath($project_id, $task_id, $filename)
- {
- return $project_id.DIRECTORY_SEPARATOR.$task_id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
- }
-
- /**
- * Generate the path for a thumbnails
- *
- * @access public
- * @param string $key Storage key
- * @return string
- */
- public function getThumbnailPath($key)
+ public function generatePath($id, $filename)
{
- return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
+ return static::PATH_PREFIX.DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
}
/**
- * Handle file upload
+ * Upload multiple files
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param string $form_name File form name
+ * @param integer $id
+ * @param array $files
* @return bool
*/
- public function uploadFiles($project_id, $task_id, $form_name)
+ public function uploadFiles($id, array $files)
{
try {
- if (empty($_FILES[$form_name])) {
+ if (empty($files)) {
return false;
}
- foreach ($_FILES[$form_name]['error'] as $key => $error) {
- if ($error == UPLOAD_ERR_OK && $_FILES[$form_name]['size'][$key] > 0) {
- $original_filename = $_FILES[$form_name]['name'][$key];
- $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key];
- $destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
-
- if ($this->isImage($original_filename)) {
- $this->generateThumbnailFromFile($uploaded_filename, $destination_filename);
- }
-
- $this->objectStorage->moveUploadedFile($uploaded_filename, $destination_filename);
-
- $this->create(
- $task_id,
- $original_filename,
- $destination_filename,
- $_FILES[$form_name]['size'][$key]
- );
- }
+ foreach (array_keys($files['error']) as $key) {
+ $file = array(
+ 'name' => $files['name'][$key],
+ 'tmp_name' => $files['tmp_name'][$key],
+ 'size' => $files['size'][$key],
+ 'error' => $files['error'][$key],
+ );
+
+ $this->uploadFile($id, $file);
}
return true;
- } catch (ObjectStorageException $e) {
+ } catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
}
/**
- * Handle screenshot upload
+ * Upload a file
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param string $blob Base64 encoded image
- * @return bool|integer
+ * @param integer $id
+ * @param array $file
*/
- public function uploadScreenshot($project_id, $task_id, $blob)
+ public function uploadFile($id, array $file)
{
- $original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time())).'.png';
- return $this->uploadContent($project_id, $task_id, $original_filename, $blob);
+ if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) {
+ $destination_filename = $this->generatePath($id, $file['name']);
+
+ if ($this->isImage($file['name'])) {
+ $this->generateThumbnailFromFile($file['tmp_name'], $destination_filename);
+ }
+
+ $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename);
+ $this->create($id, $file['name'], $destination_filename, $file['size']);
+ } else {
+ throw new Exception('File not uploaded: '.var_export($file['error'], true));
+ }
}
/**
* Handle file upload (base64 encoded content)
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param string $original_filename Filename
- * @param string $blob Base64 encoded file
+ * @param integer $id
+ * @param string $original_filename
+ * @param string $blob
* @return bool|integer
*/
- public function uploadContent($project_id, $task_id, $original_filename, $blob)
+ public function uploadContent($id, $original_filename, $blob)
{
try {
$data = base64_decode($blob);
@@ -329,7 +287,7 @@ class File extends Base
return false;
}
- $destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
+ $destination_filename = $this->generatePath($id, $original_filename);
$this->objectStorage->put($destination_filename, $data);
if ($this->isImage($original_filename)) {
@@ -337,7 +295,7 @@ class File extends Base
}
return $this->create(
- $task_id,
+ $id,
$original_filename,
$destination_filename,
strlen($data)
diff --git a/app/Model/Group.php b/app/Model/Group.php
new file mode 100644
index 00000000..67899503
--- /dev/null
+++ b/app/Model/Group.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Group Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Group extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'groups';
+
+ /**
+ * Get query to fetch all groups
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->db->table(self::TABLE);
+ }
+
+ /**
+ * Get a specific group by id
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getById($group_id)
+ {
+ return $this->getQuery()->eq('id', $group_id)->findOne();
+ }
+
+ /**
+ * Get a specific group by external id
+ *
+ * @access public
+ * @param integer $external_id
+ * @return array
+ */
+ public function getByExternalId($external_id)
+ {
+ return $this->getQuery()->eq('external_id', $external_id)->findOne();
+ }
+
+ /**
+ * Get all groups
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->getQuery()->asc('name')->findAll();
+ }
+
+ /**
+ * Search groups by name
+ *
+ * @access public
+ * @param string $input
+ * @return array
+ */
+ public function search($input)
+ {
+ return $this->db->table(self::TABLE)->ilike('name', '%'.$input.'%')->asc('name')->findAll();
+ }
+
+ /**
+ * Remove a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function remove($group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $group_id)->remove();
+ }
+
+ /**
+ * Create a new group
+ *
+ * @access public
+ * @param string $name
+ * @param string $external_id
+ * @return integer|boolean
+ */
+ public function create($name, $external_id = '')
+ {
+ return $this->persist(self::TABLE, array(
+ 'name' => $name,
+ 'external_id' => $external_id,
+ ));
+ }
+
+ /**
+ * Update existing group
+ *
+ * @access public
+ * @param array $values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+}
diff --git a/app/Model/GroupMember.php b/app/Model/GroupMember.php
new file mode 100644
index 00000000..7ed5f733
--- /dev/null
+++ b/app/Model/GroupMember.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Group Member Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class GroupMember extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'group_has_users';
+
+ /**
+ * Get query to fetch all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return \PicoDb\Table
+ */
+ public function getQuery($group_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq('group_id', $group_id);
+ }
+
+ /**
+ * Get all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getMembers($group_id)
+ {
+ return $this->getQuery($group_id)->findAll();
+ }
+
+ /**
+ * Get all not members
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getNotMembers($group_id)
+ {
+ $subquery = $this->db->table(self::TABLE)
+ ->columns('user_id')
+ ->eq('group_id', $group_id);
+
+ return $this->db->table(User::TABLE)
+ ->notInSubquery('id', $subquery)
+ ->findAll();
+ }
+
+ /**
+ * Add user to a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function addUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'group_id' => $group_id,
+ 'user_id' => $user_id,
+ ));
+ }
+
+ /**
+ * Remove user from a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->remove();
+ }
+
+ /**
+ * Check if a user is member
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function isMember($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->exists();
+ }
+}
diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php
index 0f148ead..feb5f5a3 100644
--- a/app/Model/LastLogin.php
+++ b/app/Model/LastLogin.php
@@ -36,29 +36,39 @@ class LastLogin extends Base
*/
public function create($auth_type, $user_id, $ip, $user_agent)
{
- // Cleanup old sessions if necessary
+ $this->cleanup($user_id);
+
+ return $this->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'auth_type' => $auth_type,
+ 'user_id' => $user_id,
+ 'ip' => $ip,
+ 'user_agent' => substr($user_agent, 0, 255),
+ 'date_creation' => time(),
+ ));
+ }
+
+ /**
+ * Cleanup login history
+ *
+ * @access public
+ * @param integer $user_id
+ */
+ public function cleanup($user_id)
+ {
$connections = $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
- ->desc('date_creation')
+ ->desc('id')
->findAllByColumn('id');
if (count($connections) >= self::NB_LOGINS) {
$this->db->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1))
- ->remove();
+ ->eq('user_id', $user_id)
+ ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1))
+ ->remove();
}
-
- return $this->db
- ->table(self::TABLE)
- ->insert(array(
- 'auth_type' => $auth_type,
- 'user_id' => $user_id,
- 'ip' => $ip,
- 'user_agent' => $user_agent,
- 'date_creation' => time(),
- ));
}
/**
@@ -73,7 +83,7 @@ class LastLogin extends Base
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
- ->desc('date_creation')
+ ->desc('id')
->columns('id', 'auth_type', 'ip', 'user_agent', 'date_creation')
->findAll();
}
diff --git a/app/Model/Link.php b/app/Model/Link.php
index 00b6dfc5..7b81a237 100644
--- a/app/Model/Link.php
+++ b/app/Model/Link.php
@@ -3,8 +3,6 @@
namespace Kanboard\Model;
use PDO;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
/**
* Link model
@@ -176,47 +174,4 @@ class Link extends Base
$this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0));
return $this->db->table(self::TABLE)->eq('id', $link_id)->remove();
}
-
- /**
- * Validate creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('label', t('Field required')),
- new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
- new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('id', t('Field required')),
- new Validators\Required('opposite_id', t('Field required')),
- new Validators\Required('label', t('Field required')),
- new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/Metadata.php b/app/Model/Metadata.php
index 83c8f499..690b2265 100644
--- a/app/Model/Metadata.php
+++ b/app/Model/Metadata.php
@@ -95,4 +95,17 @@ abstract class Metadata extends Base
return ! in_array(false, $results, true);
}
+
+ /**
+ * Remove a metadata
+ *
+ * @access public
+ * @param integer $entity_id
+ * @param string $name
+ * @return bool
+ */
+ public function remove($entity_id, $name)
+ {
+ return $this->db->table(static::TABLE)->eq($this->getEntityKey(), $entity_id)->eq('name', $name)->remove();
+ }
}
diff --git a/app/Model/Notification.php b/app/Model/Notification.php
index f1122993..c252aa31 100644
--- a/app/Model/Notification.php
+++ b/app/Model/Notification.php
@@ -72,8 +72,12 @@ class Notification extends Base
return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']);
case Comment::EVENT_CREATE:
return e('%s commented on the task #%d', $event_author, $event_data['task']['id']);
- case File::EVENT_CREATE:
+ case TaskFile::EVENT_CREATE:
return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']);
+ case Task::EVENT_USER_MENTION:
+ return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']);
+ case Comment::EVENT_USER_MENTION:
+ return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']);
default:
return e('Notification');
}
@@ -90,53 +94,41 @@ class Notification extends Base
public function getTitleWithoutAuthor($event_name, array $event_data)
{
switch ($event_name) {
- case File::EVENT_CREATE:
- $title = e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']);
- break;
+ case TaskFile::EVENT_CREATE:
+ return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']);
case Comment::EVENT_CREATE:
- $title = e('New comment on task #%d', $event_data['comment']['task_id']);
- break;
+ return e('New comment on task #%d', $event_data['comment']['task_id']);
case Comment::EVENT_UPDATE:
- $title = e('Comment updated on task #%d', $event_data['comment']['task_id']);
- break;
+ return e('Comment updated on task #%d', $event_data['comment']['task_id']);
case Subtask::EVENT_CREATE:
- $title = e('New subtask on task #%d', $event_data['subtask']['task_id']);
- break;
+ return e('New subtask on task #%d', $event_data['subtask']['task_id']);
case Subtask::EVENT_UPDATE:
- $title = e('Subtask updated on task #%d', $event_data['subtask']['task_id']);
- break;
+ return e('Subtask updated on task #%d', $event_data['subtask']['task_id']);
case Task::EVENT_CREATE:
- $title = e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']);
- break;
+ return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']);
case Task::EVENT_UPDATE:
- $title = e('Task updated #%d', $event_data['task']['id']);
- break;
+ return e('Task updated #%d', $event_data['task']['id']);
case Task::EVENT_CLOSE:
- $title = e('Task #%d closed', $event_data['task']['id']);
- break;
+ return e('Task #%d closed', $event_data['task']['id']);
case Task::EVENT_OPEN:
- $title = e('Task #%d opened', $event_data['task']['id']);
- break;
+ return e('Task #%d opened', $event_data['task']['id']);
case Task::EVENT_MOVE_COLUMN:
- $title = e('Column changed for task #%d', $event_data['task']['id']);
- break;
+ return e('Column changed for task #%d', $event_data['task']['id']);
case Task::EVENT_MOVE_POSITION:
- $title = e('New position for task #%d', $event_data['task']['id']);
- break;
+ return e('New position for task #%d', $event_data['task']['id']);
case Task::EVENT_MOVE_SWIMLANE:
- $title = e('Swimlane changed for task #%d', $event_data['task']['id']);
- break;
+ return e('Swimlane changed for task #%d', $event_data['task']['id']);
case Task::EVENT_ASSIGNEE_CHANGE:
- $title = e('Assignee changed on task #%d', $event_data['task']['id']);
- break;
+ return e('Assignee changed on task #%d', $event_data['task']['id']);
case Task::EVENT_OVERDUE:
$nb = count($event_data['tasks']);
- $title = $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']);
- break;
+ return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']);
+ case Task::EVENT_USER_MENTION:
+ return e('You were mentioned in the task #%d', $event_data['task']['id']);
+ case Comment::EVENT_USER_MENTION:
+ return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']);
default:
- $title = e('Notification');
+ return e('Notification');
}
-
- return $title;
}
}
diff --git a/app/Model/PasswordReset.php b/app/Model/PasswordReset.php
new file mode 100644
index 00000000..c2d7dde9
--- /dev/null
+++ b/app/Model/PasswordReset.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Password Reset Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class PasswordReset extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'password_reset';
+
+ /**
+ * Token duration (30 minutes)
+ *
+ * @var string
+ */
+ const DURATION = 1800;
+
+ /**
+ * Get all tokens
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getAll($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_creation')->limit(100)->findAll();
+ }
+
+ /**
+ * Generate a new reset token for a user
+ *
+ * @access public
+ * @param string $username
+ * @param integer $expiration
+ * @return boolean|string
+ */
+ public function create($username, $expiration = 0)
+ {
+ $user_id = $this->db->table(User::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id');
+
+ if (! $user_id) {
+ return false;
+ }
+
+ $token = $this->token->getToken();
+
+ $result = $this->db->table(self::TABLE)->insert(array(
+ 'token' => $token,
+ 'user_id' => $user_id,
+ 'date_expiration' => $expiration ?: time() + self::DURATION,
+ 'date_creation' => time(),
+ 'ip' => $this->request->getIpAddress(),
+ 'user_agent' => $this->request->getUserAgent(),
+ 'is_active' => 1,
+ ));
+
+ return $result ? $token : false;
+ }
+
+ /**
+ * Get user id from the token
+ *
+ * @access public
+ * @param string $token
+ * @return integer
+ */
+ public function getUserIdByToken($token)
+ {
+ return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_active', 1)->gte('date_expiration', time())->findOneColumn('user_id');
+ }
+
+ /**
+ * Disable all tokens for a user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function disable($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->update(array('is_active' => 0));
+ }
+}
diff --git a/app/Model/Project.php b/app/Model/Project.php
index b767af26..a79e46a1 100644
--- a/app/Model/Project.php
+++ b/app/Model/Project.php
@@ -2,9 +2,8 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use Kanboard\Core\Security;
+use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
/**
* Project model
@@ -48,6 +47,22 @@ class Project extends Base
}
/**
+ * Get a project by id with owner name
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getByIdWithOwner($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(self::TABLE.'.*', User::TABLE.'.username AS owner_username', User::TABLE.'.name AS owner_name')
+ ->eq(self::TABLE.'.id', $project_id)
+ ->join(User::TABLE, 'id', 'owner_id')
+ ->findOne();
+ }
+
+ /**
* Get a project by the name
*
* @access public
@@ -226,7 +241,7 @@ class Project extends Base
{
$stats = array();
$stats['nb_active_tasks'] = 0;
- $columns = $this->board->getColumns($project_id);
+ $columns = $this->column->getAll($project_id);
$column_stats = $this->board->getColumnStats($project_id);
foreach ($columns as &$column) {
@@ -250,7 +265,7 @@ class Project extends Base
*/
public function getColumnStats(array &$project)
{
- $project['columns'] = $this->board->getColumns($project['id']);
+ $project['columns'] = $this->column->getAll($project['id']);
$stats = $this->board->getColumnStats($project['id']);
foreach ($project['columns'] as &$column) {
@@ -277,23 +292,6 @@ class Project extends Base
}
/**
- * Fetch more information for each project
- *
- * @access public
- * @param array $projects
- * @return array
- */
- public function applyProjectDetails(array $projects)
- {
- foreach ($projects as &$project) {
- $this->getColumnStats($project);
- $project = array_merge($project, $this->projectPermission->getProjectUsers($project['id']));
- }
-
- return $projects;
- }
-
- /**
* Get project summary for a list of project
*
* @access public
@@ -308,30 +306,13 @@ class Project extends Base
return $this->db
->table(Project::TABLE)
- ->in('id', $project_ids)
+ ->columns(self::TABLE.'.*', User::TABLE.'.username AS owner_username', User::TABLE.'.name AS owner_name')
+ ->join(User::TABLE, 'id', 'owner_id')
+ ->in(self::TABLE.'.id', $project_ids)
->callback(array($this, 'applyColumnStats'));
}
/**
- * Get project details (users + columns) for a list of project
- *
- * @access public
- * @param array $project_ids List of project id
- * @return \PicoDb\Table
- */
- public function getQueryProjectDetails(array $project_ids)
- {
- if (empty($project_ids)) {
- return $this->db->table(Project::TABLE)->limit(0);
- }
-
- return $this->db
- ->table(Project::TABLE)
- ->in('id', $project_ids)
- ->callback(array($this, 'applyProjectDetails'));
- }
-
- /**
* Create a project
*
* @access public
@@ -347,11 +328,14 @@ class Project extends Base
$values['token'] = '';
$values['last_modified'] = time();
$values['is_private'] = empty($values['is_private']) ? 0 : 1;
+ $values['owner_id'] = $user_id;
if (! empty($values['identifier'])) {
$values['identifier'] = strtoupper($values['identifier']);
}
+ $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end'));
+
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
@@ -365,7 +349,7 @@ class Project extends Base
}
if ($add_user && $user_id) {
- $this->projectPermission->addManager($project_id, $user_id);
+ $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MANAGER);
}
$this->category->createDefaultCategories($project_id);
@@ -418,6 +402,8 @@ class Project extends Base
$values['identifier'] = strtoupper($values['identifier']);
}
+ $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end'));
+
return $this->exists($values['id']) &&
$this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
}
@@ -491,7 +477,7 @@ class Project extends Base
$this->db
->table(self::TABLE)
->eq('id', $project_id)
- ->save(array('is_public' => 1, 'token' => Security::generateToken()));
+ ->save(array('is_public' => 1, 'token' => Token::getToken()));
}
/**
@@ -509,72 +495,4 @@ class Project extends Base
->eq('id', $project_id)
->save(array('is_public' => 0, 'token' => ''));
}
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('is_active', t('This value must be an integer')),
- new Validators\Required('name', t('The project name is required')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
- new Validators\MaxLength('identifier', t('The maximum length is %d characters', 50), 50),
- new Validators\MaxLength('start_date', t('The maximum length is %d characters', 10), 10),
- new Validators\MaxLength('end_date', t('The maximum length is %d characters', 10), 10),
- new Validators\AlphaNumeric('identifier', t('This value must be alphanumeric')) ,
- new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE),
- new Validators\Unique('identifier', t('The identifier must be unique'), $this->db->getConnection(), self::TABLE),
- );
- }
-
- /**
- * Validate project creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- if (! empty($values['identifier'])) {
- $values['identifier'] = strtoupper($values['identifier']);
- }
-
- $v = new Validator($values, $this->commonValidationRules());
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate project modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- if (! empty($values['identifier'])) {
- $values['identifier'] = strtoupper($values['identifier']);
- }
-
- $rules = array(
- new Validators\Required('id', t('This value is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 309bab9a..74df26a1 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -168,15 +168,11 @@ class ProjectActivity extends Base
*/
public function cleanup($max)
{
- if ($this->db->table(self::TABLE)->count() > $max) {
- $this->db->execute('
- DELETE FROM '.self::TABLE.'
- WHERE id <= (
- SELECT id FROM (
- SELECT id FROM '.self::TABLE.' ORDER BY id DESC LIMIT 1 OFFSET '.$max.'
- ) foo
- )'
- );
+ $total = $this->db->table(self::TABLE)->count();
+
+ if ($total > $max) {
+ $ids = $this->db->table(self::TABLE)->asc('id')->limit($total - $max)->findAllByColumn('id');
+ $this->db->table(self::TABLE)->in('id', $ids)->remove();
}
}
diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php
deleted file mode 100644
index 92364c0c..00000000
--- a/app/Model/ProjectAnalytic.php
+++ /dev/null
@@ -1,182 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project analytic model
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectAnalytic extends Base
-{
- /**
- * Get tasks repartition
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getTaskRepartition($project_id)
- {
- $metrics = array();
- $total = 0;
- $columns = $this->board->getColumns($project_id);
-
- foreach ($columns as $column) {
- $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
- $total += $nb_tasks;
-
- $metrics[] = array(
- 'column_title' => $column['title'],
- 'nb_tasks' => $nb_tasks,
- );
- }
-
- if ($total === 0) {
- return array();
- }
-
- foreach ($metrics as &$metric) {
- $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
- }
-
- return $metrics;
- }
-
- /**
- * Get users repartition
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getUserRepartition($project_id)
- {
- $metrics = array();
- $total = 0;
- $tasks = $this->taskFinder->getAll($project_id);
- $users = $this->projectPermission->getMemberList($project_id);
-
- foreach ($tasks as $task) {
- $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
- $total++;
-
- if (! isset($metrics[$user])) {
- $metrics[$user] = array(
- 'nb_tasks' => 0,
- 'percentage' => 0,
- 'user' => $user,
- );
- }
-
- $metrics[$user]['nb_tasks']++;
- }
-
- if ($total === 0) {
- return array();
- }
-
- foreach ($metrics as &$metric) {
- $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
- }
-
- ksort($metrics);
-
- return array_values($metrics);
- }
-
- /**
- * Get the average lead and cycle time
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getAverageLeadAndCycleTime($project_id)
- {
- $stats = array(
- 'count' => 0,
- 'total_lead_time' => 0,
- 'total_cycle_time' => 0,
- 'avg_lead_time' => 0,
- 'avg_cycle_time' => 0,
- );
-
- $tasks = $this->db
- ->table(Task::TABLE)
- ->columns('date_completed', 'date_creation', 'date_started')
- ->eq('project_id', $project_id)
- ->desc('id')
- ->limit(1000)
- ->findAll();
-
- foreach ($tasks as &$task) {
- $stats['count']++;
- $stats['total_lead_time'] += ($task['date_completed'] ?: time()) - $task['date_creation'];
- $stats['total_cycle_time'] += empty($task['date_started']) ? 0 : ($task['date_completed'] ?: time()) - $task['date_started'];
- }
-
- $stats['avg_lead_time'] = $stats['count'] > 0 ? (int) ($stats['total_lead_time'] / $stats['count']) : 0;
- $stats['avg_cycle_time'] = $stats['count'] > 0 ? (int) ($stats['total_cycle_time'] / $stats['count']) : 0;
-
- return $stats;
- }
-
- /**
- * Get the average time spent into each column
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getAverageTimeSpentByColumn($project_id)
- {
- $stats = array();
- $columns = $this->board->getColumnsList($project_id);
-
- // Get the time spent of the last move for each tasks
- $tasks = $this->db
- ->table(Task::TABLE)
- ->columns('id', 'date_completed', 'date_moved', 'column_id')
- ->eq('project_id', $project_id)
- ->desc('id')
- ->limit(1000)
- ->findAll();
-
- // Init values
- foreach ($columns as $column_id => $column_title) {
- $stats[$column_id] = array(
- 'count' => 0,
- 'time_spent' => 0,
- 'average' => 0,
- 'title' => $column_title,
- );
- }
-
- // Get time spent foreach task/column and take into account the last move
- foreach ($tasks as &$task) {
- $sums = $this->transition->getTimeSpentByTask($task['id']);
-
- if (! isset($sums[$task['column_id']])) {
- $sums[$task['column_id']] = 0;
- }
-
- $sums[$task['column_id']] += ($task['date_completed'] ?: time()) - $task['date_moved'];
-
- foreach ($sums as $column_id => $time_spent) {
- if (isset($stats[$column_id])) {
- $stats[$column_id]['count']++;
- $stats[$column_id]['time_spent'] += $time_spent;
- }
- }
- }
-
- // Calculate average for each column
- foreach ($columns as $column_id => $column_title) {
- $stats[$column_id]['average'] = $stats[$column_id]['count'] > 0 ? (int) ($stats[$column_id]['time_spent'] / $stats[$column_id]['count']) : 0;
- }
-
- return $stats;
- }
-}
diff --git a/app/Model/ProjectDailyColumnStats.php b/app/Model/ProjectDailyColumnStats.php
index 8ed6137f..2bcc4d55 100644
--- a/app/Model/ProjectDailyColumnStats.php
+++ b/app/Model/ProjectDailyColumnStats.php
@@ -2,8 +2,6 @@
namespace Kanboard\Model;
-use PicoDb\Database;
-
/**
* Project Daily Column Stats
*
@@ -20,7 +18,7 @@ class ProjectDailyColumnStats extends Base
const TABLE = 'project_daily_column_stats';
/**
- * Update daily totals for the project and foreach column
+ * Update daily totals for the project and for each column
*
* "total" is the number open of tasks in the column
* "score" is the sum of tasks score in the column
@@ -32,42 +30,22 @@ class ProjectDailyColumnStats extends Base
*/
public function updateTotals($project_id, $date)
{
- $status = $this->config->get('cfd_include_closed_tasks') == 1 ? array(Task::STATUS_OPEN, Task::STATUS_CLOSED) : array(Task::STATUS_OPEN);
-
- return $this->db->transaction(function (Database $db) use ($project_id, $date, $status) {
-
- $column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id');
-
- foreach ($column_ids as $column_id) {
-
- // This call will fail if the record already exists
- // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
- $db->table(ProjectDailyColumnStats::TABLE)->insert(array(
- 'day' => $date,
- 'project_id' => $project_id,
- 'column_id' => $column_id,
- 'total' => 0,
- 'score' => 0,
- ));
-
- $db->table(ProjectDailyColumnStats::TABLE)
- ->eq('project_id', $project_id)
- ->eq('column_id', $column_id)
- ->eq('day', $date)
- ->update(array(
- 'score' => $db->table(Task::TABLE)
- ->eq('project_id', $project_id)
- ->eq('column_id', $column_id)
- ->eq('is_active', Task::STATUS_OPEN)
- ->sum('score'),
- 'total' => $db->table(Task::TABLE)
- ->eq('project_id', $project_id)
- ->eq('column_id', $column_id)
- ->in('is_active', $status)
- ->count()
- ));
- }
- });
+ $this->db->startTransaction();
+ $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('day', $date)->remove();
+
+ foreach ($this->getStatsByColumns($project_id) as $column_id => $column) {
+ $this->db->table(self::TABLE)->insert(array(
+ 'day' => $date,
+ 'project_id' => $project_id,
+ 'column_id' => $column_id,
+ 'total' => $column['total'],
+ 'score' => $column['score'],
+ ));
+ }
+
+ $this->db->closeTransaction();
+
+ return true;
}
/**
@@ -81,43 +59,38 @@ class ProjectDailyColumnStats extends Base
*/
public function countDays($project_id, $from, $to)
{
- $rq = $this->db->execute(
- 'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?',
- array($from, $to, $project_id)
- );
-
- return $rq !== false ? $rq->fetchColumn(0) : 0;
+ return $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->findOneColumn('COUNT(DISTINCT day)');
}
/**
- * Get raw metrics for the project within a data range
+ * Get aggregated metrics for the project within a data range
+ *
+ * [
+ * ['Date', 'Column1', 'Column2'],
+ * ['2014-11-16', 2, 5],
+ * ['2014-11-17', 20, 15],
+ * ]
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
+ * @param string $field Column to aggregate
* @return array
*/
- public function getRawMetrics($project_id, $from, $to)
+ public function getAggregatedMetrics($project_id, $from, $to, $field = 'total')
{
- return $this->db->table(ProjectDailyColumnStats::TABLE)
- ->columns(
- ProjectDailyColumnStats::TABLE.'.column_id',
- ProjectDailyColumnStats::TABLE.'.day',
- ProjectDailyColumnStats::TABLE.'.total',
- ProjectDailyColumnStats::TABLE.'.score',
- Board::TABLE.'.title AS column_title'
- )
- ->join(Board::TABLE, 'id', 'column_id')
- ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->asc(ProjectDailyColumnStats::TABLE.'.day')
- ->findAll();
+ $columns = $this->column->getList($project_id);
+ $metrics = $this->getMetrics($project_id, $from, $to);
+ return $this->buildAggregate($metrics, $columns, $field);
}
/**
- * Get raw metrics for the project within a data range grouped by day
+ * Fetch metrics
*
* @access public
* @param integer $project_id Project id
@@ -125,72 +98,155 @@ class ProjectDailyColumnStats extends Base
* @param string $to End date
* @return array
*/
- public function getRawMetricsByDay($project_id, $from, $to)
+ public function getMetrics($project_id, $from, $to)
{
- return $this->db->table(ProjectDailyColumnStats::TABLE)
- ->columns(
- ProjectDailyColumnStats::TABLE.'.day',
- 'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total',
- 'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score'
- )
- ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->asc(ProjectDailyColumnStats::TABLE.'.day')
- ->groupBy(ProjectDailyColumnStats::TABLE.'.day')
- ->findAll();
+ return $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->asc(self::TABLE.'.day')
+ ->findAll();
}
/**
- * Get aggregated metrics for the project within a data range
- *
- * [
- * ['Date', 'Column1', 'Column2'],
- * ['2014-11-16', 2, 5],
- * ['2014-11-17', 20, 15],
- * ]
+ * Build aggregate
*
- * @access public
- * @param integer $project_id Project id
- * @param string $from Start date (ISO format YYYY-MM-DD)
- * @param string $to End date
- * @param string $column Column to aggregate
+ * @access private
+ * @param array $metrics
+ * @param array $columns
+ * @param string $field
* @return array
*/
- public function getAggregatedMetrics($project_id, $from, $to, $column = 'total')
+ private function buildAggregate(array &$metrics, array &$columns, $field)
{
- $columns = $this->board->getColumnsList($project_id);
$column_ids = array_keys($columns);
- $metrics = array(array_merge(array(e('Date')), array_values($columns)));
- $aggregates = array();
-
- // Fetch metrics for the project
- $records = $this->db->table(ProjectDailyColumnStats::TABLE)
- ->eq('project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->findAll();
-
- // Aggregate by day
- foreach ($records as $record) {
- if (! isset($aggregates[$record['day']])) {
- $aggregates[$record['day']] = array($record['day']);
- }
+ $days = array_unique(array_column($metrics, 'day'));
+ $rows = array(array_merge(array(e('Date')), array_values($columns)));
- $aggregates[$record['day']][$record['column_id']] = $record[$column];
+ foreach ($days as $day) {
+ $rows[] = $this->buildRowAggregate($metrics, $column_ids, $day, $field);
}
- // Aggregate by row
- foreach ($aggregates as $aggregate) {
- $row = array($aggregate[0]);
+ return $rows;
+ }
- foreach ($column_ids as $column_id) {
- $row[] = (int) $aggregate[$column_id];
+ /**
+ * Build one row of the aggregate
+ *
+ * @access private
+ * @param array $metrics
+ * @param array $column_ids
+ * @param string $day
+ * @param string $field
+ * @return array
+ */
+ private function buildRowAggregate(array &$metrics, array &$column_ids, $day, $field)
+ {
+ $row = array($day);
+
+ foreach ($column_ids as $column_id) {
+ $row[] = $this->findValueInMetrics($metrics, $day, $column_id, $field);
+ }
+
+ return $row;
+ }
+
+ /**
+ * Find the value in the metrics
+ *
+ * @access private
+ * @param array $metrics
+ * @param string $day
+ * @param string $column_id
+ * @param string $field
+ * @return integer
+ */
+ private function findValueInMetrics(array &$metrics, $day, $column_id, $field)
+ {
+ foreach ($metrics as $metric) {
+ if ($metric['day'] === $day && $metric['column_id'] == $column_id) {
+ return $metric[$field];
}
+ }
+
+ return 0;
+ }
- $metrics[] = $row;
+ /**
+ * Get number of tasks and score by columns
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getStatsByColumns($project_id)
+ {
+ $totals = $this->getTotalByColumns($project_id);
+ $scores = $this->getScoreByColumns($project_id);
+ $columns = array();
+
+ foreach ($totals as $column_id => $total) {
+ $columns[$column_id] = array('total' => $total, 'score' => 0);
+ }
+
+ foreach ($scores as $column_id => $score) {
+ $columns[$column_id]['score'] = (int) $score;
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Get number of tasks and score by columns
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getScoreByColumns($project_id)
+ {
+ $stats = $this->db->table(Task::TABLE)
+ ->columns('column_id', 'SUM(score) AS score')
+ ->eq('project_id', $project_id)
+ ->eq('is_active', Task::STATUS_OPEN)
+ ->notNull('score')
+ ->groupBy('column_id')
+ ->findAll();
+
+ return array_column($stats, 'score', 'column_id');
+ }
+
+ /**
+ * Get number of tasks and score by columns
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getTotalByColumns($project_id)
+ {
+ $stats = $this->db->table(Task::TABLE)
+ ->columns('column_id', 'COUNT(*) AS total')
+ ->eq('project_id', $project_id)
+ ->in('is_active', $this->getTaskStatusConfig())
+ ->groupBy('column_id')
+ ->findAll();
+
+ return array_column($stats, 'total', 'column_id');
+ }
+
+ /**
+ * Get task status to use for total calculation
+ *
+ * @access private
+ * @return array
+ */
+ private function getTaskStatusConfig()
+ {
+ if ($this->config->get('cfd_include_closed_tasks') == 1) {
+ return array(Task::STATUS_OPEN, Task::STATUS_CLOSED);
}
- return $metrics;
+ return array(Task::STATUS_OPEN);
}
}
diff --git a/app/Model/ProjectDailyStats.php b/app/Model/ProjectDailyStats.php
index 46ca0a4b..957ad51d 100644
--- a/app/Model/ProjectDailyStats.php
+++ b/app/Model/ProjectDailyStats.php
@@ -2,8 +2,6 @@
namespace Kanboard\Model;
-use PicoDb\Database;
-
/**
* Project Daily Stats
*
@@ -29,27 +27,22 @@ class ProjectDailyStats extends Base
*/
public function updateTotals($project_id, $date)
{
- $lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id);
+ $this->db->startTransaction();
+
+ $lead_cycle_time = $this->averageLeadCycleTimeAnalytic->build($project_id);
+
+ $this->db->table(self::TABLE)->eq('day', $date)->eq('project_id', $project_id)->remove();
- return $this->db->transaction(function (Database $db) use ($project_id, $date, $lead_cycle_time) {
+ $this->db->table(self::TABLE)->insert(array(
+ 'day' => $date,
+ 'project_id' => $project_id,
+ 'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
+ 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
+ ));
- // This call will fail if the record already exists
- // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
- $db->table(ProjectDailyStats::TABLE)->insert(array(
- 'day' => $date,
- 'project_id' => $project_id,
- 'avg_lead_time' => 0,
- 'avg_cycle_time' => 0,
- ));
+ $this->db->closeTransaction();
- $db->table(ProjectDailyStats::TABLE)
- ->eq('project_id', $project_id)
- ->eq('day', $date)
- ->update(array(
- 'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
- 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
- ));
- });
+ return true;
}
/**
@@ -64,11 +57,11 @@ class ProjectDailyStats extends Base
public function getRawMetrics($project_id, $from, $to)
{
return $this->db->table(self::TABLE)
- ->columns('day', 'avg_lead_time', 'avg_cycle_time')
- ->eq(self::TABLE.'.project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->asc(self::TABLE.'.day')
- ->findAll();
+ ->columns('day', 'avg_lead_time', 'avg_cycle_time')
+ ->eq('project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->asc('day')
+ ->findAll();
}
}
diff --git a/app/Model/ProjectDuplication.php b/app/Model/ProjectDuplication.php
index f0c66834..9c5f80ad 100644
--- a/app/Model/ProjectDuplication.php
+++ b/app/Model/ProjectDuplication.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use Kanboard\Core\Security\Role;
+
/**
* Project Duplication
*
@@ -12,6 +14,28 @@ namespace Kanboard\Model;
class ProjectDuplication extends Base
{
/**
+ * Get list of optional models to duplicate
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getOptionalSelection()
+ {
+ return array('category', 'projectPermission', 'action', 'swimlane', 'task');
+ }
+
+ /**
+ * Get list of all possible models to duplicate
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getPossibleSelection()
+ {
+ return array('board', 'category', 'projectPermission', 'action', 'swimlane', 'task');
+ }
+
+ /**
* Get a valid project name for the duplication
*
* @access public
@@ -31,78 +55,106 @@ class ProjectDuplication extends Base
}
/**
- * Create a project from another one
- *
- * @param integer $project_id Project Id
- * @return integer Cloned Project Id
- */
- public function copy($project_id)
- {
- $project = $this->project->getById($project_id);
-
- $values = array(
- 'name' => $this->getClonedProjectName($project['name']),
- 'is_active' => true,
- 'last_modified' => 0,
- 'token' => '',
- 'is_public' => 0,
- 'is_private' => empty($project['is_private']) ? 0 : 1,
- );
-
- if (! $this->db->table(Project::TABLE)->save($values)) {
- return 0;
- }
-
- return $this->db->getLastId();
- }
-
- /**
* Clone a project with all settings
*
- * @param integer $project_id Project Id
- * @param array $part_selection Selection of optional project parts to duplicate. Possible options: 'swimlane', 'action', 'category', 'task'
- * @return integer Cloned Project Id
+ * @param integer $src_project_id Project Id
+ * @param array $selection Selection of optional project parts to duplicate
+ * @param integer $owner_id Owner of the project
+ * @param string $name Name of the project
+ * @param boolean $private Force the project to be private
+ * @return integer Cloned Project Id
*/
- public function duplicate($project_id, $part_selection = array('category', 'action'))
+ public function duplicate($src_project_id, $selection = array('projectPermission', 'category', 'action'), $owner_id = 0, $name = null, $private = null)
{
$this->db->startTransaction();
// Get the cloned project Id
- $clone_project_id = $this->copy($project_id);
+ $dst_project_id = $this->copy($src_project_id, $owner_id, $name, $private);
- if (! $clone_project_id) {
+ if ($dst_project_id === false) {
$this->db->cancelTransaction();
return false;
}
// Clone Columns, Categories, Permissions and Actions
- $optional_parts = array('swimlane', 'action', 'category');
- foreach (array('board', 'category', 'projectPermission', 'action', 'swimlane') as $model) {
+ foreach ($this->getPossibleSelection() as $model) {
// Skip if optional part has not been selected
- if (in_array($model, $optional_parts) && ! in_array($model, $part_selection)) {
+ if (in_array($model, $this->getOptionalSelection()) && ! in_array($model, $selection)) {
continue;
}
- if (! $this->$model->duplicate($project_id, $clone_project_id)) {
+ // Skip permissions for private projects
+ if ($private && $model === 'projectPermission') {
+ continue;
+ }
+
+ if (! $this->$model->duplicate($src_project_id, $dst_project_id)) {
$this->db->cancelTransaction();
return false;
}
}
+ if (! $this->makeOwnerManager($dst_project_id, $owner_id)) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
$this->db->closeTransaction();
- // Clone Tasks if in $part_selection
- if (in_array('task', $part_selection)) {
- $tasks = $this->taskFinder->getAll($project_id);
+ return (int) $dst_project_id;
+ }
+
+ /**
+ * Create a project from another one
+ *
+ * @access private
+ * @param integer $src_project_id
+ * @param integer $owner_id
+ * @param string $name
+ * @param boolean $private
+ * @return integer
+ */
+ private function copy($src_project_id, $owner_id = 0, $name = null, $private = null)
+ {
+ $project = $this->project->getById($src_project_id);
+ $is_private = empty($project['is_private']) ? 0 : 1;
+
+ $values = array(
+ 'name' => $name ?: $this->getClonedProjectName($project['name']),
+ 'is_active' => 1,
+ 'last_modified' => time(),
+ 'token' => '',
+ 'is_public' => 0,
+ 'is_private' => $private ? 1 : $is_private,
+ 'owner_id' => $owner_id,
+ );
+
+ if (! $this->db->table(Project::TABLE)->save($values)) {
+ return false;
+ }
+
+ return $this->db->getLastId();
+ }
+
+ /**
+ * Make sure that the creator of the duplicated project is alsp owner
+ *
+ * @access private
+ * @param integer $dst_project_id
+ * @param integer $owner_id
+ * @return boolean
+ */
+ private function makeOwnerManager($dst_project_id, $owner_id)
+ {
+ if ($owner_id > 0) {
+ $this->projectUserRole->removeUser($dst_project_id, $owner_id);
- foreach ($tasks as $task) {
- if (! $this->taskDuplication->duplicateToProject($task['id'], $clone_project_id)) {
- return false;
- }
+ if (! $this->projectUserRole->addUser($dst_project_id, $owner_id, Role::PROJECT_MANAGER)) {
+ return false;
}
}
- return (int) $clone_project_id;
+ return true;
}
}
diff --git a/app/Model/ProjectFile.php b/app/Model/ProjectFile.php
new file mode 100644
index 00000000..aa9bf15b
--- /dev/null
+++ b/app/Model/ProjectFile.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Project File Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectFile extends File
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_files';
+
+ /**
+ * SQL foreign key
+ *
+ * @var string
+ */
+ const FOREIGN_KEY = 'project_id';
+
+ /**
+ * Path prefix
+ *
+ * @var string
+ */
+ const PATH_PREFIX = 'projects';
+
+ /**
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_CREATE = 'project.file.create';
+}
diff --git a/app/Model/ProjectGroupRole.php b/app/Model/ProjectGroupRole.php
new file mode 100644
index 00000000..750ba7fb
--- /dev/null
+++ b/app/Model/ProjectGroupRole.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project Group Role
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectGroupRole extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_groups';
+
+ /**
+ * Get the list of project visible by the given user according to groups
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $status
+ * @return array
+ */
+ public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE))
+ {
+ return $this->db
+ ->hashtable(Project::TABLE)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->eq(GroupMember::TABLE.'.user_id', $user_id)
+ ->in(Project::TABLE.'.is_active', $status)
+ ->getAll(Project::TABLE.'.id', Project::TABLE.'.name');
+ }
+
+ /**
+ * For a given project get the role of the specified user
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return string
+ */
+ public function getUserRole($project_id, $user_id)
+ {
+ $roles = $this->db->table(self::TABLE)
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->eq(GroupMember::TABLE.'.user_id', $user_id)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->findAllByColumn('role');
+
+ return $this->projectAccessMap->getHighestRole($roles);
+ }
+
+ /**
+ * Get all groups associated directly to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getGroups($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(Group::TABLE.'.id', Group::TABLE.'.name', self::TABLE.'.role')
+ ->join(Group::TABLE, 'id', 'group_id')
+ ->eq('project_id', $project_id)
+ ->asc('name')
+ ->findAll();
+ }
+
+ /**
+ * From groups get all users associated to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getUsers($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->asc(User::TABLE.'.username')
+ ->findAll();
+ }
+
+ /**
+ * From groups get all users assignable to tasks
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAssignableUsers($project_id)
+ {
+ return $this->db->table(User::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(GroupMember::TABLE, 'user_id', 'id', User::TABLE)
+ ->join(self::TABLE, 'group_id', 'group_id', GroupMember::TABLE)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->eq(User::TABLE.'.is_active', 1)
+ ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER))
+ ->asc(User::TABLE.'.username')
+ ->findAll();
+ }
+
+ /**
+ * Add a group to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @param string $role
+ * @return boolean
+ */
+ public function addGroup($project_id, $group_id, $role)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'group_id' => $group_id,
+ 'project_id' => $project_id,
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Remove a group from the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @return boolean
+ */
+ public function removeGroup($project_id, $group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('group_id', $group_id)->eq('project_id', $project_id)->remove();
+ }
+
+ /**
+ * Change a group role for the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @param string $role
+ * @return boolean
+ */
+ public function changeGroupRole($project_id, $group_id, $role)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('project_id', $project_id)
+ ->update(array(
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Copy group access from a project to another one
+ *
+ * @param integer $project_src_id Project Template
+ * @return integer $project_dst_id Project that receives the copy
+ * @return boolean
+ */
+ public function duplicate($project_src_id, $project_dst_id)
+ {
+ $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
+
+ foreach ($rows as $row) {
+ $result = $this->db->table(self::TABLE)->save(array(
+ 'project_id' => $project_dst_id,
+ 'group_id' => $row['group_id'],
+ 'role' => $row['role'],
+ ));
+
+ if (! $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/ProjectGroupRoleFilter.php b/app/Model/ProjectGroupRoleFilter.php
new file mode 100644
index 00000000..989d3073
--- /dev/null
+++ b/app/Model/ProjectGroupRoleFilter.php
@@ -0,0 +1,89 @@
+<?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 d9eef4db..db1573ae 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -2,11 +2,10 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
+use Kanboard\Core\Security\Role;
/**
- * Project permission model
+ * Project Permission
*
* @package model
* @author Frederic Guillot
@@ -14,117 +13,14 @@ use SimpleValidator\Validators;
class ProjectPermission extends Base
{
/**
- * SQL table name for permissions
- *
- * @var string
- */
- const TABLE = 'project_has_users';
-
- /**
- * Get a list of people that can be assigned for tasks
- *
- * @access public
- * @param integer $project_id Project id
- * @param bool $prepend_unassigned Prepend the 'Unassigned' value
- * @param bool $prepend_everybody Prepend the 'Everbody' value
- * @param bool $allow_single_user If there is only one user return only this user
- * @return array
- */
- public function getMemberList($project_id, $prepend_unassigned = true, $prepend_everybody = false, $allow_single_user = false)
- {
- $allowed_users = $this->getMembers($project_id);
-
- if ($allow_single_user && count($allowed_users) === 1) {
- return $allowed_users;
- }
-
- if ($prepend_unassigned) {
- $allowed_users = array(t('Unassigned')) + $allowed_users;
- }
-
- if ($prepend_everybody) {
- $allowed_users = array(User::EVERYBODY_ID => t('Everybody')) + $allowed_users;
- }
-
- return $allowed_users;
- }
-
- /**
- * Get a list of members and managers with a single SQL query
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getProjectUsers($project_id)
- {
- $result = array(
- 'managers' => array(),
- 'members' => array(),
- );
-
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.is_owner')
- ->findAll();
-
- foreach ($users as $user) {
- $key = $user['is_owner'] == 1 ? 'managers' : 'members';
- $result[$key][$user['id']] = $user['name'] ?: $user['username'];
- }
-
- return $result;
- }
-
- /**
- * Get a list of allowed people for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getMembers($project_id)
- {
- if ($this->isEverybodyAllowed($project_id)) {
- return $this->user->getList();
- }
-
- return $this->getAssociatedUsers($project_id);
- }
-
- /**
- * Get a list of owners for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getManagers($project_id)
- {
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->eq('is_owner', 1)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
- ->findAll();
-
- return $this->user->prepareList($users);
- }
-
- /**
* Get query for project users overview
*
* @access public
* @param array $project_ids
- * @param integer $is_owner
+ * @param string $role
* @return \PicoDb\Table
*/
- public function getQueryByRole(array $project_ids, $is_owner = 0)
+ public function getQueryByRole(array $project_ids, $role)
{
if (empty($project_ids)) {
$project_ids = array(-1);
@@ -132,10 +28,10 @@ class ProjectPermission extends Base
return $this
->db
- ->table(self::TABLE)
+ ->table(ProjectUserRole::TABLE)
->join(User::TABLE, 'id', 'user_id')
->join(Project::TABLE, 'id', 'project_id')
- ->eq(self::TABLE.'.is_owner', $is_owner)
+ ->eq(ProjectUserRole::TABLE.'.role', $role)
->eq(Project::TABLE.'.is_private', 0)
->in(Project::TABLE.'.id', $project_ids)
->columns(
@@ -148,169 +44,22 @@ class ProjectPermission extends Base
}
/**
- * Get a list of people associated to the project
+ * Get all usernames (fetch users from groups)
*
* @access public
- * @param integer $project_id Project id
+ * @param integer $project_id
+ * @param string $input
* @return array
*/
- public function getAssociatedUsers($project_id)
+ public function findUsernames($project_id, $input)
{
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
- ->findAll();
+ $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
+ $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
+ $members = array_unique(array_merge($userMembers, $groupMembers));
- return $this->user->prepareList($users);
- }
+ sort($members);
- /**
- * Get allowed and not allowed users for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getAllUsers($project_id)
- {
- $users = array(
- 'allowed' => array(),
- 'not_allowed' => array(),
- 'managers' => array(),
- );
-
- $all_users = $this->user->getList();
-
- $users['allowed'] = $this->getMembers($project_id);
- $users['managers'] = $this->getManagers($project_id);
-
- foreach ($all_users as $user_id => $username) {
- if (! isset($users['allowed'][$user_id])) {
- $users['not_allowed'][$user_id] = $username;
- }
- }
-
- return $users;
- }
-
- /**
- * Add a new project member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function addMember($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->save(array('project_id' => $project_id, 'user_id' => $user_id));
- }
-
- /**
- * Remove a member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function revokeMember($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->remove();
- }
-
- /**
- * Add a project manager
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function addManager($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->save(array('project_id' => $project_id, 'user_id' => $user_id, 'is_owner' => 1));
- }
-
- /**
- * Change the role of a member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @param integer $is_owner Is user owner of the project
- * @return bool
- */
- public function changeRole($project_id, $user_id, $is_owner)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->update(array('is_owner' => (int) $is_owner));
- }
-
- /**
- * Check if a specific user is member of a project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isMember($project_id, $user_id)
- {
- if ($this->isEverybodyAllowed($project_id)) {
- return true;
- }
-
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->exists();
- }
-
- /**
- * Check if a specific user is manager of a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isManager($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->eq('is_owner', 1)
- ->exists();
- }
-
- /**
- * Check if a specific user is allowed to access to a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isUserAllowed($project_id, $user_id)
- {
- return $project_id === 0 || $this->user->isAdmin($user_id) || $this->isMember($project_id, $user_id);
+ return $members;
}
/**
@@ -330,172 +79,73 @@ class ProjectPermission extends Base
}
/**
- * Return a list of allowed active projects for a given user
+ * Return true if the user is allowed to access a project
*
- * @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getAllowedProjects($user_id)
+ public function isUserAllowed($project_id, $user_id)
{
- if ($this->user->isAdmin($user_id)) {
- return $this->project->getListByStatus(Project::ACTIVE);
+ if ($this->userSession->isAdmin()) {
+ return true;
}
- return $this->getActiveMemberProjects($user_id);
- }
-
- /**
- * Return a list of projects where the user is member
- *
- * @access public
- * @param integer $user_id User id
- * @return array
- */
- public function getMemberProjects($user_id)
- {
- return $this->db
- ->hashtable(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->join(self::TABLE, 'project_id', 'id')
- ->getAll('projects.id', 'name');
+ return in_array(
+ $this->projectUserRole->getUserRole($project_id, $user_id),
+ array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER, Role::PROJECT_VIEWER)
+ );
}
/**
- * Return a list of project ids where the user is member
+ * Return true if the user is assignable
*
* @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getMemberProjectIds($user_id)
+ public function isAssignable($project_id, $user_id)
{
- return $this->db
- ->table(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->join(self::TABLE, 'project_id', 'id')
- ->findAllByColumn('projects.id');
+ return $this->user->isActive($user_id) &&
+ in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER));
}
/**
- * Return a list of active project ids where the user is member
+ * Return true if the user is member
*
* @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getActiveMemberProjectIds($user_id)
+ public function isMember($project_id, $user_id)
{
- return $this->db
- ->table(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
- ->join(self::TABLE, 'project_id', 'id')
- ->findAllByColumn('projects.id');
+ return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER, Role::PROJECT_VIEWER));
}
/**
- * Return a list of active projects where the user is member
+ * Get active project ids by user
*
* @access public
- * @param integer $user_id User id
+ * @param integer $user_id
* @return array
*/
- public function getActiveMemberProjects($user_id)
+ public function getActiveProjectIds($user_id)
{
- return $this->db
- ->hashtable(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
- ->join(self::TABLE, 'project_id', 'id')
- ->getAll('projects.id', 'name');
+ return array_keys($this->projectUserRole->getActiveProjectsByUser($user_id));
}
/**
- * Copy user access from a project to another one
+ * Copy permissions to another project
*
- * @param integer $project_src Project Template
- * @return integer $project_dst Project that receives the copy
+ * @param integer $project_src_id Project Template
+ * @param integer $project_dst_id Project that receives the copy
* @return boolean
*/
- public function duplicate($project_src, $project_dst)
- {
- $rows = $this->db
- ->table(self::TABLE)
- ->columns('project_id', 'user_id', 'is_owner')
- ->eq('project_id', $project_src)
- ->findAll();
-
- foreach ($rows as $row) {
- $result = $this->db
- ->table(self::TABLE)
- ->save(array(
- 'project_id' => $project_dst,
- 'user_id' => $row['user_id'],
- 'is_owner' => (int) $row['is_owner'], // (int) for postgres
- ));
-
- if (! $result) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Validate allow user
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateUserModification(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Required('user_id', t('The user id is required')),
- new Validators\Integer('user_id', t('This value must be an integer')),
- new Validators\Integer('is_owner', t('This value must be an integer')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate allow everybody
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateProjectModification(array $values)
+ public function duplicate($project_src_id, $project_dst_id)
{
- $v = new Validator($values, array(
- new Validators\Required('id', t('The project id is required')),
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('is_everybody_allowed', t('This value must be an integer')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
+ return $this->projectUserRole->duplicate($project_src_id, $project_dst_id) &&
+ $this->projectGroupRole->duplicate($project_src_id, $project_dst_id);
}
}
diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php
new file mode 100644
index 00000000..56da679c
--- /dev/null
+++ b/app/Model/ProjectUserRole.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project User Role
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectUserRole extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_users';
+
+ /**
+ * Get the list of active project for the given user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getActiveProjectsByUser($user_id)
+ {
+ return $this->getProjectsByUser($user_id, array(Project::ACTIVE));
+ }
+
+ /**
+ * Get the list of project visible for the given user
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $status
+ * @return array
+ */
+ public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE))
+ {
+ $userProjects = $this->db
+ ->hashtable(Project::TABLE)
+ ->beginOr()
+ ->eq(self::TABLE.'.user_id', $user_id)
+ ->eq(Project::TABLE.'.is_everybody_allowed', 1)
+ ->closeOr()
+ ->in(Project::TABLE.'.is_active', $status)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->getAll(Project::TABLE.'.id', Project::TABLE.'.name');
+
+ $groupProjects = $this->projectGroupRole->getProjectsByUser($user_id, $status);
+ $projects = $userProjects + $groupProjects;
+
+ asort($projects);
+
+ return $projects;
+ }
+
+ /**
+ * For a given project get the role of the specified user
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return string
+ */
+ public function getUserRole($project_id, $user_id)
+ {
+ if ($this->projectPermission->isEverybodyAllowed($project_id)) {
+ return Role::PROJECT_MEMBER;
+ }
+
+ $role = $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->findOneColumn('role');
+
+ if (empty($role)) {
+ $role = $this->projectGroupRole->getUserRole($project_id, $user_id);
+ }
+
+ return $role;
+ }
+
+ /**
+ * Get all users associated directly to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getUsers($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq('project_id', $project_id)
+ ->asc(User::TABLE.'.username')
+ ->asc(User::TABLE.'.name')
+ ->findAll();
+ }
+
+ /**
+ * Get all users (fetch users from groups)
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAllUsers($project_id)
+ {
+ $userMembers = $this->getUsers($project_id);
+ $groupMembers = $this->projectGroupRole->getUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ return $this->user->prepareList($members);
+ }
+
+ /**
+ * Get users grouped by role
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getAllUsersGroupedByRole($project_id)
+ {
+ $users = array();
+
+ $userMembers = $this->getUsers($project_id);
+ $groupMembers = $this->projectGroupRole->getUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ foreach ($members as $user) {
+ if (! isset($users[$user['role']])) {
+ $users[$user['role']] = array();
+ }
+
+ $users[$user['role']][$user['id']] = $user['name'] ?: $user['username'];
+ }
+
+ return $users;
+ }
+
+ /**
+ * Get list of users that can be assigned to a task (only Manager and Member)
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAssignableUsers($project_id)
+ {
+ if ($this->projectPermission->isEverybodyAllowed($project_id)) {
+ return $this->user->getActiveUsersList();
+ }
+
+ $userMembers = $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq(User::TABLE.'.is_active', 1)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER))
+ ->findAll();
+
+ $groupMembers = $this->projectGroupRole->getAssignableUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ return $this->user->prepareList($members);
+ }
+
+ /**
+ * Get list of users that can be assigned to a task (only Manager and Member)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param bool $unassigned Prepend the 'Unassigned' value
+ * @param bool $everybody Prepend the 'Everbody' value
+ * @param bool $singleUser If there is only one user return only this user
+ * @return array
+ */
+ public function getAssignableUsersList($project_id, $unassigned = true, $everybody = false, $singleUser = false)
+ {
+ $users = $this->getAssignableUsers($project_id);
+
+ if ($singleUser && count($users) === 1) {
+ return $users;
+ }
+
+ if ($unassigned) {
+ $users = array(t('Unassigned')) + $users;
+ }
+
+ if ($everybody) {
+ $users = array(User::EVERYBODY_ID => t('Everybody')) + $users;
+ }
+
+ return $users;
+ }
+
+ /**
+ * Add a user to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @param string $role
+ * @return boolean
+ */
+ public function addUser($project_id, $user_id, $role)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'user_id' => $user_id,
+ 'project_id' => $project_id,
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Remove a user from the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeUser($project_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->remove();
+ }
+
+ /**
+ * Change a user role for the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @param string $role
+ * @return boolean
+ */
+ public function changeUserRole($project_id, $user_id, $role)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->eq('project_id', $project_id)
+ ->update(array(
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * 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
+ * @return boolean
+ */
+ public function duplicate($project_src_id, $project_dst_id)
+ {
+ $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
+
+ foreach ($rows as $row) {
+ $result = $this->db->table(self::TABLE)->save(array(
+ 'project_id' => $project_dst_id,
+ 'user_id' => $row['user_id'],
+ 'role' => $row['role'],
+ ));
+
+ if (! $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/ProjectUserRoleFilter.php b/app/Model/ProjectUserRoleFilter.php
new file mode 100644
index 00000000..64403643
--- /dev/null
+++ b/app/Model/ProjectUserRoleFilter.php
@@ -0,0 +1,88 @@
+<?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/RememberMeSession.php b/app/Model/RememberMeSession.php
new file mode 100644
index 00000000..8989a6d7
--- /dev/null
+++ b/app/Model/RememberMeSession.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Token;
+
+/**
+ * Remember Me Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class RememberMeSession extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'remember_me';
+
+ /**
+ * Expiration (60 days)
+ *
+ * @var integer
+ */
+ const EXPIRATION = 5184000;
+
+ /**
+ * Get a remember me record
+ *
+ * @access public
+ * @param $token
+ * @param $sequence
+ * @return mixed
+ */
+ public function find($token, $sequence)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('token', $token)
+ ->eq('sequence', $sequence)
+ ->gt('expiration', time())
+ ->findOne();
+ }
+
+ /**
+ * Get all sessions for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getAll($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->desc('date_creation')
+ ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
+ ->findAll();
+ }
+
+ /**
+ * Create a new RememberMe session
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param string $ip IP Address
+ * @param string $user_agent User Agent
+ * @return array
+ */
+ public function create($user_id, $ip, $user_agent)
+ {
+ $token = hash('sha256', $user_id.$user_agent.$ip.Token::getToken());
+ $sequence = Token::getToken();
+ $expiration = time() + self::EXPIRATION;
+
+ $this->cleanup($user_id);
+
+ $this
+ ->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'user_id' => $user_id,
+ 'ip' => $ip,
+ 'user_agent' => $user_agent,
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ 'date_creation' => time(),
+ ));
+
+ return array(
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ );
+ }
+
+ /**
+ * Remove a session record
+ *
+ * @access public
+ * @param integer $session_id Session id
+ * @return mixed
+ */
+ public function remove($session_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $session_id)
+ ->remove();
+ }
+
+ /**
+ * Remove old sessions for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return bool
+ */
+ public function cleanup($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->lt('expiration', time())
+ ->remove();
+ }
+
+ /**
+ * Return a new sequence token and update the database
+ *
+ * @access public
+ * @param string $token Session token
+ * @return string
+ */
+ public function updateSequence($token)
+ {
+ $sequence = Token::getToken();
+
+ $this
+ ->db
+ ->table(self::TABLE)
+ ->eq('token', $token)
+ ->update(array('sequence' => $sequence));
+
+ return $sequence;
+ }
+}
diff --git a/app/Model/Setting.php b/app/Model/Setting.php
index 3507d424..44e6c065 100644
--- a/app/Model/Setting.php
+++ b/app/Model/Setting.php
@@ -47,10 +47,12 @@ abstract class Setting extends Base
*/
public function getOption($name, $default = '')
{
- return $this->db
+ $value = $this->db
->table(self::TABLE)
->eq('option', $name)
- ->findOneColumn('value') ?: $default;
+ ->findOneColumn('value');
+
+ return $value === null || $value === false || $value === '' ? $default : $value;
}
/**
diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php
index 664e41e1..b5898fcf 100644
--- a/app/Model/Subtask.php
+++ b/app/Model/Subtask.php
@@ -4,11 +4,9 @@ namespace Kanboard\Model;
use PicoDb\Database;
use Kanboard\Event\SubtaskEvent;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
/**
- * Subtask model
+ * Subtask Model
*
* @package model
* @author Frederic Guillot
@@ -265,89 +263,36 @@ class Subtask extends Base
}
/**
- * Get subtasks with consecutive positions
- *
- * If you remove a subtask, the positions are not anymore consecutives
- *
- * @access public
- * @param integer $task_id
- * @return array
- */
- public function getNormalizedPositions($task_id)
- {
- $subtasks = $this->db->hashtable(self::TABLE)->eq('task_id', $task_id)->asc('position')->getAll('id', 'position');
- $position = 1;
-
- foreach ($subtasks as $subtask_id => $subtask_position) {
- $subtasks[$subtask_id] = $position++;
- }
-
- return $subtasks;
- }
-
- /**
- * Save the new positions for a set of subtasks
- *
- * @access public
- * @param array $subtasks Hashmap of column_id/column_position
- * @return boolean
- */
- public function savePositions(array $subtasks)
- {
- return $this->db->transaction(function (Database $db) use ($subtasks) {
-
- foreach ($subtasks as $subtask_id => $position) {
- if (! $db->table(Subtask::TABLE)->eq('id', $subtask_id)->update(array('position' => $position))) {
- return false;
- }
- }
- });
- }
-
- /**
- * Move a subtask down, increment the position value
+ * Save subtask position
*
* @access public
* @param integer $task_id
* @param integer $subtask_id
+ * @param integer $position
* @return boolean
*/
- public function moveDown($task_id, $subtask_id)
+ public function changePosition($task_id, $subtask_id, $position)
{
- $subtasks = $this->getNormalizedPositions($task_id);
- $positions = array_flip($subtasks);
-
- if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] < count($subtasks)) {
- $position = ++$subtasks[$subtask_id];
- $subtasks[$positions[$position]]--;
-
- return $this->savePositions($subtasks);
+ if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('task_id', $task_id)->count()) {
+ return false;
}
- return false;
- }
-
- /**
- * Move a subtask up, decrement the position value
- *
- * @access public
- * @param integer $task_id
- * @param integer $subtask_id
- * @return boolean
- */
- public function moveUp($task_id, $subtask_id)
- {
- $subtasks = $this->getNormalizedPositions($task_id);
- $positions = array_flip($subtasks);
+ $subtask_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id');
+ $offset = 1;
+ $results = array();
- if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] > 1) {
- $position = --$subtasks[$subtask_id];
- $subtasks[$positions[$position]]++;
+ foreach ($subtask_ids as $current_subtask_id) {
+ if ($offset == $position) {
+ $offset++;
+ }
- return $this->savePositions($subtasks);
+ $results[] = $this->db->table(self::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset));
+ $offset++;
}
- return false;
+ $results[] = $this->db->table(self::TABLE)->eq('id', $subtask_id)->update(array('position' => $position));
+
+ return !in_array(false, $results, true);
}
/**
@@ -355,15 +300,16 @@ class Subtask extends Base
*
* @access public
* @param integer $subtask_id
- * @return bool
+ * @return boolean|integer
*/
public function toggleStatus($subtask_id)
{
$subtask = $this->getById($subtask_id);
+ $status = ($subtask['status'] + 1) % 3;
$values = array(
'id' => $subtask['id'],
- 'status' => ($subtask['status'] + 1) % 3,
+ 'status' => $status,
'task_id' => $subtask['task_id'],
);
@@ -371,7 +317,7 @@ class Subtask extends Base
$values['user_id'] = $this->userSession->getId();
}
- return $this->update($values);
+ return $this->update($values) ? $status : false;
}
/**
@@ -437,10 +383,10 @@ class Subtask extends Base
return $this->db->transaction(function (Database $db) use ($src_task_id, $dst_task_id) {
$subtasks = $db->table(Subtask::TABLE)
- ->columns('title', 'time_estimated', 'position')
- ->eq('task_id', $src_task_id)
- ->asc('position')
- ->findAll();
+ ->columns('title', 'time_estimated', 'position')
+ ->eq('task_id', $src_task_id)
+ ->asc('position')
+ ->findAll();
foreach ($subtasks as &$subtask) {
$subtask['task_id'] = $dst_task_id;
@@ -451,90 +397,4 @@ class Subtask extends Base
}
});
}
-
- /**
- * Validate creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $rules = array(
- new Validators\Required('task_id', t('The task id is required')),
- new Validators\Required('title', t('The title is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The subtask id is required')),
- new Validators\Required('task_id', t('The task id is required')),
- new Validators\Required('title', t('The title is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate API modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateApiModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The subtask id is required')),
- new Validators\Required('task_id', t('The task id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('The subtask id must be an integer')),
- new Validators\Integer('task_id', t('The task id must be an integer')),
- new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255),
- new Validators\Integer('user_id', t('The user id must be an integer')),
- new Validators\Integer('status', t('The status must be an integer')),
- new Validators\Numeric('time_estimated', t('The time must be a numeric value')),
- new Validators\Numeric('time_spent', t('The time must be a numeric value')),
- );
- }
}
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index a741dbb5..b766b542 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -302,11 +302,11 @@ class SubtaskTimeTracking extends Base
{
$hook = 'model:subtask-time-tracking:calculate:time-spent';
$start_time = $this->db
- ->table(self::TABLE)
- ->eq('subtask_id', $subtask_id)
- ->eq('user_id', $user_id)
- ->eq('end', 0)
- ->findOneColumn('start');
+ ->table(self::TABLE)
+ ->eq('subtask_id', $subtask_id)
+ ->eq('user_id', $user_id)
+ ->eq('end', 0)
+ ->findOneColumn('start');
if (empty($start_time)) {
return 0;
diff --git a/app/Model/Swimlane.php b/app/Model/Swimlane.php
index df44985a..721f20d3 100644
--- a/app/Model/Swimlane.php
+++ b/app/Model/Swimlane.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Swimlanes
*
@@ -99,10 +96,11 @@ class Swimlane extends Base
*/
public function getDefault($project_id)
{
- $result = $this->db->table(Project::TABLE)
- ->eq('id', $project_id)
- ->columns('id', 'default_swimlane', 'show_default_swimlane')
- ->findOne();
+ $result = $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->columns('id', 'default_swimlane', 'show_default_swimlane')
+ ->findOne();
if ($result['default_swimlane'] === 'Default swimlane') {
$result['default_swimlane'] = t($result['default_swimlane']);
@@ -120,10 +118,11 @@ class Swimlane extends Base
*/
public function getAll($project_id)
{
- return $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->orderBy('position', 'asc')
- ->findAll();
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->orderBy('position', 'asc')
+ ->findAll();
}
/**
@@ -136,9 +135,10 @@ class Swimlane extends Base
*/
public function getAllByStatus($project_id, $status = self::ACTIVE)
{
- $query = $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', $status);
+ $query = $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', $status);
if ($status == self::ACTIVE) {
$query->asc('position');
@@ -158,17 +158,19 @@ class Swimlane extends Base
*/
public function getSwimlanes($project_id)
{
- $swimlanes = $this->db->table(self::TABLE)
- ->columns('id', 'name', 'description')
- ->eq('project_id', $project_id)
- ->eq('is_active', self::ACTIVE)
- ->orderBy('position', 'asc')
- ->findAll();
-
- $default_swimlane = $this->db->table(Project::TABLE)
- ->eq('id', $project_id)
- ->eq('show_default_swimlane', 1)
- ->findOneColumn('default_swimlane');
+ $swimlanes = $this->db
+ ->table(self::TABLE)
+ ->columns('id', 'name', 'description')
+ ->eq('project_id', $project_id)
+ ->eq('is_active', self::ACTIVE)
+ ->orderBy('position', 'asc')
+ ->findAll();
+
+ $default_swimlane = $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->eq('show_default_swimlane', 1)
+ ->findOneColumn('default_swimlane');
if ($default_swimlane) {
if ($default_swimlane === 'Default swimlane') {
@@ -203,11 +205,12 @@ class Swimlane extends Base
$swimlanes[0] = $default === 'Default swimlane' ? t($default) : $default;
}
- return $swimlanes + $this->db->hashtable(self::TABLE)
- ->eq('project_id', $project_id)
- ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE))
- ->orderBy('position', 'asc')
- ->getAll('id', 'name');
+ return $swimlanes + $this->db
+ ->hashtable(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE))
+ ->orderBy('position', 'asc')
+ ->getAll('id', 'name');
}
/**
@@ -235,9 +238,10 @@ class Swimlane extends Base
*/
public function update(array $values)
{
- return $this->db->table(self::TABLE)
- ->eq('id', $values['id'])
- ->update($values);
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $values['id'])
+ ->update($values);
}
/**
@@ -250,12 +254,46 @@ class Swimlane extends Base
public function updateDefault(array $values)
{
return $this->db
- ->table(Project::TABLE)
- ->eq('id', $values['id'])
- ->update(array(
- 'default_swimlane' => $values['default_swimlane'],
- 'show_default_swimlane' => $values['show_default_swimlane'],
- ));
+ ->table(Project::TABLE)
+ ->eq('id', $values['id'])
+ ->update(array(
+ 'default_swimlane' => $values['default_swimlane'],
+ 'show_default_swimlane' => $values['show_default_swimlane'],
+ ));
+ }
+
+ /**
+ * Enable the default swimlane
+ *
+ * @access public
+ * @param integer $project_id
+ * @return bool
+ */
+ public function enableDefault($project_id)
+ {
+ return $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->update(array(
+ 'show_default_swimlane' => 1,
+ ));
+ }
+
+ /**
+ * Disable the default swimlane
+ *
+ * @access public
+ * @param integer $project_id
+ * @return bool
+ */
+ public function disableDefault($project_id)
+ {
+ return $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->update(array(
+ 'show_default_swimlane' => 0,
+ ));
}
/**
@@ -267,10 +305,11 @@ class Swimlane extends Base
*/
public function getLastPosition($project_id)
{
- return $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', 1)
- ->count() + 1;
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', 1)
+ ->count() + 1;
}
/**
@@ -284,12 +323,12 @@ class Swimlane extends Base
public function disable($project_id, $swimlane_id)
{
$result = $this->db
- ->table(self::TABLE)
- ->eq('id', $swimlane_id)
- ->update(array(
- 'is_active' => self::INACTIVE,
- 'position' => 0,
- ));
+ ->table(self::TABLE)
+ ->eq('id', $swimlane_id)
+ ->update(array(
+ 'is_active' => self::INACTIVE,
+ 'position' => 0,
+ ));
if ($result) {
// Re-order positions
@@ -310,12 +349,12 @@ class Swimlane extends Base
public function enable($project_id, $swimlane_id)
{
return $this->db
- ->table(self::TABLE)
- ->eq('id', $swimlane_id)
- ->update(array(
- 'is_active' => self::ACTIVE,
- 'position' => $this->getLastPosition($project_id),
- ));
+ ->table(self::TABLE)
+ ->eq('id', $swimlane_id)
+ ->update(array(
+ 'is_active' => self::ACTIVE,
+ 'position' => $this->getLastPosition($project_id),
+ ));
}
/**
@@ -356,11 +395,13 @@ class Swimlane extends Base
public function updatePositions($project_id)
{
$position = 0;
- $swimlanes = $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', 1)
- ->asc('position')
- ->findAllByColumn('id');
+ $swimlanes = $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', 1)
+ ->asc('position')
+ ->asc('id')
+ ->findAllByColumn('id');
if (! $swimlanes) {
return false;
@@ -368,77 +409,50 @@ class Swimlane extends Base
foreach ($swimlanes as $swimlane_id) {
$this->db->table(self::TABLE)
- ->eq('id', $swimlane_id)
- ->update(array('position' => ++$position));
+ ->eq('id', $swimlane_id)
+ ->update(array('position' => ++$position));
}
return true;
}
/**
- * Move a swimlane down, increment the position value
+ * Change swimlane position
*
* @access public
- * @param integer $project_id Project id
- * @param integer $swimlane_id Swimlane id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $position
* @return boolean
*/
- public function moveDown($project_id, $swimlane_id)
+ public function changePosition($project_id, $swimlane_id, $position)
{
- $swimlanes = $this->db->hashtable(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', self::ACTIVE)
- ->asc('position')
- ->getAll('id', 'position');
-
- $positions = array_flip($swimlanes);
-
- if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] < count($swimlanes)) {
- $position = ++$swimlanes[$swimlane_id];
- $swimlanes[$positions[$position]]--;
-
- $this->db->startTransaction();
- $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position));
- $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]]));
- $this->db->closeTransaction();
-
- return true;
+ if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) {
+ return false;
}
- return false;
- }
+ $swimlane_ids = $this->db->table(self::TABLE)
+ ->eq('is_active', 1)
+ ->eq('project_id', $project_id)
+ ->neq('id', $swimlane_id)
+ ->asc('position')
+ ->findAllByColumn('id');
- /**
- * Move a swimlane up, decrement the position value
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $swimlane_id Swimlane id
- * @return boolean
- */
- public function moveUp($project_id, $swimlane_id)
- {
- $swimlanes = $this->db->hashtable(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', self::ACTIVE)
- ->asc('position')
- ->getAll('id', 'position');
-
- $positions = array_flip($swimlanes);
+ $offset = 1;
+ $results = array();
- if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] > 1) {
- $position = --$swimlanes[$swimlane_id];
- $swimlanes[$positions[$position]]++;
-
- $this->db->startTransaction();
- $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position));
- $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]]));
- $this->db->closeTransaction();
+ foreach ($swimlane_ids as $current_swimlane_id) {
+ if ($offset == $position) {
+ $offset++;
+ }
- return true;
+ $results[] = $this->db->table(self::TABLE)->eq('id', $current_swimlane_id)->update(array('position' => $offset));
+ $offset++;
}
- return false;
+ $results[] = $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position));
+
+ return !in_array(false, $results, true);
}
/**
@@ -470,85 +484,4 @@ class Swimlane extends Base
return true;
}
-
- /**
- * Validate creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $rules = array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate default swimlane modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateDefaultModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('default_swimlane', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('The id must be an integer')),
- new Validators\Integer('project_id', t('The project id must be an integer')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
- );
- }
}
diff --git a/app/Model/Task.php b/app/Model/Task.php
index f1cd094f..f8b41b9f 100644
--- a/app/Model/Task.php
+++ b/app/Model/Task.php
@@ -41,6 +41,8 @@ class Task extends Base
const EVENT_CREATE_UPDATE = 'task.create_update';
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
const EVENT_OVERDUE = 'task.overdue';
+ const EVENT_USER_MENTION = 'task.user.mention';
+ const EVENT_DAILY_CRONJOB = 'task.cronjob.daily';
/**
* Recurrence: status
@@ -90,7 +92,7 @@ class Task extends Base
return false;
}
- $this->file->removeAll($task_id);
+ $this->taskFile->removeAll($task_id);
return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
}
@@ -197,4 +199,25 @@ class Task extends Base
return round(($position * 100) / count($columns), 1);
}
+
+ /**
+ * Helper method to duplicate all tasks to another project
+ *
+ * @access public
+ * @param integer $src_project_id
+ * @param integer $dst_project_id
+ * @return boolean
+ */
+ public function duplicate($src_project_id, $dst_project_id)
+ {
+ $task_ids = $this->taskFinder->getAllIds($src_project_id, array(Task::STATUS_OPEN, Task::STATUS_CLOSED));
+
+ foreach ($task_ids as $task_id) {
+ if (! $this->taskDuplication->duplicateToProject($task_id, $dst_project_id)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/app/Model/TaskAnalytic.php b/app/Model/TaskAnalytic.php
index bdfec3cb..cff56744 100644
--- a/app/Model/TaskAnalytic.php
+++ b/app/Model/TaskAnalytic.php
@@ -48,7 +48,7 @@ class TaskAnalytic extends Base
public function getTimeSpentByColumn(array $task)
{
$result = array();
- $columns = $this->board->getColumnsList($task['project_id']);
+ $columns = $this->column->getList($task['project_id']);
$sums = $this->transition->getTimeSpentByTask($task['id']);
foreach ($columns as $column_id => $column_title) {
diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php
index 5ef1a04b..576eb18c 100644
--- a/app/Model/TaskCreation.php
+++ b/app/Model/TaskCreation.php
@@ -49,13 +49,14 @@ class TaskCreation extends Base
*/
public function prepare(array &$values)
{
- $this->dateParser->convert($values, array('date_due'));
- $this->dateParser->convert($values, array('date_started'), true);
+ $values = $this->dateParser->convert($values, array('date_due'));
+ $values = $this->dateParser->convert($values, array('date_started'), true);
+
$this->removeFields($values, array('another_task'));
$this->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated'));
if (empty($values['column_id'])) {
- $values['column_id'] = $this->board->getFirstColumn($values['project_id']);
+ $values['column_id'] = $this->column->getFirstColumnId($values['project_id']);
}
if (empty($values['color_id'])) {
@@ -86,8 +87,16 @@ class TaskCreation extends Base
*/
private function fireEvents($task_id, array $values)
{
- $values['task_id'] = $task_id;
- $this->container['dispatcher']->dispatch(Task::EVENT_CREATE_UPDATE, new TaskEvent($values));
- $this->container['dispatcher']->dispatch(Task::EVENT_CREATE, new TaskEvent($values));
+ $event = new TaskEvent(array('task_id' => $task_id) + $values);
+
+ $this->logger->debug('Event fired: '.Task::EVENT_CREATE_UPDATE);
+ $this->logger->debug('Event fired: '.Task::EVENT_CREATE);
+
+ $this->dispatcher->dispatch(Task::EVENT_CREATE_UPDATE, $event);
+ $this->dispatcher->dispatch(Task::EVENT_CREATE, $event);
+
+ if (! empty($values['description'])) {
+ $this->userMention->fireEvents($values['description'], Task::EVENT_USER_MENTION, $event);
+ }
}
}
diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php
index e81fb232..b081aac1 100644
--- a/app/Model/TaskDuplication.php
+++ b/app/Model/TaskDuplication.php
@@ -64,7 +64,7 @@ class TaskDuplication extends Base
if ($values['recurrence_status'] == Task::RECURRING_STATUS_PENDING) {
$values['recurrence_parent'] = $task_id;
- $values['column_id'] = $this->board->getFirstColumn($values['project_id']);
+ $values['column_id'] = $this->column->getFirstColumnId($values['project_id']);
$this->calculateRecurringTaskDueDate($values);
$recurring_task_id = $this->save($task_id, $values);
@@ -181,12 +181,12 @@ class TaskDuplication extends Base
// Check if the column exists for the destination project
if ($values['column_id'] > 0) {
- $values['column_id'] = $this->board->getColumnIdByTitle(
+ $values['column_id'] = $this->column->getColumnIdByTitle(
$values['project_id'],
- $this->board->getColumnTitleById($values['column_id'])
+ $this->column->getColumnTitleById($values['column_id'])
);
- $values['column_id'] = $values['column_id'] ?: $this->board->getFirstColumn($values['project_id']);
+ $values['column_id'] = $values['column_id'] ?: $this->column->getFirstColumnId($values['project_id']);
}
return $values;
diff --git a/app/Model/TaskExport.php b/app/Model/TaskExport.php
index d38a5384..ed179a4f 100644
--- a/app/Model/TaskExport.php
+++ b/app/Model/TaskExport.php
@@ -58,6 +58,7 @@ class TaskExport extends Base
tasks.date_due,
creators.username AS creator_username,
users.username AS assignee_username,
+ users.name AS assignee_name,
tasks.score,
tasks.title,
tasks.date_creation,
@@ -105,7 +106,7 @@ class TaskExport extends Base
$task['score'] = $task['score'] ?: 0;
$task['swimlane_id'] = isset($swimlanes[$task['swimlane_id']]) ? $swimlanes[$task['swimlane_id']] : '?';
- $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d');
+ $task = $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d');
return $task;
}
@@ -129,7 +130,8 @@ class TaskExport extends Base
e('Color'),
e('Due date'),
e('Creator'),
- e('Assignee'),
+ e('Assignee Username'),
+ e('Assignee Name'),
e('Complexity'),
e('Title'),
e('Creation date'),
diff --git a/app/Model/TaskExternalLink.php b/app/Model/TaskExternalLink.php
new file mode 100644
index 00000000..f2c756b4
--- /dev/null
+++ b/app/Model/TaskExternalLink.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Task External Link Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskExternalLink extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_external_links';
+
+ /**
+ * Get all links
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getAll($task_id)
+ {
+ $types = $this->externalLinkManager->getTypes();
+
+ $links = $this->db->table(self::TABLE)
+ ->columns(self::TABLE.'.*', User::TABLE.'.name AS creator_name', User::TABLE.'.username AS creator_username')
+ ->eq('task_id', $task_id)
+ ->asc('title')
+ ->join(User::TABLE, 'id', 'creator_id')
+ ->findAll();
+
+ foreach ($links as &$link) {
+ $link['dependency_label'] = $this->externalLinkManager->getDependencyLabel($link['link_type'], $link['dependency']);
+ $link['type'] = isset($types[$link['link_type']]) ? $types[$link['link_type']] : t('Unknown');
+ }
+
+ return $links;
+ }
+
+ /**
+ * Get link
+ *
+ * @access public
+ * @param integer $link_id
+ * @return array
+ */
+ public function getById($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne();
+ }
+
+ /**
+ * Add a new link in the database
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean|integer
+ */
+ public function create(array $values)
+ {
+ unset($values['id']);
+ $values['creator_id'] = $this->userSession->getId();
+ $values['date_creation'] = time();
+ $values['date_modification'] = $values['date_creation'];
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Modify external link
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ $values['date_modification'] = time();
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ * @param integer $link_id
+ * @return boolean
+ */
+ public function remove($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->remove();
+ }
+}
diff --git a/app/Model/TaskFile.php b/app/Model/TaskFile.php
new file mode 100644
index 00000000..45a3b97f
--- /dev/null
+++ b/app/Model/TaskFile.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Task File Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskFile extends File
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_files';
+
+ /**
+ * SQL foreign key
+ *
+ * @var string
+ */
+ const FOREIGN_KEY = 'task_id';
+
+ /**
+ * Path prefix
+ *
+ * @var string
+ */
+ const PATH_PREFIX = 'tasks';
+
+ /**
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_CREATE = 'task.file.create';
+
+ /**
+ * Handle screenshot upload
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param string $blob Base64 encoded image
+ * @return bool|integer
+ */
+ public function uploadScreenshot($task_id, $blob)
+ {
+ $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png';
+ return $this->uploadContent($task_id, $original_filename, $blob);
+ }
+}
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index 137a7a8e..1883298d 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -30,6 +30,7 @@ class TaskFilter extends Base
'T_COLUMN' => 'filterByColumnName',
'T_REFERENCE' => 'filterByReference',
'T_SWIMLANE' => 'filterBySwimlaneName',
+ 'T_LINK' => 'filterByLinkName',
);
/**
@@ -108,6 +109,22 @@ class TaskFilter extends Base
}
/**
+ * 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
@@ -452,7 +469,7 @@ class TaskFilter extends Base
$this->query->beginOr();
foreach ($values as $project) {
- $this->query->ilike(Board::TABLE.'.title', $project);
+ $this->query->ilike(Column::TABLE.'.title', $project);
}
$this->query->closeOr();
@@ -507,6 +524,30 @@ class TaskFilter extends Base
}
/**
+ * 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
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 9514fe4a..0492a9bf 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -38,14 +38,14 @@ class TaskFinder extends Base
Task::TABLE.'.time_spent',
Task::TABLE.'.time_estimated',
Project::TABLE.'.name AS project_name',
- Board::TABLE.'.title AS column_name',
+ Column::TABLE.'.title AS column_name',
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name'
)
->eq(Task::TABLE.'.is_active', $is_active)
->in(Project::TABLE.'.id', $project_ids)
->join(Project::TABLE, 'id', 'project_id')
- ->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE);
}
@@ -88,11 +88,12 @@ class TaskFinder extends Base
return $this->db
->table(Task::TABLE)
->columns(
- '(SELECT count(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
- '(SELECT count(*) FROM '.File::TABLE.' WHERE task_id=tasks.id) AS nb_files',
- '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks',
- '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
- '(SELECT count(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
+ '(SELECT COUNT(*) FROM '.TaskFile::TABLE.' WHERE task_id=tasks.id) AS nb_files',
+ '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks',
+ '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT COUNT(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.TaskExternalLink::TABLE.' WHERE '.TaskExternalLink::TABLE.'.task_id = tasks.id) AS nb_external_links',
'(SELECT DISTINCT 1 FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id AND '.TaskLink::TABLE.'.link_id = 9) AS is_milestone',
'tasks.id',
'tasks.reference',
@@ -113,6 +114,7 @@ class TaskFinder extends Base
'tasks.is_active',
'tasks.score',
'tasks.category_id',
+ 'tasks.priority',
'tasks.date_moved',
'tasks.recurrence_status',
'tasks.recurrence_trigger',
@@ -122,19 +124,20 @@ class TaskFinder extends Base
'tasks.recurrence_parent',
'tasks.recurrence_child',
'tasks.time_estimated',
+ 'tasks.time_spent',
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name',
Category::TABLE.'.name AS category_name',
Category::TABLE.'.description AS category_description',
- Board::TABLE.'.title AS column_name',
- Board::TABLE.'.position AS column_position',
+ Column::TABLE.'.title AS column_name',
+ Column::TABLE.'.position AS column_position',
Swimlane::TABLE.'.name AS swimlane_name',
Project::TABLE.'.default_swimlane',
Project::TABLE.'.name AS project_name'
)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
->join(Category::TABLE, 'id', 'category_id', Task::TABLE)
- ->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE)
->join(Project::TABLE, 'id', 'project_id', Task::TABLE);
}
@@ -177,6 +180,23 @@ class TaskFinder extends Base
}
/**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id
+ * @param array $status
+ * @return array
+ */
+ public function getAllIds($project_id, array $status = array(Task::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(Task::TABLE)
+ ->eq(Task::TABLE.'.project_id', $project_id)
+ ->in(Task::TABLE.'.is_active', $status)
+ ->findAllByColumn('id');
+ }
+
+ /**
* Get overdue tasks query
*
* @access public
@@ -307,6 +327,7 @@ class TaskFinder extends Base
tasks.is_active,
tasks.score,
tasks.category_id,
+ tasks.priority,
tasks.swimlane_id,
tasks.date_moved,
tasks.recurrence_status,
diff --git a/app/Model/TaskImport.php b/app/Model/TaskImport.php
index e8dd1946..ccab0152 100644
--- a/app/Model/TaskImport.php
+++ b/app/Model/TaskImport.php
@@ -111,7 +111,7 @@ class TaskImport extends Base
}
if (! empty($row['column'])) {
- $values['column_id'] = $this->board->getColumnIdByTitle($this->projectId, $row['column']);
+ $values['column_id'] = $this->column->getColumnIdByTitle($this->projectId, $row['column']);
}
if (! empty($row['category'])) {
diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php
index 1ac59203..a57bf3b0 100644
--- a/app/Model/TaskLink.php
+++ b/app/Model/TaskLink.php
@@ -2,8 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
use Kanboard\Event\TaskLinkEvent;
/**
@@ -77,22 +75,23 @@ class TaskLink extends Base
Task::TABLE.'.title',
Task::TABLE.'.is_active',
Task::TABLE.'.project_id',
+ Task::TABLE.'.column_id',
Task::TABLE.'.time_spent AS task_time_spent',
Task::TABLE.'.time_estimated AS task_time_estimated',
Task::TABLE.'.owner_id AS task_assignee_id',
User::TABLE.'.username AS task_assignee_username',
User::TABLE.'.name AS task_assignee_name',
- Board::TABLE.'.title AS column_title',
+ Column::TABLE.'.title AS column_title',
Project::TABLE.'.name AS project_name'
)
->eq(self::TABLE.'.task_id', $task_id)
->join(Link::TABLE, 'id', 'link_id')
->join(Task::TABLE, 'id', 'opposite_task_id')
- ->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
->join(Project::TABLE, 'id', 'project_id', Task::TABLE)
->asc(Link::TABLE.'.id')
- ->desc(Board::TABLE.'.position')
+ ->desc(Column::TABLE.'.position')
->desc(Task::TABLE.'.is_active')
->asc(Task::TABLE.'.position')
->asc(Task::TABLE.'.id')
@@ -261,59 +260,4 @@ class TaskLink extends Base
return true;
}
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Required('task_id', t('Field required')),
- new Validators\Required('opposite_task_id', t('Field required')),
- new Validators\Required('link_id', t('Field required')),
- new Validators\NotEquals('opposite_task_id', 'task_id', t('A task cannot be linked to itself')),
- new Validators\Exists('opposite_task_id', t('This linked task id doesn\'t exists'), $this->db->getConnection(), Task::TABLE, 'id')
- );
- }
-
- /**
- * Validate creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $v = new Validator($values, $this->commonValidationRules());
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('Field required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php
index 781646b8..8e59b3fe 100644
--- a/app/Model/TaskModification.php
+++ b/app/Model/TaskModification.php
@@ -17,16 +17,17 @@ class TaskModification extends Base
*
* @access public
* @param array $values
+ * @param boolean $fire_events
* @return boolean
*/
- public function update(array $values)
+ public function update(array $values, $fire_events = true)
{
$original_task = $this->taskFinder->getById($values['id']);
$this->prepare($values);
$result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values);
- if ($result) {
+ if ($fire_events && $result) {
$this->fireEvents($original_task, $values);
}
@@ -51,13 +52,14 @@ class TaskModification extends Base
if ($this->isFieldModified('owner_id', $event_data['changes'])) {
$events[] = Task::EVENT_ASSIGNEE_CHANGE;
- } else {
+ } elseif (! empty($event_data['changes'])) {
$events[] = Task::EVENT_CREATE_UPDATE;
$events[] = Task::EVENT_UPDATE;
}
foreach ($events as $event) {
- $this->container['dispatcher']->dispatch($event, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.$event);
+ $this->dispatcher->dispatch($event, new TaskEvent($event_data));
}
}
@@ -82,11 +84,12 @@ class TaskModification extends Base
*/
public function prepare(array &$values)
{
- $this->dateParser->convert($values, array('date_due'));
- $this->dateParser->convert($values, array('date_started'), true);
+ $values = $this->dateParser->convert($values, array('date_due'));
+ $values = $this->dateParser->convert($values, array('date_started'), true);
+
$this->removeFields($values, array('another_task', 'id'));
$this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
- $this->convertIntegerFields($values, array('is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate'));
+ $this->convertIntegerFields($values, array('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate'));
$values['date_modification'] = time();
}
diff --git a/app/Model/TaskPermission.php b/app/Model/TaskPermission.php
index 4bbe6d1d..fac2153e 100644
--- a/app/Model/TaskPermission.php
+++ b/app/Model/TaskPermission.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use Kanboard\Core\Security\Role;
+
/**
* Task permission model
*
@@ -20,7 +22,7 @@ class TaskPermission extends Base
*/
public function canRemoveTask(array $task)
{
- if ($this->userSession->isAdmin() || $this->projectPermission->isManager($task['project_id'], $this->userSession->getId())) {
+ if ($this->userSession->isAdmin() || $this->projectUserRole->getUserRole($task['project_id'], $this->userSession->getId()) === Role::PROJECT_MANAGER) {
return true;
} elseif (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) {
return true;
diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php
index da363cb3..4c9928d7 100644
--- a/app/Model/TaskPosition.php
+++ b/app/Model/TaskPosition.php
@@ -32,7 +32,6 @@ class TaskPosition extends Base
$task = $this->taskFinder->getById($task_id);
- // Ignore closed tasks
if ($task['is_active'] == Task::STATUS_CLOSED) {
return true;
}
@@ -167,7 +166,12 @@ class TaskPosition extends Base
return false;
}
- return true;
+ $now = time();
+
+ return $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array(
+ 'date_moved' => $now,
+ 'date_modification' => $now,
+ ));
}
/**
@@ -221,11 +225,14 @@ class TaskPosition extends Base
);
if ($task['swimlane_id'] != $new_swimlane_id) {
- $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.Task::EVENT_MOVE_SWIMLANE);
+ $this->dispatcher->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data));
} elseif ($task['column_id'] != $new_column_id) {
- $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.Task::EVENT_MOVE_COLUMN);
+ $this->dispatcher->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data));
} elseif ($task['position'] != $new_position) {
- $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.Task::EVENT_MOVE_POSITION);
+ $this->dispatcher->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data));
}
}
}
diff --git a/app/Model/TaskStatus.php b/app/Model/TaskStatus.php
index a5199ed9..2b902815 100644
--- a/app/Model/TaskStatus.php
+++ b/app/Model/TaskStatus.php
@@ -62,6 +62,32 @@ class TaskStatus extends Base
}
/**
+ * Close multiple tasks
+ *
+ * @access public
+ * @param array $task_ids
+ */
+ public function closeMultipleTasks(array $task_ids)
+ {
+ foreach ($task_ids as $task_id) {
+ $this->close($task_id);
+ }
+ }
+
+ /**
+ * Close all tasks within a column/swimlane
+ *
+ * @access public
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ */
+ public function closeTasksBySwimlaneAndColumn($swimlane_id, $column_id)
+ {
+ $task_ids = $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->eq('column_id', $column_id)->findAllByColumn('id');
+ $this->closeMultipleTasks($task_ids);
+ }
+
+ /**
* Common method to change the status of task
*
* @access private
@@ -87,10 +113,8 @@ class TaskStatus extends Base
));
if ($result) {
- $this->container['dispatcher']->dispatch(
- $event,
- new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id))
- );
+ $this->logger->debug('Event fired: '.$event);
+ $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id)));
}
return $result;
diff --git a/app/Model/Transition.php b/app/Model/Transition.php
index b1f8f678..aa76d58f 100644
--- a/app/Model/Transition.php
+++ b/app/Model/Transition.php
@@ -76,8 +76,8 @@ class Transition extends Base
->eq('task_id', $task_id)
->desc('date')
->join(User::TABLE, 'id', 'user_id')
- ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
- ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
+ ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
+ ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
->findAll();
}
@@ -118,8 +118,8 @@ class Transition extends Base
->desc('date')
->join(Task::TABLE, 'id', 'task_id')
->join(User::TABLE, 'id', 'user_id')
- ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
- ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
+ ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
+ ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
->findAll();
}
diff --git a/app/Model/User.php b/app/Model/User.php
index 6e7e94e0..2d87d35b 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -3,10 +3,8 @@
namespace Kanboard\Model;
use PicoDb\Database;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use Kanboard\Core\Session;
-use Kanboard\Core\Security;
+use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
/**
* User model
@@ -43,6 +41,18 @@ class User extends Base
}
/**
+ * Return true if the user is active
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return boolean
+ */
+ public function isActive($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->eq('is_active', 1)->exists();
+ }
+
+ /**
* Get query to fetch all users
*
* @access public
@@ -50,21 +60,7 @@ class User extends Base
*/
public function getQuery()
{
- return $this->db
- ->table(self::TABLE)
- ->columns(
- 'id',
- 'username',
- 'name',
- 'email',
- 'is_admin',
- 'is_project_admin',
- 'is_ldap_user',
- 'notifications_enabled',
- 'google_id',
- 'github_id',
- 'twofactor_activated'
- );
+ return $this->db->table(self::TABLE);
}
/**
@@ -91,7 +87,7 @@ class User extends Base
$this->db
->table(User::TABLE)
->eq('id', $user_id)
- ->eq('is_admin', 1)
+ ->eq('role', Role::APP_ADMIN)
->exists();
}
@@ -111,48 +107,17 @@ class User extends Base
* Get a specific user by the Google id
*
* @access public
- * @param string $google_id Google unique id
+ * @param string $column
+ * @param string $id
* @return array|boolean
*/
- public function getByGoogleId($google_id)
+ public function getByExternalId($column, $id)
{
- if (empty($google_id)) {
+ if (empty($id)) {
return false;
}
- return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne();
- }
-
- /**
- * Get a specific user by the Github id
- *
- * @access public
- * @param string $github_id Github user id
- * @return array|boolean
- */
- public function getByGithubId($github_id)
- {
- if (empty($github_id)) {
- return false;
- }
-
- return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne();
- }
-
- /**
- * Get a specific user by the Gitlab id
- *
- * @access public
- * @param string $gitlab_id Gitlab user id
- * @return array|boolean
- */
- public function getByGitlabId($gitlab_id)
- {
- if (empty($gitlab_id)) {
- return false;
- }
-
- return $this->db->table(self::TABLE)->eq('gitlab_id', $gitlab_id)->findOne();
+ return $this->db->table(self::TABLE)->eq($column, $id)->findOne();
}
/**
@@ -172,7 +137,7 @@ class User extends Base
*
* @access public
* @param string $username Username
- * @return array
+ * @return integer
*/
public function getIdByUsername($username)
{
@@ -240,9 +205,9 @@ class User extends Base
* @param boolean $prepend Prepend "All users"
* @return array
*/
- public function getList($prepend = false)
+ public function getActiveUsersList($prepend = false)
{
- $users = $this->db->table(self::TABLE)->columns('id', 'username', 'name')->findAll();
+ $users = $this->db->table(self::TABLE)->eq('is_active', 1)->columns('id', 'username', 'name')->findAll();
$listing = $this->prepareList($users);
if ($prepend) {
@@ -289,7 +254,7 @@ class User extends Base
}
$this->removeFields($values, array('confirmation', 'current_password'));
- $this->resetFields($values, array('is_admin', 'is_ldap_user', 'is_project_admin', 'disable_login_form'));
+ $this->resetFields($values, array('is_ldap_user', 'disable_login_form'));
$this->convertNullFields($values, array('gitlab_id'));
$this->convertIntegerFields($values, array('gitlab_id'));
}
@@ -312,7 +277,7 @@ class User extends Base
*
* @access public
* @param array $values Form values
- * @return array
+ * @return boolean
*/
public function update(array $values)
{
@@ -320,14 +285,38 @@ class User extends Base
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
// If the user is connected refresh his session
- if (Session::isOpen() && $this->userSession->getId() == $values['id']) {
- $this->userSession->refresh();
+ if ($this->userSession->getId() == $values['id']) {
+ $this->userSession->initialize($this->getById($this->userSession->getId()));
}
return $result;
}
/**
+ * Disable a specific user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function disable($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 0));
+ }
+
+ /**
+ * Enable a specific user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function enable($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 1));
+ }
+
+ /**
* Remove a specific user
*
* @access public
@@ -355,10 +344,10 @@ class User extends Base
// All private projects are removed
$project_ids = $db->table(Project::TABLE)
- ->eq('is_private', 1)
- ->eq(ProjectPermission::TABLE.'.user_id', $user_id)
- ->join(ProjectPermission::TABLE, 'project_id', 'id')
- ->findAllByColumn(Project::TABLE.'.id');
+ ->eq('is_private', 1)
+ ->eq(ProjectUserRole::TABLE.'.user_id', $user_id)
+ ->join(ProjectUserRole::TABLE, 'project_id', 'id')
+ ->findAllByColumn(Project::TABLE.'.id');
if (! empty($project_ids)) {
$db->table(Project::TABLE)->in('id', $project_ids)->remove();
@@ -383,7 +372,7 @@ class User extends Base
return $this->db
->table(self::TABLE)
->eq('id', $user_id)
- ->save(array('token' => Security::generateToken()));
+ ->save(array('token' => Token::getToken()));
}
/**
@@ -400,200 +389,4 @@ class User extends Base
->eq('id', $user_id)
->save(array('token' => ''));
}
-
- /**
- * Get the number of failed login for the user
- *
- * @access public
- * @param string $username
- * @return integer
- */
- public function getFailedLogin($username)
- {
- return (int) $this->db->table(self::TABLE)->eq('username', $username)->findOneColumn('nb_failed_login');
- }
-
- /**
- * Reset to 0 the counter of failed login
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function resetFailedLogin($username)
- {
- return $this->db->table(self::TABLE)->eq('username', $username)->update(array('nb_failed_login' => 0, 'lock_expiration_date' => 0));
- }
-
- /**
- * Increment failed login counter
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function incrementFailedLogin($username)
- {
- return $this->db->execute('UPDATE '.self::TABLE.' SET nb_failed_login=nb_failed_login+1 WHERE username=?', array($username)) !== false;
- }
-
- /**
- * Check if the account is locked
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function isLocked($username)
- {
- return $this->db->table(self::TABLE)
- ->eq('username', $username)
- ->neq('lock_expiration_date', 0)
- ->gte('lock_expiration_date', time())
- ->exists();
- }
-
- /**
- * Lock the account for the specified duration
- *
- * @access public
- * @param string $username Username
- * @param integer $duration Duration in minutes
- * @return boolean
- */
- public function lock($username, $duration = 15)
- {
- return $this->db->table(self::TABLE)->eq('username', $username)->update(array('lock_expiration_date' => time() + $duration * 60));
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
- new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
- new Validators\Email('email', t('Email address invalid')),
- new Validators\Integer('is_admin', t('This value must be an integer')),
- new Validators\Integer('is_project_admin', t('This value must be an integer')),
- new Validators\Integer('is_ldap_user', t('This value must be an integer')),
- );
- }
-
- /**
- * Common password validation rules
- *
- * @access private
- * @return array
- */
- private function commonPasswordValidationRules()
- {
- return array(
- new Validators\Required('password', t('The password is required')),
- new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
- new Validators\Required('confirmation', t('The confirmation is required')),
- new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
- );
- }
-
- /**
- * Validate user creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $rules = array(
- new Validators\Required('username', t('The username is required')),
- );
-
- if (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) {
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
- } else {
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
- }
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate user modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The user id is required')),
- new Validators\Required('username', t('The username is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate user API modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateApiModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The user id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate password modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validatePasswordModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The user id is required')),
- new Validators\Required('current_password', t('The current password is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules()));
-
- if ($v->execute()) {
-
- // Check password
- if ($this->authentication->authenticate($this->session['user']['username'], $values['current_password'])) {
- return array(true, array());
- } else {
- return array(false, array('current_password' => array(t('Wrong password'))));
- }
- }
-
- return array(false, $v->getErrors());
- }
}
diff --git a/app/Model/UserFilter.php b/app/Model/UserFilter.php
new file mode 100644
index 00000000..ff546e96
--- /dev/null
+++ b/app/Model/UserFilter.php
@@ -0,0 +1,80 @@
+<?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/UserImport.php b/app/Model/UserImport.php
index 3c9e7a57..0ec4e802 100644
--- a/app/Model/UserImport.php
+++ b/app/Model/UserImport.php
@@ -4,6 +4,7 @@ namespace Kanboard\Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
+use Kanboard\Core\Security\Role;
use Kanboard\Core\Csv;
/**
@@ -36,7 +37,7 @@ class UserImport extends Base
'email' => 'Email',
'name' => 'Full Name',
'is_admin' => 'Administrator',
- 'is_project_admin' => 'Project Administrator',
+ 'is_manager' => 'Manager',
'is_ldap_user' => 'Remote User',
);
}
@@ -75,10 +76,21 @@ class UserImport extends Base
{
$row['username'] = strtolower($row['username']);
- foreach (array('is_admin', 'is_project_admin', 'is_ldap_user') as $field) {
+ foreach (array('is_admin', 'is_manager', 'is_ldap_user') as $field) {
$row[$field] = Csv::getBooleanValue($row[$field]);
}
+ if ($row['is_admin'] == 1) {
+ $row['role'] = Role::APP_ADMIN;
+ } elseif ($row['is_manager'] == 1) {
+ $row['role'] = Role::APP_MANAGER;
+ } else {
+ $row['role'] = Role::APP_USER;
+ }
+
+ unset($row['is_admin']);
+ unset($row['is_manager']);
+
$this->removeEmptyFields($row, array('password', 'email', 'name'));
return $row;
@@ -98,8 +110,6 @@ class UserImport extends Base
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), User::TABLE, 'id'),
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Email('email', t('Email address invalid')),
- new Validators\Integer('is_admin', t('This value must be an integer')),
- new Validators\Integer('is_project_admin', t('This value must be an integer')),
new Validators\Integer('is_ldap_user', t('This value must be an integer')),
));
diff --git a/app/Model/UserLocking.php b/app/Model/UserLocking.php
new file mode 100644
index 00000000..67e4c244
--- /dev/null
+++ b/app/Model/UserLocking.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * User Locking Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class UserLocking extends Base
+{
+ /**
+ * Get the number of failed login for the user
+ *
+ * @access public
+ * @param string $username
+ * @return integer
+ */
+ public function getFailedLogin($username)
+ {
+ return (int) $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->findOneColumn('nb_failed_login');
+ }
+
+ /**
+ * Reset to 0 the counter of failed login
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function resetFailedLogin($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->update(array(
+ 'nb_failed_login' => 0,
+ 'lock_expiration_date' => 0,
+ ));
+ }
+
+ /**
+ * Increment failed login counter
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function incrementFailedLogin($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->increment('nb_failed_login', 1);
+ }
+
+ /**
+ * Check if the account is locked
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function isLocked($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->neq('lock_expiration_date', 0)
+ ->gte('lock_expiration_date', time())
+ ->exists();
+ }
+
+ /**
+ * Lock the account for the specified duration
+ *
+ * @access public
+ * @param string $username Username
+ * @param integer $duration Duration in minutes
+ * @return boolean
+ */
+ public function lock($username, $duration = 15)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->update(array(
+ 'lock_expiration_date' => time() + $duration * 60
+ ));
+ }
+
+ /**
+ * Return true if the captcha must be shown
+ *
+ * @access public
+ * @param string $username
+ * @param integer $tries
+ * @return boolean
+ */
+ public function hasCaptcha($username, $tries = BRUTEFORCE_CAPTCHA)
+ {
+ return $this->getFailedLogin($username) >= $tries;
+ }
+}
diff --git a/app/Model/UserMention.php b/app/Model/UserMention.php
new file mode 100644
index 00000000..97a4e419
--- /dev/null
+++ b/app/Model/UserMention.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Event\GenericEvent;
+
+/**
+ * User Mention
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class UserMention extends Base
+{
+ /**
+ * Get list of mentioned users
+ *
+ * @access public
+ * @param string $content
+ * @return array
+ */
+ public function getMentionedUsers($content)
+ {
+ $users = array();
+
+ if (preg_match_all('/@([^\s]+)/', $content, $matches)) {
+ $users = $this->db->table(User::TABLE)
+ ->columns('id', 'username', 'name', 'email', 'language')
+ ->eq('notifications_enabled', 1)
+ ->neq('id', $this->userSession->getId())
+ ->in('username', array_unique($matches[1]))
+ ->findAll();
+ }
+
+ return $users;
+ }
+
+ /**
+ * Fire events for user mentions
+ *
+ * @access public
+ * @param string $content
+ * @param string $eventName
+ * @param GenericEvent $event
+ */
+ public function fireEvents($content, $eventName, GenericEvent $event)
+ {
+ if (empty($event['project_id'])) {
+ $event['project_id'] = $this->taskFinder->getProjectId($event['task_id']);
+ }
+
+ $users = $this->getMentionedUsers($content);
+
+ foreach ($users as $user) {
+ if ($this->projectPermission->isMember($event['project_id'], $user['id'])) {
+ $event['mention'] = $user;
+ $this->dispatcher->dispatch($eventName, $event);
+ }
+ }
+ }
+}
diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php
index 3d98ebe9..e8a967ac 100644
--- a/app/Model/UserNotification.php
+++ b/app/Model/UserNotification.php
@@ -21,18 +21,12 @@ class UserNotification extends Base
*/
public function sendNotifications($event_name, array $event_data)
{
- $logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0;
- $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id);
-
- if (! empty($users)) {
- foreach ($users as $user) {
- if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) {
- $this->sendUserNotification($user, $event_name, $event_data);
- }
- }
+ $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $this->userSession->getId());
- // Restore locales
- $this->config->setupTranslations();
+ foreach ($users as $user) {
+ if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) {
+ $this->sendUserNotification($user, $event_name, $event_data);
+ }
}
}
@@ -58,6 +52,9 @@ class UserNotification extends Base
foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) {
$this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data);
}
+
+ // Restore locales
+ $this->config->setupTranslations();
}
/**
@@ -74,7 +71,17 @@ class UserNotification extends Base
return $this->getEverybodyWithNotificationEnabled($exclude_user_id);
}
- return $this->getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id);
+ $users = array();
+ $members = $this->getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id);
+ $groups = $this->getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id);
+
+ foreach (array_merge($members, $groups) as $user) {
+ if (! isset($users[$user['id']])) {
+ $users[$user['id']] = $user;
+ }
+ }
+
+ return array_values($users);
}
/**
@@ -145,17 +152,17 @@ class UserNotification extends Base
}
/**
- * Get a list of project members with notification enabled
+ * Get a list of group members with notification enabled
*
* @access private
* @param integer $project_id Project id
* @param integer $exclude_user_id User id to exclude
* @return array
*/
- private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id)
+ private function getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id)
{
return $this->db
- ->table(ProjectPermission::TABLE)
+ ->table(ProjectUserRole::TABLE)
->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
->join(User::TABLE, 'id', 'user_id')
->eq('project_id', $project_id)
@@ -164,6 +171,19 @@ class UserNotification extends Base
->findAll();
}
+ private function getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id)
+ {
+ return $this->db
+ ->table(ProjectGroupRole::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->eq(ProjectGroupRole::TABLE.'.project_id', $project_id)
+ ->eq(User::TABLE.'.notifications_enabled', '1')
+ ->neq(User::TABLE.'.id', $exclude_user_id)
+ ->findAll();
+ }
+
/**
* Get a list of project members with notification enabled
*
diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php
deleted file mode 100644
index 1778114e..00000000
--- a/app/Model/UserSession.php
+++ /dev/null
@@ -1,177 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * User Session
- *
- * @package model
- * @author Frederic Guillot
- */
-class UserSession extends Base
-{
- /**
- * Update user session information
- *
- * @access public
- * @param array $user User data
- */
- public function refresh(array $user = array())
- {
- if (empty($user)) {
- $user = $this->user->getById($this->userSession->getId());
- }
-
- if (isset($user['password'])) {
- unset($user['password']);
- }
-
- if (isset($user['twofactor_secret'])) {
- unset($user['twofactor_secret']);
- }
-
- $user['id'] = (int) $user['id'];
- $user['is_admin'] = (bool) $user['is_admin'];
- $user['is_project_admin'] = (bool) $user['is_project_admin'];
- $user['is_ldap_user'] = (bool) $user['is_ldap_user'];
- $user['twofactor_activated'] = (bool) $user['twofactor_activated'];
-
- $this->session['user'] = $user;
- }
-
- /**
- * Return true if the user has validated the 2FA key
- *
- * @access public
- * @return bool
- */
- public function check2FA()
- {
- return isset($this->session['2fa_validated']) && $this->session['2fa_validated'] === true;
- }
-
- /**
- * Return true if the user has 2FA enabled
- *
- * @access public
- * @return bool
- */
- public function has2FA()
- {
- return isset($this->session['user']['twofactor_activated']) && $this->session['user']['twofactor_activated'] === true;
- }
-
- /**
- * Return true if the logged user is admin
- *
- * @access public
- * @return bool
- */
- public function isAdmin()
- {
- return isset($this->session['user']['is_admin']) && $this->session['user']['is_admin'] === true;
- }
-
- /**
- * Return true if the logged user is project admin
- *
- * @access public
- * @return bool
- */
- public function isProjectAdmin()
- {
- return isset($this->session['user']['is_project_admin']) && $this->session['user']['is_project_admin'] === true;
- }
-
- /**
- * Get the connected user id
- *
- * @access public
- * @return integer
- */
- public function getId()
- {
- return isset($this->session['user']['id']) ? (int) $this->session['user']['id'] : 0;
- }
-
- /**
- * Check is the user is connected
- *
- * @access public
- * @return bool
- */
- public function isLogged()
- {
- return ! empty($this->session['user']);
- }
-
- /**
- * Get project filters from the session
- *
- * @access public
- * @param integer $project_id
- * @return string
- */
- public function getFilters($project_id)
- {
- return ! empty($_SESSION['filters'][$project_id]) ? $_SESSION['filters'][$project_id] : 'status:open';
- }
-
- /**
- * Save project filters in the session
- *
- * @access public
- * @param integer $project_id
- * @param string $filters
- */
- public function setFilters($project_id, $filters)
- {
- $_SESSION['filters'][$project_id] = $filters;
- }
-
- /**
- * Is board collapsed or expanded
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function isBoardCollapsed($project_id)
- {
- return ! empty($_SESSION['board_collapsed'][$project_id]) ? $_SESSION['board_collapsed'][$project_id] : false;
- }
-
- /**
- * Set board display mode
- *
- * @access public
- * @param integer $project_id
- * @param boolean $collapsed
- */
- public function setBoardDisplayMode($project_id, $collapsed)
- {
- $_SESSION['board_collapsed'][$project_id] = $collapsed;
- }
-
- /**
- * Set comments sorting
- *
- * @access public
- * @param string $order
- */
- public function setCommentSorting($order)
- {
- $this->session['comment_sorting'] = $order;
- }
-
- /**
- * Get comments sorting direction
- *
- * @access public
- * @return string
- */
- public function getCommentSorting()
- {
- return $this->session['comment_sorting'] ?: 'ASC';
- }
-}
diff --git a/app/Notification/Mail.php b/app/Notification/Mail.php
index cd8af852..c924fb50 100644
--- a/app/Notification/Mail.php
+++ b/app/Notification/Mail.php
@@ -4,7 +4,7 @@ namespace Kanboard\Notification;
use Kanboard\Core\Base;
use Kanboard\Model\Task;
-use Kanboard\Model\File;
+use Kanboard\Model\TaskFile;
use Kanboard\Model\Comment;
use Kanboard\Model\Subtask;
@@ -17,6 +17,13 @@ use Kanboard\Model\Subtask;
class Mail extends Base implements NotificationInterface
{
/**
+ * Notification type
+ *
+ * @var string
+ */
+ const TYPE = 'email';
+
+ /**
* Send notification to a user
*
* @access public
@@ -75,7 +82,7 @@ class Mail extends Base implements NotificationInterface
public function getMailSubject($event_name, array $event_data)
{
switch ($event_name) {
- case File::EVENT_CREATE:
+ case TaskFile::EVENT_CREATE:
$subject = $this->getStandardMailSubject(e('New attachment'), $event_data);
break;
case Comment::EVENT_CREATE:
@@ -114,6 +121,10 @@ class Mail extends Base implements NotificationInterface
case Task::EVENT_ASSIGNEE_CHANGE:
$subject = $this->getStandardMailSubject(e('Assignee change'), $event_data);
break;
+ case Task::EVENT_USER_MENTION:
+ case Comment::EVENT_USER_MENTION:
+ $subject = $this->getStandardMailSubject(e('Mentioned'), $event_data);
+ break;
case Task::EVENT_OVERDUE:
$subject = e('[%s] Overdue tasks', $event_data['project_name']);
break;
diff --git a/app/Notification/Web.php b/app/Notification/Web.php
index 989ae06e..9271c193 100644
--- a/app/Notification/Web.php
+++ b/app/Notification/Web.php
@@ -13,6 +13,13 @@ use Kanboard\Core\Base;
class Web extends Base implements NotificationInterface
{
/**
+ * Notification type
+ *
+ * @var string
+ */
+ const TYPE = 'web';
+
+ /**
* Send notification to a user
*
* @access public
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index a021c1cc..9a551e99 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -3,9 +3,209 @@
namespace Schema;
use PDO;
-use Kanboard\Core\Security;
+use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
-const VERSION = 93;
+const VERSION = 107;
+
+function version_107(PDO $pdo)
+{
+ $pdo->exec("UPDATE project_activities SET event_name='task.file.create' WHERE event_name='file.create'");
+}
+
+function version_106(PDO $pdo)
+{
+ $pdo->exec('RENAME TABLE files TO task_has_files');
+
+ $pdo->exec("
+ CREATE TABLE project_has_files (
+ `id` INT NOT NULL AUTO_INCREMENT,
+ `project_id` INT NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `path` VARCHAR(255) NOT NULL,
+ `is_image` TINYINT(1) DEFAULT 0,
+ `size` INT DEFAULT 0 NOT NULL,
+ `user_id` INT DEFAULT 0 NOT NULL,
+ `date` INT DEFAULT 0 NOT NULL,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8"
+ );
+}
+
+function version_105(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN is_active TINYINT(1) DEFAULT 1");
+}
+
+function version_104(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_external_links (
+ id INT NOT NULL AUTO_INCREMENT,
+ link_type VARCHAR(100) NOT NULL,
+ dependency VARCHAR(100) NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ url VARCHAR(255) NOT NULL,
+ date_creation INT NOT NULL,
+ date_modification INT NOT NULL,
+ task_id INT NOT NULL,
+ creator_id INT DEFAULT 0,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
+
+function version_103(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_default INT DEFAULT 0");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_start INT DEFAULT 0");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_end INT DEFAULT 3");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN priority INT DEFAULT 0");
+}
+
+function version_102(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN owner_id INT DEFAULT 0");
+}
+
+function version_101(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE password_reset (
+ token VARCHAR(80) PRIMARY KEY,
+ user_id INT NOT NULL,
+ date_expiration INT NOT NULL,
+ date_creation INT NOT NULL,
+ ip VARCHAR(45) NOT NULL,
+ user_agent VARCHAR(255) NOT NULL,
+ is_active TINYINT(1) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+
+ $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
+}
+
+function version_100(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE `actions` MODIFY `action_name` VARCHAR(255)');
+}
+
+function version_99(PDO $pdo)
+{
+ $rq = $pdo->prepare('SELECT * FROM actions');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?');
+
+ foreach ($rows as $row) {
+ if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') {
+ $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn';
+ } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') {
+ $row['action_name'] = '\Kanboard\Action\TaskCloseColumn';
+ } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') {
+ $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn';
+ } elseif ($row['action_name']{0} !== '\\') {
+ $row['action_name'] = '\Kanboard\Action\\'.$row['action_name'];
+ }
+
+ $rq->execute(array($row['action_name'], $row['id']));
+ }
+}
+
+function version_98(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE `users` MODIFY `language` VARCHAR(5)');
+}
+
+function version_97(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE `users` ADD COLUMN `role` VARCHAR(25) NOT NULL DEFAULT '".Role::APP_USER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM `users`');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE `users` SET `role`=? WHERE `id`=?');
+
+ foreach ($rows as $row) {
+ $role = Role::APP_USER;
+
+ if ($row['is_admin'] == 1) {
+ $role = Role::APP_ADMIN;
+ } else if ($row['is_project_admin']) {
+ $role = Role::APP_MANAGER;
+ }
+
+ $rq->execute(array($role, $row['id']));
+ }
+
+ $pdo->exec('ALTER TABLE `users` DROP COLUMN `is_admin`');
+ $pdo->exec('ALTER TABLE `users` DROP COLUMN `is_project_admin`');
+}
+
+function version_96(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_has_groups (
+ `group_id` INT NOT NULL,
+ `project_id` INT NOT NULL,
+ `role` VARCHAR(25) NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE(group_id, project_id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+
+ $pdo->exec("ALTER TABLE `project_has_users` ADD COLUMN `role` VARCHAR(25) NOT NULL DEFAULT '".Role::PROJECT_VIEWER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM project_has_users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE `project_has_users` SET `role`=? WHERE `id`=?');
+
+ foreach ($rows as $row) {
+ $rq->execute(array(
+ $row['is_owner'] == 1 ? Role::PROJECT_MANAGER : Role::PROJECT_MEMBER,
+ $row['id'],
+ ));
+ }
+
+ $pdo->exec('ALTER TABLE `project_has_users` DROP COLUMN `is_owner`');
+ $pdo->exec('ALTER TABLE `project_has_users` DROP COLUMN `id`');
+}
+
+function version_95(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE groups (
+ id INT NOT NULL AUTO_INCREMENT,
+ external_id VARCHAR(255) DEFAULT '',
+ name VARCHAR(100) NOT NULL UNIQUE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+
+ $pdo->exec("
+ CREATE TABLE group_has_users (
+ group_id INT NOT NULL,
+ user_id INT NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
+
+function version_94(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE `projects` DROP INDEX `name`');
+ $pdo->exec('ALTER TABLE `projects` DROP INDEX `name_2`');
+}
function version_93(PDO $pdo)
{
@@ -869,7 +1069,7 @@ function version_20(PDO $pdo)
function version_19(PDO $pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN api_token VARCHAR(255) DEFAULT ''");
- $pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'");
+ $pdo->exec("UPDATE config SET api_token='".Token::getToken()."'");
}
function version_18(PDO $pdo)
@@ -943,7 +1143,7 @@ function version_12(PDO $pdo)
CREATE TABLE remember_me (
id INT NOT NULL AUTO_INCREMENT,
user_id INT,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
@@ -959,7 +1159,7 @@ function version_12(PDO $pdo)
id INT NOT NULL AUTO_INCREMENT,
auth_type VARCHAR(25),
user_id INT,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
date_creation INT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
@@ -1091,6 +1291,6 @@ function version_1(PDO $pdo)
$pdo->exec("
INSERT INTO config
(webhooks_token)
- VALUES ('".Security::generateToken()."')
+ VALUES ('".Token::getToken()."')
");
}
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index a3fb6d49..6aed1491 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -3,9 +3,205 @@
namespace Schema;
use PDO;
-use Kanboard\Core\Security;
+use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
-const VERSION = 73;
+const VERSION = 87;
+
+function version_87(PDO $pdo)
+{
+ $pdo->exec("UPDATE project_activities SET event_name='task.file.create' WHERE event_name='file.create'");
+}
+
+function version_86(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE files RENAME TO task_has_files');
+
+ $pdo->exec("
+ CREATE TABLE project_has_files (
+ id SERIAL PRIMARY KEY,
+ project_id INTEGER NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ path VARCHAR(255) NOT NULL,
+ is_image BOOLEAN DEFAULT '0',
+ size INTEGER DEFAULT 0 NOT NULL,
+ user_id INTEGER DEFAULT 0 NOT NULL,
+ date INTEGER DEFAULT 0 NOT NULL,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )"
+ );
+}
+
+function version_85(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT '1'");
+}
+
+function version_84(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_external_links (
+ id SERIAL PRIMARY KEY,
+ link_type VARCHAR(100) NOT NULL,
+ dependency VARCHAR(100) NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ url VARCHAR(255) NOT NULL,
+ date_creation INT NOT NULL,
+ date_modification INT NOT NULL,
+ task_id INT NOT NULL,
+ creator_id INT DEFAULT 0,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )
+ ");
+}
+
+function version_83(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_default INTEGER DEFAULT 0");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_start INTEGER DEFAULT 0");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_end INTEGER DEFAULT 3");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0");
+}
+
+function version_82(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN owner_id INTEGER DEFAULT 0");
+}
+
+function version_81(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE password_reset (
+ token VARCHAR(80) PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ date_expiration INTEGER NOT NULL,
+ date_creation INTEGER NOT NULL,
+ ip VARCHAR(45) NOT NULL,
+ user_agent VARCHAR(255) NOT NULL,
+ is_active BOOLEAN NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ ");
+
+ $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
+}
+
+function version_80(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE "actions" ALTER COLUMN "action_name" TYPE VARCHAR(255)');
+}
+
+function version_79(PDO $pdo)
+{
+ $rq = $pdo->prepare('SELECT * FROM actions');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?');
+
+ foreach ($rows as $row) {
+ if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') {
+ $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn';
+ } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') {
+ $row['action_name'] = '\Kanboard\Action\TaskCloseColumn';
+ } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') {
+ $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn';
+ } elseif ($row['action_name']{0} !== '\\') {
+ $row['action_name'] = '\Kanboard\Action\\'.$row['action_name'];
+ }
+
+ $rq->execute(array($row['action_name'], $row['id']));
+ }
+}
+
+function version_78(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE "users" ALTER COLUMN "language" TYPE VARCHAR(5)');
+}
+
+function version_77(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE "users" ADD COLUMN "role" VARCHAR(25) NOT NULL DEFAULT \''.Role::APP_USER.'\'');
+
+ $rq = $pdo->prepare('SELECT * FROM "users"');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE "users" SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $role = Role::APP_USER;
+
+ if ($row['is_admin'] == 1) {
+ $role = Role::APP_ADMIN;
+ } else if ($row['is_project_admin']) {
+ $role = Role::APP_MANAGER;
+ }
+
+ $rq->execute(array($role, $row['id']));
+ }
+
+ $pdo->exec('ALTER TABLE users DROP COLUMN "is_admin"');
+ $pdo->exec('ALTER TABLE users DROP COLUMN "is_project_admin"');
+}
+
+function version_76(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_has_groups (
+ group_id INTEGER NOT NULL,
+ project_id INTEGER NOT NULL,
+ role VARCHAR(25) NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE(group_id, project_id)
+ )
+ ");
+
+ $pdo->exec("ALTER TABLE project_has_users ADD COLUMN role VARCHAR(25) NOT NULL DEFAULT '".Role::PROJECT_VIEWER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM project_has_users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE project_has_users SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $rq->execute(array(
+ $row['is_owner'] == 1 ? Role::PROJECT_MANAGER : Role::PROJECT_MEMBER,
+ $row['id'],
+ ));
+ }
+
+ $pdo->exec('ALTER TABLE project_has_users DROP COLUMN "is_owner"');
+ $pdo->exec('ALTER TABLE project_has_users DROP COLUMN "id"');
+}
+
+function version_75(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE groups (
+ id SERIAL PRIMARY KEY,
+ external_id VARCHAR(255) DEFAULT '',
+ name VARCHAR(100) NOT NULL UNIQUE
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE group_has_users (
+ group_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ )
+ ");
+}
+
+function version_74(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE projects DROP CONSTRAINT IF EXISTS projects_name_key');
+}
function version_73(PDO $pdo)
{
@@ -865,7 +1061,7 @@ function version_1(PDO $pdo)
CREATE TABLE remember_me (
id SERIAL PRIMARY KEY,
user_id INTEGER,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
@@ -878,7 +1074,7 @@ function version_1(PDO $pdo)
id SERIAL PRIMARY KEY,
auth_type VARCHAR(25),
user_id INTEGER,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
@@ -994,6 +1190,6 @@ function version_1(PDO $pdo)
$pdo->exec("
INSERT INTO config
(webhooks_token, api_token)
- VALUES ('".Security::generateToken()."', '".Security::generateToken()."')
+ VALUES ('".Token::getToken()."', '".Token::getToken()."')
");
}
diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql
index bb87a6ca..ef219490 100644
--- a/app/Schema/Sql/mysql.sql
+++ b/app/Schema/Sql/mysql.sql
@@ -29,7 +29,7 @@ CREATE TABLE `actions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`project_id` int(11) NOT NULL,
`event_name` varchar(50) NOT NULL,
- `action_name` varchar(50) NOT NULL,
+ `action_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `project_id` (`project_id`),
CONSTRAINT `actions_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
@@ -112,6 +112,29 @@ CREATE TABLE `files` (
CONSTRAINT `files_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `group_has_users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `group_has_users` (
+ `group_id` int(11) NOT NULL,
+ `user_id` int(11) NOT NULL,
+ UNIQUE KEY `group_id` (`group_id`,`user_id`),
+ KEY `user_id` (`user_id`),
+ CONSTRAINT `group_has_users_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `group_has_users_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `groups`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `groups` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `external_id` varchar(255) DEFAULT '',
+ `name` varchar(100) NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `last_logins`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
@@ -119,7 +142,7 @@ CREATE TABLE `last_logins` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`auth_type` varchar(25) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
- `ip` varchar(40) DEFAULT NULL,
+ `ip` varchar(45) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`date_creation` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`),
@@ -138,6 +161,22 @@ CREATE TABLE `links` (
UNIQUE KEY `label` (`label`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `password_reset`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `password_reset` (
+ `token` varchar(80) NOT NULL,
+ `user_id` int(11) NOT NULL,
+ `date_expiration` int(11) NOT NULL,
+ `date_creation` int(11) NOT NULL,
+ `ip` varchar(45) NOT NULL,
+ `user_agent` varchar(255) NOT NULL,
+ `is_active` tinyint(1) NOT NULL,
+ PRIMARY KEY (`token`),
+ KEY `user_id` (`user_id`),
+ CONSTRAINT `password_reset_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `plugin_schema_versions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
@@ -214,6 +253,19 @@ CREATE TABLE `project_has_categories` (
CONSTRAINT `project_has_categories_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `project_has_groups`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `project_has_groups` (
+ `group_id` int(11) NOT NULL,
+ `project_id` int(11) NOT NULL,
+ `role` varchar(25) NOT NULL,
+ UNIQUE KEY `group_id` (`group_id`,`project_id`),
+ KEY `project_id` (`project_id`),
+ CONSTRAINT `project_has_groups_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `project_has_groups_ibfk_2` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `project_has_metadata`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
@@ -241,11 +293,9 @@ DROP TABLE IF EXISTS `project_has_users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `project_has_users` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
`project_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
- `is_owner` tinyint(1) DEFAULT '0',
- PRIMARY KEY (`id`),
+ `role` varchar(25) NOT NULL DEFAULT 'project-viewer',
UNIQUE KEY `idx_project_user` (`project_id`,`user_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `project_has_users_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE,
@@ -270,9 +320,11 @@ CREATE TABLE `projects` (
`identifier` varchar(50) DEFAULT '',
`start_date` varchar(10) DEFAULT '',
`end_date` varchar(10) DEFAULT '',
- PRIMARY KEY (`id`),
- UNIQUE KEY `name` (`name`),
- UNIQUE KEY `name_2` (`name`)
+ `owner_id` int(11) DEFAULT '0',
+ `priority_default` int(11) DEFAULT '0',
+ `priority_start` int(11) DEFAULT '0',
+ `priority_end` int(11) DEFAULT '3',
+ PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `remember_me`;
@@ -281,7 +333,7 @@ DROP TABLE IF EXISTS `remember_me`;
CREATE TABLE `remember_me` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
- `ip` varchar(40) DEFAULT NULL,
+ `ip` varchar(45) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`token` varchar(255) DEFAULT NULL,
`sequence` varchar(255) DEFAULT NULL,
@@ -358,6 +410,24 @@ CREATE TABLE `swimlanes` (
CONSTRAINT `swimlanes_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `task_has_external_links`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `task_has_external_links` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `link_type` varchar(100) NOT NULL,
+ `dependency` varchar(100) NOT NULL,
+ `title` varchar(255) NOT NULL,
+ `url` varchar(255) NOT NULL,
+ `date_creation` int(11) NOT NULL,
+ `date_modification` int(11) NOT NULL,
+ `task_id` int(11) NOT NULL,
+ `creator_id` int(11) DEFAULT '0',
+ PRIMARY KEY (`id`),
+ KEY `task_id` (`task_id`),
+ CONSTRAINT `task_has_external_links_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `task_has_links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
@@ -419,6 +489,7 @@ CREATE TABLE `tasks` (
`recurrence_basedate` int(11) NOT NULL DEFAULT '0',
`recurrence_parent` int(11) DEFAULT NULL,
`recurrence_child` int(11) DEFAULT NULL,
+ `priority` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_task_active` (`is_active`),
KEY `column_id` (`column_id`),
@@ -509,7 +580,6 @@ CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) DEFAULT NULL,
- `is_admin` tinyint(4) DEFAULT '0',
`is_ldap_user` tinyint(1) DEFAULT '0',
`name` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
@@ -517,7 +587,7 @@ CREATE TABLE `users` (
`github_id` varchar(30) DEFAULT NULL,
`notifications_enabled` tinyint(1) DEFAULT '0',
`timezone` varchar(50) DEFAULT NULL,
- `language` char(5) DEFAULT NULL,
+ `language` varchar(5) DEFAULT NULL,
`disable_login_form` tinyint(1) DEFAULT '0',
`twofactor_activated` tinyint(1) DEFAULT '0',
`twofactor_secret` char(16) DEFAULT NULL,
@@ -525,11 +595,10 @@ CREATE TABLE `users` (
`notifications_filter` int(11) DEFAULT '4',
`nb_failed_login` int(11) DEFAULT '0',
`lock_expiration_date` bigint(20) DEFAULT NULL,
- `is_project_admin` int(11) DEFAULT '0',
`gitlab_id` int(11) DEFAULT NULL,
+ `role` varchar(25) NOT NULL DEFAULT 'app-user',
PRIMARY KEY (`id`),
- UNIQUE KEY `users_username_idx` (`username`),
- KEY `users_admin_idx` (`is_admin`)
+ UNIQUE KEY `users_username_idx` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -551,7 +620,7 @@ CREATE TABLE `users` (
LOCK TABLES `settings` WRITE;
/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
-INSERT INTO `settings` VALUES ('api_token','ccff8d37146322410479c8c6707cdaddde840af28ccbd6fbb5a7d7908844'),('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'),('project_categories',''),('subtask_restriction','0'),('subtask_time_tracking','1'),('webhook_token','7a9d4cd8c7fc4d52c60f01b775d4f2bd6e186d2e44f5b3723157b8eb372b'),('webhook_url','');
+INSERT INTO `settings` VALUES ('api_token','dcae89653bfd780f56a5c2ffabc54174bcdd877940e97eaa5d800b35df0c'),('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','6c0a16d06e31add3ca1ca8713a69c6b410f72c80fd49cdfe27c4a62eef13'),('webhook_url','');
/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -580,4 +649,4 @@ UNLOCK TABLES;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-INSERT INTO users (username, password, is_admin) VALUES ('admin', '$2y$10$fDbO.nKAjDxm70DyghADCuqIhF919BAkRTAq0bARDTGwcxZscqIZq', '1');INSERT INTO schema_version VALUES ('93');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$f6brAPgJ6iTtOWUMJANDuuRG.VWDH61.aCb5v3MJnSrZodhDaCcmy', 'app-admin');INSERT INTO schema_version VALUES ('104');
diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql
index 8738010f..68415805 100644
--- a/app/Schema/Sql/postgres.sql
+++ b/app/Schema/Sql/postgres.sql
@@ -68,7 +68,7 @@ CREATE TABLE actions (
id integer NOT NULL,
project_id integer NOT NULL,
event_name character varying(50) NOT NULL,
- action_name character varying(50) NOT NULL
+ action_name character varying(255) NOT NULL
);
@@ -218,6 +218,46 @@ CREATE TABLE files (
--
+-- Name: group_has_users; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE group_has_users (
+ group_id integer NOT NULL,
+ user_id integer NOT NULL
+);
+
+
+--
+-- Name: groups; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE groups (
+ id integer NOT NULL,
+ external_id character varying(255) DEFAULT ''::character varying,
+ name character varying(100) NOT NULL
+);
+
+
+--
+-- Name: groups_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE groups_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE groups_id_seq OWNED BY groups.id;
+
+
+--
-- Name: last_logins; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@@ -225,7 +265,7 @@ CREATE TABLE last_logins (
id integer NOT NULL,
auth_type character varying(25),
user_id integer,
- ip character varying(40),
+ ip character varying(45),
user_agent character varying(255),
date_creation bigint
);
@@ -281,6 +321,21 @@ ALTER SEQUENCE links_id_seq OWNED BY links.id;
--
+-- Name: password_reset; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE password_reset (
+ token character varying(80) NOT NULL,
+ user_id integer NOT NULL,
+ date_expiration integer NOT NULL,
+ date_creation integer NOT NULL,
+ ip character varying(45) NOT NULL,
+ user_agent character varying(255) NOT NULL,
+ is_active boolean NOT NULL
+);
+
+
+--
-- Name: plugin_schema_versions; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@@ -421,6 +476,17 @@ ALTER SEQUENCE project_has_categories_id_seq OWNED BY project_has_categories.id;
--
+-- Name: project_has_groups; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE project_has_groups (
+ group_id integer NOT NULL,
+ project_id integer NOT NULL,
+ role character varying(25) NOT NULL
+);
+
+
+--
-- Name: project_has_metadata; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@@ -466,33 +532,13 @@ ALTER SEQUENCE project_has_notification_types_id_seq OWNED BY project_has_notifi
--
CREATE TABLE project_has_users (
- id integer NOT NULL,
project_id integer NOT NULL,
user_id integer NOT NULL,
- is_owner boolean DEFAULT false
+ role character varying(25) DEFAULT 'project-viewer'::character varying NOT NULL
);
--
--- Name: project_has_users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
---
-
-CREATE SEQUENCE project_has_users_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-
---
--- Name: project_has_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
---
-
-ALTER SEQUENCE project_has_users_id_seq OWNED BY project_has_users.id;
-
-
---
-- Name: projects; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@@ -510,7 +556,11 @@ CREATE TABLE projects (
description text,
identifier character varying(50) DEFAULT ''::character varying,
start_date character varying(10) DEFAULT ''::character varying,
- end_date character varying(10) DEFAULT ''::character varying
+ end_date character varying(10) DEFAULT ''::character varying,
+ owner_id integer DEFAULT 0,
+ priority_default integer DEFAULT 0,
+ priority_start integer DEFAULT 0,
+ priority_end integer DEFAULT 3
);
@@ -540,7 +590,7 @@ ALTER SEQUENCE projects_id_seq OWNED BY projects.id;
CREATE TABLE remember_me (
id integer NOT NULL,
user_id integer,
- ip character varying(40),
+ ip character varying(45),
user_agent character varying(255),
token character varying(255),
sequence character varying(255),
@@ -670,6 +720,42 @@ ALTER SEQUENCE swimlanes_id_seq OWNED BY swimlanes.id;
--
+-- Name: task_has_external_links; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE task_has_external_links (
+ id integer NOT NULL,
+ link_type character varying(100) NOT NULL,
+ dependency character varying(100) NOT NULL,
+ title character varying(255) NOT NULL,
+ url character varying(255) NOT NULL,
+ date_creation integer NOT NULL,
+ date_modification integer NOT NULL,
+ task_id integer NOT NULL,
+ creator_id integer DEFAULT 0
+);
+
+
+--
+-- Name: task_has_external_links_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE task_has_external_links_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: task_has_external_links_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE task_has_external_links_id_seq OWNED BY task_has_external_links.id;
+
+
+--
-- Name: task_has_files_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
@@ -782,7 +868,8 @@ CREATE TABLE tasks (
recurrence_timeframe integer DEFAULT 0 NOT NULL,
recurrence_basedate integer DEFAULT 0 NOT NULL,
recurrence_parent integer,
- recurrence_child integer
+ recurrence_child integer,
+ priority integer DEFAULT 0
);
@@ -931,7 +1018,6 @@ CREATE TABLE users (
id integer NOT NULL,
username character varying(50) NOT NULL,
password character varying(255),
- is_admin boolean DEFAULT false,
is_ldap_user boolean DEFAULT false,
name character varying(255),
email character varying(255),
@@ -939,7 +1025,7 @@ CREATE TABLE users (
github_id character varying(30),
notifications_enabled boolean DEFAULT false,
timezone character varying(50),
- language character(5),
+ language character varying(5),
disable_login_form boolean DEFAULT false,
twofactor_activated boolean DEFAULT false,
twofactor_secret character(16),
@@ -947,8 +1033,8 @@ CREATE TABLE users (
notifications_filter integer DEFAULT 4,
nb_failed_login integer DEFAULT 0,
lock_expiration_date bigint DEFAULT 0,
- is_project_admin boolean DEFAULT false,
- gitlab_id integer
+ gitlab_id integer,
+ role character varying(25) DEFAULT 'app-user'::character varying NOT NULL
);
@@ -1017,6 +1103,13 @@ ALTER TABLE ONLY files ALTER COLUMN id SET DEFAULT nextval('task_has_files_id_se
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
+ALTER TABLE ONLY groups ALTER COLUMN id SET DEFAULT nextval('groups_id_seq'::regclass);
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
ALTER TABLE ONLY last_logins ALTER COLUMN id SET DEFAULT nextval('last_logins_id_seq'::regclass);
@@ -1066,42 +1159,42 @@ ALTER TABLE ONLY project_has_notification_types ALTER COLUMN id SET DEFAULT next
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
-ALTER TABLE ONLY project_has_users ALTER COLUMN id SET DEFAULT nextval('project_has_users_id_seq'::regclass);
+ALTER TABLE ONLY projects ALTER COLUMN id SET DEFAULT nextval('projects_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
-ALTER TABLE ONLY projects ALTER COLUMN id SET DEFAULT nextval('projects_id_seq'::regclass);
+ALTER TABLE ONLY remember_me ALTER COLUMN id SET DEFAULT nextval('remember_me_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
-ALTER TABLE ONLY remember_me ALTER COLUMN id SET DEFAULT nextval('remember_me_id_seq'::regclass);
+ALTER TABLE ONLY subtask_time_tracking ALTER COLUMN id SET DEFAULT nextval('subtask_time_tracking_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
-ALTER TABLE ONLY subtask_time_tracking ALTER COLUMN id SET DEFAULT nextval('subtask_time_tracking_id_seq'::regclass);
+ALTER TABLE ONLY subtasks ALTER COLUMN id SET DEFAULT nextval('task_has_subtasks_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
-ALTER TABLE ONLY subtasks ALTER COLUMN id SET DEFAULT nextval('task_has_subtasks_id_seq'::regclass);
+ALTER TABLE ONLY swimlanes ALTER COLUMN id SET DEFAULT nextval('swimlanes_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
-ALTER TABLE ONLY swimlanes ALTER COLUMN id SET DEFAULT nextval('swimlanes_id_seq'::regclass);
+ALTER TABLE ONLY task_has_external_links ALTER COLUMN id SET DEFAULT nextval('task_has_external_links_id_seq'::regclass);
--
@@ -1203,6 +1296,30 @@ ALTER TABLE ONLY custom_filters
--
+-- Name: group_has_users_group_id_user_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
+--
+
+ALTER TABLE ONLY group_has_users
+ ADD CONSTRAINT group_has_users_group_id_user_id_key UNIQUE (group_id, user_id);
+
+
+--
+-- Name: groups_name_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
+--
+
+ALTER TABLE ONLY groups
+ ADD CONSTRAINT groups_name_key UNIQUE (name);
+
+
+--
+-- Name: groups_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
+--
+
+ALTER TABLE ONLY groups
+ ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
+
+
+--
-- Name: last_logins_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -1227,6 +1344,14 @@ ALTER TABLE ONLY links
--
+-- Name: password_reset_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
+--
+
+ALTER TABLE ONLY password_reset
+ ADD CONSTRAINT password_reset_pkey PRIMARY KEY (token);
+
+
+--
-- Name: plugin_schema_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -1275,6 +1400,14 @@ ALTER TABLE ONLY project_has_categories
--
+-- Name: project_has_groups_group_id_project_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
+--
+
+ALTER TABLE ONLY project_has_groups
+ ADD CONSTRAINT project_has_groups_group_id_project_id_key UNIQUE (group_id, project_id);
+
+
+--
-- Name: project_has_metadata_project_id_name_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -1299,14 +1432,6 @@ ALTER TABLE ONLY project_has_notification_types
--
--- Name: project_has_users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
---
-
-ALTER TABLE ONLY project_has_users
- ADD CONSTRAINT project_has_users_pkey PRIMARY KEY (id);
-
-
---
-- Name: project_has_users_project_id_user_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -1315,14 +1440,6 @@ ALTER TABLE ONLY project_has_users
--
--- Name: projects_name_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
---
-
-ALTER TABLE ONLY projects
- ADD CONSTRAINT projects_name_key UNIQUE (name);
-
-
---
-- Name: projects_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -1578,13 +1695,6 @@ CREATE UNIQUE INDEX user_has_notification_types_user_idx ON user_has_notificatio
--
--- Name: users_admin_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
---
-
-CREATE INDEX users_admin_idx ON users USING btree (is_admin);
-
-
---
-- Name: users_username_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
@@ -1624,6 +1734,22 @@ ALTER TABLE ONLY comments
--
+-- Name: group_has_users_group_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY group_has_users
+ ADD CONSTRAINT group_has_users_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
+
+
+--
+-- Name: group_has_users_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY group_has_users
+ ADD CONSTRAINT group_has_users_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
+
+--
-- Name: last_logins_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1632,6 +1758,14 @@ ALTER TABLE ONLY last_logins
--
+-- Name: password_reset_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY password_reset
+ ADD CONSTRAINT password_reset_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
+
+--
-- Name: project_activities_creator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1688,6 +1822,22 @@ ALTER TABLE ONLY project_has_categories
--
+-- Name: project_has_groups_group_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY project_has_groups
+ ADD CONSTRAINT project_has_groups_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
+
+
+--
+-- Name: project_has_groups_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY project_has_groups
+ ADD CONSTRAINT project_has_groups_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
+
+--
-- Name: project_has_metadata_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1752,6 +1902,14 @@ ALTER TABLE ONLY swimlanes
--
+-- Name: task_has_external_links_task_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY task_has_external_links
+ ADD CONSTRAINT task_has_external_links_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE;
+
+
+--
-- Name: task_has_files_task_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1930,8 +2088,8 @@ 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', '29877f0b69d230e57bee9d02e0aa9034a69f7a2c0ba1e3b5d3b390241f36');
-INSERT INTO settings (option, value) VALUES ('api_token', '5682955e965bd0cd7618559a25131fe6094d9fff3bb56c31291d64991353');
+INSERT INTO settings (option, value) VALUES ('webhook_token', '083531659240f36021edc73a4ed3920646ef8798b798c5894e2277f048a2');
+INSERT INTO settings (option, value) VALUES ('api_token', 'b97b4abc7e8f0e0569bdad8cb54f7fb1931615932e552fa255f87a3e1360');
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', '');
@@ -1948,6 +2106,7 @@ 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');
--
@@ -1995,4 +2154,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true);
-- PostgreSQL database dump complete
--
-INSERT INTO users (username, password, is_admin) VALUES ('admin', '$2y$10$fDbO.nKAjDxm70DyghADCuqIhF919BAkRTAq0bARDTGwcxZscqIZq', '1');INSERT INTO schema_version VALUES ('73');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$f6brAPgJ6iTtOWUMJANDuuRG.VWDH61.aCb5v3MJnSrZodhDaCcmy', 'app-admin');INSERT INTO schema_version VALUES ('84');
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index b9ab86f8..8bcad291 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -2,10 +2,185 @@
namespace Schema;
-use Kanboard\Core\Security;
+use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 88;
+const VERSION = 99;
+
+function version_99(PDO $pdo)
+{
+ $pdo->exec("UPDATE project_activities SET event_name='task.file.create' WHERE event_name='file.create'");
+}
+
+function version_98(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE files RENAME TO task_has_files');
+
+ $pdo->exec("
+ CREATE TABLE project_has_files (
+ id INTEGER PRIMARY KEY,
+ project_id INTEGER NOT NULL,
+ name TEXT COLLATE NOCASE NOT NULL,
+ path TEXT NOT NULL,
+ is_image INTEGER DEFAULT 0,
+ size INTEGER DEFAULT 0 NOT NULL,
+ user_id INTEGER DEFAULT 0 NOT NULL,
+ date INTEGER DEFAULT 0 NOT NULL,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )"
+ );
+}
+
+function version_97(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1");
+}
+
+function version_96(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_external_links (
+ id INTEGER PRIMARY KEY,
+ link_type TEXT NOT NULL,
+ dependency TEXT NOT NULL,
+ title TEXT NOT NULL,
+ url TEXT NOT NULL,
+ date_creation INTEGER NOT NULL,
+ date_modification INTEGER NOT NULL,
+ task_id INTEGER NOT NULL,
+ creator_id INTEGER DEFAULT 0,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )
+ ");
+}
+
+function version_95(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_default INTEGER DEFAULT 0");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_start INTEGER DEFAULT 0");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN priority_end INTEGER DEFAULT 3");
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0");
+}
+
+function version_94(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN owner_id INTEGER DEFAULT 0");
+}
+
+function version_93(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE password_reset (
+ token TEXT PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ date_expiration INTEGER NOT NULL,
+ date_creation INTEGER NOT NULL,
+ ip TEXT NOT NULL,
+ user_agent TEXT NOT NULL,
+ is_active INTEGER NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ ");
+
+ $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
+}
+
+function version_92(PDO $pdo)
+{
+ $rq = $pdo->prepare('SELECT * FROM actions');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?');
+
+ foreach ($rows as $row) {
+ if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') {
+ $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn';
+ } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') {
+ $row['action_name'] = '\Kanboard\Action\TaskCloseColumn';
+ } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') {
+ $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn';
+ } elseif ($row['action_name']{0} !== '\\') {
+ $row['action_name'] = '\Kanboard\Action\\'.$row['action_name'];
+ }
+
+ $rq->execute(array($row['action_name'], $row['id']));
+ }
+}
+
+function version_91(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT '".Role::APP_USER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE users SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $role = Role::APP_USER;
+
+ if ($row['is_admin'] == 1) {
+ $role = Role::APP_ADMIN;
+ } else if ($row['is_project_admin']) {
+ $role = Role::APP_MANAGER;
+ }
+
+ $rq->execute(array($role, $row['id']));
+ }
+}
+
+function version_90(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_has_groups (
+ group_id INTEGER NOT NULL,
+ project_id INTEGER NOT NULL,
+ role TEXT NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE(group_id, project_id)
+ )
+ ");
+
+ $pdo->exec("ALTER TABLE project_has_users ADD COLUMN role TEXT NOT NULL DEFAULT '".Role::PROJECT_VIEWER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM project_has_users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE project_has_users SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $rq->execute(array(
+ $row['is_owner'] == 1 ? Role::PROJECT_MANAGER : Role::PROJECT_MEMBER,
+ $row['id'],
+ ));
+ }
+}
+
+function version_89(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE groups (
+ id INTEGER PRIMARY KEY,
+ external_id TEXT DEFAULT '',
+ name TEXT NOCASE NOT NULL UNIQUE
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE group_has_users (
+ group_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ )
+ ");
+}
function version_88(PDO $pdo)
{
@@ -799,7 +974,7 @@ function version_20(PDO $pdo)
function version_19(PDO $pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT ''");
- $pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'");
+ $pdo->exec("UPDATE config SET api_token='".Token::getToken()."'");
}
function version_18(PDO $pdo)
@@ -969,7 +1144,6 @@ function version_7(PDO $pdo)
{
$pdo->exec("
CREATE TABLE project_has_users (
- id INTEGER PRIMARY KEY,
project_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
@@ -1026,7 +1200,7 @@ function version_1(PDO $pdo)
$pdo->exec("
CREATE TABLE projects (
id INTEGER PRIMARY KEY,
- name TEXT NOCASE NOT NULL UNIQUE,
+ name TEXT NOCASE NOT NULL,
is_active INTEGER DEFAULT 1
)
");
@@ -1068,6 +1242,6 @@ function version_1(PDO $pdo)
$pdo->exec("
INSERT INTO config
(webhooks_token)
- VALUES ('".Security::generateToken()."')
+ VALUES ('".Token::getToken()."')
");
}
diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php
new file mode 100644
index 00000000..3692f190
--- /dev/null
+++ b/app/ServiceProvider/ActionProvider.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Action\ActionManager;
+use Kanboard\Action\CommentCreation;
+use Kanboard\Action\CommentCreationMoveTaskColumn;
+use Kanboard\Action\TaskAssignCategoryColor;
+use Kanboard\Action\TaskAssignCategoryLabel;
+use Kanboard\Action\TaskAssignCategoryLink;
+use Kanboard\Action\TaskAssignColorCategory;
+use Kanboard\Action\TaskAssignColorColumn;
+use Kanboard\Action\TaskAssignColorLink;
+use Kanboard\Action\TaskAssignColorUser;
+use Kanboard\Action\TaskAssignCurrentUser;
+use Kanboard\Action\TaskAssignCurrentUserColumn;
+use Kanboard\Action\TaskAssignSpecificUser;
+use Kanboard\Action\TaskAssignUser;
+use Kanboard\Action\TaskClose;
+use Kanboard\Action\TaskCloseColumn;
+use Kanboard\Action\TaskCreation;
+use Kanboard\Action\TaskDuplicateAnotherProject;
+use Kanboard\Action\TaskEmail;
+use Kanboard\Action\TaskEmailNoActivity;
+use Kanboard\Action\TaskMoveAnotherProject;
+use Kanboard\Action\TaskMoveColumnAssigned;
+use Kanboard\Action\TaskMoveColumnCategoryChange;
+use Kanboard\Action\TaskMoveColumnUnAssigned;
+use Kanboard\Action\TaskOpen;
+use Kanboard\Action\TaskUpdateStartDate;
+use Kanboard\Action\TaskCloseNoActivity;
+
+/**
+ * Action Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class ActionProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['actionManager'] = new ActionManager($container);
+ $container['actionManager']->register(new CommentCreation($container));
+ $container['actionManager']->register(new CommentCreationMoveTaskColumn($container));
+ $container['actionManager']->register(new TaskAssignCategoryColor($container));
+ $container['actionManager']->register(new TaskAssignCategoryLabel($container));
+ $container['actionManager']->register(new TaskAssignCategoryLink($container));
+ $container['actionManager']->register(new TaskAssignColorCategory($container));
+ $container['actionManager']->register(new TaskAssignColorColumn($container));
+ $container['actionManager']->register(new TaskAssignColorLink($container));
+ $container['actionManager']->register(new TaskAssignColorUser($container));
+ $container['actionManager']->register(new TaskAssignCurrentUser($container));
+ $container['actionManager']->register(new TaskAssignCurrentUserColumn($container));
+ $container['actionManager']->register(new TaskAssignSpecificUser($container));
+ $container['actionManager']->register(new TaskAssignUser($container));
+ $container['actionManager']->register(new TaskClose($container));
+ $container['actionManager']->register(new TaskCloseColumn($container));
+ $container['actionManager']->register(new TaskCloseNoActivity($container));
+ $container['actionManager']->register(new TaskCreation($container));
+ $container['actionManager']->register(new TaskDuplicateAnotherProject($container));
+ $container['actionManager']->register(new TaskEmail($container));
+ $container['actionManager']->register(new TaskEmailNoActivity($container));
+ $container['actionManager']->register(new TaskMoveAnotherProject($container));
+ $container['actionManager']->register(new TaskMoveColumnAssigned($container));
+ $container['actionManager']->register(new TaskMoveColumnCategoryChange($container));
+ $container['actionManager']->register(new TaskMoveColumnUnAssigned($container));
+ $container['actionManager']->register(new TaskOpen($container));
+ $container['actionManager']->register(new TaskUpdateStartDate($container));
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
new file mode 100644
index 00000000..700fe05b
--- /dev/null
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Security\AuthenticationManager;
+use Kanboard\Core\Security\AccessMap;
+use Kanboard\Core\Security\Authorization;
+use Kanboard\Core\Security\Role;
+use Kanboard\Auth\RememberMeAuth;
+use Kanboard\Auth\DatabaseAuth;
+use Kanboard\Auth\LdapAuth;
+use Kanboard\Auth\TotpAuth;
+use Kanboard\Auth\ReverseProxyAuth;
+
+/**
+ * Authentication Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class AuthenticationProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['authenticationManager'] = new AuthenticationManager($container);
+ $container['authenticationManager']->register(new TotpAuth($container));
+ $container['authenticationManager']->register(new RememberMeAuth($container));
+ $container['authenticationManager']->register(new DatabaseAuth($container));
+
+ if (REVERSE_PROXY_AUTH) {
+ $container['authenticationManager']->register(new ReverseProxyAuth($container));
+ }
+
+ if (LDAP_AUTH) {
+ $container['authenticationManager']->register(new LdapAuth($container));
+ }
+
+ $container['projectAccessMap'] = $this->getProjectAccessMap();
+ $container['applicationAccessMap'] = $this->getApplicationAccessMap();
+
+ $container['projectAuthorization'] = new Authorization($container['projectAccessMap']);
+ $container['applicationAuthorization'] = new Authorization($container['applicationAccessMap']);
+
+ return $container;
+ }
+
+ /**
+ * Get ACL for projects
+ *
+ * @access public
+ * @return AccessMap
+ */
+ public function getProjectAccessMap()
+ {
+ $acl = new AccessMap;
+ $acl->setDefaultRole(Role::PROJECT_VIEWER);
+ $acl->setRoleHierarchy(Role::PROJECT_MANAGER, array(Role::PROJECT_MEMBER, Role::PROJECT_VIEWER));
+ $acl->setRoleHierarchy(Role::PROJECT_MEMBER, array(Role::PROJECT_VIEWER));
+
+ $acl->add('Action', '*', Role::PROJECT_MANAGER);
+ $acl->add('Analytic', '*', Role::PROJECT_MANAGER);
+ $acl->add('Board', 'save', Role::PROJECT_MEMBER);
+ $acl->add('BoardPopover', '*', Role::PROJECT_MEMBER);
+ $acl->add('Calendar', 'save', Role::PROJECT_MEMBER);
+ $acl->add('Category', '*', Role::PROJECT_MANAGER);
+ $acl->add('Column', '*', Role::PROJECT_MANAGER);
+ $acl->add('Comment', '*', Role::PROJECT_MEMBER);
+ $acl->add('Customfilter', '*', Role::PROJECT_MEMBER);
+ $acl->add('Export', '*', Role::PROJECT_MANAGER);
+ $acl->add('TaskFile', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER);
+ $acl->add('Gantt', '*', Role::PROJECT_MANAGER);
+ $acl->add('Project', array('share', 'integrations', 'notifications', 'duplicate', 'disable', 'enable', 'remove'), Role::PROJECT_MANAGER);
+ $acl->add('ProjectPermission', '*', Role::PROJECT_MANAGER);
+ $acl->add('ProjectEdit', '*', Role::PROJECT_MANAGER);
+ $acl->add('ProjectFile', '*', Role::PROJECT_MEMBER);
+ $acl->add('Projectuser', '*', Role::PROJECT_MANAGER);
+ $acl->add('Subtask', '*', Role::PROJECT_MEMBER);
+ $acl->add('SubtaskRestriction', '*', Role::PROJECT_MEMBER);
+ $acl->add('SubtaskStatus', '*', Role::PROJECT_MEMBER);
+ $acl->add('Swimlane', '*', Role::PROJECT_MANAGER);
+ $acl->add('Task', 'remove', Role::PROJECT_MEMBER);
+ $acl->add('Taskcreation', '*', Role::PROJECT_MEMBER);
+ $acl->add('Taskduplication', '*', Role::PROJECT_MEMBER);
+ $acl->add('TaskRecurrence', '*', Role::PROJECT_MEMBER);
+ $acl->add('TaskImport', '*', Role::PROJECT_MANAGER);
+ $acl->add('Tasklink', '*', Role::PROJECT_MEMBER);
+ $acl->add('Tasklink', array('show'), Role::PROJECT_VIEWER);
+ $acl->add('TaskExternalLink', '*', Role::PROJECT_MEMBER);
+ $acl->add('TaskExternalLink', array('show'), Role::PROJECT_VIEWER);
+ $acl->add('Taskmodification', '*', Role::PROJECT_MEMBER);
+ $acl->add('Taskstatus', '*', Role::PROJECT_MEMBER);
+ $acl->add('UserHelper', array('mention'), Role::PROJECT_MEMBER);
+
+ return $acl;
+ }
+
+ /**
+ * Get ACL for the application
+ *
+ * @access public
+ * @return AccessMap
+ */
+ public function getApplicationAccessMap()
+ {
+ $acl = new AccessMap;
+ $acl->setDefaultRole(Role::APP_USER);
+ $acl->setRoleHierarchy(Role::APP_ADMIN, array(Role::APP_MANAGER, Role::APP_USER, Role::APP_PUBLIC));
+ $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER, Role::APP_PUBLIC));
+ $acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC));
+
+ $acl->add('Auth', array('login', 'check'), Role::APP_PUBLIC);
+ $acl->add('Captcha', '*', Role::APP_PUBLIC);
+ $acl->add('PasswordReset', '*', Role::APP_PUBLIC);
+ $acl->add('Webhook', '*', Role::APP_PUBLIC);
+ $acl->add('Task', 'readonly', Role::APP_PUBLIC);
+ $acl->add('Board', 'readonly', Role::APP_PUBLIC);
+ $acl->add('Ical', '*', Role::APP_PUBLIC);
+ $acl->add('Feed', '*', Role::APP_PUBLIC);
+
+ $acl->add('Config', '*', Role::APP_ADMIN);
+ $acl->add('Currency', '*', Role::APP_ADMIN);
+ $acl->add('Gantt', array('projects', 'saveProjectDate'), Role::APP_MANAGER);
+ $acl->add('Group', '*', Role::APP_ADMIN);
+ $acl->add('Link', '*', Role::APP_ADMIN);
+ $acl->add('ProjectCreation', 'create', Role::APP_MANAGER);
+ $acl->add('Projectuser', '*', Role::APP_MANAGER);
+ $acl->add('Twofactor', 'disable', Role::APP_ADMIN);
+ $acl->add('UserImport', '*', Role::APP_ADMIN);
+ $acl->add('User', array('index', 'create', 'save', 'authentication'), Role::APP_ADMIN);
+ $acl->add('UserStatus', '*', Role::APP_ADMIN);
+
+ return $acl;
+ }
+}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index c103d639..0f2fbab5 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -4,44 +4,55 @@ namespace Kanboard\ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
-use League\HTMLToMarkdown\HtmlConverter;
-use Kanboard\Core\Plugin\Loader;
use Kanboard\Core\Mail\Client as EmailClient;
use Kanboard\Core\ObjectStorage\FileStorage;
use Kanboard\Core\Paginator;
-use Kanboard\Core\OAuth2;
+use Kanboard\Core\Http\OAuth2;
use Kanboard\Core\Tool;
-use Kanboard\Model\UserNotificationType;
-use Kanboard\Model\ProjectNotificationType;
+use Kanboard\Core\Http\Client as HttpClient;
class ClassProvider implements ServiceProviderInterface
{
private $classes = array(
+ 'Analytic' => array(
+ 'TaskDistributionAnalytic',
+ 'UserDistributionAnalytic',
+ 'EstimatedTimeComparisonAnalytic',
+ 'AverageLeadCycleTimeAnalytic',
+ 'AverageTimeSpentColumnAnalytic',
+ ),
'Model' => array(
- 'Acl',
'Action',
- 'Authentication',
+ 'ActionParameter',
'Board',
'Category',
'Color',
+ 'Column',
'Comment',
'Config',
'Currency',
'CustomFilter',
- 'File',
+ 'Group',
+ 'GroupMember',
'LastLogin',
'Link',
'Notification',
'OverdueNotification',
+ 'PasswordReset',
'Project',
+ 'ProjectFile',
'ProjectActivity',
- 'ProjectAnalytic',
'ProjectDuplication',
'ProjectDailyColumnStats',
'ProjectDailyStats',
'ProjectPermission',
'ProjectNotification',
'ProjectMetadata',
+ 'ProjectGroupRole',
+ 'ProjectGroupRoleFilter',
+ 'ProjectUserRole',
+ 'ProjectUserRoleFilter',
+ 'RememberMeSession',
'Subtask',
'SubtaskExport',
'SubtaskTimeTracking',
@@ -51,22 +62,23 @@ class ClassProvider implements ServiceProviderInterface
'TaskCreation',
'TaskDuplication',
'TaskExport',
+ 'TaskExternalLink',
'TaskFinder',
+ 'TaskFile',
'TaskFilter',
'TaskLink',
'TaskModification',
'TaskPermission',
'TaskPosition',
'TaskStatus',
- 'TaskValidator',
'TaskImport',
'TaskMetadata',
'Transition',
'User',
'UserImport',
- 'UserSession',
+ 'UserLocking',
+ 'UserMention',
'UserNotification',
- 'UserNotificationType',
'UserNotificationFilter',
'UserUnreadNotification',
'UserMetadata',
@@ -77,27 +89,57 @@ class ClassProvider implements ServiceProviderInterface
'TaskFilterCalendarFormatter',
'TaskFilterICalendarFormatter',
'ProjectGanttFormatter',
+ 'UserFilterAutoCompleteFormatter',
+ 'GroupAutoCompleteFormatter',
+ ),
+ 'Validator' => array(
+ 'ActionValidator',
+ 'AuthValidator',
+ 'CategoryValidator',
+ 'ColumnValidator',
+ 'CommentValidator',
+ 'CurrencyValidator',
+ 'CustomFilterValidator',
+ 'ExternalLinkValidator',
+ 'GroupValidator',
+ 'LinkValidator',
+ 'PasswordResetValidator',
+ 'ProjectValidator',
+ 'SubtaskValidator',
+ 'SwimlaneValidator',
+ 'TaskValidator',
+ 'TaskLinkValidator',
+ 'UserValidator',
),
'Core' => array(
'DateParser',
'Helper',
- 'HttpClient',
'Lexer',
- 'Request',
- 'Router',
- 'Session',
'Template',
),
+ 'Core\Event' => array(
+ 'EventManager',
+ ),
+ 'Core\Http' => array(
+ 'Request',
+ 'Response',
+ 'RememberMeCookie',
+ ),
'Core\Cache' => array(
'MemoryCache',
),
'Core\Plugin' => array(
'Hook',
),
- 'Integration' => array(
- 'BitbucketWebhook',
- 'GithubWebhook',
- 'GitlabWebhook',
+ 'Core\Security' => array(
+ 'Token',
+ 'Role',
+ ),
+ 'Core\User' => array(
+ 'GroupSync',
+ 'UserSync',
+ 'UserSession',
+ 'UserProfile',
)
);
@@ -113,8 +155,8 @@ class ClassProvider implements ServiceProviderInterface
return new OAuth2($c);
});
- $container['htmlConverter'] = function () {
- return new HtmlConverter(array('strip_tags' => true));
+ $container['httpClient'] = function ($c) {
+ return new HttpClient($c);
};
$container['objectStorage'] = function () {
@@ -129,22 +171,12 @@ class ClassProvider implements ServiceProviderInterface
return $mailer;
};
- $container['userNotificationType'] = function ($container) {
- $type = new UserNotificationType($container);
- $type->setType('email', t('Email'), '\Kanboard\Notification\Mail');
- $type->setType('web', t('Web'), '\Kanboard\Notification\Web');
- return $type;
- };
-
- $container['projectNotificationType'] = function ($container) {
- $type = new ProjectNotificationType($container);
- $type->setType('webhook', 'Webhook', '\Kanboard\Notification\Webhook', true);
- $type->setType('activity_stream', 'ActivityStream', '\Kanboard\Notification\ActivityStream', true);
- return $type;
- };
-
- $container['pluginLoader'] = new Loader($container);
+ $container['cspRules'] = array(
+ 'default-src' => "'self'",
+ 'style-src' => "'self' 'unsafe-inline'",
+ 'img-src' => '* data:',
+ );
- $container['cspRules'] = array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '* data:');
+ return $container;
}
}
diff --git a/app/ServiceProvider/DatabaseProvider.php b/app/ServiceProvider/DatabaseProvider.php
index b2115644..8cede8af 100644
--- a/app/ServiceProvider/DatabaseProvider.php
+++ b/app/ServiceProvider/DatabaseProvider.php
@@ -15,6 +15,8 @@ class DatabaseProvider implements ServiceProviderInterface
$container['db'] = $this->getInstance();
$container['db']->stopwatch = DEBUG;
$container['db']->logQueries = DEBUG;
+
+ return $container;
}
/**
diff --git a/app/ServiceProvider/EventDispatcherProvider.php b/app/ServiceProvider/EventDispatcherProvider.php
index 1711919e..880caa41 100644
--- a/app/ServiceProvider/EventDispatcherProvider.php
+++ b/app/ServiceProvider/EventDispatcherProvider.php
@@ -11,7 +11,6 @@ use Kanboard\Subscriber\NotificationSubscriber;
use Kanboard\Subscriber\ProjectDailySummarySubscriber;
use Kanboard\Subscriber\ProjectModificationDateSubscriber;
use Kanboard\Subscriber\SubtaskTimeTrackingSubscriber;
-use Kanboard\Subscriber\TaskMovedDateSubscriber;
use Kanboard\Subscriber\TransitionSubscriber;
use Kanboard\Subscriber\RecurringTaskSubscriber;
@@ -26,11 +25,9 @@ class EventDispatcherProvider implements ServiceProviderInterface
$container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container));
$container['dispatcher']->addSubscriber(new NotificationSubscriber($container));
$container['dispatcher']->addSubscriber(new SubtaskTimeTrackingSubscriber($container));
- $container['dispatcher']->addSubscriber(new TaskMovedDateSubscriber($container));
$container['dispatcher']->addSubscriber(new TransitionSubscriber($container));
$container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container));
- // Automatic actions
- $container['action']->attachEvents();
+ return $container;
}
}
diff --git a/app/ServiceProvider/ExternalLinkProvider.php b/app/ServiceProvider/ExternalLinkProvider.php
new file mode 100644
index 00000000..c4bbc4cf
--- /dev/null
+++ b/app/ServiceProvider/ExternalLinkProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\ExternalLink\ExternalLinkManager;
+use Kanboard\ExternalLink\WebLinkProvider;
+use Kanboard\ExternalLink\AttachmentLinkProvider;
+
+/**
+ * External Link Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class ExternalLinkProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['externalLinkManager'] = new ExternalLinkManager($container);
+ $container['externalLinkManager']->register(new WebLinkProvider($container));
+ $container['externalLinkManager']->register(new AttachmentLinkProvider($container));
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/GroupProvider.php b/app/ServiceProvider/GroupProvider.php
new file mode 100644
index 00000000..b222b218
--- /dev/null
+++ b/app/ServiceProvider/GroupProvider.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Group\GroupManager;
+use Kanboard\Group\DatabaseBackendGroupProvider;
+use Kanboard\Group\LdapBackendGroupProvider;
+
+/**
+ * Group Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class GroupProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['groupManager'] = new GroupManager;
+
+ if (DB_GROUP_PROVIDER) {
+ $container['groupManager']->register(new DatabaseBackendGroupProvider($container));
+ }
+
+ if (LDAP_AUTH && LDAP_GROUP_PROVIDER) {
+ $container['groupManager']->register(new LdapBackendGroupProvider($container));
+ }
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/LoggingProvider.php b/app/ServiceProvider/LoggingProvider.php
index 4344bccc..68c074f0 100644
--- a/app/ServiceProvider/LoggingProvider.php
+++ b/app/ServiceProvider/LoggingProvider.php
@@ -26,5 +26,7 @@ class LoggingProvider implements ServiceProviderInterface
}
$container['logger'] = $logger;
+
+ return $container;
}
}
diff --git a/app/ServiceProvider/NotificationProvider.php b/app/ServiceProvider/NotificationProvider.php
new file mode 100644
index 00000000..83daf65d
--- /dev/null
+++ b/app/ServiceProvider/NotificationProvider.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Model\UserNotificationType;
+use Kanboard\Model\ProjectNotificationType;
+use Kanboard\Notification\Mail as MailNotification;
+use Kanboard\Notification\Web as WebNotification;
+
+/**
+ * Notification Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class NotificationProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['userNotificationType'] = function ($container) {
+ $type = new UserNotificationType($container);
+ $type->setType(MailNotification::TYPE, t('Email'), '\Kanboard\Notification\Mail');
+ $type->setType(WebNotification::TYPE, t('Web'), '\Kanboard\Notification\Web');
+ return $type;
+ };
+
+ $container['projectNotificationType'] = function ($container) {
+ $type = new ProjectNotificationType($container);
+ $type->setType('webhook', 'Webhook', '\Kanboard\Notification\Webhook', true);
+ $type->setType('activity_stream', 'ActivityStream', '\Kanboard\Notification\ActivityStream', true);
+ return $type;
+ };
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/PluginProvider.php b/app/ServiceProvider/PluginProvider.php
new file mode 100644
index 00000000..d2f1666b
--- /dev/null
+++ b/app/ServiceProvider/PluginProvider.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Plugin\Loader;
+
+/**
+ * Plugin Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class PluginProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['pluginLoader'] = new Loader($container);
+ $container['pluginLoader']->scan();
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
new file mode 100644
index 00000000..d551f25d
--- /dev/null
+++ b/app/ServiceProvider/RouteProvider.php
@@ -0,0 +1,201 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Http\Route;
+use Kanboard\Core\Http\Router;
+
+/**
+ * Route Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class RouteProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['router'] = new Router($container);
+ $container['route'] = new Route($container);
+
+ if (ENABLE_URL_REWRITE) {
+ $container['route']->enable();
+
+ // Dashboard
+ $container['route']->addRoute('dashboard', 'app', 'index');
+ $container['route']->addRoute('dashboard/:user_id', 'app', 'index');
+ $container['route']->addRoute('dashboard/:user_id/projects', 'app', 'projects');
+ $container['route']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks');
+ $container['route']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks');
+ $container['route']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar');
+ $container['route']->addRoute('dashboard/:user_id/activity', 'app', 'activity');
+ $container['route']->addRoute('dashboard/:user_id/notifications', 'app', 'notifications');
+
+ // Search routes
+ $container['route']->addRoute('search', 'search', 'index');
+ $container['route']->addRoute('search/:search', 'search', 'index');
+
+ // ProjectCreation routes
+ $container['route']->addRoute('project/create', 'ProjectCreation', 'create');
+ $container['route']->addRoute('project/create/private', 'ProjectCreation', 'createPrivate');
+
+ // Project routes
+ $container['route']->addRoute('projects', 'project', 'index');
+ $container['route']->addRoute('project/:project_id', 'project', 'show');
+ $container['route']->addRoute('p/:project_id', 'project', 'show');
+ $container['route']->addRoute('project/:project_id/customer-filters', 'customfilter', 'index');
+ $container['route']->addRoute('project/:project_id/share', 'project', 'share');
+ $container['route']->addRoute('project/:project_id/notifications', 'project', 'notifications');
+ $container['route']->addRoute('project/:project_id/integrations', 'project', 'integrations');
+ $container['route']->addRoute('project/:project_id/duplicate', 'project', 'duplicate');
+ $container['route']->addRoute('project/:project_id/remove', 'project', 'remove');
+ $container['route']->addRoute('project/:project_id/disable', 'project', 'disable');
+ $container['route']->addRoute('project/:project_id/enable', 'project', 'enable');
+ $container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index');
+ $container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1');
+
+ // Project Overview
+ $container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show');
+
+ // ProjectEdit routes
+ $container['route']->addRoute('project/:project_id/edit', 'ProjectEdit', 'edit');
+ $container['route']->addRoute('project/:project_id/edit/dates', 'ProjectEdit', 'dates');
+ $container['route']->addRoute('project/:project_id/edit/description', 'ProjectEdit', 'description');
+ $container['route']->addRoute('project/:project_id/edit/priority', 'ProjectEdit', 'priority');
+
+ // ProjectUser routes
+ $container['route']->addRoute('projects/managers/:user_id', 'projectuser', 'managers');
+ $container['route']->addRoute('projects/members/:user_id', 'projectuser', 'members');
+ $container['route']->addRoute('projects/tasks/:user_id/opens', 'projectuser', 'opens');
+ $container['route']->addRoute('projects/tasks/:user_id/closed', 'projectuser', 'closed');
+ $container['route']->addRoute('projects/managers', 'projectuser', 'managers');
+
+ // Action routes
+ $container['route']->addRoute('project/:project_id/actions', 'action', 'index');
+
+ // Column routes
+ $container['route']->addRoute('project/:project_id/columns', 'column', 'index');
+
+ // Swimlane routes
+ $container['route']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index');
+
+ // Category routes
+ $container['route']->addRoute('project/:project_id/categories', 'category', 'index');
+
+ // Task routes
+ $container['route']->addRoute('project/:project_id/task/:task_id', 'task', 'show');
+ $container['route']->addRoute('t/:task_id', 'task', 'show');
+ $container['route']->addRoute('public/task/:task_id/:token', 'task', 'readonly');
+
+ $container['route']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task');
+ $container['route']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions');
+ $container['route']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics');
+ $container['route']->addRoute('project/:project_id/task/:task_id/subtasks', 'subtask', 'show');
+ $container['route']->addRoute('project/:project_id/task/:task_id/time-tracking', 'task', 'timetracking');
+ $container['route']->addRoute('project/:project_id/task/:task_id/internal/links', 'tasklink', 'show');
+ $container['route']->addRoute('project/:project_id/task/:task_id/external/links', 'TaskExternalLink', 'show');
+
+ // Exports
+ $container['route']->addRoute('export/tasks/:project_id', 'export', 'tasks');
+ $container['route']->addRoute('export/subtasks/:project_id', 'export', 'subtasks');
+ $container['route']->addRoute('export/transitions/:project_id', 'export', 'transitions');
+ $container['route']->addRoute('export/summary/:project_id', 'export', 'summary');
+
+ // Analytics routes
+ $container['route']->addRoute('analytics/tasks/:project_id', 'analytic', 'tasks');
+ $container['route']->addRoute('analytics/users/:project_id', 'analytic', 'users');
+ $container['route']->addRoute('analytics/cfd/:project_id', 'analytic', 'cfd');
+ $container['route']->addRoute('analytics/burndown/:project_id', 'analytic', 'burndown');
+ $container['route']->addRoute('analytics/average-time-column/:project_id', 'analytic', 'averageTimeByColumn');
+ $container['route']->addRoute('analytics/lead-cycle-time/:project_id', 'analytic', 'leadAndCycleTime');
+ $container['route']->addRoute('analytics/estimated-spent-time/:project_id', 'analytic', 'compareHours');
+
+ // Board routes
+ $container['route']->addRoute('board/:project_id', 'board', 'show');
+ $container['route']->addRoute('b/:project_id', 'board', 'show');
+ $container['route']->addRoute('public/board/:token', 'board', 'readonly');
+
+ // Calendar routes
+ $container['route']->addRoute('calendar/:project_id', 'calendar', 'show');
+ $container['route']->addRoute('c/:project_id', 'calendar', 'show');
+
+ // Listing routes
+ $container['route']->addRoute('list/:project_id', 'listing', 'show');
+ $container['route']->addRoute('l/:project_id', 'listing', 'show');
+
+ // Gantt routes
+ $container['route']->addRoute('gantt/:project_id', 'gantt', 'project');
+ $container['route']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project');
+
+ // Feed routes
+ $container['route']->addRoute('feed/project/:token', 'feed', 'project');
+ $container['route']->addRoute('feed/user/:token', 'feed', 'user');
+
+ // Ical routes
+ $container['route']->addRoute('ical/project/:token', 'ical', 'project');
+ $container['route']->addRoute('ical/user/:token', 'ical', 'user');
+
+ // Users
+ $container['route']->addRoute('users', 'user', 'index');
+ $container['route']->addRoute('user/profile/:user_id', 'user', 'profile');
+ $container['route']->addRoute('user/show/:user_id', 'user', 'show');
+ $container['route']->addRoute('user/show/:user_id/timesheet', 'user', 'timesheet');
+ $container['route']->addRoute('user/show/:user_id/last-logins', 'user', 'last');
+ $container['route']->addRoute('user/show/:user_id/sessions', 'user', 'sessions');
+ $container['route']->addRoute('user/:user_id/edit', 'user', 'edit');
+ $container['route']->addRoute('user/:user_id/password', 'user', 'password');
+ $container['route']->addRoute('user/:user_id/share', 'user', 'share');
+ $container['route']->addRoute('user/:user_id/notifications', 'user', 'notifications');
+ $container['route']->addRoute('user/:user_id/accounts', 'user', 'external');
+ $container['route']->addRoute('user/:user_id/integrations', 'user', 'integrations');
+ $container['route']->addRoute('user/:user_id/authentication', 'user', 'authentication');
+ $container['route']->addRoute('user/:user_id/2fa', 'twofactor', 'index');
+
+ // Groups
+ $container['route']->addRoute('groups', 'group', 'index');
+ $container['route']->addRoute('groups/create', 'group', 'create');
+ $container['route']->addRoute('group/:group_id/associate', 'group', 'associate');
+ $container['route']->addRoute('group/:group_id/dissociate/:user_id', 'group', 'dissociate');
+ $container['route']->addRoute('group/:group_id/edit', 'group', 'edit');
+ $container['route']->addRoute('group/:group_id/members', 'group', 'users');
+ $container['route']->addRoute('group/:group_id/remove', 'group', 'confirm');
+
+ // Config
+ $container['route']->addRoute('settings', 'config', 'index');
+ $container['route']->addRoute('settings/plugins', 'config', 'plugins');
+ $container['route']->addRoute('settings/application', 'config', 'application');
+ $container['route']->addRoute('settings/project', 'config', 'project');
+ $container['route']->addRoute('settings/project', 'config', 'project');
+ $container['route']->addRoute('settings/board', 'config', 'board');
+ $container['route']->addRoute('settings/calendar', 'config', 'calendar');
+ $container['route']->addRoute('settings/integrations', 'config', 'integrations');
+ $container['route']->addRoute('settings/webhook', 'config', 'webhook');
+ $container['route']->addRoute('settings/api', 'config', 'api');
+ $container['route']->addRoute('settings/links', 'link', 'index');
+ $container['route']->addRoute('settings/currencies', 'currency', 'index');
+
+ // Doc
+ $container['route']->addRoute('documentation/:file', 'doc', 'show');
+ $container['route']->addRoute('documentation', 'doc', 'show');
+
+ // Auth routes
+ $container['route']->addRoute('login', 'auth', 'login');
+ $container['route']->addRoute('logout', 'auth', 'logout');
+
+ // PasswordReset
+ $container['route']->addRoute('forgot-password', 'PasswordReset', 'create');
+ $container['route']->addRoute('forgot-password/change/:token', 'PasswordReset', 'change');
+ }
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/SessionProvider.php b/app/ServiceProvider/SessionProvider.php
new file mode 100644
index 00000000..0999d531
--- /dev/null
+++ b/app/ServiceProvider/SessionProvider.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Session\SessionManager;
+use Kanboard\Core\Session\SessionStorage;
+use Kanboard\Core\Session\FlashMessage;
+
+/**
+ * Session Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class SessionProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['sessionStorage'] = function() {
+ return new SessionStorage;
+ };
+
+ $container['sessionManager'] = function($c) {
+ return new SessionManager($c);
+ };
+
+ $container['flash'] = function($c) {
+ return new FlashMessage($c);
+ };
+
+ return $container;
+ }
+}
diff --git a/app/Subscriber/AuthSubscriber.php b/app/Subscriber/AuthSubscriber.php
index 2461b52c..e839385f 100644
--- a/app/Subscriber/AuthSubscriber.php
+++ b/app/Subscriber/AuthSubscriber.php
@@ -2,26 +2,105 @@
namespace Kanboard\Subscriber;
-use Kanboard\Core\Request;
-use Kanboard\Event\AuthEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Kanboard\Core\Security\AuthenticationManager;
+use Kanboard\Core\Session\SessionManager;
+use Kanboard\Event\AuthSuccessEvent;
+use Kanboard\Event\AuthFailureEvent;
-class AuthSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+/**
+ * Authentication Subscriber
+ *
+ * @package subscriber
+ * @author Frederic Guillot
+ */
+class AuthSubscriber extends BaseSubscriber implements EventSubscriberInterface
{
+ /**
+ * Get event listeners
+ *
+ * @static
+ * @access public
+ * @return array
+ */
public static function getSubscribedEvents()
{
return array(
- 'auth.success' => array('onSuccess', 0),
+ AuthenticationManager::EVENT_SUCCESS => 'afterLogin',
+ AuthenticationManager::EVENT_FAILURE => 'onLoginFailure',
+ SessionManager::EVENT_DESTROY => 'afterLogout',
);
}
- public function onSuccess(AuthEvent $event)
+ /**
+ * After Login callback
+ *
+ * @access public
+ * @param AuthSuccessEvent $event
+ */
+ public function afterLogin(AuthSuccessEvent $event)
{
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
+
+ $userAgent = $this->request->getUserAgent();
+ $ipAddress = $this->request->getIpAddress();
+
+ $this->userLocking->resetFailedLogin($this->userSession->getUsername());
+
$this->lastLogin->create(
$event->getAuthType(),
- $event->getUserId(),
- Request::getIpAddress(),
- Request::getUserAgent()
+ $this->userSession->getId(),
+ $ipAddress,
+ $userAgent
);
+
+ if ($event->getAuthType() === 'RememberMe') {
+ $this->userSession->validatePostAuthentication();
+ }
+
+ if (isset($this->sessionStorage->hasRememberMe) && $this->sessionStorage->hasRememberMe) {
+ $session = $this->rememberMeSession->create($this->userSession->getId(), $ipAddress, $userAgent);
+ $this->rememberMeCookie->write($session['token'], $session['sequence'], $session['expiration']);
+ }
+ }
+
+ /**
+ * Destroy RememberMe session on logout
+ *
+ * @access public
+ */
+ public function afterLogout()
+ {
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
+ $credentials = $this->rememberMeCookie->read();
+
+ if ($credentials !== false) {
+ $session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']);
+
+ if (! empty($session)) {
+ $this->rememberMeSession->remove($session['id']);
+ }
+
+ $this->rememberMeCookie->remove();
+ }
+ }
+
+ /**
+ * Increment failed login counter
+ *
+ * @access public
+ */
+ public function onLoginFailure(AuthFailureEvent $event)
+ {
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
+ $username = $event->getUsername();
+
+ if (! empty($username)) {
+ $this->userLocking->incrementFailedLogin($username);
+
+ if ($this->userLocking->getFailedLogin($username) > BRUTEFORCE_LOCKDOWN) {
+ $this->userLocking->lock($username, BRUTEFORCE_LOCKDOWN_DURATION);
+ }
+ }
}
}
diff --git a/app/Subscriber/BaseSubscriber.php b/app/Subscriber/BaseSubscriber.php
new file mode 100644
index 00000000..2e41da76
--- /dev/null
+++ b/app/Subscriber/BaseSubscriber.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Subscriber;
+
+use Kanboard\Core\Base;
+
+/**
+ * Base class for subscribers
+ *
+ * @package subscriber
+ * @author Frederic Guillot
+ */
+class BaseSubscriber extends Base
+{
+ /**
+ * Method called
+ *
+ * @access private
+ * @var array
+ */
+ private $called = array();
+
+ /**
+ * Check if a listener has been executed
+ *
+ * @access public
+ * @param string $key
+ * @return boolean
+ */
+ public function isExecuted($key = '')
+ {
+ if (isset($this->called[$key])) {
+ return true;
+ }
+
+ $this->called[$key] = true;
+
+ return false;
+ }
+}
diff --git a/app/Subscriber/BootstrapSubscriber.php b/app/Subscriber/BootstrapSubscriber.php
index 25b919f7..ef0215f3 100644
--- a/app/Subscriber/BootstrapSubscriber.php
+++ b/app/Subscriber/BootstrapSubscriber.php
@@ -4,20 +4,39 @@ namespace Kanboard\Subscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+class BootstrapSubscriber extends BaseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
- 'session.bootstrap' => array('setup', 0),
- 'api.bootstrap' => array('setup', 0),
- 'console.bootstrap' => array('setup', 0),
+ 'app.bootstrap' => 'execute',
);
}
- public function setup()
+ public function execute()
{
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
$this->config->setupTranslations();
$this->config->setupTimezone();
+ $this->actionManager->attachEvents();
+
+ if ($this->userSession->isLogged()) {
+ $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
+ }
+ }
+
+ public function __destruct()
+ {
+ if (DEBUG) {
+ foreach ($this->db->getLogMessages() as $message) {
+ $this->logger->debug($message);
+ }
+
+ $this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries));
+ $this->logger->debug('RENDERING={time}', array('time' => microtime(true) - $this->request->getStartTime()));
+ $this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage()));
+ $this->logger->debug('URI='.$this->request->getUri());
+ $this->logger->debug('###############################################');
+ }
}
}
diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php
index 394573e4..651b8a96 100644
--- a/app/Subscriber/NotificationSubscriber.php
+++ b/app/Subscriber/NotificationSubscriber.php
@@ -6,37 +6,46 @@ use Kanboard\Event\GenericEvent;
use Kanboard\Model\Task;
use Kanboard\Model\Comment;
use Kanboard\Model\Subtask;
-use Kanboard\Model\File;
+use Kanboard\Model\TaskFile;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-class NotificationSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+class NotificationSubscriber extends BaseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
- Task::EVENT_CREATE => array('execute', 0),
- Task::EVENT_UPDATE => array('execute', 0),
- Task::EVENT_CLOSE => array('execute', 0),
- Task::EVENT_OPEN => array('execute', 0),
- Task::EVENT_MOVE_COLUMN => array('execute', 0),
- Task::EVENT_MOVE_POSITION => array('execute', 0),
- Task::EVENT_MOVE_SWIMLANE => array('execute', 0),
- Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0),
- Subtask::EVENT_CREATE => array('execute', 0),
- Subtask::EVENT_UPDATE => array('execute', 0),
- Comment::EVENT_CREATE => array('execute', 0),
- Comment::EVENT_UPDATE => array('execute', 0),
- File::EVENT_CREATE => array('execute', 0),
+ Task::EVENT_USER_MENTION => 'handleEvent',
+ Task::EVENT_CREATE => 'handleEvent',
+ Task::EVENT_UPDATE => 'handleEvent',
+ Task::EVENT_CLOSE => 'handleEvent',
+ Task::EVENT_OPEN => 'handleEvent',
+ Task::EVENT_MOVE_COLUMN => 'handleEvent',
+ Task::EVENT_MOVE_POSITION => 'handleEvent',
+ Task::EVENT_MOVE_SWIMLANE => 'handleEvent',
+ Task::EVENT_ASSIGNEE_CHANGE => 'handleEvent',
+ Subtask::EVENT_CREATE => 'handleEvent',
+ Subtask::EVENT_UPDATE => 'handleEvent',
+ Comment::EVENT_CREATE => 'handleEvent',
+ Comment::EVENT_UPDATE => 'handleEvent',
+ Comment::EVENT_USER_MENTION => 'handleEvent',
+ TaskFile::EVENT_CREATE => 'handleEvent',
);
}
- public function execute(GenericEvent $event, $event_name)
+ public function handleEvent(GenericEvent $event, $event_name)
{
- $event_data = $this->getEventData($event);
+ if (! $this->isExecuted($event_name)) {
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
+ $event_data = $this->getEventData($event);
- if (! empty($event_data)) {
- $this->userNotification->sendNotifications($event_name, $event_data);
- $this->projectNotification->sendNotifications($event_data['task']['project_id'], $event_name, $event_data);
+ if (! empty($event_data)) {
+ if (! empty($event['mention'])) {
+ $this->userNotification->sendUserNotification($event['mention'], $event_name, $event_data);
+ } else {
+ $this->userNotification->sendNotifications($event_name, $event_data);
+ $this->projectNotification->sendNotifications($event_data['task']['project_id'], $event_name, $event_data);
+ }
+ }
}
}
diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php
index bfa6cd42..44138f43 100644
--- a/app/Subscriber/ProjectDailySummarySubscriber.php
+++ b/app/Subscriber/ProjectDailySummarySubscriber.php
@@ -6,22 +6,23 @@ use Kanboard\Event\TaskEvent;
use Kanboard\Model\Task;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-class ProjectDailySummarySubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
- Task::EVENT_CREATE => array('execute', 0),
- Task::EVENT_UPDATE => array('execute', 0),
- Task::EVENT_CLOSE => array('execute', 0),
- Task::EVENT_OPEN => array('execute', 0),
- Task::EVENT_MOVE_COLUMN => array('execute', 0),
+ Task::EVENT_CREATE_UPDATE => 'execute',
+ Task::EVENT_CLOSE => 'execute',
+ Task::EVENT_OPEN => 'execute',
+ Task::EVENT_MOVE_COLUMN => 'execute',
+ Task::EVENT_MOVE_SWIMLANE => 'execute',
);
}
public function execute(TaskEvent $event)
{
- if (isset($event['project_id'])) {
+ if (isset($event['project_id']) && !$this->isExecuted()) {
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
$this->projectDailyColumnStats->updateTotals($event['project_id'], date('Y-m-d'));
$this->projectDailyStats->updateTotals($event['project_id'], date('Y-m-d'));
}
diff --git a/app/Subscriber/ProjectModificationDateSubscriber.php b/app/Subscriber/ProjectModificationDateSubscriber.php
index 1f99840d..62804a84 100644
--- a/app/Subscriber/ProjectModificationDateSubscriber.php
+++ b/app/Subscriber/ProjectModificationDateSubscriber.php
@@ -6,25 +6,26 @@ use Kanboard\Event\GenericEvent;
use Kanboard\Model\Task;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-class ProjectModificationDateSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+class ProjectModificationDateSubscriber extends BaseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
- Task::EVENT_CREATE_UPDATE => array('execute', 0),
- Task::EVENT_CLOSE => array('execute', 0),
- Task::EVENT_OPEN => array('execute', 0),
- Task::EVENT_MOVE_SWIMLANE => array('execute', 0),
- Task::EVENT_MOVE_COLUMN => array('execute', 0),
- Task::EVENT_MOVE_POSITION => array('execute', 0),
- Task::EVENT_MOVE_PROJECT => array('execute', 0),
- Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0),
+ Task::EVENT_CREATE_UPDATE => 'execute',
+ Task::EVENT_CLOSE => 'execute',
+ Task::EVENT_OPEN => 'execute',
+ Task::EVENT_MOVE_SWIMLANE => 'execute',
+ Task::EVENT_MOVE_COLUMN => 'execute',
+ Task::EVENT_MOVE_POSITION => 'execute',
+ Task::EVENT_MOVE_PROJECT => 'execute',
+ Task::EVENT_ASSIGNEE_CHANGE => 'execute',
);
}
public function execute(GenericEvent $event)
{
- if (isset($event['project_id'])) {
+ if (isset($event['project_id']) && !$this->isExecuted()) {
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
$this->project->updateModificationDate($event['project_id']);
}
}
diff --git a/app/Subscriber/RecurringTaskSubscriber.php b/app/Subscriber/RecurringTaskSubscriber.php
index b03ffcf8..09a5665a 100644
--- a/app/Subscriber/RecurringTaskSubscriber.php
+++ b/app/Subscriber/RecurringTaskSubscriber.php
@@ -6,22 +6,24 @@ use Kanboard\Event\TaskEvent;
use Kanboard\Model\Task;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-class RecurringTaskSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
- Task::EVENT_MOVE_COLUMN => array('onMove', 0),
- Task::EVENT_CLOSE => array('onClose', 0),
+ Task::EVENT_MOVE_COLUMN => 'onMove',
+ Task::EVENT_CLOSE => 'onClose',
);
}
public function onMove(TaskEvent $event)
{
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
+
if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) {
- if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) {
+ if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->column->getFirstColumnId($event['project_id']) == $event['src_column_id']) {
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
- } elseif ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->board->getLastColumn($event['project_id']) == $event['dst_column_id']) {
+ } elseif ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->column->getLastColumnId($event['project_id']) == $event['dst_column_id']) {
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
}
}
@@ -29,6 +31,8 @@ class RecurringTaskSubscriber extends \Kanboard\Core\Base implements EventSubscr
public function onClose(TaskEvent $event)
{
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
+
if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == Task::RECURRING_TRIGGER_CLOSE) {
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
}
diff --git a/app/Subscriber/SubtaskTimeTrackingSubscriber.php b/app/Subscriber/SubtaskTimeTrackingSubscriber.php
index b5e0354d..c0852bc8 100644
--- a/app/Subscriber/SubtaskTimeTrackingSubscriber.php
+++ b/app/Subscriber/SubtaskTimeTrackingSubscriber.php
@@ -6,13 +6,13 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Kanboard\Model\Subtask;
use Kanboard\Event\SubtaskEvent;
-class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+class SubtaskTimeTrackingSubscriber extends BaseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
- Subtask::EVENT_CREATE => array('updateTaskTime', 0),
- Subtask::EVENT_DELETE => array('updateTaskTime', 0),
+ Subtask::EVENT_CREATE => 'updateTaskTime',
+ Subtask::EVENT_DELETE => 'updateTaskTime',
Subtask::EVENT_UPDATE => array(
array('logStartEnd', 10),
array('updateTaskTime', 0),
@@ -23,6 +23,7 @@ class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements Event
public function updateTaskTime(SubtaskEvent $event)
{
if (isset($event['task_id'])) {
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
$this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']);
}
}
@@ -30,6 +31,7 @@ class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements Event
public function logStartEnd(SubtaskEvent $event)
{
if (isset($event['status']) && $this->config->get('subtask_time_tracking') == 1) {
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
$subtask = $this->subtask->getById($event['id']);
if (empty($subtask['user_id'])) {
diff --git a/app/Subscriber/TaskMovedDateSubscriber.php b/app/Subscriber/TaskMovedDateSubscriber.php
deleted file mode 100644
index 9857f4b3..00000000
--- a/app/Subscriber/TaskMovedDateSubscriber.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace Kanboard\Subscriber;
-
-use Kanboard\Event\TaskEvent;
-use Kanboard\Model\Task;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-
-class TaskMovedDateSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
-{
- public static function getSubscribedEvents()
- {
- return array(
- Task::EVENT_MOVE_COLUMN => array('execute', 0),
- Task::EVENT_MOVE_SWIMLANE => array('execute', 0),
- );
- }
-
- public function execute(TaskEvent $event)
- {
- if (isset($event['task_id'])) {
- $this->container['db']->table(Task::TABLE)->eq('id', $event['task_id'])->update(array('date_moved' => time()));
- }
- }
-}
diff --git a/app/Subscriber/TransitionSubscriber.php b/app/Subscriber/TransitionSubscriber.php
index 35352dc7..bd537484 100644
--- a/app/Subscriber/TransitionSubscriber.php
+++ b/app/Subscriber/TransitionSubscriber.php
@@ -6,17 +6,19 @@ use Kanboard\Event\TaskEvent;
use Kanboard\Model\Task;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-class TransitionSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+class TransitionSubscriber extends BaseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
- Task::EVENT_MOVE_COLUMN => array('execute', 0),
+ Task::EVENT_MOVE_COLUMN => 'execute',
);
}
public function execute(TaskEvent $event)
{
+ $this->logger->debug('Subscriber executed: '.__METHOD__);
+
$user_id = $this->userSession->getId();
if (! empty($user_id)) {
diff --git a/app/Template/action/event.php b/app/Template/action/event.php
index 7f968a97..b4741a98 100644
--- a/app/Template/action/event.php
+++ b/app/Template/action/event.php
@@ -11,7 +11,7 @@
<?= $this->form->hidden('action_name', $values) ?>
<?= $this->form->label(t('Event'), 'event_name') ?>
- <?= $this->form->select('event_name', $events, $values) ?><br/>
+ <?= $this->form->select('event_name', $events, $values) ?>
<div class="form-help">
<?= t('When the selected event occurs execute the corresponding action.') ?>
diff --git a/app/Template/action/index.php b/app/Template/action/index.php
index bf2f7475..66cfed77 100644
--- a/app/Template/action/index.php
+++ b/app/Template/action/index.php
@@ -28,24 +28,24 @@
</td>
<td>
<ul>
- <?php foreach ($action['params'] as $param): ?>
+ <?php foreach ($action['params'] as $param_name => $param_value): ?>
<li>
- <?= $this->text->in($param['name'], $available_params) ?> =
+ <?= $this->text->in($param_name, $available_params[$action['action_name']]) ?> =
<strong>
- <?php if ($this->text->contains($param['name'], 'column_id')): ?>
- <?= $this->text->in($param['value'], $columns_list) ?>
- <?php elseif ($this->text->contains($param['name'], 'user_id')): ?>
- <?= $this->text->in($param['value'], $users_list) ?>
- <?php elseif ($this->text->contains($param['name'], 'project_id')): ?>
- <?= $this->text->in($param['value'], $projects_list) ?>
- <?php elseif ($this->text->contains($param['name'], 'color_id')): ?>
- <?= $this->text->in($param['value'], $colors_list) ?>
- <?php elseif ($this->text->contains($param['name'], 'category_id')): ?>
- <?= $this->text->in($param['value'], $categories_list) ?>
- <?php elseif ($this->text->contains($param['name'], 'link_id')): ?>
- <?= $this->text->in($param['value'], $links_list) ?>
+ <?php if ($this->text->contains($param_name, 'column_id')): ?>
+ <?= $this->text->in($param_value, $columns_list) ?>
+ <?php elseif ($this->text->contains($param_name, 'user_id')): ?>
+ <?= $this->text->in($param_value, $users_list) ?>
+ <?php elseif ($this->text->contains($param_name, 'project_id')): ?>
+ <?= $this->text->in($param_value, $projects_list) ?>
+ <?php elseif ($this->text->contains($param_name, 'color_id')): ?>
+ <?= $this->text->in($param_value, $colors_list) ?>
+ <?php elseif ($this->text->contains($param_name, 'category_id')): ?>
+ <?= $this->text->in($param_value, $categories_list) ?>
+ <?php elseif ($this->text->contains($param_name, 'link_id')): ?>
+ <?= $this->text->in($param_value, $links_list) ?>
<?php else: ?>
- <?= $this->e($param['value']) ?>
+ <?= $this->e($param_value) ?>
<?php endif ?>
</strong>
</li>
@@ -53,7 +53,7 @@
</ul>
</td>
<td>
- <?= $this->url->link(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id'])) ?>
+ <?= $this->url->link(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id']), false, 'popover') ?>
</td>
</tr>
<?php endforeach ?>
@@ -67,9 +67,9 @@
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Action'), 'action_name') ?>
- <?= $this->form->select('action_name', $available_actions, $values) ?><br/>
+ <?= $this->form->select('action_name', $available_actions, $values) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue">
</div>
</form> \ No newline at end of file
diff --git a/app/Template/action/params.php b/app/Template/action/params.php
index dcfaa9cc..a2350dea 100644
--- a/app/Template/action/params.php
+++ b/app/Template/action/params.php
@@ -15,22 +15,25 @@
<?php if ($this->text->contains($param_name, 'column_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'user_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'project_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'color_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'category_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'link_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'duration')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->number('params['.$param_name.']', $values) ?>
<?php else: ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->text('params['.$param_name.']', $values) ?>
diff --git a/app/Template/action/remove.php b/app/Template/action/remove.php
index c8d4dfe4..070a7918 100644
--- a/app/Template/action/remove.php
+++ b/app/Template/action/remove.php
@@ -10,6 +10,6 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'action', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/activity/project.php b/app/Template/activity/project.php
index bc585212..ba6d6629 100644
--- a/app/Template/activity/project.php
+++ b/app/Template/activity/project.php
@@ -19,7 +19,7 @@
<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->isProjectManagementAllowed($project['id'])): ?>
+ <?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'])) ?>
diff --git a/app/Template/analytic/compare_hours.php b/app/Template/analytic/compare_hours.php
new file mode 100644
index 00000000..bb145d61
--- /dev/null
+++ b/app/Template/analytic/compare_hours.php
@@ -0,0 +1,62 @@
+<div class="page-header">
+ <h2><?= t('Compare Estimated Time vs Actual Time') ?></h2>
+</div>
+
+<div class="listing">
+ <ul>
+ <li><?= t('Estimated hours: ').'<strong>'.$this->e($metrics['open']['time_estimated'] + $metrics['closed']['time_estimated']) ?></strong></li>
+ <li><?= t('Actual hours: ').'<strong>'.$this->e($metrics['open']['time_spent'] + $metrics['closed']['time_spent']) ?></strong></li>
+ </ul>
+</div>
+
+<?php if (empty($metrics)): ?>
+ <p class="alert"><?= t('Not enough data to show the graph.') ?></p>
+<?php else: ?>
+<section id="analytic-compare-hours">
+ <div id="chart"
+ data-metrics='<?= json_encode($metrics, JSON_HEX_APOS)?>'
+ data-label-spent="<?= t('Hours Spent') ?>"
+ data-label-estimated="<?= t('Hours Estimated') ?>"
+ data-label-closed="<?= t('Closed') ?>"
+ data-label-open="<?= t('Open') ?>"></div>
+
+ <?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('No tasks found.') ?></p>
+ <?php elseif (! $paginator->isEmpty()): ?>
+ <table class="table-fixed table-small">
+ <tr>
+ <th class="column-5"><?= $paginator->order(t('Id'), 'tasks.id') ?></th>
+ <th><?= $paginator->order(t('Title'), 'tasks.title') ?></th>
+ <th class="column-5"><?= $paginator->order(t('Status'), 'tasks.is_active') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Estimated Time'), 'tasks.time_estimated') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Actual Time'), 'tasks.time_spent') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $task): ?>
+ <tr>
+ <td class="task-table color-<?= $task['color_id'] ?>">
+ <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
+ </td>
+ <td>
+ <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
+ </td>
+ <td>
+ <?php if ($task['is_active'] == \Kanboard\Model\Task::STATUS_OPEN): ?>
+ <?= t('Open') ?>
+ <?php else: ?>
+ <?= t('Closed') ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?= $this->e($task['time_estimated']) ?>
+ </td>
+ <td>
+ <?= $this->e($task['time_spent']) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+ <?php endif ?>
+</section>
+<?php endif ?>
diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php
index fd2090ae..f1dba552 100644
--- a/app/Template/analytic/layout.php
+++ b/app/Template/analytic/layout.php
@@ -19,7 +19,7 @@
<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->isProjectManagementAllowed($project['id'])): ?>
+ <?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'])) ?>
@@ -31,9 +31,8 @@
</li>
</ul>
</div>
- <section class="sidebar-container" id="analytic-section">
-
- <?= $this->render('analytic/sidebar', array('project' => $project)) ?>
+ <section class="sidebar-container">
+ <?= $this->render($sidebar_template, array('project' => $project)) ?>
<div class="sidebar-content">
<?= $content_for_sublayout ?>
diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php
index c942f7ed..76289b9f 100644
--- a/app/Template/analytic/sidebar.php
+++ b/app/Template/analytic/sidebar.php
@@ -1,25 +1,29 @@
<div class="sidebar">
<h2><?= t('Reportings') ?></h2>
<ul>
- <li <?= $this->app->getRouterAction() === 'tasks' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('analytic', 'tasks') ?>>
<?= $this->url->link(t('Task distribution'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'users' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('analytic', 'users') ?>>
<?= $this->url->link(t('User repartition'), 'analytic', 'users', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'cfd' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('analytic', 'cfd') ?>>
<?= $this->url->link(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'burndown' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('analytic', 'burndown') ?>>
<?= $this->url->link(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'averagetimebycolumn' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('analytic', 'averageTimeByColumn') ?>>
<?= $this->url->link(t('Average time into each column'), 'analytic', 'averageTimeByColumn', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'leadandcycletime' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('analytic', 'leadAndCycleTime') ?>>
<?= $this->url->link(t('Lead and cycle time'), 'analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?>
</li>
+ <li <?= $this->app->checkMenuSelection('analytic', 'compareHours') ?>>
+ <?= $this->url->link(t('Estimated vs actual time'), 'analytic', 'compareHours', array('project_id' => $project['id'])) ?>
+ </li>
+
+ <?= $this->hook->render('template:analytic:sidebar', array('project' => $project)) ?>
+
</ul>
- <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
- <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
-</div> \ No newline at end of file
+</div>
diff --git a/app/Template/app/filters_helper.php b/app/Template/app/filters_helper.php
index 71b57a8c..c16c2251 100644
--- a/app/Template/app/filters_helper.php
+++ b/app/Template/app/filters_helper.php
@@ -1,5 +1,6 @@
-<div class="dropdown filters">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Filters') ?></a>
+<?= $this->hook->render('template:app:filters-helper:before', isset($project) ? array('project' => $project) : array()) ?>
+<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="<?= isset($reset) ? $reset : '' ?>" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open assignee:me"><?= t('My tasks') ?></a></li>
@@ -15,4 +16,5 @@
<?= $this->url->doc(t('View advanced search syntax'), 'search') ?>
</li>
</ul>
-</div> \ No newline at end of file
+</div>
+<?= $this->hook->render('template:app:filters-helper:after', isset($project) ? array('project' => $project) : array()) ?> \ No newline at end of file
diff --git a/app/Template/app/layout.php b/app/Template/app/layout.php
index 4f82121e..200cb0d7 100644
--- a/app/Template/app/layout.php
+++ b/app/Template/app/layout.php
@@ -1,16 +1,18 @@
<section id="main">
<div class="page-header page-header-mobile">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('ProjectCreation', 'create')): ?>
<li>
<i class="fa fa-plus fa-fw"></i>
- <?= $this->url->link(t('New project'), 'project', 'create') ?>
+ <?= $this->url->link(t('New project'), 'ProjectCreation', 'create', array(), false, 'popover') ?>
</li>
<?php endif ?>
+ <?php if ($this->app->config('disable_private_project', 0) == 0): ?>
<li>
<i class="fa fa-lock fa-fw"></i>
- <?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?>
+ <?= $this->url->link(t('New private project'), 'ProjectCreation', 'createPrivate', array(), false, 'popover') ?>
</li>
+ <?php endif ?>
<li>
<i class="fa fa-search fa-fw"></i>
<?= $this->url->link(t('Search'), 'search', 'index') ?>
@@ -19,20 +21,10 @@
<i class="fa fa-folder fa-fw"></i>
<?= $this->url->link(t('Project management'), 'project', 'index') ?>
</li>
- <?php if ($this->user->isAdmin()): ?>
- <li>
- <i class="fa fa-user fa-fw"></i>
- <?= $this->url->link(t('User management'), 'user', 'index') ?>
- </li>
- <li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Settings'), 'config', 'index') ?>
- </li>
- <?php endif ?>
</ul>
</div>
<section class="sidebar-container" id="dashboard">
- <?= $this->render('app/sidebar', array('user' => $user)) ?>
+ <?= $this->render($sidebar_template, array('user' => $user)) ?>
<div class="sidebar-content">
<?= $content_for_sublayout ?>
</div>
diff --git a/app/Template/app/notifications.php b/app/Template/app/notifications.php
index 511f377b..4cb3c571 100644
--- a/app/Template/app/notifications.php
+++ b/app/Template/app/notifications.php
@@ -49,7 +49,7 @@
<?php endif ?>
</td>
<td>
- <?= dt('%B %e, %Y at %k:%M %p', $notification['date_creation']) ?>
+ <?= $this->dt->datetime($notification['date_creation']) ?>
</td>
<td>
<i class="fa fa-check fa-fw"></i>
diff --git a/app/Template/app/overview.php b/app/Template/app/overview.php
index 1b160496..0b354791 100644
--- a/app/Template/app/overview.php
+++ b/app/Template/app/overview.php
@@ -1,13 +1,12 @@
-<div class="search">
+<div class="filter-box">
<form method="get" action="<?= $this->url->dir() ?>" class="search">
<?= $this->form->hidden('controller', array('controller' => 'search')) ?>
<?= $this->form->hidden('action', array('action' => 'index')) ?>
<?= $this->form->text('search', array(), array(), array('placeholder="'.t('Search').'"'), 'form-input-large') ?>
+ <?= $this->render('app/filters_helper') ?>
</form>
-
- <?= $this->render('app/filters_helper') ?>
</div>
-<?= $this->render('app/projects', array('paginator' => $project_paginator)) ?>
-<?= $this->render('app/tasks', array('paginator' => $task_paginator)) ?>
-<?= $this->render('app/subtasks', array('paginator' => $subtask_paginator)) ?> \ No newline at end of file
+<?= $this->render('app/projects', array('paginator' => $project_paginator, 'user' => $user)) ?>
+<?= $this->render('app/tasks', array('paginator' => $task_paginator, 'user' => $user)) ?>
+<?= $this->render('app/subtasks', array('paginator' => $subtask_paginator, 'user' => $user)) ?> \ No newline at end of file
diff --git a/app/Template/app/projects.php b/app/Template/app/projects.php
index cf22707b..61899c96 100644
--- a/app/Template/app/projects.php
+++ b/app/Template/app/projects.php
@@ -1,5 +1,5 @@
<div class="page-header">
- <h2><?= t('My projects') ?> (<?= $paginator->getTotal() ?>)</h2>
+ <h2><?= $this->url->link(t('My projects'), 'app', 'projects', array('user_id' => $user['id'])) ?> (<?= $paginator->getTotal() ?>)</h2>
</div>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('Your are not member of any project.') ?></p>
@@ -22,7 +22,7 @@
<?php endif ?>
</td>
<td>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('gantt', 'project', $project['id'])): ?>
<?= $this->url->link('<i class="fa fa-sliders fa-fw"></i>', 'gantt', 'project', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?>
<?php endif ?>
diff --git a/app/Template/app/sidebar.php b/app/Template/app/sidebar.php
index 552a9c27..3f0d988b 100644
--- a/app/Template/app/sidebar.php
+++ b/app/Template/app/sidebar.php
@@ -1,29 +1,27 @@
<div class="sidebar">
<h2><?= $this->e($user['name'] ?: $user['username']) ?></h2>
<ul>
- <li <?= $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('app', 'index') ?>>
<?= $this->url->link(t('Overview'), 'app', 'index', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'projects' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('app', 'projects') ?>>
<?= $this->url->link(t('My projects'), 'app', 'projects', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'tasks' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('app', 'tasks') ?>>
<?= $this->url->link(t('My tasks'), 'app', 'tasks', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'subtasks' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('app', 'subtasks') ?>>
<?= $this->url->link(t('My subtasks'), 'app', 'subtasks', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'calendar' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('app', 'calendar') ?>>
<?= $this->url->link(t('My calendar'), 'app', 'calendar', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'activity' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('app', 'activity') ?>>
<?= $this->url->link(t('My activity stream'), 'app', 'activity', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'notifications' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('app', 'notifications') ?>>
<?= $this->url->link(t('My notifications'), 'app', 'notifications', array('user_id' => $user['id'])) ?>
</li>
<?= $this->hook->render('template:dashboard:sidebar') ?>
</ul>
- <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
- <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div> \ No newline at end of file
diff --git a/app/Template/app/subtasks.php b/app/Template/app/subtasks.php
index ad7402bd..6848112c 100644
--- a/app/Template/app/subtasks.php
+++ b/app/Template/app/subtasks.php
@@ -1,12 +1,12 @@
<div class="page-header">
- <h2><?= t('My subtasks') ?> (<?= $paginator->getTotal() ?>)</h2>
+ <h2><?= $this->url->link(t('My subtasks'), 'app', 'subtasks', array('user_id' => $user['id'])) ?> (<?= $paginator->getTotal() ?>)</h2>
</div>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('There is nothing assigned to you.') ?></p>
<?php else: ?>
<table class="table-fixed table-small">
<tr>
- <th class="column-10"><?= $paginator->order('Id', 'tasks.id') ?></th>
+ <th class="column-5"><?= $paginator->order('Id', 'tasks.id') ?></th>
<th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th>
<th><?= $paginator->order(t('Task'), 'task_name') ?></th>
<th><?= $paginator->order(t('Subtask'), 'title') ?></th>
@@ -15,7 +15,7 @@
<?php foreach ($paginator->getCollection() as $subtask): ?>
<tr>
<td class="task-table color-<?= $subtask['color_id'] ?>">
- <?= $this->url->link('#'.$subtask['task_id'], 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?>
+ <?= $this->render('task/dropdown', array('task' => array('id' => $subtask['task_id'], 'project_id' => $subtask['project_id']))) ?>
</td>
<td>
<?= $this->url->link($this->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?>
@@ -24,7 +24,7 @@
<?= $this->url->link($this->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?>
</td>
<td>
- <?= $this->subtask->toggleStatus($subtask, 'dashboard') ?>
+ <?= $this->subtask->toggleStatus($subtask, $subtask['project_id']) ?>
</td>
<td>
<?php if (! empty($subtask['time_spent'])): ?>
diff --git a/app/Template/app/tasks.php b/app/Template/app/tasks.php
index 3712750b..786d7159 100644
--- a/app/Template/app/tasks.php
+++ b/app/Template/app/tasks.php
@@ -1,12 +1,12 @@
<div class="page-header">
- <h2><?= t('My tasks') ?> (<?= $paginator->getTotal() ?>)</h2>
+ <h2><?= $this->url->link(t('My tasks'), 'app', 'tasks', array('user_id' => $user['id'])) ?> (<?= $paginator->getTotal() ?>)</h2>
</div>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('There is nothing assigned to you.') ?></p>
<?php else: ?>
<table class="table-fixed table-small">
<tr>
- <th class="column-8"><?= $paginator->order('Id', 'tasks.id') ?></th>
+ <th class="column-5"><?= $paginator->order('Id', 'tasks.id') ?></th>
<th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th>
<th><?= $paginator->order(t('Task'), 'title') ?></th>
<th class="column-20"><?= t('Time tracking') ?></th>
@@ -15,7 +15,7 @@
<?php foreach ($paginator->getCollection() as $task): ?>
<tr>
<td class="task-table color-<?= $task['color_id'] ?>">
- <?= $this->url->link('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->render('task/dropdown', array('task' => $task)) ?>
</td>
<td>
<?= $this->url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?>
@@ -33,7 +33,7 @@
<?php endif ?>
</td>
<td>
- <?= dt('%B %e, %Y', $task['date_due']) ?>
+ <?= $this->dt->date($task['date_due']) ?>
</td>
</tr>
<?php endforeach ?>
diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php
index 2f75b113..41441e69 100644
--- a/app/Template/auth/index.php
+++ b/app/Template/auth/index.php
@@ -19,36 +19,24 @@
<?php if (isset($captcha) && $captcha): ?>
<?= $this->form->label(t('Enter the text below'), 'captcha') ?>
- <img src="<?= $this->url->href('auth', 'captcha') ?>"/>
- <?= $this->form->text('captcha', $values, $errors, array('required')) ?>
+ <img src="<?= $this->url->href('Captcha', 'image') ?>"/>
+ <?= $this->form->text('captcha', array(), $errors, array('required')) ?>
<?php endif ?>
<?php if (REMEMBER_ME_AUTH): ?>
- <?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br/>
+ <?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br>
<?php endif ?>
<div class="form-actions">
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
</div>
+ <?php if ($this->app->config('password_reset') == 1): ?>
+ <div class="reset-password">
+ <?= $this->url->link(t('Forgot password?'), 'PasswordReset', 'create') ?>
+ </div>
+ <?php endif ?>
</form>
<?php endif ?>
<?= $this->hook->render('template:auth:login-form:after') ?>
-
- <?php if (GOOGLE_AUTH || GITHUB_AUTH || GITLAB_AUTH): ?>
- <ul class="no-bullet">
- <?php if (GOOGLE_AUTH): ?>
- <li><?= $this->url->link(t('Login with my Google Account'), 'oauth', 'google') ?></li>
- <?php endif ?>
-
- <?php if (GITHUB_AUTH): ?>
- <li><?= $this->url->link(t('Login with my Github Account'), 'oauth', 'github') ?></li>
- <?php endif ?>
-
- <?php if (GITLAB_AUTH): ?>
- <li><?= $this->url->link(t('Login with my Gitlab Account'), 'oauth', 'gitlab') ?></li>
- <?php endif ?>
- </ul>
- <?php endif ?>
-
</div> \ No newline at end of file
diff --git a/app/Template/board/popover_assignee.php b/app/Template/board/popover_assignee.php
index 4af19cf7..8db95323 100644
--- a/app/Template/board/popover_assignee.php
+++ b/app/Template/board/popover_assignee.php
@@ -1,21 +1,20 @@
<section id="main">
- <section>
- <h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3>
- <form method="post" action="<?= $this->url->href('board', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
+ <div class="page-header">
+ <h2><?= t('Change assignee for the task "%s"', $values['title']) ?></h2>
+ </div>
+ <form method="post" action="<?= $this->url->href('BoardPopover', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $project['id'])) ?>">
- <?= $this->form->csrf() ?>
+ <?= $this->form->csrf() ?>
- <?= $this->form->hidden('id', $values) ?>
- <?= $this->form->hidden('project_id', $values) ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('project_id', $values) ?>
- <?= $this->form->label(t('Assignee'), 'owner_id') ?>
- <?= $this->form->select('owner_id', $users_list, $values, array(), array('autofocus')) ?><br/>
+ <?= $this->task->selectAssignee($users_list, $values, array(), array('autofocus')) ?>
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
- </div>
- </form>
- </section>
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+ </form>
</section> \ No newline at end of file
diff --git a/app/Template/board/popover_category.php b/app/Template/board/popover_category.php
index f391f492..e65593cd 100644
--- a/app/Template/board/popover_category.php
+++ b/app/Template/board/popover_category.php
@@ -1,21 +1,20 @@
<section id="main">
- <section>
- <h3><?= t('Change category for the task "%s"', $values['title']) ?></h3>
- <form method="post" action="<?= $this->url->href('board', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
+ <div class="page-header">
+ <h2><?= t('Change category for the task "%s"', $values['title']) ?></h2>
+ </div>
+ <form method="post" action="<?= $this->url->href('BoardPopover', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $project['id'])) ?>">
- <?= $this->form->csrf() ?>
+ <?= $this->form->csrf() ?>
- <?= $this->form->hidden('id', $values) ?>
- <?= $this->form->hidden('project_id', $values) ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('project_id', $values) ?>
- <?= $this->form->label(t('Category'), 'category_id') ?>
- <?= $this->form->select('category_id', $categories_list, $values, array(), array('autofocus')) ?><br/>
+ <?= $this->task->selectCategory($categories_list, $values, array(), array('autofocus'), true) ?>
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
- </div>
- </form>
- </section>
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+ </form>
</section> \ No newline at end of file
diff --git a/app/Template/board/popover_close_all_tasks_column.php b/app/Template/board/popover_close_all_tasks_column.php
new file mode 100644
index 00000000..da6b9ad7
--- /dev/null
+++ b/app/Template/board/popover_close_all_tasks_column.php
@@ -0,0 +1,18 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Do you really want to close all tasks of this column?') ?></h2>
+ </div>
+ <form method="post" action="<?= $this->url->href('BoardPopover', 'closeColumnTasks', array('project_id' => $project['id'])) ?>">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('column_id', $values) ?>
+ <?= $this->form->hidden('swimlane_id', $values) ?>
+
+ <p class="alert"><?= t('%d task(s) in the column "%s" and the swimlane "%s" will be closed.', $nb_tasks, $column, $swimlane) ?></p>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-red">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+ </form>
+</section> \ No newline at end of file
diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php
index 2a6b496a..58dcaf32 100644
--- a/app/Template/board/table_column.php
+++ b/app/Template/board/table_column.php
@@ -5,14 +5,14 @@
<!-- column in collapsed mode -->
<div class="board-column-collapsed">
- <span title="<?= t('Task count') ?>" class="board-column-header-task-count" title="<?= t('Show this column') ?>">
+ <span class="board-column-header-task-count" title="<?= t('Show this column') ?>">
<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>
</span>
</div>
<!-- column in expanded mode -->
<div class="board-column-expanded">
- <?php if (! $not_editable): ?>
+ <?php if (! $not_editable && $this->user->hasProjectAccess('taskcreation', 'create', $column['project_id'])): ?>
<div class="board-add-icon">
<?= $this->url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
</div>
@@ -24,13 +24,31 @@
</span>
<?php endif ?>
- <span class="board-column-title" data-column-id="<?= $column['id'] ?>" title="<?= t('Hide this column') ?>">
- <?= $this->e($column['title']) ?>
+ <span class="board-column-title">
+ <?php if ($not_editable): ?>
+ <?= $this->e($column['title']) ?>
+ <?php else: ?>
+ <span class="dropdown">
+ <a href="#" class="dropdown-menu"><?= $this->e($column['title']) ?> <i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li>
+ <i class="fa fa-minus-square fa-fw"></i>
+ <a href="#" class="board-toggle-column-view" data-column-id="<?= $column['id'] ?>"><?= t('Hide this column') ?></a>
+ </li>
+ <?php if ($this->user->hasProjectAccess('BoardPopover', 'closeColumnTasks', $column['project_id']) && $column['nb_tasks'] > 0): ?>
+ <li>
+ <i class="fa fa-close fa-fw"></i>
+ <?= $this->url->link(t('Close all tasks of this column'), 'BoardPopover', 'confirmCloseColumnTasks', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </span>
+ <?php endif ?>
</span>
<?php if (! $not_editable && ! empty($column['description'])): ?>
<span class="tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
- <i class="fa fa-info-circle"></i>
+ &nbsp;<i class="fa fa-info-circle"></i>
</span>
<?php endif ?>
diff --git a/app/Template/board/table_swimlane.php b/app/Template/board/table_swimlane.php
index dd38fc97..44607859 100644
--- a/app/Template/board/table_swimlane.php
+++ b/app/Template/board/table_swimlane.php
@@ -14,7 +14,7 @@
<span
title="<?= t('Description') ?>"
class="tooltip"
- data-href="<?= $this->url->href('board', 'swimlane', array('swimlane_id' => $swimlane['id'], 'project_id' => $project['id'])) ?>">
+ data-href="<?= $this->url->href('BoardTooltip', 'swimlane', array('swimlane_id' => $swimlane['id'], 'project_id' => $project['id'])) ?>">
<i class="fa fa-info-circle"></i>
</span>
<?php endif ?>
diff --git a/app/Template/board/table_tasks.php b/app/Template/board/table_tasks.php
index f10d48e4..e99e14fb 100644
--- a/app/Template/board/table_tasks.php
+++ b/app/Template/board/table_tasks.php
@@ -21,8 +21,8 @@
<!-- column in collapsed mode (rotated text) -->
<div class="board-column-collapsed">
<div class="board-rotation-wrapper">
- <div class="board-column-title board-rotation" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>">
- <i class="fa fa-chevron-circle-up tooltip" title="<?= $this->e($column['title']) ?>"></i> <?= $this->e($column['title']) ?>
+ <div class="board-column-title board-rotation board-toggle-column-view" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>">
+ <i class="fa fa-plus-square tooltip" title="<?= $this->e($column['title']) ?>"></i> <?= $this->e($column['title']) ?>
</div>
</div>
</div>
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
index d486b638..73e68602 100644
--- a/app/Template/board/task_footer.php
+++ b/app/Template/board/task_footer.php
@@ -6,7 +6,7 @@
<?php else: ?>
<?= $this->url->link(
$this->e($task['category_name']),
- 'board',
+ 'boardPopover',
'changeCategory',
array('task_id' => $task['id'], 'project_id' => $task['project_id']),
false,
@@ -22,36 +22,40 @@
<?php if (! empty($task['date_due'])): ?>
<span class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>">
<i class="fa fa-calendar"></i>
- <?= (date('Y') === date('Y', $task['date_due']) ? dt('%b %e', $task['date_due']) : dt('%b %e %Y', $task['date_due'])) ?>
+ <?= $this->dt->date($task['date_due']) ?>
</span>
<?php endif ?>
<?php if ($task['recurrence_status'] == \Kanboard\Model\Task::RECURRING_STATUS_PENDING): ?>
- <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90"></i></span>
+ <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90"></i></span>
<?php endif ?>
<?php if ($task['recurrence_status'] == \Kanboard\Model\Task::RECURRING_STATUS_PROCESSED): ?>
- <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90 fa-inverse"></i></span>
+ <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90 fa-inverse"></i></span>
<?php endif ?>
<?php if (! empty($task['nb_links'])): ?>
- <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork"></i>&nbsp;<?= $task['nb_links'] ?></span>
+ <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork fa-fw"></i><?= $task['nb_links'] ?></span>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_external_links'])): ?>
+ <span title="<?= t('External links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'externallinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-external-link fa-fw"></i><?= $task['nb_external_links'] ?></span>
<?php endif ?>
<?php if (! empty($task['nb_subtasks'])): ?>
- <span title="<?= t('Sub-Tasks') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-bars"></i>&nbsp;<?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?></span>
+ <span title="<?= t('Sub-Tasks') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-bars"></i>&nbsp;<?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?></span>
<?php endif ?>
<?php if (! empty($task['nb_files'])): ?>
- <span title="<?= t('Attachments') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-paperclip"></i>&nbsp;<?= $task['nb_files'] ?></span>
+ <span title="<?= t('Attachments') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-paperclip"></i>&nbsp;<?= $task['nb_files'] ?></span>
<?php endif ?>
<?php if (! empty($task['nb_comments'])): ?>
- <span title="<?= $task['nb_comments'] == 1 ? t('%d comment', $task['nb_comments']) : t('%d comments', $task['nb_comments']) ?>" class="tooltip" data-href="<?= $this->url->href('board', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-comment-o"></i>&nbsp;<?= $task['nb_comments'] ?></span>
+ <span title="<?= $task['nb_comments'] == 1 ? t('%d comment', $task['nb_comments']) : t('%d comments', $task['nb_comments']) ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-comment-o"></i>&nbsp;<?= $task['nb_comments'] ?></span>
<?php endif ?>
<?php if (! empty($task['description'])): ?>
- <span title="<?= t('Description') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+ <span title="<?= t('Description') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
<i class="fa fa-file-text-o"></i>
</span>
<?php endif ?>
@@ -69,4 +73,6 @@
<i class="fa fa-flag flag-milestone"></i>
</span>
<?php endif ?>
+
+ <?= $this->task->formatPriority($project, $task) ?>
</div>
diff --git a/app/Template/board/task_menu.php b/app/Template/board/task_menu.php
index 3eb35705..bd582185 100644
--- a/app/Template/board/task_menu.php
+++ b/app/Template/board/task_menu.php
@@ -1,17 +1,18 @@
<span class="dropdown">
<a href="#" class="dropdown-menu"><?= '#'.$task['id'] ?></a>
<ul>
- <li><i class="fa fa-user fa-fw"></i>&nbsp;<?= $this->url->link(t('Change assignee'), 'board', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
- <li><i class="fa fa-tag fa-fw"></i>&nbsp;<?= $this->url->link(t('Change category'), 'board', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><i class="fa fa-user fa-fw"></i>&nbsp;<?= $this->url->link(t('Change assignee'), 'BoardPopover', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><i class="fa fa-tag fa-fw"></i>&nbsp;<?= $this->url->link(t('Change category'), 'BoardPopover', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-align-left fa-fw"></i>&nbsp;<?= $this->url->link(t('Change description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-pencil-square-o fa-fw"></i>&nbsp;<?= $this->url->link(t('Edit this task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-comment-o fa-fw"></i>&nbsp;<?= $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-code-fork fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
- <li><i class="fa fa-camera fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a screenshot'), 'board', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><i class="fa fa-external-link fa-fw"></i>&nbsp;<?= $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-camera fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a screenshot'), 'BoardPopover', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<?php if ($task['is_active'] == 1): ?>
- <li><i class="fa fa-close fa-fw"></i>&nbsp;<?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li>
+ <li><i class="fa fa-close fa-fw"></i>&nbsp;<?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<?php else: ?>
- <li><i class="fa fa-check-square-o fa-fw"></i>&nbsp;<?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li>
+ <li><i class="fa fa-check-square-o fa-fw"></i>&nbsp;<?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<?php endif ?>
</ul>
</span> \ No newline at end of file
diff --git a/app/Template/board/task_private.php b/app/Template/board/task_private.php
index da993fdd..4880af00 100644
--- a/app/Template/board/task_private.php
+++ b/app/Template/board/task_private.php
@@ -1,8 +1,11 @@
<div class="
task-board
- <?= $task['is_active'] == 1 ? 'draggable-item task-board-status-open '.($task['date_modification'] > (time() - $board_highlight_period) ? 'task-board-recent' : '') : 'task-board-status-closed' ?>
+ <?= $task['is_active'] == 1 ? ($this->user->hasProjectAccess('board', 'save', $task['project_id']) ? 'draggable-item ' : '').'task-board-status-open '.($task['date_modification'] > (time() - $board_highlight_period) ? 'task-board-recent' : '') : 'task-board-status-closed' ?>
color-<?= $task['color_id'] ?>"
data-task-id="<?= $task['id'] ?>"
+ data-column-id="<?= $task['column_id'] ?>"
+ data-swimlane-id="<?= $task['swimlane_id'] ?>"
+ data-position="<?= $task['position'] ?>"
data-owner-id="<?= $task['owner_id'] ?>"
data-category-id="<?= $task['category_id'] ?>"
data-due-date="<?= $task['date_due'] ?>"
@@ -12,7 +15,12 @@
<?php if ($this->board->isCollapsed($task['project_id'])): ?>
<div class="task-board-collapsed">
- <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <div class="task-board-saving-icon" style="display: none;"><i class="fa fa-spinner fa-pulse"></i></div>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
+ <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <?php else: ?>
+ <strong><?= '#'.$task['id'] ?></strong>
+ <?php endif ?>
<?php if (! empty($task['assignee_username'])): ?>
<span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>">
@@ -23,7 +31,12 @@
</div>
<?php else: ?>
<div class="task-board-expanded">
- <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <div class="task-board-saving-icon" style="display: none;"><i class="fa fa-spinner fa-pulse fa-2x"></i></div>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
+ <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <?php else: ?>
+ <strong><?= '#'.$task['id'] ?></strong>
+ <?php endif ?>
<?php if ($task['reference']): ?>
<span class="task-board-reference" title="<?= t('Reference') ?>">
@@ -33,15 +46,19 @@
<?php if (! empty($task['owner_id'])): ?>
<span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>">
- <?= $this->url->link(
- $task['assignee_name'] ?: $task['assignee_username'],
- 'board',
- 'changeAssignee',
- array('task_id' => $task['id'], 'project_id' => $task['project_id']),
- false,
- 'popover',
- t('Change assignee')
- ) ?>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
+ <?= $this->url->link(
+ $task['assignee_name'] ?: $task['assignee_username'],
+ 'BoardPopover',
+ 'changeAssignee',
+ array('task_id' => $task['id'], 'project_id' => $task['project_id']),
+ false,
+ 'popover',
+ t('Change assignee')
+ ) ?>
+ <?php else: ?>
+ <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>
+ <?php endif ?>
</span>
<?php endif ?>
@@ -61,6 +78,7 @@
<?= $this->render('board/task_footer', array(
'task' => $task,
'not_editable' => $not_editable,
+ 'project' => $project,
)) ?>
</div>
<?php endif ?>
diff --git a/app/Template/board/task_public.php b/app/Template/board/task_public.php
index bacdcef4..d02722bb 100644
--- a/app/Template/board/task_public.php
+++ b/app/Template/board/task_public.php
@@ -25,5 +25,6 @@
<?= $this->render('board/task_footer', array(
'task' => $task,
'not_editable' => $not_editable,
+ 'project' => $project,
)) ?>
</div> \ No newline at end of file
diff --git a/app/Template/board/tooltip_comments.php b/app/Template/board/tooltip_comments.php
index 2e2c0c1e..ca91e13f 100644
--- a/app/Template/board/tooltip_comments.php
+++ b/app/Template/board/tooltip_comments.php
@@ -4,7 +4,7 @@
<?php if (! empty($comment['username'])): ?>
<span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @
<?php endif ?>
- <span class="comment-date"><?= dt('%b %e, %Y, %k:%M %p', $comment['date_creation']) ?></span>
+ <span class="comment-date"><?= $this->dt->datetime($comment['date_creation']) ?></span>
</p>
<div class="comment-inner">
diff --git a/app/Template/board/tooltip_external_links.php b/app/Template/board/tooltip_external_links.php
new file mode 100644
index 00000000..7681c06c
--- /dev/null
+++ b/app/Template/board/tooltip_external_links.php
@@ -0,0 +1,20 @@
+<table class="table-striped table-small">
+ <tr>
+ <th class="column-20"><?= t('Type') ?></th>
+ <th class="column-80"><?= t('Title') ?></th>
+ <th class="column-10"><?= t('Dependency') ?></th>
+ </tr>
+ <?php foreach ($links as $link): ?>
+ <tr>
+ <td>
+ <?= $link['type'] ?>
+ </td>
+ <td>
+ <a href="<?= $link['url'] ?>" target="_blank"><?= $this->e($link['title']) ?></a>
+ </td>
+ <td>
+ <?= $this->e($link['dependency_label']) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table> \ No newline at end of file
diff --git a/app/Template/board/tooltip_files.php b/app/Template/board/tooltip_files.php
index 407309b3..4fa14b57 100644
--- a/app/Template/board/tooltip_files.php
+++ b/app/Template/board/tooltip_files.php
@@ -8,9 +8,9 @@
</tr>
<tr>
<td>
- <i class="fa fa-download fa-fw"></i><?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
+ <i class="fa fa-download fa-fw"></i><?= $this->url->link(t('download'), 'TaskFile', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
<?php if ($file['is_image'] == 1): ?>
- &nbsp;<i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
+ &nbsp;<i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'TaskFile', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
<?php endif ?>
</td>
</tr>
diff --git a/app/Template/board/tooltip_subtasks.php b/app/Template/board/tooltip_subtasks.php
index 950da925..dc076d26 100644
--- a/app/Template/board/tooltip_subtasks.php
+++ b/app/Template/board/tooltip_subtasks.php
@@ -1,7 +1,14 @@
-<section id="tooltip-subtasks">
+<table class="table-stripped">
<?php foreach ($subtasks as $subtask): ?>
- <?= $this->subtask->toggleStatus($subtask, 'board') ?>
- <?= $this->e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?>
- <br/>
+ <tr>
+ <td class="column-80">
+ <?= $this->subtask->toggleStatus($subtask, $task['project_id']) ?>
+ </td>
+ <td>
+ <?php if (! empty($subtask['username'])): ?>
+ <?= $this->e($subtask['name'] ?: $subtask['username']) ?>
+ <?php endif ?>
+ </td>
+ </tr>
<?php endforeach ?>
-</section>
+</table>
diff --git a/app/Template/board/tooltip_tasklinks.php b/app/Template/board/tooltip_tasklinks.php
index 62304330..b51f90ed 100644
--- a/app/Template/board/tooltip_tasklinks.php
+++ b/app/Template/board/tooltip_tasklinks.php
@@ -1,19 +1,24 @@
<div class="tooltip-tasklinks">
- <ul>
- <?php foreach ($links as $link): ?>
- <li>
- <strong><?= t($link['label']) ?></strong>
- [<i><?= $link['project_name'] ?></i>]
- <?= $this->url->link(
- $this->e('#'.$link['task_id'].' - '.$link['title']),
- 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
- false,
- $link['is_active'] ? '' : 'task-link-closed'
- ) ?>
- <?php if (! empty($link['task_assignee_username'])): ?>
- [<?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>]
- <?php endif ?>
- </li>
+ <dl>
+ <?php foreach ($links as $label => $grouped_links): ?>
+ <dt><strong><?= t($label) ?></strong></dt>
+ <?php foreach ($grouped_links as $link): ?>
+ <dd>
+ <span class="progress"><?= $this->task->getProgress($link).'%' ?></span>
+ <?= $this->url->link(
+ $this->e('#'.$link['task_id'].' '.$link['title']),
+ 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
+ false,
+ $link['is_active'] ? '' : 'task-link-closed'
+ ) ?>
+ <?php if (! empty($link['task_assignee_username'])): ?>
+ [<?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>]
+ <?php endif ?>
+ <?php if ($task['project_id'] != $link['project_id']): ?>
+ (<i><?= $link['project_name'] ?></i>)
+ <?php endif ?>
+ </dd>
+ <?php endforeach ?>
<?php endforeach ?>
- </ul>
+ </dl>
</div> \ No newline at end of file
diff --git a/app/Template/board/view_private.php b/app/Template/board/view_private.php
index 63d261f6..b5e38c66 100644
--- a/app/Template/board/view_private.php
+++ b/app/Template/board/view_private.php
@@ -1,6 +1,6 @@
<section id="main">
- <?= $this->render('project/filters', array(
+ <?= $this->render('project_header/header', array(
'project' => $project,
'filters' => $filters,
'categories_list' => $categories_list,
diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php
index 0406414c..7085b51e 100644
--- a/app/Template/calendar/show.php
+++ b/app/Template/calendar/show.php
@@ -1,11 +1,11 @@
<section id="main">
- <?= $this->render('project/filters', array(
+ <?= $this->render('project_header/header', array(
'project' => $project,
'filters' => $filters,
)) ?>
<div id="calendar"
- data-save-url="<?= $this->url->href('calendar', 'save') ?>"
+ 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'])) ?>"
data-check-interval="<?= $check_interval ?>"
>
diff --git a/app/Template/category/edit.php b/app/Template/category/edit.php
index 1aae2f2a..d788f673 100644
--- a/app/Template/category/edit.php
+++ b/app/Template/category/edit.php
@@ -2,7 +2,7 @@
<h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('category', 'update', array('project_id' => $project['id'], 'category_id' => $values['id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('category', 'update', array('project_id' => $project['id'], 'category_id' => $values['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
@@ -33,8 +33,8 @@
<div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</form> \ No newline at end of file
diff --git a/app/Template/category/index.php b/app/Template/category/index.php
index dba537d0..e99b6d52 100644
--- a/app/Template/category/index.php
+++ b/app/Template/category/index.php
@@ -5,20 +5,23 @@
<table>
<tr>
<th><?= t('Category Name') ?></th>
- <th><?= t('Actions') ?></th>
+ <th class="column-8"><?= t('Actions') ?></th>
</tr>
<?php foreach ($categories as $category_id => $category_name): ?>
<tr>
<td><?= $this->e($category_name) ?></td>
<td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
<ul>
<li>
- <?= $this->url->link(t('Edit'), 'category', 'edit', array('project_id' => $project['id'], 'category_id' => $category_id)) ?>
+ <?= $this->url->link(t('Edit'), 'category', 'edit', array('project_id' => $project['id'], 'category_id' => $category_id), false, 'popover') ?>
</li>
<li>
- <?= $this->url->link(t('Remove'), 'category', 'confirm', array('project_id' => $project['id'], 'category_id' => $category_id)) ?>
+ <?= $this->url->link(t('Remove'), 'category', 'confirm', array('project_id' => $project['id'], 'category_id' => $category_id), false, 'popover') ?>
</li>
</ul>
+ </div>
</td>
</tr>
<?php endforeach ?>
diff --git a/app/Template/category/remove.php b/app/Template/category/remove.php
index ce589785..cad58d37 100644
--- a/app/Template/category/remove.php
+++ b/app/Template/category/remove.php
@@ -11,7 +11,7 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'category', 'remove', array('project_id' => $project['id'], 'category_id' => $category['id']), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</div>
</section> \ No newline at end of file
diff --git a/app/Template/column/create.php b/app/Template/column/create.php
new file mode 100644
index 00000000..58b130f5
--- /dev/null
+++ b/app/Template/column/create.php
@@ -0,0 +1,41 @@
+<div class="page-header">
+ <h2><?= t('Add a new column') ?></h2>
+</div>
+<form class="popover-form" method="post" action="<?= $this->url->href('Column', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('project_id', $values) ?>
+
+ <?= $this->form->label(t('Title'), 'title') ?>
+ <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
+
+ <?= $this->form->label(t('Task limit'), 'task_limit') ?>
+ <?= $this->form->number('task_limit', $values, $errors) ?>
+
+ <?= $this->form->label(t('Description'), 'description') ?>
+
+ <div class="form-tabs">
+ <div class="write-area">
+ <?= $this->form->textarea('description', $values, $errors) ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ <ul class="form-tabs-nav">
+ <li class="form-tab form-tab-selected">
+ <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a>
+ </li>
+ <li class="form-tab">
+ <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a>
+ </li>
+ </ul>
+ </div>
+ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php
index a17affd8..618cb134 100644
--- a/app/Template/column/edit.php
+++ b/app/Template/column/edit.php
@@ -2,7 +2,7 @@
<h2><?= t('Edit column "%s"', $column['title']) ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('column', 'update', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('column', 'update', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
@@ -37,8 +37,8 @@
<div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</form> \ No newline at end of file
diff --git a/app/Template/column/index.php b/app/Template/column/index.php
index 689cbbf5..1aa44688 100644
--- a/app/Template/column/index.php
+++ b/app/Template/column/index.php
@@ -1,89 +1,56 @@
<div class="page-header">
<h2><?= t('Edit the board for "%s"', $project['name']) ?></h2>
+ <ul>
+ <li>
+ <i class="fa fa-plus fa-fw"></i>
+ <?= $this->url->link(t('Add a new column'), 'Column', 'create', array('project_id' => $project['id']), false, 'popover') ?>
+ </li>
+ </ul>
</div>
-<?php if (! empty($columns)): ?>
-
- <?php $first_position = $columns[0]['position']; ?>
- <?php $last_position = $columns[count($columns) - 1]['position']; ?>
-
- <h3><?= t('Change columns') ?></h3>
- <table>
+<?php if (empty($columns)): ?>
+ <p class="alert alert-error"><?= t('Your board doesn\'t have any column!') ?></p>
+<?php else: ?>
+ <table
+ class="columns-table table-stripped"
+ data-save-position-url="<?= $this->url->href('Column', 'move', array('project_id' => $project['id'])) ?>">
+ <thead>
<tr>
- <th><?= t('Column title') ?></th>
- <th><?= t('Task limit') ?></th>
- <th><?= t('Actions') ?></th>
+ <th class="column-70"><?= t('Column title') ?></th>
+ <th class="column-25"><?= t('Task limit') ?></th>
+ <th class="column-5"><?= t('Actions') ?></th>
</tr>
+ </thead>
+ <tbody>
<?php foreach ($columns as $column): ?>
- <tr>
- <td class="column-60"><?= $this->e($column['title']) ?>
- <?php if (! empty($column['description'])): ?>
- <span class="tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
- <i class="fa fa-info-circle"></i>
- </span>
- <?php endif ?>
+ <tr data-column-id="<?= $column['id'] ?>">
+ <td>
+ <i class="fa fa-arrows-alt draggable-row-handle" title="<?= t('Change column position') ?>"></i>
+ <?= $this->e($column['title']) ?>
+ <?php if (! empty($column['description'])): ?>
+ <span class="tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
</td>
- <td class="column-10"><?= $this->e($column['task_limit']) ?></td>
- <td class="column-30">
+ <td>
+ <?= $this->e($column['task_limit']) ?>
+ </td>
+ <td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
<ul>
<li>
- <?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>
+ <?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
</li>
- <?php if ($column['position'] != $first_position): ?>
<li>
- <?= $this->url->link(t('Move Up'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?>
- </li>
- <?php endif ?>
- <?php if ($column['position'] != $last_position): ?>
- <li>
- <?= $this->url->link(t('Move Down'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?>
- </li>
- <?php endif ?>
- <li>
- <?= $this->url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>
+ <?= $this->url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
</li>
</ul>
+ </div>
</td>
</tr>
<?php endforeach ?>
+ </tbody>
</table>
-
<?php endif ?>
-
-<h3><?= t('Add a new column') ?></h3>
-<form method="post" action="<?= $this->url->href('column', 'create', array('project_id' => $project['id'])) ?>" autocomplete="off">
-
- <?= $this->form->csrf() ?>
-
- <?= $this->form->hidden('project_id', $values) ?>
-
- <?= $this->form->label(t('Title'), 'title') ?>
- <?= $this->form->text('title', $values, $errors, array('required', 'maxlength="50"')) ?>
-
- <?= $this->form->label(t('Task limit'), 'task_limit') ?>
- <?= $this->form->number('task_limit', $values, $errors) ?>
-
- <?= $this->form->label(t('Description'), 'description') ?>
-
- <div class="form-tabs">
- <div class="write-area">
- <?= $this->form->textarea('description', $values, $errors) ?>
- </div>
- <div class="preview-area">
- <div class="markdown"></div>
- </div>
- <ul class="form-tabs-nav">
- <li class="form-tab form-tab-selected">
- <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a>
- </li>
- <li class="form-tab">
- <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a>
- </li>
- </ul>
- </div>
- <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Template/column/remove.php b/app/Template/column/remove.php
index 28d0928f..ccab889d 100644
--- a/app/Template/column/remove.php
+++ b/app/Template/column/remove.php
@@ -10,6 +10,6 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'column', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?>
- <?= t('or') ?> <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id'])) ?>
+ <?= t('or') ?> <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/comment/create.php b/app/Template/comment/create.php
index 8bcbe0f7..15dd3a8e 100644
--- a/app/Template/comment/create.php
+++ b/app/Template/comment/create.php
@@ -1,8 +1,7 @@
<div class="page-header">
<h2><?= t('Add a comment') ?></h2>
</div>
-
-<form method="post" action="<?= $this->url->href('comment', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" autocomplete="off" class="form-comment">
+<form class="popover-form" method="post" action="<?= $this->url->href('comment', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off" class="form-comment">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('task_id', $values) ?>
<?= $this->form->hidden('user_id', $values) ?>
@@ -17,7 +16,18 @@
</li>
</ul>
<div class="write-area">
- <?= $this->form->textarea('comment', $values, $errors, array(! isset($skip_cancel) ? 'autofocus' : '', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?>
+ <?= $this->form->textarea(
+ 'comment',
+ $values,
+ $errors,
+ array(
+ ! isset($skip_cancel) ? 'autofocus' : '',
+ 'required',
+ 'placeholder="'.t('Leave a comment').'"',
+ 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $task['project_id'])).'"',
+ ),
+ 'comment-textarea'
+ ) ?>
</div>
<div class="preview-area">
<div class="markdown"></div>
@@ -30,11 +40,7 @@
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?php if (! isset($skip_cancel)): ?>
<?= t('or') ?>
- <?php if (isset($ajax)): ?>
- <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?>
- <?php else: ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- <?php endif ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
<?php endif ?>
</div>
</form>
diff --git a/app/Template/comment/edit.php b/app/Template/comment/edit.php
index e01f3da4..6db952cc 100644
--- a/app/Template/comment/edit.php
+++ b/app/Template/comment/edit.php
@@ -2,7 +2,7 @@
<h2><?= t('Edit a comment') ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('comment', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('comment', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
@@ -29,8 +29,8 @@
<div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
diff --git a/app/Template/comment/forbidden.php b/app/Template/comment/forbidden.php
deleted file mode 100644
index 1e306d45..00000000
--- a/app/Template/comment/forbidden.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<div class="page-header">
- <h2><?= t('Forbidden') ?></h2>
-</div>
-
-<p class="alert alert-error">
- <?= t('Only administrators or the creator of the comment can access to this page.') ?>
-</p> \ No newline at end of file
diff --git a/app/Template/comment/remove.php b/app/Template/comment/remove.php
index afc3346f..1b5004f4 100644
--- a/app/Template/comment/remove.php
+++ b/app/Template/comment/remove.php
@@ -12,6 +12,6 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'comment', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php
index 84077668..2c9b426f 100644
--- a/app/Template/comment/show.php
+++ b/app/Template/comment/show.php
@@ -9,19 +9,19 @@
<span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @
<?php endif ?>
- <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date_creation']) ?></span>
+ <span class="comment-date"><?= $this->dt->datetime($comment['date_creation']) ?></span>
</p>
<div class="comment-inner">
<?php if (! isset($preview)): ?>
<ul class="comment-actions">
<li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li>
- <?php if ((! isset($not_editable) || ! $not_editable) && ($this->user->isAdmin() || $this->user->isCurrentUser($comment['user_id']))): ?>
+ <?php if ($editable && ($this->user->isAdmin() || $this->user->isCurrentUser($comment['user_id']))): ?>
<li>
- <?= $this->url->link(t('remove'), 'comment', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>
+ <?= $this->url->link(t('remove'), 'comment', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?>
</li>
<li>
- <?= $this->url->link(t('edit'), 'comment', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>
+ <?= $this->url->link(t('edit'), 'comment', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?>
</li>
<?php endif ?>
</ul>
diff --git a/app/Template/config/about.php b/app/Template/config/about.php
index e0652a2f..f8534029 100644
--- a/app/Template/config/about.php
+++ b/app/Template/config/about.php
@@ -54,6 +54,7 @@
<div class="listing">
<h3><?= t('Board/Calendar/List view') ?></h3>
<ul>
+ <li><?= t('Switch to the project overview') ?> = <strong>v o</strong></li>
<li><?= t('Switch to the board view') ?> = <strong>v b</strong></li>
<li><?= t('Switch to the calendar view') ?> = <strong>v c</strong></li>
<li><?= t('Switch to the list view') ?> = <strong>v l</strong></li>
diff --git a/app/Template/config/application.php b/app/Template/config/application.php
index 7d4c811d..48fed7f0 100644
--- a/app/Template/config/application.php
+++ b/app/Template/config/application.php
@@ -1,30 +1,36 @@
<div class="page-header">
<h2><?= t('Application settings') ?></h2>
</div>
-<section>
<form method="post" action="<?= $this->url->href('config', 'application') ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Application URL'), 'application_url') ?>
- <?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?><br/>
- <p class="form-help"><?= t('Example: http://example.kanboard.net/ (used by email notifications)') ?></p>
+ <?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?>
+ <p class="form-help"><?= t('Example: http://example.kanboard.net/ (used to generate absolute URLs)') ?></p>
<?= $this->form->label(t('Language'), 'application_language') ?>
- <?= $this->form->select('application_language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('application_language', $languages, $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'application_timezone') ?>
- <?= $this->form->select('application_timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('application_timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Date format'), 'application_date_format') ?>
- <?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?><br/>
+ <?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?>
<p class="form-help"><?= t('ISO format is always accepted, example: "%s" and "%s"', date('Y-m-d'), date('Y_m_d')) ?></p>
+ <?= $this->form->label(t('Date and time format'), 'application_datetime_format') ?>
+ <?= $this->form->select('application_datetime_format', $datetime_formats, $values, $errors) ?>
+
+ <?= $this->form->label(t('Time format'), 'application_time_format') ?>
+ <?= $this->form->select('application_time_format', $time_formats, $values, $errors) ?>
+
+ <?= $this->form->checkbox('password_reset', t('Enable "Forget Password"'), 1, $values['password_reset'] == 1) ?>
+
<?= $this->form->label(t('Custom Stylesheet'), 'application_stylesheet') ?>
- <?= $this->form->textarea('application_stylesheet', $values, $errors) ?><br/>
+ <?= $this->form->textarea('application_stylesheet', $values, $errors) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
</div>
</form>
-</section> \ No newline at end of file
diff --git a/app/Template/config/board.php b/app/Template/config/board.php
index 19a4bcd7..f787a931 100644
--- a/app/Template/config/board.php
+++ b/app/Template/config/board.php
@@ -1,25 +1,23 @@
<div class="page-header">
<h2><?= t('Board settings') ?></h2>
</div>
-<section>
<form method="post" action="<?= $this->url->href('config', 'board') ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Task highlight period'), 'board_highlight_period') ?>
- <?= $this->form->number('board_highlight_period', $values, $errors) ?><br/>
+ <?= $this->form->number('board_highlight_period', $values, $errors) ?>
<p class="form-help"><?= t('Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)') ?></p>
<?= $this->form->label(t('Refresh interval for public board'), 'board_public_refresh_interval') ?>
- <?= $this->form->number('board_public_refresh_interval', $values, $errors) ?><br/>
+ <?= $this->form->number('board_public_refresh_interval', $values, $errors) ?>
<p class="form-help"><?= t('Frequency in second (60 seconds by default)') ?></p>
<?= $this->form->label(t('Refresh interval for private board'), 'board_private_refresh_interval') ?>
- <?= $this->form->number('board_private_refresh_interval', $values, $errors) ?><br/>
+ <?= $this->form->number('board_private_refresh_interval', $values, $errors) ?>
<p class="form-help"><?= t('Frequency in second (0 to disable this feature, 10 seconds by default)') ?></p>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
</div>
</form>
-</section> \ No newline at end of file
diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php
index bba85672..ced051f7 100644
--- a/app/Template/config/integrations.php
+++ b/app/Template/config/integrations.php
@@ -3,29 +3,9 @@
</div>
<form method="post" action="<?= $this->url->href('config', 'integrations') ?>" autocomplete="off">
-
<?= $this->form->csrf() ?>
-
<?= $this->hook->render('template:config:integrations', array('values' => $values)) ?>
- <h3><i class="fa fa-google"></i> <?= t('Google Authentication') ?></h3>
- <div class="listing">
- <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('oauth', 'google', array(), false, '', true) ?>"/><br/>
- <p class="form-help"><?= $this->url->doc(t('Help on Google authentication'), 'google-authentication') ?></p>
- </div>
-
- <h3><i class="fa fa-github"></i> <?= t('Github Authentication') ?></h3>
- <div class="listing">
- <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('oauth', 'github', array(), false, '', true) ?>"/><br/>
- <p class="form-help"><?= $this->url->doc(t('Help on Github authentication'), 'github-authentication') ?></p>
- </div>
-
- <h3><img src="<?= $this->url->dir() ?>assets/img/gitlab-icon.png"/>&nbsp;<?= t('Gitlab Authentication') ?></h3>
- <div class="listing">
- <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('oauth', 'gitlab', array(), false, '', true) ?>"/><br/>
- <p class="form-help"><?= $this->url->doc(t('Help on Gitlab authentication'), 'gitlab-authentication') ?></p>
- </div>
-
<h3><img src="<?= $this->url->dir() ?>assets/img/gravatar-icon.png"/>&nbsp;<?= t('Gravatar') ?></h3>
<div class="listing">
<?= $this->form->checkbox('integration_gravatar', t('Enable Gravatar images'), 1, $values['integration_gravatar'] == 1) ?>
diff --git a/app/Template/config/layout.php b/app/Template/config/layout.php
index 028f138c..f34caaab 100644
--- a/app/Template/config/layout.php
+++ b/app/Template/config/layout.php
@@ -1,10 +1,10 @@
<section id="main">
<section class="sidebar-container" id="config-section">
- <?= $this->render('config/sidebar') ?>
+ <?= $this->render($sidebar_template) ?>
<div class="sidebar-content">
- <?= $config_content_for_layout ?>
+ <?= $content_for_sublayout ?>
</div>
</section>
</section> \ No newline at end of file
diff --git a/app/Template/config/project.php b/app/Template/config/project.php
index c58a7bac..1d32a14f 100644
--- a/app/Template/config/project.php
+++ b/app/Template/config/project.php
@@ -1,7 +1,6 @@
<div class="page-header">
<h2><?= t('Project settings') ?></h2>
</div>
-<section>
<form method="post" action="<?= $this->url->href('config', 'project') ?>" autocomplete="off">
<?= $this->form->csrf() ?>
@@ -10,19 +9,19 @@
<?= $this->form->select('default_color', $colors, $values, $errors) ?>
<?= $this->form->label(t('Default columns for new projects (Comma-separated)'), 'board_columns') ?>
- <?= $this->form->text('board_columns', $values, $errors) ?><br/>
+ <?= $this->form->text('board_columns', $values, $errors) ?>
<p class="form-help"><?= t('Default values are "%s"', $default_columns) ?></p>
<?= $this->form->label(t('Default categories for new projects (Comma-separated)'), 'project_categories') ?>
- <?= $this->form->text('project_categories', $values, $errors) ?><br/>
+ <?= $this->form->text('project_categories', $values, $errors) ?>
<p class="form-help"><?= t('Example: "Bug, Feature Request, Improvement"') ?></p>
+ <?= $this->form->checkbox('disable_private_project', t('Disable private projects'), 1, isset($values['disable_private_project']) && $values['disable_private_project'] == 1) ?>
<?= $this->form->checkbox('subtask_restriction', t('Allow only one subtask in progress at the same time for a user'), 1, $values['subtask_restriction'] == 1) ?>
<?= $this->form->checkbox('subtask_time_tracking', t('Trigger automatically subtask time tracking'), 1, $values['subtask_time_tracking'] == 1) ?>
<?= $this->form->checkbox('cfd_include_closed_tasks', t('Include closed tasks in the cumulative flow diagram'), 1, $values['cfd_include_closed_tasks'] == 1) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
</div>
</form>
-</section> \ No newline at end of file
diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php
index 4195cde1..dd51bc74 100644
--- a/app/Template/config/sidebar.php
+++ b/app/Template/config/sidebar.php
@@ -1,44 +1,39 @@
<div class="sidebar">
<h2><?= t('Actions') ?></h2>
<ul>
- <li <?= $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'index') ?>>
<?= $this->url->link(t('About'), 'config', 'index') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'plugins' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'plugins') ?>>
<?= $this->url->link(t('Plugins'), 'config', 'plugins') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'application' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'application') ?>>
<?= $this->url->link(t('Application settings'), 'config', 'application') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'project' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'project') ?>>
<?= $this->url->link(t('Project settings'), 'config', 'project') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'board' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'board') ?>>
<?= $this->url->link(t('Board settings'), 'config', 'board') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'calendar' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'calendar') ?>>
<?= $this->url->link(t('Calendar settings'), 'config', 'calendar') ?>
</li>
- <li <?= $this->app->getRouterController() === 'link' && $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('link') ?>>
<?= $this->url->link(t('Link settings'), 'link', 'index') ?>
</li>
- <li <?= $this->app->getRouterController() === 'currency' && $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('currency', 'index') ?>>
<?= $this->url->link(t('Currency rates'), 'currency', 'index') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'integrations' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'integrations') ?>>
<?= $this->url->link(t('Integrations'), 'config', 'integrations') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'webhook' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'webhook') ?>>
<?= $this->url->link(t('Webhooks'), 'config', 'webhook') ?>
</li>
- <li <?= $this->app->getRouterAction() === 'api' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('config', 'api') ?>>
<?= $this->url->link(t('API'), 'config', 'api') ?>
</li>
- <li>
- <?= $this->url->link(t('Documentation'), 'doc', 'show') ?>
- </li>
<?= $this->hook->render('template:config:sidebar') ?>
</ul>
- <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
- <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div> \ No newline at end of file
diff --git a/app/Template/currency/index.php b/app/Template/currency/index.php
index 1c78c47a..07b58a8b 100644
--- a/app/Template/currency/index.php
+++ b/app/Template/currency/index.php
@@ -29,10 +29,10 @@
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Reference currency'), 'application_currency') ?>
- <?= $this->form->select('application_currency', $currencies, $config_values, $errors) ?><br/>
+ <?= $this->form->select('application_currency', $currencies, $config_values, $errors) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
</div>
</form>
@@ -43,12 +43,12 @@
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Currency'), 'currency') ?>
- <?= $this->form->select('currency', $currencies, $values, $errors) ?><br/>
+ <?= $this->form->select('currency', $currencies, $values, $errors) ?>
<?= $this->form->label(t('Rate'), 'rate') ?>
- <?= $this->form->text('rate', $values, $errors, array(), 'form-numeric') ?><br/>
+ <?= $this->form->text('rate', $values, $errors, array(), 'form-numeric') ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
</div>
</form>
diff --git a/app/Template/custom_filter/add.php b/app/Template/custom_filter/add.php
index 61df148c..361083ee 100644
--- a/app/Template/custom_filter/add.php
+++ b/app/Template/custom_filter/add.php
@@ -12,10 +12,10 @@
<?= $this->form->label(t('Filter'), 'filter') ?>
<?= $this->form->text('filter', $values, $errors, array('required', 'maxlength="100"')) ?>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
<?= $this->form->checkbox('is_shared', t('Share with all project members'), 1) ?>
<?php endif ?>
-
+
<?= $this->form->checkbox('append', t('Append filter (instead of replacement)'), 1) ?>
<div class="form-actions">
diff --git a/app/Template/custom_filter/edit.php b/app/Template/custom_filter/edit.php
index 9d296b84..01fb4ec5 100644
--- a/app/Template/custom_filter/edit.php
+++ b/app/Template/custom_filter/edit.php
@@ -2,7 +2,7 @@
<h2><?= t('Edit custom filter') ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('customfilter', 'update', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id'])) ?>" autocomplete="off">
+<form class="form-popover" method="post" action="<?= $this->url->href('customfilter', 'update', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
@@ -16,17 +16,17 @@
<?= $this->form->label(t('Filter'), 'filter') ?>
<?= $this->form->text('filter', $values, $errors, array('required', 'maxlength="100"')) ?>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
<?= $this->form->checkbox('is_shared', t('Share with all project members'), 1, $values['is_shared'] == 1) ?>
<?php else: ?>
<?= $this->form->hidden('is_shared', $values) ?>
<?php endif ?>
-
+
<?= $this->form->checkbox('append', t('Append filter (instead of replacement)'), 1, $values['append'] == 1) ?>
-
+
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</form> \ No newline at end of file
diff --git a/app/Template/custom_filter/index.php b/app/Template/custom_filter/index.php
index c857e206..28b6a878 100644
--- a/app/Template/custom_filter/index.php
+++ b/app/Template/custom_filter/index.php
@@ -5,12 +5,12 @@
<div>
<table>
<tr>
- <th><?= t('Name') ?></th>
- <th><?= t('Filter') ?></th>
- <th><?= t('Shared') ?></th>
- <th><?= t('Append/Replace') ?></th>
- <th><?= t('Owner') ?></th>
- <th><?= t('Actions') ?></th>
+ <th class="column-15"><?= t('Name') ?></th>
+ <th class="column-30"><?= t('Filter') ?></th>
+ <th class="column-10"><?= t('Shared') ?></th>
+ <th class="column-15"><?= t('Append/Replace') ?></th>
+ <th class="column-25"><?= t('Owner') ?></th>
+ <th class="column-5"><?= t('Actions') ?></th>
</tr>
<?php foreach ($custom_filters as $filter): ?>
<tr>
@@ -32,11 +32,14 @@
</td>
<td><?= $this->e($filter['owner_name'] ?: $filter['owner_username']) ?></td>
<td>
- <?php if ($filter['user_id'] == $this->user->getId() || $this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($filter['user_id'] == $this->user->getId() || $this->user->hasProjectAccess('customfilter', 'edit', $project['id'])): ?>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
<ul>
- <li><?= $this->url->link(t('Remove'), 'customfilter', 'remove', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id']), true) ?></li>
- <li><?= $this->url->link(t('Edit'), 'customfilter', 'edit', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id'])) ?></li>
+ <li><?= $this->url->link(t('Remove'), 'customfilter', 'confirm', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id']), false, 'popover') ?></li>
+ <li><?= $this->url->link(t('Edit'), 'customfilter', 'edit', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id']), false, 'popover') ?></li>
</ul>
+ </div>
<?php endif ?>
</td>
</tr>
diff --git a/app/Template/custom_filter/remove.php b/app/Template/custom_filter/remove.php
new file mode 100644
index 00000000..d4c67a2b
--- /dev/null
+++ b/app/Template/custom_filter/remove.php
@@ -0,0 +1,17 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Remove a custom filter') ?></h2>
+ </div>
+
+ <div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this custom filter: "%s"?', $filter['name']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'customfilter', 'remove', array('project_id' => $project['id'], 'filter_id' => $filter['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+ </div>
+</section>
diff --git a/app/Template/event/events.php b/app/Template/event/events.php
index aec0b29e..bbb01be4 100644
--- a/app/Template/event/events.php
+++ b/app/Template/event/events.php
@@ -14,7 +14,7 @@
<?php elseif ($this->text->contains($event['event_name'], 'comment')): ?>
<i class="fa fa-comments-o"></i>
<?php endif ?>
- &nbsp;<?= dt('%B %e, %Y at %k:%M %p', $event['date_creation']) ?>
+ &nbsp;<?= $this->dt->datetime($event['date_creation']) ?>
</p>
<div class="activity-content"><?= $event['event_content'] ?></div>
</div>
diff --git a/app/Template/event/task_file_create.php b/app/Template/event/task_file_create.php
new file mode 100644
index 00000000..1a36bc8f
--- /dev/null
+++ b/app/Template/event/task_file_create.php
@@ -0,0 +1,11 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s attached a new file to the task %s',
+ $this->e($author),
+ $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))
+ ) ?>
+</p>
+<p class="activity-description">
+ <em><?= $this->e($file['name']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/export/sidebar.php b/app/Template/export/sidebar.php
index 44448520..6a1de7e9 100644
--- a/app/Template/export/sidebar.php
+++ b/app/Template/export/sidebar.php
@@ -15,6 +15,4 @@
</li>
<?= $this->hook->render('template:export:sidebar') ?>
</ul>
- <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
- <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div> \ No newline at end of file
diff --git a/app/Template/export/subtasks.php b/app/Template/export/subtasks.php
index 4aad2641..f2a00f84 100644
--- a/app/Template/export/subtasks.php
+++ b/app/Template/export/subtasks.php
@@ -1,7 +1,5 @@
<div class="page-header">
- <h2>
- <?= t('Subtasks exportation for "%s"', $project['name']) ?>
- </h2>
+ <h2><?= t('Subtasks exportation for "%s"', $project['name']) ?></h2>
</div>
<p class="alert alert-info"><?= t('This report contains all subtasks information for the given date range.') ?></p>
@@ -13,7 +11,7 @@
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Start Date'), 'from') ?>
- <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/>
+ <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
<?= $this->form->label(t('End Date'), 'to') ?>
<?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
@@ -21,6 +19,6 @@
<div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue">
</div>
</form> \ No newline at end of file
diff --git a/app/Template/export/summary.php b/app/Template/export/summary.php
index ffbd6ac2..0c2a96fb 100644
--- a/app/Template/export/summary.php
+++ b/app/Template/export/summary.php
@@ -1,7 +1,5 @@
<div class="page-header">
- <h2>
- <?= t('Daily project summary export for "%s"', $project['name']) ?>
- </h2>
+ <h2><?= t('Daily project summary export for "%s"', $project['name']) ?></h2>
</div>
<p class="alert alert-info"><?= t('This export contains the number of tasks per column grouped per day.') ?></p>
@@ -13,7 +11,7 @@
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Start Date'), 'from') ?>
- <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/>
+ <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
<?= $this->form->label(t('End Date'), 'to') ?>
<?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
@@ -21,6 +19,6 @@
<div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue">
</div>
</form> \ No newline at end of file
diff --git a/app/Template/export/tasks.php b/app/Template/export/tasks.php
index c74c8f98..c27149d2 100644
--- a/app/Template/export/tasks.php
+++ b/app/Template/export/tasks.php
@@ -1,7 +1,5 @@
<div class="page-header">
- <h2>
- <?= t('Tasks exportation for "%s"', $project['name']) ?>
- </h2>
+ <h2><?= t('Tasks exportation for "%s"', $project['name']) ?></h2>
</div>
<p class="alert alert-info"><?= t('This report contains all tasks information for the given date range.') ?></p>
@@ -13,7 +11,7 @@
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Start Date'), 'from') ?>
- <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/>
+ <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
<?= $this->form->label(t('End Date'), 'to') ?>
<?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
@@ -21,6 +19,6 @@
<div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue">
</div>
</form> \ No newline at end of file
diff --git a/app/Template/export/transitions.php b/app/Template/export/transitions.php
index bf6ef249..d935bde1 100644
--- a/app/Template/export/transitions.php
+++ b/app/Template/export/transitions.php
@@ -1,7 +1,5 @@
<div class="page-header">
- <h2>
- <?= t('Task transitions export') ?>
- </h2>
+ <h2><?= t('Task transitions export') ?></h2>
</div>
<p class="alert alert-info"><?= t('This report contains all column moves for each task with the date, the user and the time spent for each transition.') ?></p>
@@ -13,7 +11,7 @@
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Start Date'), 'from') ?>
- <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/>
+ <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
<?= $this->form->label(t('End Date'), 'to') ?>
<?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
@@ -21,6 +19,6 @@
<div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue">
</div>
</form> \ No newline at end of file
diff --git a/app/Template/file/new.php b/app/Template/file/new.php
deleted file mode 100644
index a1a59eae..00000000
--- a/app/Template/file/new.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Attach a document') ?></h2>
-</div>
-
-<form action="<?= $this->url->href('file', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" enctype="multipart/form-data">
- <?= $this->form->csrf() ?>
- <input type="file" name="files[]" multiple />
- <div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></div>
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Template/file/open.php b/app/Template/file/open.php
deleted file mode 100644
index 3df012b6..00000000
--- a/app/Template/file/open.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="page-header">
- <h2><?= $this->e($file['name']) ?></h2>
- <div class="task-file-viewer">
- <img src="<?= $this->url->href('file', 'image', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Template/file/show.php b/app/Template/file/show.php
deleted file mode 100644
index a390c9fb..00000000
--- a/app/Template/file/show.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php if (! empty($files) || ! empty($images)): ?>
-<div id="attachments" class="task-show-section">
-
- <div class="page-header">
- <h2><?= t('Attachments') ?></h2>
- </div>
- <?php if (! empty($images)): ?>
- <h3><?= t('Images') ?></h3>
- <ul class="task-show-images">
- <?php foreach ($images as $file): ?>
- <li>
- <?php if (function_exists('imagecreatetruecolor')): ?>
- <div class="img_container">
- <img src="<?= $this->url->href('file', 'thumbnail', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/>
- </div>
- <?php endif ?>
- <p>
- <?= $this->e($file['name']) ?>
- <span class="tooltip" title='<?= t('uploaded by: %s', $file['user_name'] ?: $file['username']).'<br>'.t('uploaded on: %s', dt('%B %e, %Y at %k:%M %p', $file['date'])).'<br>'.t('size: %s', $this->text->bytes($file['size'])) ?>'>
- <i class="fa fa-info-circle"></i>
- </span>
- </p>
- <span class="task-show-file-actions task-show-image-actions">
- <i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
- <i class="fa fa-trash"></i> <?= $this->url->link(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
- <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
- </span>
- </li>
- <?php endforeach ?>
- </ul>
- <?php endif ?>
-
- <?php if (! empty($files)): ?>
- <h3><?= t('Files') ?></h3>
- <table class="task-show-file-table">
- <?php foreach ($files as $file): ?>
- <tr>
- <td><i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i></td>
- <td>
- <?= $this->e($file['name']) ?>
- <span class="tooltip" title='<?= t('uploaded by: %s', $file['user_name'] ?: $file['username']).'<br>'.t('uploaded on: %s', dt('%B %e, %Y at %k:%M %p', $file['date'])).'<br>'.t('size: %s', $this->text->bytes($file['size'])) ?>'>
- <i class="fa fa-info-circle"></i>
- </span>
- </td>
- <td>
- <span class="task-show-file-actions">
- <i class="fa fa-trash"></i> <?= $this->url->link(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
- <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
- </span>
- </td>
- </tr>
- <?php endforeach ?>
- </table>
- <?php endif ?>
-</div>
-<?php endif ?> \ No newline at end of file
diff --git a/app/Template/file_viewer/show.php b/app/Template/file_viewer/show.php
new file mode 100644
index 00000000..c71ef91c
--- /dev/null
+++ b/app/Template/file_viewer/show.php
@@ -0,0 +1,14 @@
+<div class="page-header">
+ <h2><?= $this->e($file['name']) ?></h2>
+</div>
+<div class="file-viewer">
+ <?php if ($file['is_image']): ?>
+ <img src="<?= $this->url->href('FileViewer', 'image', $params) ?>" alt="<?= $this->e($file['name']) ?>">
+ <?php elseif ($type === 'markdown'): ?>
+ <article class="markdown">
+ <?= $this->text->markdown($content) ?>
+ </article>
+ <?php elseif ($type === 'text'): ?>
+ <pre><?= $content ?></pre>
+ <?php endif ?>
+</div> \ No newline at end of file
diff --git a/app/Template/gantt/project.php b/app/Template/gantt/project.php
index 1face3b8..fe193c2b 100644
--- a/app/Template/gantt/project.php
+++ b/app/Template/gantt/project.php
@@ -1,5 +1,5 @@
<section id="main">
- <?= $this->render('project/filters', array(
+ <?= $this->render('project_header/header', array(
'project' => $project,
'filters' => $filters,
'users_list' => $users_list,
diff --git a/app/Template/gantt/projects.php b/app/Template/gantt/projects.php
index 50e244a5..84b260bb 100644
--- a/app/Template/gantt/projects.php
+++ b/app/Template/gantt/projects.php
@@ -1,16 +1,10 @@
<section id="main">
<div class="page-header">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
- <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li>
- <?php endif ?>
- <li>
- <i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?>
- </li>
<li>
<i class="fa fa-folder fa-fw"></i><?= $this->url->link(t('Projects list'), 'project', 'index') ?>
</li>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('Users overview'), 'projectuser', 'managers') ?></li>
<?php endif ?>
</ul>
diff --git a/app/Template/gantt/task_creation.php b/app/Template/gantt/task_creation.php
index 7997e231..a08f41b4 100644
--- a/app/Template/gantt/task_creation.php
+++ b/app/Template/gantt/task_creation.php
@@ -33,26 +33,13 @@
</div>
<div class="form-column">
- <?= $this->form->label(t('Assignee'), 'owner_id') ?>
- <?= $this->form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?><br/>
-
- <?= $this->form->label(t('Category'), 'category_id') ?>
- <?= $this->form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?><br/>
-
- <?php if (! (count($swimlanes_list) === 1 && key($swimlanes_list) === 0)): ?>
- <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?>
- <?= $this->form->select('swimlane_id', $swimlanes_list, $values, $errors, array('tabindex="5"')) ?><br/>
- <?php endif ?>
-
- <?= $this->form->label(t('Complexity'), 'score') ?>
- <?= $this->form->number('score', $values, $errors, array('tabindex="6"')) ?><br/>
-
- <?= $this->form->label(t('Start Date'), 'date_started') ?>
- <?= $this->form->text('date_started', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="7"'), 'form-date') ?>
-
- <?= $this->form->label(t('Due Date'), 'date_due') ?>
- <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="8"'), 'form-date') ?><br/>
- <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
+ <?= $this->task->selectAssignee($users_list, $values, $errors) ?>
+ <?= $this->task->selectCategory($categories_list, $values, $errors) ?>
+ <?= $this->task->selectSwimlane($swimlanes_list, $values, $errors) ?>
+ <?= $this->task->selectPriority($project, $values) ?>
+ <?= $this->task->selectScore($values, $errors) ?>
+ <?= $this->task->selectStartDate($values, $errors) ?>
+ <?= $this->task->selectDueDate($values, $errors) ?>
</div>
<div class="form-actions">
diff --git a/app/Template/group/associate.php b/app/Template/group/associate.php
new file mode 100644
index 00000000..468281e2
--- /dev/null
+++ b/app/Template/group/associate.php
@@ -0,0 +1,25 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
+ <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('View group members'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
+ </ul>
+ </div>
+ <?php if (empty($users)): ?>
+ <p class="alert"><?= t('There is no user available.') ?></p>
+ <?php else: ?>
+ <form method="post" action="<?= $this->url->href('group', 'addUser', array('group_id' => $group['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('group_id', $values) ?>
+
+ <?= $this->form->label(t('User'), 'user_id') ?>
+ <?= $this->form->select('user_id', $users, $values, $errors, array('required'), 'chosen-select') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'group', 'index') ?>
+ </div>
+ </form>
+ <?php endif ?>
+</section>
diff --git a/app/Template/group/create.php b/app/Template/group/create.php
new file mode 100644
index 00000000..4a935c08
--- /dev/null
+++ b/app/Template/group/create.php
@@ -0,0 +1,19 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
+ </ul>
+ </div>
+ <form method="post" action="<?= $this->url->href('group', 'save') ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'group', 'index') ?>
+ </div>
+ </form>
+</section>
diff --git a/app/Template/group/dissociate.php b/app/Template/group/dissociate.php
new file mode 100644
index 00000000..e1c60764
--- /dev/null
+++ b/app/Template/group/dissociate.php
@@ -0,0 +1,17 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
+ <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('View group members'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
+ </ul>
+ </div>
+ <div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove the user "%s" from the group "%s"?', $user['name'] ?: $user['username'], $group['name']) ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'group', 'removeUser', array('group_id' => $group['id'], 'user_id' => $user['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'group', 'users', array('group_id' => $group['id'])) ?>
+ </div>
+ </div>
+</section>
diff --git a/app/Template/group/edit.php b/app/Template/group/edit.php
new file mode 100644
index 00000000..d9646ee8
--- /dev/null
+++ b/app/Template/group/edit.php
@@ -0,0 +1,22 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
+ </ul>
+ </div>
+ <form method="post" action="<?= $this->url->href('group', 'update') ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('external_id', $values) ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'group', 'index') ?>
+ </div>
+ </form>
+</section>
diff --git a/app/Template/group/index.php b/app/Template/group/index.php
new file mode 100644
index 00000000..d111b5d9
--- /dev/null
+++ b/app/Template/group/index.php
@@ -0,0 +1,46 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
+ <li><i class="fa fa-user-plus fa-fw"></i><?= $this->url->link(t('New group'), 'group', 'create') ?></li>
+ </ul>
+ </div>
+ <?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is no group.') ?></p>
+ <?php else: ?>
+ <table class="table-small table-fixed">
+ <tr>
+ <th class="column-5"><?= $paginator->order(t('Id'), 'id') ?></th>
+ <th class="column-20"><?= $paginator->order(t('External Id'), 'external_id') ?></th>
+ <th><?= $paginator->order(t('Name'), 'name') ?></th>
+ <th class="column-5"><?= t('Actions') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $group): ?>
+ <tr>
+ <td>
+ #<?= $group['id'] ?>
+ </td>
+ <td>
+ <?= $this->e($group['external_id']) ?>
+ </td>
+ <td>
+ <?= $this->e($group['name']) ?>
+ </td>
+ <td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><?= $this->url->link(t('Add group member'), 'group', 'associate', array('group_id' => $group['id'])) ?></li>
+ <li><?= $this->url->link(t('Members'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
+ <li><?= $this->url->link(t('Edit'), 'group', 'edit', array('group_id' => $group['id'])) ?></li>
+ <li><?= $this->url->link(t('Remove'), 'group', 'confirm', array('group_id' => $group['id'])) ?></li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+ <?php endif ?>
+</section>
diff --git a/app/Template/group/remove.php b/app/Template/group/remove.php
new file mode 100644
index 00000000..1cb007b1
--- /dev/null
+++ b/app/Template/group/remove.php
@@ -0,0 +1,17 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
+ <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('View group members'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
+ </ul>
+ </div>
+ <div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this group: "%s"?', $group['name']) ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'group', 'remove', array('group_id' => $group['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'group', 'index') ?>
+ </div>
+ </div>
+</section>
diff --git a/app/Template/group/users.php b/app/Template/group/users.php
new file mode 100644
index 00000000..f79cb9ad
--- /dev/null
+++ b/app/Template/group/users.php
@@ -0,0 +1,42 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('Add group member'), 'group', 'associate', array('group_id' => $group['id'])) ?></li>
+ </ul>
+ </div>
+ <?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is no user in this group.') ?></p>
+ <?php else: ?>
+ <table>
+ <tr>
+ <th><?= $paginator->order(t('Id'), 'id') ?></th>
+ <th><?= $paginator->order(t('Username'), 'username') ?></th>
+ <th><?= $paginator->order(t('Name'), 'name') ?></th>
+ <th><?= $paginator->order(t('Email'), 'email') ?></th>
+ <th><?= t('Actions') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $user): ?>
+ <tr>
+ <td>
+ <?= $this->url->link('#'.$user['id'], 'user', 'show', array('user_id' => $user['id'])) ?>
+ </td>
+ <td>
+ <?= $this->url->link($this->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </td>
+ <td>
+ <?= $this->e($user['name']) ?>
+ </td>
+ <td>
+ <a href="mailto:<?= $this->e($user['email']) ?>"><?= $this->e($user['email']) ?></a>
+ </td>
+ <td>
+ <?= $this->url->link(t('Remove this user'), 'group', 'dissociate', array('group_id' => $group['id'], 'user_id' => $user['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+ <?php endif ?>
+</section>
diff --git a/app/Template/header.php b/app/Template/header.php
index e8fd90c7..e646012a 100644
--- a/app/Template/header.php
+++ b/app/Template/header.php
@@ -1,6 +1,12 @@
<header>
<nav>
- <h1><?= $this->url->link('K<span>B</span>', 'app', 'index', array(), false, 'logo', t('Dashboard')).' '.$this->e($title) ?>
+ <h1>
+ <span class="logo">
+ <?= $this->url->link('K<span>B</span>', 'app', 'index', array(), false, '', t('Dashboard')) ?>
+ </span>
+ <span class="title">
+ <?= $this->e($title) ?>
+ </span>
<?php if (! empty($description)): ?>
<span class="tooltip" title='<?= $this->e($this->text->markdown($description)) ?>'>
<i class="fa fa-info-circle"></i>
@@ -13,6 +19,7 @@
<select id="board-selector"
class="chosen-select select-auto-redirect"
tabindex="-1"
+ data-search-threshold="0"
data-notfound="<?= t('No results match:') ?>"
data-placeholder="<?= t('Display another project') ?>"
data-redirect-regex="PROJECT_ID"
@@ -24,14 +31,75 @@
</select>
</li>
<?php endif ?>
- <li>
+ <li class="user-links">
<?php if ($this->user->hasNotifications()): ?>
- <?= $this->url->link('<i class="fa fa-bell web-notification-icon"></i>', 'app', 'notifications', array('user_id' => $this->user->getId()), false, '', t('Unread notifications')) ?>
+ <span class="notification">
+ <?= $this->url->link('<i class="fa fa-bell web-notification-icon"></i>', 'app', 'notifications', array('user_id' => $this->user->getId()), false, '', t('Unread notifications')) ?>
+ </span>
+ <?php endif ?>
+
+ <?php $has_project_creation_access = $this->user->hasAccess('ProjectCreation', 'create'); ?>
+ <?php $is_private_project_enabled = $this->app->config('disable_private_project', 0) == 0; ?>
+
+ <?php if ($has_project_creation_access || (!$has_project_creation_access && $is_private_project_enabled)): ?>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-plus fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <?php if ($has_project_creation_access): ?>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'ProjectCreation', 'create', array(), false, 'popover') ?></li>
+ <?php endif ?>
+ <?php if ($is_private_project_enabled): ?>
+ <li>
+ <i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'ProjectCreation', 'createPrivate', array(), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
<?php endif ?>
- <?= $this->url->link(t('Logout'), 'auth', 'logout') ?>
- <span class="username hide-tablet">(<?= $this->user->getProfileLink() ?>)</span>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-user fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li class="no-hover"><strong><?= $this->e($this->user->getFullname()) ?></strong></li>
+ <li>
+ <i class="fa fa-tachometer fa-fw"></i>
+ <?= $this->url->link(t('My dashboard'), 'app', 'index', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <i class="fa fa-home fa-fw"></i>
+ <?= $this->url->link(t('My profile'), 'user', 'show', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <i class="fa fa-folder fa-fw"></i>
+ <?= $this->url->link(t('Projects management'), 'project', 'index') ?>
+ </li>
+ <?php if ($this->user->hasAccess('user', 'index')): ?>
+ <li>
+ <i class="fa fa-user fa-fw"></i>
+ <?= $this->url->link(t('Users management'), 'user', 'index') ?>
+ </li>
+ <li>
+ <i class="fa fa-group fa-fw"></i>
+ <?= $this->url->link(t('Groups management'), 'group', 'index') ?>
+ </li>
+ <li>
+ <i class="fa fa-cog fa-fw"></i>
+ <?= $this->url->link(t('Settings'), 'config', 'index') ?>
+ </li>
+ <?php endif ?>
+ <li>
+ <i class="fa fa-life-ring fa-fw"></i>
+ <?= $this->url->link(t('Documentation'), 'doc', 'show') ?>
+ </li>
+ <?php if (! DISABLE_LOGOUT): ?>
+ <li>
+ <i class="fa fa-sign-out fa-fw"></i>
+ <?= $this->url->link(t('Logout'), 'auth', 'logout') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
</li>
</ul>
</nav>
-</header> \ No newline at end of file
+</header>
diff --git a/app/Template/layout.php b/app/Template/layout.php
index 20582952..0c81aac2 100644
--- a/app/Template/layout.php
+++ b/app/Template/layout.php
@@ -36,7 +36,7 @@
</head>
<body data-status-url="<?= $this->url->href('app', 'status') ?>"
data-login-url="<?= $this->url->href('auth', 'login') ?>"
- data-markdown-preview-url="<?= $this->url->href('app', 'preview') ?>"
+ data-markdown-preview-url="<?= $this->url->href('TaskHelper', 'preview') ?>"
data-timezone="<?= $this->app->getTimezone() ?>"
data-js-lang="<?= $this->app->jsLang() ?>">
diff --git a/app/Template/listing/show.php b/app/Template/listing/show.php
index aa17b228..9a5992e3 100644
--- a/app/Template/listing/show.php
+++ b/app/Template/listing/show.php
@@ -1,10 +1,13 @@
<section id="main">
- <?= $this->render('project/filters', array(
+ <?= $this->render('project_header/header', array(
'project' => $project,
'filters' => $filters,
+ 'custom_filters_list' => $custom_filters_list,
+ 'users_list' => $users_list,
+ 'categories_list' => $categories_list,
)) ?>
- <?php if (! empty($values['search']) && $paginator->isEmpty()): ?>
+ <?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('No tasks found.') ?></p>
<?php elseif (! $paginator->isEmpty()): ?>
<table class="table-fixed table-small">
@@ -21,7 +24,7 @@
<?php foreach ($paginator->getCollection() as $task): ?>
<tr>
<td class="task-table color-<?= $task['color_id'] ?>">
- <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
+ <?= $this->render('task/dropdown', array('task' => $task)) ?>
</td>
<td>
<?= $this->e($task['swimlane_name'] ?: $task['default_swimlane']) ?>
@@ -43,7 +46,7 @@
<?php endif ?>
</td>
<td>
- <?= dt('%B %e, %Y', $task['date_due']) ?>
+ <?= $this->dt->date($task['date_due']) ?>
</td>
<td>
<?php if ($task['is_active'] == \Kanboard\Model\Task::STATUS_OPEN): ?>
diff --git a/app/Template/notification/comment_user_mention.php b/app/Template/notification/comment_user_mention.php
new file mode 100644
index 00000000..59f5127e
--- /dev/null
+++ b/app/Template/notification/comment_user_mention.php
@@ -0,0 +1,7 @@
+<h2><?= t('You were mentioned in a comment on the task #%d', $task['id']) ?></h2>
+
+<p><?= $this->e($task['title']) ?></p>
+
+<?= $this->text->markdown($comment['comment']) ?>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/task_create.php b/app/Template/notification/task_create.php
index 1d834d44..fb7898fc 100644
--- a/app/Template/notification/task_create.php
+++ b/app/Template/notification/task_create.php
@@ -2,11 +2,11 @@
<ul>
<li>
- <?= dt('Created on %B %e, %Y at %k:%M %p', $task['date_creation']) ?>
+ <?= t('Created:').' '.$this->dt->datetime($task['date_creation']) ?>
</li>
<?php if ($task['date_due']): ?>
<li>
- <strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong>
+ <strong><?= t('Due date:').' '.$this->dt->date($task['date_due']) ?></strong>
</li>
<?php endif ?>
<?php if (! empty($task['creator_username'])): ?>
diff --git a/app/Template/notification/file_create.php b/app/Template/notification/task_file_create.php
index 63f7d1b8..63f7d1b8 100644
--- a/app/Template/notification/file_create.php
+++ b/app/Template/notification/task_file_create.php
diff --git a/app/Template/notification/task_overdue.php b/app/Template/notification/task_overdue.php
index a231937b..d7e6ff5a 100644
--- a/app/Template/notification/task_overdue.php
+++ b/app/Template/notification/task_overdue.php
@@ -9,7 +9,7 @@
<?php else: ?>
<?= $this->e($task['title']) ?>
<?php endif ?>
- (<?= dt('%B %e, %Y', $task['date_due']) ?>)
+ (<?= $this->dt->date($task['date_due']) ?>)
<?php if ($task['assignee_username']): ?>
(<strong><?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></strong>)
<?php endif ?>
diff --git a/app/Template/notification/task_user_mention.php b/app/Template/notification/task_user_mention.php
new file mode 100644
index 00000000..40ddddca
--- /dev/null
+++ b/app/Template/notification/task_user_mention.php
@@ -0,0 +1,7 @@
+<h2><?= t('You were mentioned in the task #%d', $task['id']) ?></h2>
+<p><?= $this->e($task['title']) ?></p>
+
+<h2><?= t('Description') ?></h2>
+<?= $this->text->markdown($task['description']) ?>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/password_reset/change.php b/app/Template/password_reset/change.php
new file mode 100644
index 00000000..6d06f442
--- /dev/null
+++ b/app/Template/password_reset/change.php
@@ -0,0 +1,16 @@
+<div class="form-login">
+ <h2><?= t('Password Reset') ?></h2>
+ <form method="post" action="<?= $this->url->href('PasswordReset', 'update', array('token' => $token)) ?>">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('New password'), 'password') ?>
+ <?= $this->form->password('password', $values, $errors) ?>
+
+ <?= $this->form->label(t('Confirmation'), 'confirmation') ?>
+ <?= $this->form->password('confirmation', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue">
+ </div>
+ </form>
+</div> \ No newline at end of file
diff --git a/app/Template/password_reset/create.php b/app/Template/password_reset/create.php
new file mode 100644
index 00000000..ef958011
--- /dev/null
+++ b/app/Template/password_reset/create.php
@@ -0,0 +1,17 @@
+<div class="form-login">
+ <h2><?= t('Password Reset') ?></h2>
+ <form method="post" action="<?= $this->url->href('PasswordReset', 'save') ?>">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Username'), 'username') ?>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?>
+
+ <?= $this->form->label(t('Enter the text below'), 'captcha') ?>
+ <img src="<?= $this->url->href('Captcha', 'image') ?>"/>
+ <?= $this->form->text('captcha', array(), $errors, array('required')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue"/>
+ </div>
+ </form>
+</div> \ No newline at end of file
diff --git a/app/Template/password_reset/email.php b/app/Template/password_reset/email.php
new file mode 100644
index 00000000..62788b49
--- /dev/null
+++ b/app/Template/password_reset/email.php
@@ -0,0 +1,6 @@
+<p><?= t('To reset your password click on this link:') ?></p>
+
+<p><?= $this->url->to('PasswordReset', 'change', array('token' => $token), '', true) ?></p>
+
+<hr>
+Kanboard \ No newline at end of file
diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php
index 1eb87b0e..980f9a44 100644
--- a/app/Template/project/dropdown.php
+++ b/app/Template/project/dropdown.php
@@ -2,10 +2,13 @@
<i class="fa fa-dashboard fa-fw"></i>&nbsp;
<?= $this->url->link(t('Activity'), 'activity', 'project', array('project_id' => $project['id'])) ?>
</li>
+
+<?php if ($this->user->hasProjectAccess('customfilter', 'index', $project['id'])): ?>
<li>
<i class="fa fa-filter fa-fw"></i>&nbsp;
<?= $this->url->link(t('Custom filters'), 'customfilter', 'index', array('project_id' => $project['id'])) ?>
</li>
+<?php endif ?>
<?php if ($project['is_public']): ?>
<li>
@@ -15,15 +18,21 @@
<?= $this->hook->render('template:project:dropdown', array('project' => $project)) ?>
-<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+<?php if ($this->user->hasProjectAccess('analytic', 'tasks', $project['id'])): ?>
<li>
<i class="fa fa-line-chart fa-fw"></i>&nbsp;
<?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?>
</li>
+<?php endif ?>
+
+<?php if ($this->user->hasProjectAccess('export', 'tasks', $project['id'])): ?>
<li>
<i class="fa fa-download fa-fw"></i>&nbsp;
<?= $this->url->link(t('Exports'), 'export', 'tasks', array('project_id' => $project['id'])) ?>
</li>
+<?php endif ?>
+
+<?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
<li>
<i class="fa fa-cog fa-fw"></i>&nbsp;
<?= $this->url->link(t('Settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
diff --git a/app/Template/project/duplicate.php b/app/Template/project/duplicate.php
index 8967c306..ca7d3302 100644
--- a/app/Template/project/duplicate.php
+++ b/app/Template/project/duplicate.php
@@ -10,13 +10,17 @@
<?= $this->form->csrf() ?>
+ <?php if ($project['is_private'] == 0): ?>
+ <?= $this->form->checkbox('projectPermission', t('Permissions'), 1, true) ?>
+ <?php endif ?>
+
<?= $this->form->checkbox('category', t('Categories'), 1, true) ?>
<?= $this->form->checkbox('action', t('Actions'), 1, true) ?>
<?= $this->form->checkbox('swimlane', t('Swimlanes'), 1, false) ?>
<?= $this->form->checkbox('task', t('Tasks'), 1, false) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Duplicate') ?>" class="btn btn-red"/>
+ <input type="submit" value="<?= t('Duplicate') ?>" class="btn btn-red">
<?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
</div>
</form>
diff --git a/app/Template/project/edit.php b/app/Template/project/edit.php
deleted file mode 100644
index 8dcbb88f..00000000
--- a/app/Template/project/edit.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit project') ?></h2>
-</div>
-<form method="post" action="<?= $this->url->href('project', 'update', array('project_id' => $project['id'])) ?>" autocomplete="off">
-
- <?= $this->form->csrf() ?>
- <?= $this->form->hidden('id', $values) ?>
-
- <?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors, array('required', 'maxlength="50"')) ?>
-
- <?= $this->form->label(t('Identifier'), 'identifier') ?>
- <?= $this->form->text('identifier', $values, $errors, array('maxlength="50"')) ?>
- <p class="form-help"><?= t('The project identifier is an optional alphanumeric code used to identify your project.') ?></p>
-
- <?= $this->form->label(t('Start date'), 'start_date') ?>
- <?= $this->form->text('start_date', $values, $errors, array('maxlength="10"'), 'form-date') ?>
-
- <?= $this->form->label(t('End date'), 'end_date') ?>
- <?= $this->form->text('end_date', $values, $errors, array('maxlength="10"'), 'form-date') ?>
-
- <?php if ($this->user->isAdmin() || $this->user->isProjectAdministrationAllowed($project['id'])): ?>
- <?= $this->form->checkbox('is_private', t('Private project'), 1, $project['is_private'] == 1) ?>
- <?php endif ?>
-
- <?= $this->form->label(t('Description'), 'description') ?>
-
- <div class="form-tabs">
-
- <div class="write-area">
- <?= $this->form->textarea('description', $values, $errors) ?>
- </div>
- <div class="preview-area">
- <div class="markdown"></div>
- </div>
- <ul class="form-tabs-nav">
- <li class="form-tab form-tab-selected">
- <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a>
- </li>
- <li class="form-tab">
- <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a>
- </li>
- </ul>
- </div>
- <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
diff --git a/app/Template/project/filters.php b/app/Template/project/filters.php
deleted file mode 100644
index c17cfb3c..00000000
--- a/app/Template/project/filters.php
+++ /dev/null
@@ -1,101 +0,0 @@
-<div class="page-header">
- <div class="dropdown">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?php if (isset($is_board)): ?>
- <li>
- <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>>
- <i class="fa fa-expand fa-fw"></i>
- <?= $this->url->link(t('Expand tasks'), 'board', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
- </span>
- <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? 'style="display: none;"' : '' ?>>
- <i class="fa fa-compress fa-fw"></i>
- <?= $this->url->link(t('Collapse tasks'), 'board', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
- </span>
- </li>
- <li>
- <span class="filter-compact">
- <i class="fa fa-th fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Compact view') ?></a>
- </span>
- <span class="filter-wide" style="display: none">
- <i class="fa fa-arrows-h fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Horizontal scrolling') ?></a>
- </span>
- </li>
- <li>
- <span class="filter-max-height" style="display: none">
- <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Set maximum column height') ?></a>
- </span>
- <span class="filter-min-height">
- <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Remove maximum column height') ?></a>
- </span>
- </li>
- <?php endif ?>
- <?= $this->render('project/dropdown', array('project' => $project)) ?>
- </ul>
- </div>
- <ul class="views">
- <li <?= $filters['controller'] === 'board' ? 'class="active"' : '' ?>>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Board'), 'board', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-board', t('Keyboard shortcut: "%s"', 'v b')) ?>
- </li>
- <li <?= $filters['controller'] === 'calendar' ? 'class="active"' : '' ?>>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?>
- </li>
- <li <?= $filters['controller'] === 'listing' ? 'class="active"' : '' ?>>
- <i class="fa fa-list fa-fw"></i>
- <?= $this->url->link(t('List'), 'listing', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-listing', t('Keyboard shortcut: "%s"', 'v l')) ?>
- </li>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
- <li <?= $filters['controller'] === 'gantt' ? 'class="active"' : '' ?>>
- <i class="fa fa-sliders fa-fw"></i>
- <?= $this->url->link(t('Gantt'), 'gantt', 'project', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-gantt', t('Keyboard shortcut: "%s"', 'v g')) ?>
- </li>
- <?php endif ?>
- </ul>
- <form method="get" action="<?= $this->url->dir() ?>" class="search">
- <?= $this->form->hidden('controller', $filters) ?>
- <?= $this->form->hidden('action', $filters) ?>
- <?= $this->form->hidden('project_id', $filters) ?>
- <?= $this->form->text('search', $filters, array(), array('placeholder="'.t('Filter').'"'), 'form-input-large') ?>
- </form>
-
- <div class="filter-dropdowns">
- <?= $this->render('app/filters_helper', array('reset' => 'status:open')) ?>
-
- <?php if (isset($custom_filters_list) && ! empty($custom_filters_list)): ?>
- <div class="dropdown filters">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('My filters') ?></a>
- <ul>
- <?php foreach ($custom_filters_list as $filter): ?>
- <li><a href="#" class="filter-helper" data-<?php if ($filter['append']): ?><?= 'append-' ?><?php endif ?>filter='<?= $this->e($filter['filter']) ?>'><?= $this->e($filter['name']) ?></a></li>
- <?php endforeach ?>
- </ul>
- </div>
- <?php endif ?>
-
- <?php if (isset($users_list)): ?>
- <div class="dropdown filters">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Users') ?></a>
- <ul>
- <li><a href="#" class="filter-helper" data-append-filter="assignee:nobody"><?= t('Not assigned') ?></a></li>
- <?php foreach ($users_list as $user): ?>
- <li><a href="#" class="filter-helper" data-append-filter='assignee:"<?= $this->e($user) ?>"'><?= $this->e($user) ?></a></li>
- <?php endforeach ?>
- </ul>
- </div>
- <?php endif ?>
-
- <?php if (isset($categories_list) && ! empty($categories_list)): ?>
- <div class="dropdown filters">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Categories') ?></a>
- <ul>
- <li><a href="#" class="filter-helper" data-append-filter="category:none"><?= t('No category') ?></a></li>
- <?php foreach ($categories_list as $category): ?>
- <li><a href="#" class="filter-helper" data-append-filter='category:"<?= $this->e($category) ?>"'><?= $this->e($category) ?></a></li>
- <?php endforeach ?>
- </ul>
- </div>
- <?php endif ?>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Template/project/index.php b/app/Template/project/index.php
index 4b62a27f..8d384e58 100644
--- a/app/Template/project/index.php
+++ b/app/Template/project/index.php
@@ -1,12 +1,10 @@
<section id="main">
<div class="page-header">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
- <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li>
- <?php endif ?>
- <li><i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?></li>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('Users overview'), 'projectuser', 'managers') ?></li>
+ <?php endif ?>
+ <?php if ($this->user->hasAccess('gantt', 'projects')): ?>
<li><i class="fa fa-sliders fa-fw"></i><?= $this->url->link(t('Projects Gantt chart'), 'gantt', 'projects') ?></li>
<?php endif ?>
</ul>
@@ -21,9 +19,9 @@
<th class="column-15"><?= $paginator->order(t('Project'), 'name') ?></th>
<th class="column-8"><?= $paginator->order(t('Start date'), 'start_date') ?></th>
<th class="column-8"><?= $paginator->order(t('End date'), 'end_date') ?></th>
- <?php if ($this->user->isAdmin() || $this->user->isProjectAdmin()): ?>
- <th class="column-12"><?= t('Managers') ?></th>
- <th class="column-12"><?= t('Members') ?></th>
+ <th class="column-15"><?= $paginator->order(t('Owner'), 'owner_id') ?></th>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
+ <th class="column-10"><?= t('Users') ?></th>
<?php endif ?>
<th><?= t('Columns') ?></th>
</tr>
@@ -59,30 +57,21 @@
<?= $this->url->link($this->e($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?>
</td>
<td>
- <?= $project['start_date'] ?>
- </td>
- <td>
- <?= $project['end_date'] ?>
+ <?= $this->dt->date($project['start_date']) ?>
</td>
- <?php if ($this->user->isAdmin() || $this->user->isProjectAdmin()): ?>
<td>
- <ul class="no-bullet">
- <?php foreach ($project['managers'] as $user_id => $user_name): ?>
- <li><?= $this->url->link($this->e($user_name), 'projectuser', 'opens', array('user_id' => $user_id)) ?></li>
- <?php endforeach ?>
- </ul>
+ <?= $this->dt->date($project['end_date']) ?>
</td>
<td>
- <?php if ($project['is_everybody_allowed'] == 1): ?>
- <?= t('Everybody') ?>
- <?php else: ?>
- <ul class="no-bullet">
- <?php foreach ($project['members'] as $user_id => $user_name): ?>
- <li><?= $this->url->link($this->e($user_name), 'projectuser', 'opens', array('user_id' => $user_id)) ?></li>
- <?php endforeach ?>
- </ul>
+ <?php if ($project['owner_id'] > 0): ?>
+ <?= $this->e($project['owner_name'] ?: $project['owner_username']) ?>
<?php endif ?>
</td>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
+ <td>
+ <i class="fa fa-users fa-fw"></i>
+ <a href="#" class="tooltip" title="<?= t('Members') ?>" data-href="<?= $this->url->href('Projectuser', 'users', array('project_id' => $project['id'])) ?>"><?= t('Members') ?></a>
+ </td>
<?php endif ?>
<td class="dashboard-project-stats">
<?php foreach ($project['columns'] as $column): ?>
diff --git a/app/Template/project/integrations.php b/app/Template/project/integrations.php
index c4d9385b..54720c69 100644
--- a/app/Template/project/integrations.php
+++ b/app/Template/project/integrations.php
@@ -5,23 +5,11 @@
<form method="post" action="<?= $this->url->href('project', 'integrations', array('project_id' => $project['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
- <?= $this->hook->render('template:project:integrations', array('values' => $values)) ?>
+ <?php $integrations = $this->hook->render('template:project:integrations', array('project' => $project, 'values' => $values, 'webhook_token' => $webhook_token)) ?>
- <h3><i class="fa fa-github fa-fw"></i>&nbsp;<?= t('Github webhooks') ?></h3>
- <div class="listing">
- <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('webhook', 'github', array('token' => $webhook_token, 'project_id' => $project['id']), false, '', true) ?>"/><br/>
- <p class="form-help"><?= $this->url->doc(t('Help on Github webhooks'), 'github-webhooks') ?></p>
- </div>
-
- <h3><img src="<?= $this->url->dir() ?>assets/img/gitlab-icon.png"/>&nbsp;<?= t('Gitlab webhooks') ?></h3>
- <div class="listing">
- <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('webhook', 'gitlab', array('token' => $webhook_token, 'project_id' => $project['id']), false, '', true) ?>"/><br/>
- <p class="form-help"><?= $this->url->doc(t('Help on Gitlab webhooks'), 'gitlab-webhooks') ?></p>
- </div>
-
- <h3><i class="fa fa-bitbucket fa-fw"></i>&nbsp;<?= t('Bitbucket webhooks') ?></h3>
- <div class="listing">
- <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('webhook', 'bitbucket', array('token' => $webhook_token, 'project_id' => $project['id']), false, '', true) ?>"/><br/>
- <p class="form-help"><?= $this->url->doc(t('Help on Bitbucket webhooks'), 'bitbucket-webhooks') ?></p>
- </div>
+ <?php if (empty($integrations)): ?>
+ <p class="alert"><?= t('There is no integration registered at the moment.') ?></p>
+ <?php else: ?>
+ <?= $integrations ?>
+ <?php endif ?>
</form> \ No newline at end of file
diff --git a/app/Template/project/layout.php b/app/Template/project/layout.php
index 8ba92ef9..eb391ae5 100644
--- a/app/Template/project/layout.php
+++ b/app/Template/project/layout.php
@@ -30,7 +30,7 @@
<?= $this->render($sidebar_template, array('project' => $project)) ?>
<div class="sidebar-content">
- <?= $project_content_for_layout ?>
+ <?= $content_for_sublayout ?>
</div>
</section>
</section> \ No newline at end of file
diff --git a/app/Template/project/new.php b/app/Template/project/new.php
deleted file mode 100644
index 8e4ccfec..00000000
--- a/app/Template/project/new.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<section id="main">
- <div class="page-header">
- <ul>
- <li><i class="fa fa-folder fa-fw"></i><?= $this->url->link(t('All projects'), 'project', 'index') ?></li>
- </ul>
- </div>
- <form method="post" action="<?= $this->url->href('project', 'save') ?>" autocomplete="off">
-
- <?= $this->form->csrf() ?>
- <?= $this->form->hidden('is_private', $values) ?>
- <?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'index') ?>
- </div>
- </form>
- <?php if (isset($is_private) && $is_private): ?>
- <div class="alert alert-info">
- <p><?= t('There is no user management for private projects.') ?></p>
- </div>
- <?php endif ?>
-</section> \ No newline at end of file
diff --git a/app/Template/project/notifications.php b/app/Template/project/notifications.php
index ac743087..b39d6c05 100644
--- a/app/Template/project/notifications.php
+++ b/app/Template/project/notifications.php
@@ -2,7 +2,7 @@
<h2><?= t('Notifications') ?></h2>
</div>
<?php if (empty($types)): ?>
- <p class="alert"><?= t('There is no notification method registered.') ?></p>
+ <p class="alert"><?= t('No plugin has registered a project notification method. You can still configure individual notifications in your user profile.') ?></p>
<?php else: ?>
<form method="post" action="<?= $this->url->href('project', 'notifications', array('project_id' => $project['id'])) ?>" autocomplete="off">
diff --git a/app/Template/project/show.php b/app/Template/project/show.php
index 5a65a26e..166b8902 100644
--- a/app/Template/project/show.php
+++ b/app/Template/project/show.php
@@ -4,6 +4,10 @@
<ul class="listing">
<li><strong><?= $project['is_active'] ? t('Active') : t('Inactive') ?></strong></li>
+ <?php if ($project['owner_id'] > 0): ?>
+ <li><?= t('Project owner: ') ?><strong><?= $this->e($project['owner_name'] ?: $project['owner_username']) ?></strong></li>
+ <?php endif ?>
+
<?php if ($project['is_private']): ?>
<li><i class="fa fa-lock"></i> <?= t('This project is private') ?></li>
<?php endif ?>
@@ -17,15 +21,15 @@
<?php endif ?>
<?php if ($project['last_modified']): ?>
- <li><?= dt('Last modified on %B %e, %Y at %k:%M %p', $project['last_modified']) ?></li>
+ <li><?= t('Modified:').' '.$this->dt->datetime($project['last_modified']) ?></li>
<?php endif ?>
<?php if ($project['start_date']): ?>
- <li><?= t('Start date: %s', $project['start_date']) ?></li>
+ <li><?= t('Start date: ').$this->dt->date($project['start_date']) ?></li>
<?php endif ?>
<?php if ($project['end_date']): ?>
- <li><?= t('End date: %s', $project['end_date']) ?></li>
+ <li><?= t('End date: ').$this->dt->date($project['end_date']) ?></li>
<?php endif ?>
<?php if ($stats['nb_tasks'] > 0): ?>
diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php
index fb5dd3bd..304b4aee 100644
--- a/app/Template/project/sidebar.php
+++ b/app/Template/project/sidebar.php
@@ -1,65 +1,66 @@
<div class="sidebar">
<h2><?= t('Actions') ?></h2>
<ul>
- <li <?= $this->app->getRouterAction() === 'show' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('project', 'show') ?>>
<?= $this->url->link(t('Summary'), 'project', 'show', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'customfilter' && $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
+ <?php if ($this->user->hasProjectAccess('customfilter', 'index', $project['id'])): ?>
+ <li <?= $this->app->checkMenuSelection('customfilter') ?>>
<?= $this->url->link(t('Custom filters'), 'customfilter', 'index', array('project_id' => $project['id'])) ?>
</li>
+ <?php endif ?>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'share' ? 'class="active"' : '' ?>>
+ <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
+ <li <?= $this->app->checkMenuSelection('ProjectEdit', 'edit') ?>>
+ <?= $this->url->link(t('Edit project'), 'ProjectEdit', 'edit', array('project_id' => $project['id'])) ?>
+ </li>
+ <li <?= $this->app->checkMenuSelection('project', 'share') ?>>
<?= $this->url->link(t('Public access'), 'project', 'share', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'notifications' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('project', 'notifications') ?>>
<?= $this->url->link(t('Notifications'), 'project', 'notifications', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'integrations' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('project', 'integrations') ?>>
<?= $this->url->link(t('Integrations'), 'project', 'integrations', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'edit' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Edit project'), 'project', 'edit', array('project_id' => $project['id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'column' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('column') ?>>
<?= $this->url->link(t('Columns'), 'column', 'index', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'swimlane' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('swimlane') ?>>
<?= $this->url->link(t('Swimlanes'), 'swimlane', 'index', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'category' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('category') ?>>
<?= $this->url->link(t('Categories'), 'category', 'index', array('project_id' => $project['id'])) ?>
</li>
- <?php if ($this->user->isAdmin() || $project['is_private'] == 0): ?>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'users' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Users'), 'project', 'users', array('project_id' => $project['id'])) ?>
+ <?php if ($project['is_private'] == 0): ?>
+ <li <?= $this->app->checkMenuSelection('ProjectPermission') ?>>
+ <?= $this->url->link(t('Permissions'), 'ProjectPermission', 'index', array('project_id' => $project['id'])) ?>
</li>
<?php endif ?>
- <li <?= $this->app->getRouterController() === 'action' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('action') ?>>
<?= $this->url->link(t('Automatic actions'), 'action', 'index', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'duplicate' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('project', 'duplicate') ?>>
<?= $this->url->link(t('Duplicate'), 'project', 'duplicate', array('project_id' => $project['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'project' && ($this->app->getRouterAction() === 'disable' || $this->app->getRouterAction() === 'enable') ? 'class="active"' : '' ?>>
<?php if ($project['is_active']): ?>
+ <li <?= $this->app->checkMenuSelection('project', 'disable') ?>>
<?= $this->url->link(t('Disable'), 'project', 'disable', array('project_id' => $project['id']), true) ?>
<?php else: ?>
+ <li <?= $this->app->checkMenuSelection('project', 'enable') ?>>
<?= $this->url->link(t('Enable'), 'project', 'enable', array('project_id' => $project['id']), true) ?>
<?php endif ?>
</li>
- <li <?= $this->app->getRouterController() === 'taskImport' && $this->app->getRouterAction() === 'step1' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('taskImport') ?>>
<?= $this->url->link(t('Import'), 'taskImport', 'step1', array('project_id' => $project['id'])) ?>
</li>
- <?php if ($this->user->isProjectAdministrationAllowed($project['id'])): ?>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
+ <?php if ($this->user->hasProjectAccess('project', 'remove', $project['id'])): ?>
+ <li <?= $this->app->checkMenuSelection('project', 'remove') ?>>
<?= $this->url->link(t('Remove'), 'project', 'remove', array('project_id' => $project['id'])) ?>
</li>
<?php endif ?>
<?php endif ?>
- <?= $this->hook->render('template:project:sidebar') ?>
+ <?= $this->hook->render('template:project:sidebar', array('project' => $project)) ?>
</ul>
- <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
- <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div>
diff --git a/app/Template/project/users.php b/app/Template/project/users.php
deleted file mode 100644
index 8863a1e4..00000000
--- a/app/Template/project/users.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<div class="page-header">
- <h2><?= t('List of authorized users') ?></h2>
-</div>
-
-<?php if ($project['is_everybody_allowed']): ?>
- <div class="alert"><?= t('Everybody have access to this project.') ?></div>
-<?php else: ?>
-
- <?php if (empty($users['allowed'])): ?>
- <div class="alert alert-error"><?= t('Nobody have access to this project.') ?></div>
- <?php else: ?>
- <table>
- <tr>
- <th><?= t('User') ?></th>
- <th><?= t('Role for this project') ?></th>
- <?php if ($project['is_private'] == 0): ?>
- <th><?= t('Actions') ?></th>
- <?php endif ?>
- </tr>
- <?php foreach ($users['allowed'] as $user_id => $username): ?>
- <tr>
- <td><?= $this->e($username) ?></td>
- <td><?= isset($users['managers'][$user_id]) ? t('Project manager') : t('Project member') ?></td>
- <?php if ($project['is_private'] == 0): ?>
- <td>
- <ul>
- <li><?= $this->url->link(t('Revoke'), 'project', 'revoke', array('project_id' => $project['id'], 'user_id' => $user_id), true) ?></li>
- <li>
- <?php if (isset($users['managers'][$user_id])): ?>
- <?= $this->url->link(t('Set project member'), 'project', 'role', array('project_id' => $project['id'], 'user_id' => $user_id, 'is_owner' => 0), true) ?>
- <?php else: ?>
- <?= $this->url->link(t('Set project manager'), 'project', 'role', array('project_id' => $project['id'], 'user_id' => $user_id, 'is_owner' => 1), true) ?>
- <?php endif ?>
- </li>
- </ul>
- </td>
- <?php endif ?>
- </tr>
- <?php endforeach ?>
- </table>
- <?php endif ?>
-
- <?php if ($project['is_private'] == 0 && ! empty($users['not_allowed'])): ?>
- <hr/>
- <form method="post" action="<?= $this->url->href('project', 'allow', array('project_id' => $project['id'])) ?>" autocomplete="off">
-
- <?= $this->form->csrf() ?>
-
- <?= $this->form->hidden('project_id', array('project_id' => $project['id'])) ?>
-
- <?= $this->form->label(t('User'), 'user_id') ?>
- <?= $this->form->select('user_id', $users['not_allowed'], array(), array(), array('data-notfound="'.t('No results match:').'"'), 'chosen-select') ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Allow this user') ?>" class="btn btn-blue"/>
- </div>
- </form>
- <?php endif ?>
-
-<?php endif ?>
-
-<?php if ($project['is_private'] == 0): ?>
-<hr/>
-<form method="post" action="<?= $this->url->href('project', 'allowEverybody', array('project_id' => $project['id'])) ?>">
- <?= $this->form->csrf() ?>
-
- <?= $this->form->hidden('id', array('id' => $project['id'])) ?>
- <?= $this->form->checkbox('is_everybody_allowed', t('Allow everybody to access to this project'), 1, $project['is_everybody_allowed']) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
-<?php endif ?>
-
-<div class="alert alert-info">
- <ul>
- <li><?= t('A project manager can change the settings of the project and have more privileges than a standard user.') ?></li>
- <li><?= t('Don\'t forget that administrators have access to everything.') ?></li>
- <li><?= $this->url->doc(t('Help with project permissions'), 'project-permissions') ?></li>
- </ul>
-</div>
diff --git a/app/Template/project_creation/create.php b/app/Template/project_creation/create.php
new file mode 100644
index 00000000..46ec5d1e
--- /dev/null
+++ b/app/Template/project_creation/create.php
@@ -0,0 +1,42 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= $title ?></h2>
+ </div>
+ <form class="popover-form" id="project-creation-form" method="post" action="<?= $this->url->href('ProjectCreation', 'save') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('is_private', $values) ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
+
+ <?php if (count($projects_list) > 1): ?>
+ <?= $this->form->label(t('Create from another project'), 'src_project_id') ?>
+ <?= $this->form->select('src_project_id', $projects_list, $values) ?>
+ <?php endif ?>
+
+ <div class="project-creation-options" <?= isset($values['src_project_id']) && $values['src_project_id'] > 0 ? '' : 'style="display: none"' ?>>
+ <p class="alert"><?= t('Which parts of the project do you want to duplicate?') ?></p>
+
+ <?php if (! $is_private): ?>
+ <?= $this->form->checkbox('projectPermission', t('Permissions'), 1, true) ?>
+ <?php endif ?>
+
+ <?= $this->form->checkbox('category', t('Categories'), 1, true) ?>
+ <?= $this->form->checkbox('action', t('Actions'), 1, true) ?>
+ <?= $this->form->checkbox('swimlane', t('Swimlanes'), 1, true) ?>
+ <?= $this->form->checkbox('task', t('Tasks'), 1, false) ?>
+ </div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'project', 'index', array(), false, 'close-popover') ?>
+ </div>
+ </form>
+ <?php if ($is_private): ?>
+ <div class="alert alert-info">
+ <p><?= t('There is no user management for private projects.') ?></p>
+ </div>
+ <?php endif ?>
+</section> \ No newline at end of file
diff --git a/app/Template/project_edit/dates.php b/app/Template/project_edit/dates.php
new file mode 100644
index 00000000..cb585c6a
--- /dev/null
+++ b/app/Template/project_edit/dates.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <h2><?= t('Edit project') ?></h2>
+ <ul>
+ <li ><?= $this->url->link(t('General'), 'ProjectEdit', 'edit', array('project_id' => $project['id'])) ?></li>
+ <li class="active"><?= $this->url->link(t('Dates'), 'ProjectEdit', 'dates', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Description'), 'ProjectEdit', 'description', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Task priority'), 'ProjectEdit', 'priority', array('project_id' => $project['id'])) ?></li>
+ </ul>
+</div>
+<form method="post" action="<?= $this->url->href('ProjectEdit', 'update', array('project_id' => $project['id'], 'redirect' => 'dates')) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('name', $values) ?>
+
+ <?= $this->form->label(t('Start date'), 'start_date') ?>
+ <?= $this->form->text('start_date', $values, $errors, array('maxlength="10"'), 'form-date') ?>
+
+ <?= $this->form->label(t('End date'), 'end_date') ?>
+ <?= $this->form->text('end_date', $values, $errors, array('maxlength="10"'), 'form-date') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<p class="alert alert-info"><?= t('Those dates are useful for the project Gantt chart.') ?></p>
diff --git a/app/Template/project_edit/description.php b/app/Template/project_edit/description.php
new file mode 100644
index 00000000..dce8ab10
--- /dev/null
+++ b/app/Template/project_edit/description.php
@@ -0,0 +1,37 @@
+<div class="page-header">
+ <h2><?= t('Edit project') ?></h2>
+ <ul>
+ <li><?= $this->url->link(t('General'), 'ProjectEdit', 'edit', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Dates'), 'ProjectEdit', 'dates', array('project_id' => $project['id'])) ?></li>
+ <li class="active"><?= $this->url->link(t('Description'), 'ProjectEdit', 'description', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Task priority'), 'ProjectEdit', 'priority', array('project_id' => $project['id'])) ?></li>
+ </ul>
+</div>
+<form method="post" action="<?= $this->url->href('ProjectEdit', 'update', array('project_id' => $project['id'], 'redirect' => 'description')) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('name', $values) ?>
+
+ <?= $this->form->label(t('Description'), 'description') ?>
+ <div class="form-tabs">
+ <div class="write-area">
+ <?= $this->form->textarea('description', $values, $errors) ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ <ul class="form-tabs-nav">
+ <li class="form-tab form-tab-selected">
+ <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a>
+ </li>
+ <li class="form-tab">
+ <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a>
+ </li>
+ </ul>
+ </div>
+ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
diff --git a/app/Template/project_edit/general.php b/app/Template/project_edit/general.php
new file mode 100644
index 00000000..28cbb66a
--- /dev/null
+++ b/app/Template/project_edit/general.php
@@ -0,0 +1,36 @@
+<div class="page-header">
+ <h2><?= t('Edit project') ?></h2>
+ <ul>
+ <li class="active"><?= $this->url->link(t('General'), 'ProjectEdit', 'edit', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Dates'), 'ProjectEdit', 'dates', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Description'), 'ProjectEdit', 'description', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Task priority'), 'ProjectEdit', 'priority', array('project_id' => $project['id'])) ?></li>
+ </ul>
+</div>
+<form method="post" action="<?= $this->url->href('ProjectEdit', 'update', array('project_id' => $project['id'], 'redirect' => 'edit')) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array('required', 'maxlength="50"')) ?>
+
+ <?= $this->form->label(t('Identifier'), 'identifier') ?>
+ <?= $this->form->text('identifier', $values, $errors, array('maxlength="50"')) ?>
+ <p class="form-help"><?= t('The project identifier is optional and must be alphanumeric, example: MYPROJECT.') ?></p>
+
+ <hr>
+ <div class="form-inline">
+ <?= $this->form->label(t('Project owner'), 'owner_id') ?>
+ <?= $this->form->select('owner_id', $owners, $values, $errors) ?>
+ </div>
+
+ <?php if ($this->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])): ?>
+ <hr>
+ <?= $this->form->checkbox('is_private', t('Private project'), 1, $project['is_private'] == 1) ?>
+ <p class="form-help"><?= t('Private projects do not have users and groups management.') ?></p>
+ <?php endif ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
diff --git a/app/Template/project_edit/task_priority.php b/app/Template/project_edit/task_priority.php
new file mode 100644
index 00000000..e54215b2
--- /dev/null
+++ b/app/Template/project_edit/task_priority.php
@@ -0,0 +1,29 @@
+<div class="page-header">
+ <h2><?= t('Edit project') ?></h2>
+ <ul>
+ <li ><?= $this->url->link(t('General'), 'ProjectEdit', 'edit', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Dates'), 'ProjectEdit', 'dates', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->url->link(t('Description'), 'ProjectEdit', 'description', array('project_id' => $project['id'])) ?></li>
+ <li class="active"><?= $this->url->link(t('Task priority'), 'ProjectEdit', 'priority', array('project_id' => $project['id'])) ?></li>
+ </ul>
+</div>
+<form method="post" action="<?= $this->url->href('ProjectEdit', 'update', array('project_id' => $project['id'], 'redirect' => 'priority')) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('name', $values) ?>
+
+ <?= $this->form->label(t('Default priority'), 'priority_default') ?>
+ <?= $this->form->number('priority_default', $values, $errors) ?>
+
+ <?= $this->form->label(t('Lowest priority'), 'priority_start') ?>
+ <?= $this->form->number('priority_start', $values, $errors) ?>
+
+ <?= $this->form->label(t('Highest priority'), 'priority_end') ?>
+ <?= $this->form->number('priority_end', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<p class="alert alert-info"><?= t('If you put zero to the low and high priority, this feature will be disabled.') ?></p>
diff --git a/app/Template/project_file/create.php b/app/Template/project_file/create.php
new file mode 100644
index 00000000..67315285
--- /dev/null
+++ b/app/Template/project_file/create.php
@@ -0,0 +1,33 @@
+<div class="page-header">
+ <h2><?= t('Attach a document') ?></h2>
+</div>
+<div id="file-done" style="display:none">
+ <p class="alert alert-success">
+ <?= t('All files have been uploaded successfully.') ?>
+ <?= $this->url->link(t('View uploaded files'), 'ProjectOverview', 'show', array('project_id' => $project['id'])) ?>
+ </p>
+</div>
+
+<div id="file-error-max-size" style="display:none">
+ <p class="alert alert-error">
+ <?= t('The maximum allowed file size is %sB.', $this->text->bytes($max_size)) ?>
+ <a href="#" id="file-browser"><?= t('Choose files again') ?></a>
+ </p>
+</div>
+
+<div
+ id="file-dropzone"
+ data-max-size="<?= $max_size ?>"
+ data-url="<?= $this->url->href('ProjectFile', 'save', array('project_id' => $project['id'])) ?>">
+ <div id="file-dropzone-inner">
+ <?= t('Drag and drop your files here') ?> <?= t('or') ?> <a href="#" id="file-browser"><?= t('choose files') ?></a>
+ </div>
+</div>
+
+<input type="file" name="files[]" multiple style="display:none" id="file-form-element">
+
+<div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" id="file-upload-button" disabled>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
+</div>
diff --git a/app/Template/project_file/remove.php b/app/Template/project_file/remove.php
new file mode 100644
index 00000000..4f0ba465
--- /dev/null
+++ b/app/Template/project_file/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove a file') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this file: "%s"?', $this->e($file['name'])) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'ProjectFile', 'remove', array('project_id' => $project['id'], 'file_id' => $file['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/project_header/dropdown.php b/app/Template/project_header/dropdown.php
new file mode 100644
index 00000000..bbc033bf
--- /dev/null
+++ b/app/Template/project_header/dropdown.php
@@ -0,0 +1,34 @@
+<div class="dropdown">
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <?php if ($is_board): ?>
+ <li>
+ <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>>
+ <i class="fa fa-expand fa-fw"></i>
+ <?= $this->url->link(t('Expand tasks'), 'board', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
+ </span>
+ <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? 'style="display: none;"' : '' ?>>
+ <i class="fa fa-compress fa-fw"></i>
+ <?= $this->url->link(t('Collapse tasks'), 'board', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
+ </span>
+ </li>
+ <li>
+ <span class="filter-compact">
+ <i class="fa fa-th fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Compact view') ?></a>
+ </span>
+ <span class="filter-wide" style="display: none">
+ <i class="fa fa-arrows-h fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Horizontal scrolling') ?></a>
+ </span>
+ </li>
+ <li>
+ <span class="filter-max-height" style="display: none">
+ <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Set maximum column height') ?></a>
+ </span>
+ <span class="filter-min-height">
+ <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Remove maximum column height') ?></a>
+ </span>
+ </li>
+ <?php endif ?>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/project_header/header.php b/app/Template/project_header/header.php
new file mode 100644
index 00000000..f6e5af9e
--- /dev/null
+++ b/app/Template/project_header/header.php
@@ -0,0 +1,15 @@
+<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/views', array('project' => $project, 'filters' => $filters)) ?>
+ <?= $this->render('project_header/search', array(
+ 'project' => $project,
+ 'filters' => $filters,
+ 'custom_filters_list' => isset($custom_filters_list) ? $custom_filters_list : array(),
+ 'users_list' => isset($users_list) ? $users_list : array(),
+ 'categories_list' => isset($categories_list) ? $categories_list : array(),
+ )) ?>
+
+ <?= $this->hook->render('template:project:header:after', array('project' => $project)) ?>
+</div> \ No newline at end of file
diff --git a/app/Template/project_header/search.php b/app/Template/project_header/search.php
new file mode 100644
index 00000000..2b2a2c39
--- /dev/null
+++ b/app/Template/project_header/search.php
@@ -0,0 +1,45 @@
+<div class="filter-box">
+ <form method="get" action="<?= $this->url->dir() ?>" class="search">
+ <?= $this->form->hidden('controller', $filters) ?>
+ <?= $this->form->hidden('action', $filters) ?>
+ <?= $this->form->hidden('project_id', $filters) ?>
+ <?= $this->form->text('search', $filters, array(), array('placeholder="'.t('Filter').'"')) ?>
+
+ <?= $this->render('app/filters_helper', array('reset' => 'status:open', 'project' => $project)) ?>
+
+ <?php if (isset($custom_filters_list) && ! empty($custom_filters_list)): ?>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Custom filters') ?>"><i class="fa fa-bookmark fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <?php foreach ($custom_filters_list as $filter): ?>
+ <li><a href="#" class="filter-helper" data-<?php if ($filter['append']): ?><?= 'append-' ?><?php endif ?>filter='<?= $this->e($filter['filter']) ?>'><?= $this->e($filter['name']) ?></a></li>
+ <?php endforeach ?>
+ </ul>
+ </div>
+ <?php endif ?>
+
+ <?php if (isset($users_list)): ?>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('User filters') ?>"><i class="fa fa-users fa-fw"></i> <i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><a href="#" class="filter-helper" data-append-filter="assignee:nobody"><?= t('Not assigned') ?></a></li>
+ <?php foreach ($users_list as $user): ?>
+ <li><a href="#" class="filter-helper" data-append-filter='assignee:"<?= $this->e($user) ?>"'><?= $this->e($user) ?></a></li>
+ <?php endforeach ?>
+ </ul>
+ </div>
+ <?php endif ?>
+
+ <?php if (isset($categories_list) && ! empty($categories_list)): ?>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Category filters') ?>"><i class="fa fa-tags fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><a href="#" class="filter-helper" data-append-filter="category:none"><?= t('No category') ?></a></li>
+ <?php foreach ($categories_list as $category): ?>
+ <li><a href="#" class="filter-helper" data-append-filter='category:"<?= $this->e($category) ?>"'><?= $this->e($category) ?></a></li>
+ <?php endforeach ?>
+ </ul>
+ </div>
+ <?php endif ?>
+ </form>
+</div>
diff --git a/app/Template/project_header/views.php b/app/Template/project_header/views.php
new file mode 100644
index 00000000..f8fdbb02
--- /dev/null
+++ b/app/Template/project_header/views.php
@@ -0,0 +1,24 @@
+<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')) ?>
+ </li>
+ <li <?= $this->app->getRouterController() === 'Board' ? 'class="active"' : '' ?>>
+ <i class="fa fa-th fa-fw"></i>
+ <?= $this->url->link(t('Board'), 'board', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-board', t('Keyboard shortcut: "%s"', 'v b')) ?>
+ </li>
+ <li <?= $this->app->getRouterController() === 'Calendar' ? 'class="active"' : '' ?>>
+ <i class="fa fa-calendar fa-fw"></i>
+ <?= $this->url->link(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?>
+ </li>
+ <li <?= $this->app->getRouterController() === 'Listing' ? 'class="active"' : '' ?>>
+ <i class="fa fa-list fa-fw"></i>
+ <?= $this->url->link(t('List'), 'listing', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-listing', t('Keyboard shortcut: "%s"', 'v l')) ?>
+ </li>
+ <?php if ($this->user->hasProjectAccess('gantt', 'project', $project['id'])): ?>
+ <li <?= $this->app->getRouterController() === 'Gantt' ? 'class="active"' : '' ?>>
+ <i class="fa fa-sliders fa-fw"></i>
+ <?= $this->url->link(t('Gantt'), 'gantt', 'project', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-gantt', t('Keyboard shortcut: "%s"', 'v g')) ?>
+ </li>
+ <?php endif ?>
+</ul> \ No newline at end of file
diff --git a/app/Template/project_overview/columns.php b/app/Template/project_overview/columns.php
new file mode 100644
index 00000000..870d753f
--- /dev/null
+++ b/app/Template/project_overview/columns.php
@@ -0,0 +1,8 @@
+<div class="project-overview-columns">
+ <?php foreach ($project['columns'] as $column): ?>
+ <div class="project-overview-column">
+ <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong><br>
+ <span><?= $this->e($column['title']) ?></span>
+ </div>
+ <?php endforeach ?>
+</div>
diff --git a/app/Template/project_overview/description.php b/app/Template/project_overview/description.php
new file mode 100644
index 00000000..4137bf9f
--- /dev/null
+++ b/app/Template/project_overview/description.php
@@ -0,0 +1,8 @@
+<?php if (! empty($project['description'])): ?>
+ <div class="page-header">
+ <h2><?= $this->e($project['name']) ?></h2>
+ </div>
+ <article class="markdown">
+ <?= $this->text->markdown($project['description']) ?>
+ </article>
+<?php endif ?>
diff --git a/app/Template/project_overview/files.php b/app/Template/project_overview/files.php
new file mode 100644
index 00000000..7eb8c762
--- /dev/null
+++ b/app/Template/project_overview/files.php
@@ -0,0 +1,98 @@
+<div class="page-header">
+ <h2><?= t('Attachments') ?></h2>
+ <?php if ($this->user->hasProjectAccess('ProjectFile', 'create', $project['id'])): ?>
+ <ul>
+ <li>
+ <i class="fa fa-plus fa-fw"></i>
+ <?= $this->url->link(t('Upload a file'), 'ProjectFile', 'create', array('project_id' => $project['id']), false, 'popover') ?>
+ </li>
+ </ul>
+ <?php endif ?>
+</div>
+
+<?php if (empty($files) && empty($images)): ?>
+ <p class="alert"><?= t('There is no attachment at the moment.') ?></p>
+<?php endif ?>
+
+<?php if (! empty($images)): ?>
+<div class="file-thumbnails">
+ <?php foreach ($images as $file): ?>
+ <div class="file-thumbnail">
+ <a href="<?= $this->url->href('FileViewer', 'show', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?>" class="popover"><img src="<?= $this->url->href('FileViewer', 'thumbnail', array('file_id' => $file['id'], 'project_id' => $project['id'])) ?>" title="<?= $this->e($file['name']) ?>" alt="<?= $this->e($file['name']) ?>"></a>
+ <div class="file-thumbnail-content">
+ <div class="file-thumbnail-title">
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->e($file['name']) ?> <i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li>
+ <i class="fa fa-download fa-fw"></i>
+ <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?>
+ </li>
+ <?php if ($this->user->hasProjectAccess('ProjectFile', 'remove', $project['id'])): ?>
+ <li>
+ <i class="fa fa-trash fa-fw"></i>
+ <?= $this->url->link(t('Remove'), 'ProjectFile', 'confirm', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ </div>
+ <div class="file-thumbnail-description">
+ <span class="tooltip" title='<?= t('Uploaded: %s', $this->dt->datetime($file['date'])).'<br>'.t('Size: %s', $this->text->bytes($file['size'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?= t('Uploaded by %s', $file['user_name'] ?: $file['username']) ?>
+ </div>
+ </div>
+ </div>
+ <?php endforeach ?>
+</div>
+<?php endif ?>
+
+<?php if (! empty($files)): ?>
+<table class="table-stripped">
+ <tr>
+ <th><?= t('Filename') ?></th>
+ <th><?= t('Creator') ?></th>
+ <th><?= t('Date') ?></th>
+ <th><?= t('Size') ?></th>
+ </tr>
+ <?php foreach ($files as $file): ?>
+ <tr>
+ <td>
+ <i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->e($file['name']) ?> <i class="fa fa-caret-down"></i></a>
+ <ul>
+ <?php if ($this->file->getPreviewType($file['name']) !== null): ?>
+ <li>
+ <i class="fa fa-eye fa-fw"></i>
+ <?= $this->url->link(t('View file'), 'FileViewer', 'show', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ <li>
+ <i class="fa fa-download fa-fw"></i>
+ <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?>
+ </li>
+ <?php if ($this->user->hasProjectAccess('ProjectFile', 'remove', $project['id'])): ?>
+ <li>
+ <i class="fa fa-trash fa-fw"></i>
+ <?= $this->url->link(t('Remove'), 'ProjectFile', 'confirm', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ </td>
+ <td>
+ <?= $this->e($file['user_name'] ?: $file['username']) ?>
+ </td>
+ <td>
+ <?= $this->dt->date($file['date']) ?>
+ </td>
+ <td>
+ <?= $this->text->bytes($file['size']) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+<?php endif ?>
diff --git a/app/Template/project_overview/information.php b/app/Template/project_overview/information.php
new file mode 100644
index 00000000..12a1317d
--- /dev/null
+++ b/app/Template/project_overview/information.php
@@ -0,0 +1,35 @@
+<div class="page-header">
+ <h2><?= t('Information') ?></h2>
+</div>
+<div class="listing">
+<ul>
+ <?php if ($project['owner_id'] > 0): ?>
+ <li><?= t('Project owner: ') ?><strong><?= $this->e($project['owner_name'] ?: $project['owner_username']) ?></strong></li>
+ <?php endif ?>
+
+ <?php if (! empty($users)): ?>
+ <?php foreach ($roles as $role => $role_name): ?>
+ <?php if (isset($users[$role])): ?>
+ <li>
+ <?= $role_name ?>:
+ <strong><?= implode(', ', $users[$role]) ?></strong>
+ </li>
+ <?php endif ?>
+ <?php endforeach ?>
+ <?php endif ?>
+
+ <?php if ($project['start_date']): ?>
+ <li><?= t('Start date: ').$this->dt->date($project['start_date']) ?></li>
+ <?php endif ?>
+
+ <?php if ($project['end_date']): ?>
+ <li><?= t('End date: ').$this->dt->date($project['end_date']) ?></li>
+ <?php endif ?>
+
+ <?php if ($project['is_public']): ?>
+ <li><i class="fa fa-share-alt"></i> <?= $this->url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?></li>
+ <li><i class="fa fa-rss-square"></i> <?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li>
+ <li><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
+ <?php endif ?>
+</ul>
+</div>
diff --git a/app/Template/project_overview/show.php b/app/Template/project_overview/show.php
new file mode 100644
index 00000000..0038d952
--- /dev/null
+++ b/app/Template/project_overview/show.php
@@ -0,0 +1,16 @@
+<section id="main">
+ <?= $this->render('project_header/header', array(
+ 'project' => $project,
+ 'filters' => $filters,
+ )) ?>
+
+ <?= $this->render('project_overview/columns', array('project' => $project)) ?>
+ <?= $this->render('project_overview/description', array('project' => $project)) ?>
+ <?= $this->render('project_overview/files', array('project' => $project, 'images' => $images, 'files' => $files)) ?>
+ <?= $this->render('project_overview/information', array('project' => $project, 'users' => $users, 'roles' => $roles)) ?>
+
+ <div class="page-header">
+ <h2><?= t('Last activity') ?></h2>
+ </div>
+ <?= $this->render('event/events', array('events' => $events)) ?>
+</section>
diff --git a/app/Template/project_permission/index.php b/app/Template/project_permission/index.php
new file mode 100644
index 00000000..5f0edc2b
--- /dev/null
+++ b/app/Template/project_permission/index.php
@@ -0,0 +1,141 @@
+<div class="page-header">
+ <h2><?= t('Allowed Users') ?></h2>
+</div>
+
+<?php if ($project['is_everybody_allowed']): ?>
+ <div class="alert"><?= t('Everybody have access to this project.') ?></div>
+<?php else: ?>
+
+ <?php if (empty($users)): ?>
+ <div class="alert"><?= t('No user have been allowed specifically.') ?></div>
+ <?php else: ?>
+ <table>
+ <tr>
+ <th class="column-50"><?= t('User') ?></th>
+ <th><?= t('Role') ?></th>
+ <?php if ($project['is_private'] == 0): ?>
+ <th class="column-15"><?= t('Actions') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($users as $user): ?>
+ <tr>
+ <td><?= $this->e($user['name'] ?: $user['username']) ?></td>
+ <td>
+ <?= $this->form->select(
+ 'role-'.$user['id'],
+ $roles,
+ array('role-'.$user['id'] => $user['role']),
+ array(),
+ array('data-url="'.$this->url->href('ProjectPermission', 'changeUserRole', array('project_id' => $project['id'])).'"', 'data-id="'.$user['id'].'"'),
+ 'project-change-role'
+ ) ?>
+ </td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'ProjectPermission', 'removeUser', array('project_id' => $project['id'], 'user_id' => $user['id']), true) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+
+ <?php if ($project['is_private'] == 0): ?>
+ <div class="listing">
+ <form method="post" action="<?= $this->url->href('ProjectPermission', 'addUser', array('project_id' => $project['id'])) ?>" autocomplete="off" class="form-inline">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', array('project_id' => $project['id'])) ?>
+ <?= $this->form->hidden('user_id', $values) ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array(
+ 'required',
+ 'placeholder="'.t('Enter user name...').'"',
+ 'title="'.t('Enter user name...').'"',
+ 'data-dst-field="user_id"',
+ 'data-search-url="'.$this->url->href('UserHelper', 'autocomplete').'"',
+ ),
+ 'autocomplete') ?>
+
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
+
+ <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
+ </form>
+ </div>
+ <?php endif ?>
+
+ <div class="page-header">
+ <h2><?= t('Allowed Groups') ?></h2>
+ </div>
+
+ <?php if (empty($groups)): ?>
+ <div class="alert"><?= t('No group have been allowed specifically.') ?></div>
+ <?php else: ?>
+ <table>
+ <tr>
+ <th class="column-50"><?= t('Group') ?></th>
+ <th><?= t('Role') ?></th>
+ <?php if ($project['is_private'] == 0): ?>
+ <th class="column-15"><?= t('Actions') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($groups as $group): ?>
+ <tr>
+ <td><?= $this->e($group['name']) ?></td>
+ <td>
+ <?= $this->form->select(
+ 'role-'.$group['id'],
+ $roles,
+ array('role-'.$group['id'] => $group['role']),
+ array(),
+ array('data-url="'.$this->url->href('ProjectPermission', 'changeGroupRole', array('project_id' => $project['id'])).'"', 'data-id="'.$group['id'].'"'),
+ 'project-change-role'
+ ) ?>
+ </td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'ProjectPermission', 'removeGroup', array('project_id' => $project['id'], 'group_id' => $group['id']), true) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+
+ <?php if ($project['is_private'] == 0): ?>
+ <div class="listing">
+ <form method="post" action="<?= $this->url->href('ProjectPermission', 'addGroup', array('project_id' => $project['id'])) ?>" autocomplete="off" class="form-inline">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', array('project_id' => $project['id'])) ?>
+ <?= $this->form->hidden('group_id', $values) ?>
+ <?= $this->form->hidden('external_id', $values) ?>
+
+ <?= $this->form->label(t('Group Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array(
+ 'required',
+ 'placeholder="'.t('Enter group name...').'"',
+ 'title="'.t('Enter group name...').'"',
+ 'data-dst-field="group_id"',
+ 'data-dst-extra-field="external_id"',
+ 'data-search-url="'.$this->url->href('GroupHelper', 'autocomplete').'"',
+ ),
+ 'autocomplete') ?>
+
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
+
+ <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
+ </form>
+ </div>
+ <?php endif ?>
+
+<?php endif ?>
+
+<?php if ($project['is_private'] == 0): ?>
+<hr/>
+<form method="post" action="<?= $this->url->href('ProjectPermission', 'allowEverybody', array('project_id' => $project['id'])) ?>">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', array('id' => $project['id'])) ?>
+ <?= $this->form->checkbox('is_everybody_allowed', t('Allow everybody to access to this project'), 1, $project['is_everybody_allowed']) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+<?php endif ?>
diff --git a/app/Template/project_user/layout.php b/app/Template/project_user/layout.php
index 4cf732d6..3ced5590 100644
--- a/app/Template/project_user/layout.php
+++ b/app/Template/project_user/layout.php
@@ -1,18 +1,11 @@
<section id="main">
<div class="page-header">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
- <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li>
- <?php endif ?>
- <li>
- <i class="fa fa-lock fa-fw"></i>
- <?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?>
- </li>
<li>
<i class="fa fa-folder fa-fw"></i>
<?= $this->url->link(t('Projects list'), 'project', 'index') ?>
</li>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('gantt', 'projects')): ?>
<li>
<i class="fa fa-sliders fa-fw"></i>
<?= $this->url->link(t('Projects Gantt chart'), 'gantt', 'projects') ?>
@@ -22,7 +15,7 @@
</div>
<section class="sidebar-container">
- <?= $this->render('project_user/sidebar', array('users' => $users, 'filter' => $filter)) ?>
+ <?= $this->render($sidebar_template, array('users' => $users, 'filter' => $filter)) ?>
<div class="sidebar-content">
<div class="page-header">
diff --git a/app/Template/project_user/sidebar.php b/app/Template/project_user/sidebar.php
index b81ba14a..ff113ebb 100644
--- a/app/Template/project_user/sidebar.php
+++ b/app/Template/project_user/sidebar.php
@@ -10,18 +10,18 @@
'chosen-select select-auto-redirect'
) ?>
- <br/><br/>
+ <br><br>
<ul>
- <li <?= $this->app->getRouterAction() === 'managers' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('projectuser', 'managers') ?>>
<?= $this->url->link(t('Project managers'), 'projectuser', 'managers', $filter) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'members' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('projectuser', 'members') ?>>
<?= $this->url->link(t('Project members'), 'projectuser', 'members', $filter) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'opens' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('projectuser', 'opens') ?>>
<?= $this->url->link(t('Open tasks'), 'projectuser', 'opens', $filter) ?>
</li>
- <li <?= $this->app->getRouterAction() === 'closed' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('projectuser', 'closed') ?>>
<?= $this->url->link(t('Closed tasks'), 'projectuser', 'closed', $filter) ?>
</li>
diff --git a/app/Template/project_user/tasks.php b/app/Template/project_user/tasks.php
index f4fc2723..8d1cbd96 100644
--- a/app/Template/project_user/tasks.php
+++ b/app/Template/project_user/tasks.php
@@ -33,10 +33,10 @@
<?php endif ?>
</td>
<td>
- <?= dt('%B %e, %Y', $task['date_started']) ?>
+ <?= $this->dt->date($task['date_started']) ?>
</td>
<td>
- <?= dt('%B %e, %Y', $task['date_due']) ?>
+ <?= $this->dt->date($task['date_due']) ?>
</td>
</tr>
<?php endforeach ?>
diff --git a/app/Template/project_user/tooltip_users.php b/app/Template/project_user/tooltip_users.php
new file mode 100644
index 00000000..7a07caad
--- /dev/null
+++ b/app/Template/project_user/tooltip_users.php
@@ -0,0 +1,14 @@
+<?php if (empty($users)): ?>
+ <p><?= t('There is no project member.') ?></p>
+<?php else: ?>
+ <?php foreach ($roles as $role => $role_name): ?>
+ <?php if (isset($users[$role])): ?>
+ <strong><?= $role_name ?></strong>
+ <ul>
+ <?php foreach ($users[$role] as $user_id => $user): ?>
+ <li><?= $this->url->link($this->e($user), 'Projectuser', 'opens', array('user_id' => $user_id)) ?></li>
+ <?php endforeach ?>
+ </ul>
+ <?php endif ?>
+ <?php endforeach ?>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/search/index.php b/app/Template/search/index.php
index 329c072a..9231a6f3 100644
--- a/app/Template/search/index.php
+++ b/app/Template/search/index.php
@@ -8,14 +8,13 @@
</ul>
</div>
- <div class="search">
+ <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('app/filters_helper') ?>
</form>
-
- <?= $this->render('app/filters_helper') ?>
</div>
<?php if (empty($values['search'])): ?>
diff --git a/app/Template/search/results.php b/app/Template/search/results.php
index 88eed87c..3bb0e603 100644
--- a/app/Template/search/results.php
+++ b/app/Template/search/results.php
@@ -38,7 +38,7 @@
<?php endif ?>
</td>
<td>
- <?= dt('%B %e, %Y', $task['date_due']) ?>
+ <?= $this->dt->date($task['date_due']) ?>
</td>
<td>
<?php if ($task['is_active'] == \Kanboard\Model\Task::STATUS_OPEN): ?>
diff --git a/app/Template/subtask/create.php b/app/Template/subtask/create.php
index 82e378f5..8fffd3a9 100644
--- a/app/Template/subtask/create.php
+++ b/app/Template/subtask/create.php
@@ -2,26 +2,19 @@
<h2><?= t('Add a sub-task') ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
-
<?= $this->form->hidden('task_id', $values) ?>
-
- <?= $this->form->label(t('Title'), 'title') ?>
- <?= $this->form->text('title', $values, $errors, array('required', 'autofocus', 'maxlength="255"')) ?><br/>
-
- <?= $this->form->label(t('Assignee'), 'user_id') ?>
- <?= $this->form->select('user_id', $users_list, $values, $errors) ?><br/>
-
- <?= $this->form->label(t('Original estimate'), 'time_estimated') ?>
- <?= $this->form->numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
+ <?= $this->subtask->selectTitle($values, $errors, array('autofocus')) ?>
+ <?= $this->subtask->selectAssignee($users_list, $values, $errors) ?>
+ <?= $this->subtask->selectTimeEstimated($values, $errors) ?>
<?= $this->form->checkbox('another_subtask', t('Create another sub-task'), 1, isset($values['another_subtask']) && $values['another_subtask'] == 1) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
diff --git a/app/Template/subtask/edit.php b/app/Template/subtask/edit.php
index 2e583069..acce625e 100644
--- a/app/Template/subtask/edit.php
+++ b/app/Template/subtask/edit.php
@@ -2,28 +2,19 @@
<h2><?= t('Edit a sub-task') ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('subtask', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('subtask', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
-
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('task_id', $values) ?>
-
- <?= $this->form->label(t('Title'), 'title') ?>
- <?= $this->form->text('title', $values, $errors, array('required', 'autofocus', 'maxlength="255"')) ?><br/>
-
- <?= $this->form->label(t('Assignee'), 'user_id') ?>
- <?= $this->form->select('user_id', $users_list, $values, $errors) ?><br/>
-
- <?= $this->form->label(t('Original estimate'), 'time_estimated') ?>
- <?= $this->form->numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
-
- <?= $this->form->label(t('Time spent'), 'time_spent') ?>
- <?= $this->form->numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/>
+ <?= $this->subtask->selectTitle($values, $errors, array('autofocus')) ?>
+ <?= $this->subtask->selectAssignee($users_list, $values, $errors) ?>
+ <?= $this->subtask->selectTimeEstimated($values, $errors) ?>
+ <?= $this->subtask->selectTimeSpent($values, $errors) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
diff --git a/app/Template/subtask/icons.php b/app/Template/subtask/icons.php
deleted file mode 100644
index 1f31d51f..00000000
--- a/app/Template/subtask/icons.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php if ($subtask['status'] == 0): ?>
- <i class="fa fa-square-o fa-fw"></i>
-<?php elseif ($subtask['status'] == 1): ?>
- <i class="fa fa-gears fa-fw"></i>
-<?php else: ?>
- <i class="fa fa-check-square-o fa-fw"></i>
-<?php endif ?> \ No newline at end of file
diff --git a/app/Template/subtask/menu.php b/app/Template/subtask/menu.php
new file mode 100644
index 00000000..6c98b951
--- /dev/null
+++ b/app/Template/subtask/menu.php
@@ -0,0 +1,11 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?>
+ </li>
+ </ul>
+</div>
diff --git a/app/Template/subtask/remove.php b/app/Template/subtask/remove.php
index 65ade31d..9aef6842 100644
--- a/app/Template/subtask/remove.php
+++ b/app/Template/subtask/remove.php
@@ -12,6 +12,6 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'subtask', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/subtask/show.php b/app/Template/subtask/show.php
index 1f0f9bba..b0326c48 100644
--- a/app/Template/subtask/show.php
+++ b/app/Template/subtask/show.php
@@ -1,93 +1,12 @@
-<?php if (! empty($subtasks)): ?>
-
-<?php $first_position = $subtasks[0]['position']; ?>
-<?php $last_position = $subtasks[count($subtasks) - 1]['position']; ?>
-
-<div id="subtasks" class="task-show-section">
-
- <div class="page-header">
- <h2><?= t('Sub-Tasks') ?></h2>
- </div>
+<div class="page-header">
+ <h2><?= t('Sub-Tasks') ?></h2>
+</div>
- <table class="subtasks-table">
- <tr>
- <th class="column-40"><?= t('Title') ?></th>
- <th><?= t('Assignee') ?></th>
- <th><?= t('Time tracking') ?></th>
- <?php if (! isset($not_editable)): ?>
- <th><?= t('Actions') ?></th>
- <?php endif ?>
- </tr>
- <?php foreach ($subtasks as $subtask): ?>
- <tr>
- <td>
- <?php if (! isset($not_editable)): ?>
- <?= $this->subtask->toggleStatus($subtask, 'task') ?>
- <?php else: ?>
- <?= $this->render('subtask/icons', array('subtask' => $subtask)) . $this->e($subtask['title']) ?>
- <?php endif ?>
- </td>
- <td>
- <?php if (! empty($subtask['username'])): ?>
- <?php if (! isset($not_editable)): ?>
- <?= $this->url->link($this->e($subtask['name'] ?: $subtask['username']), 'user', 'show', array('user_id' => $subtask['user_id'])) ?>
- <?php else: ?>
- <?= $this->e($subtask['name'] ?: $subtask['username']) ?>
- <?php endif ?>
- <?php endif ?>
- </td>
- <td>
- <ul class="no-bullet">
- <li>
- <?php if (! empty($subtask['time_spent'])): ?>
- <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?>
- <?php endif ?>
+<div id="subtasks">
- <?php if (! empty($subtask['time_estimated'])): ?>
- <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
- <?php endif ?>
- </li>
- <?php if (! isset($not_editable) && $subtask['user_id'] == $this->user->getId()): ?>
- <li>
- <?php if ($subtask['is_timer_started']): ?>
- <i class="fa fa-pause"></i>
- <?= $this->url->link(t('Stop timer'), 'timer', 'subtask', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
- (<?= $this->dt->age($subtask['timer_start_date']) ?>)
- <?php else: ?>
- <i class="fa fa-play-circle-o"></i>
- <?= $this->url->link(t('Start timer'), 'timer', 'subtask', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
- <?php endif ?>
- </li>
- <?php endif ?>
- </ul>
- </td>
- <?php if (! isset($not_editable)): ?>
- <td>
- <ul>
- <?php if ($subtask['position'] != $first_position): ?>
- <li>
- <?= $this->url->link(t('Move Up'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'up'), true) ?>
- </li>
- <?php endif ?>
- <?php if ($subtask['position'] != $last_position): ?>
- <li>
- <?= $this->url->link(t('Move Down'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'down'), true) ?>
- </li>
- <?php endif ?>
- <li>
- <?= $this->url->link(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>
- </li>
- <li>
- <?= $this->url->link(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>
- </li>
- </ul>
- </td>
- <?php endif ?>
- </tr>
- <?php endforeach ?>
- </table>
+ <?= $this->render('subtask/table', array('subtasks' => $subtasks, 'task' => $task, 'editable' => $editable)) ?>
- <?php if (! isset($not_editable)): ?>
+ <?php if ($editable && $this->user->hasProjectAccess('subtask', 'save', $task['project_id'])): ?>
<form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
@@ -99,4 +18,3 @@
<?php endif ?>
</div>
-<?php endif ?>
diff --git a/app/Template/subtask/table.php b/app/Template/subtask/table.php
new file mode 100644
index 00000000..13d2c7cc
--- /dev/null
+++ b/app/Template/subtask/table.php
@@ -0,0 +1,71 @@
+<?php if (! empty($subtasks)): ?>
+ <table
+ class="subtasks-table table-stripped"
+ data-save-position-url="<?= $this->url->href('Subtask', 'movePosition', array('project_id' => $task['project_id'], 'task_id' => $task['id'])) ?>"
+ >
+ <thead>
+ <tr>
+ <th class="column-40"><?= t('Title') ?></th>
+ <th><?= t('Assignee') ?></th>
+ <th><?= t('Time tracking') ?></th>
+ <?php if ($editable): ?>
+ <th class="column-5"></th>
+ <?php endif ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($subtasks as $subtask): ?>
+ <tr data-subtask-id="<?= $subtask['id'] ?>">
+ <td>
+ <?php if ($editable): ?>
+ <i class="fa fa-arrows-alt draggable-row-handle" title="<?= t('Change subtask position') ?>"></i>
+ <?= $this->subtask->toggleStatus($subtask, $task['project_id'], true) ?>
+ <?php else: ?>
+ <?= $this->subtask->getTitle($subtask) ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?php if (! empty($subtask['username'])): ?>
+ <?= $this->e($subtask['name'] ?: $subtask['username']) ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <ul class="no-bullet">
+ <li>
+ <?php if (! empty($subtask['time_spent'])): ?>
+ <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?>
+ <?php endif ?>
+
+ <?php if (! empty($subtask['time_estimated'])): ?>
+ <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
+ <?php endif ?>
+ </li>
+ <?php if ($editable && $subtask['user_id'] == $this->user->getId()): ?>
+ <li>
+ <?php if ($subtask['is_timer_started']): ?>
+ <i class="fa fa-pause"></i>
+ <?= $this->url->link(t('Stop timer'), 'SubtaskStatus', 'timer', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?>
+ (<?= $this->dt->age($subtask['timer_start_date']) ?>)
+ <?php else: ?>
+ <i class="fa fa-play-circle-o"></i>
+ <?= $this->url->link(t('Start timer'), 'SubtaskStatus', 'timer', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?>
+ <?php endif ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </td>
+ <?php if ($editable): ?>
+ <td>
+ <?= $this->render('subtask/menu', array(
+ 'task' => $task,
+ 'subtask' => $subtask,
+ )) ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php else: ?>
+ <p class="alert"><?= t('There is no subtask at the moment.') ?></p>
+<?php endif ?>
diff --git a/app/Template/subtask/restriction_change_status.php b/app/Template/subtask_restriction/popover.php
index 88e91d82..e80d6b6d 100644
--- a/app/Template/subtask/restriction_change_status.php
+++ b/app/Template/subtask_restriction/popover.php
@@ -1,18 +1,16 @@
<div class="page-header">
<h2><?= t('You already have one subtask in progress') ?></h2>
</div>
-
- <form action="<?= $this->url->href('subtask', 'changeRestrictionStatus', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" method="post">
+<form class="popover-form" action="<?= $this->url->href('SubtaskRestriction', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" method="post">
<?= $this->form->csrf() ?>
- <?= $this->form->hidden('redirect', array('redirect' => $redirect)) ?>
<p><?= t('Select the new status of the subtask: "%s"', $subtask_inprogress['title']) ?></p>
<?= $this->form->radios('status', $status_list) ?>
<?= $this->form->hidden('id', $subtask_inprogress) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-red"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-red">
<?= t('or') ?>
<a href="#" class="close-popover"><?= t('cancel') ?></a>
</div>
diff --git a/app/Template/swimlane/create.php b/app/Template/swimlane/create.php
new file mode 100644
index 00000000..bb389555
--- /dev/null
+++ b/app/Template/swimlane/create.php
@@ -0,0 +1,37 @@
+<div class="page-header">
+ <h2><?= t('Add a new swimlane') ?></h2>
+</div>
+<form class="popover-form" method="post" action="<?= $this->url->href('swimlane', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
+
+ <?= $this->form->label(t('Description'), 'description') ?>
+
+ <div class="form-tabs">
+ <div class="write-area">
+ <?= $this->form->textarea('description', $values, $errors) ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ <ul class="form-tabs-nav">
+ <li class="form-tab form-tab-selected">
+ <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a>
+ </li>
+ <li class="form-tab">
+ <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a>
+ </li>
+ </ul>
+ </div>
+ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'Swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+</form>
diff --git a/app/Template/swimlane/edit.php b/app/Template/swimlane/edit.php
index dfc5cf0b..31d819d4 100644
--- a/app/Template/swimlane/edit.php
+++ b/app/Template/swimlane/edit.php
@@ -2,7 +2,7 @@
<h2><?= t('Swimlane modification for the project "%s"', $project['name']) ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('swimlane', 'update', array('project_id' => $project['id'], 'swimlane_id' => $values['id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('swimlane', 'update', array('project_id' => $project['id'], 'swimlane_id' => $values['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
@@ -34,8 +34,8 @@
<div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</form> \ No newline at end of file
diff --git a/app/Template/swimlane/edit_default.php b/app/Template/swimlane/edit_default.php
new file mode 100644
index 00000000..df25ec12
--- /dev/null
+++ b/app/Template/swimlane/edit_default.php
@@ -0,0 +1,18 @@
+<div class="page-header">
+ <h2><?= t('Change default swimlane') ?></h2>
+</div>
+<form class="popover-form" method="post" action="<?= $this->url->href('swimlane', 'updateDefault', array('project_id' => $project['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+
+ <?= $this->form->label(t('Name'), 'default_swimlane') ?>
+ <?= $this->form->text('default_swimlane', $values, $errors, array('required', 'maxlength="50"')) ?>
+
+ <?= $this->form->checkbox('show_default_swimlane', t('Show default swimlane'), 1, $values['show_default_swimlane'] == 1) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'Swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+</form>
diff --git a/app/Template/swimlane/index.php b/app/Template/swimlane/index.php
index 9502cffd..fad35306 100644
--- a/app/Template/swimlane/index.php
+++ b/app/Template/swimlane/index.php
@@ -1,71 +1,28 @@
<div class="page-header">
- <h2><?= t('Change default swimlane') ?></h2>
+ <h2><?= t('Swimlanes') ?></h2>
+ <ul>
+ <li>
+ <i class="fa fa-plus fa-fw"></i>
+ <?= $this->url->link(t('Add a new swimlane'), 'Swimlane', 'create', array('project_id' => $project['id']), false, 'popover') ?>
+ </li>
+ </ul>
</div>
-<form method="post" action="<?= $this->url->href('swimlane', 'change', array('project_id' => $project['id'])) ?>" autocomplete="off">
- <?= $this->form->csrf() ?>
- <?= $this->form->hidden('id', $default_swimlane) ?>
-
- <?= $this->form->label(t('Rename'), 'default_swimlane') ?>
- <?= $this->form->text('default_swimlane', $default_swimlane, array(), array('required', 'maxlength="50"')) ?><br/>
-
- <?php if (! empty($active_swimlanes) || $default_swimlane['show_default_swimlane'] == 0): ?>
- <?= $this->form->checkbox('show_default_swimlane', t('Show default swimlane'), 1, $default_swimlane['show_default_swimlane'] == 1) ?>
- <?php else: ?>
- <?= $this->form->hidden('show_default_swimlane', $default_swimlane) ?>
- <?php endif ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
-
-<?php if (! empty($active_swimlanes)): ?>
-<div class="page-header">
- <h2><?= t('Active swimlanes') ?></h2>
-</div>
-<?= $this->render('swimlane/table', array('swimlanes' => $active_swimlanes, 'project' => $project)) ?>
+<?php if (! empty($active_swimlanes) || $default_swimlane['show_default_swimlane'] == 1): ?>
+<h3><?= t('Active swimlanes') ?></h3>
+ <?= $this->render('swimlane/table', array(
+ 'swimlanes' => $active_swimlanes,
+ 'project' => $project,
+ 'default_swimlane' => $default_swimlane['show_default_swimlane'] == 1 ? $default_swimlane : array()
+ )) ?>
<?php endif ?>
-<?php if (! empty($inactive_swimlanes)): ?>
-<div class="page-header">
- <h2><?= t('Inactive swimlanes') ?></h2>
-</div>
-<?= $this->render('swimlane/table', array('swimlanes' => $inactive_swimlanes, 'project' => $project, 'hide_position' => true)) ?>
+<?php if (! empty($inactive_swimlanes) || $default_swimlane['show_default_swimlane'] == 0): ?>
+ <h3><?= t('Inactive swimlanes') ?></h3>
+ <?= $this->render('swimlane/table', array(
+ 'swimlanes' => $inactive_swimlanes,
+ 'project' => $project,
+ 'default_swimlane' => $default_swimlane['show_default_swimlane'] == 0 ? $default_swimlane : array(),
+ 'disable_handler' => true
+ )) ?>
<?php endif ?>
-
-<div class="page-header">
- <h2><?= t('Add a new swimlane') ?></h2>
-</div>
-<form method="post" action="<?= $this->url->href('swimlane', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
-
- <?= $this->form->csrf() ?>
- <?= $this->form->hidden('project_id', $values) ?>
-
- <?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors, array('required', 'maxlength="50"')) ?>
-
- <?= $this->form->label(t('Description'), 'description') ?>
-
- <div class="form-tabs">
- <div class="write-area">
- <?= $this->form->textarea('description', $values, $errors) ?>
- </div>
- <div class="preview-area">
- <div class="markdown"></div>
- </div>
- <ul class="form-tabs-nav">
- <li class="form-tab form-tab-selected">
- <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a>
- </li>
- <li class="form-tab">
- <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a>
- </li>
- </ul>
- </div>
- <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
diff --git a/app/Template/swimlane/remove.php b/app/Template/swimlane/remove.php
index 1d7c2b7a..9be39ff8 100644
--- a/app/Template/swimlane/remove.php
+++ b/app/Template/swimlane/remove.php
@@ -11,7 +11,7 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'swimlane', 'remove', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</div>
</section> \ No newline at end of file
diff --git a/app/Template/swimlane/table.php b/app/Template/swimlane/table.php
index b708e633..1e6a86bc 100644
--- a/app/Template/swimlane/table.php
+++ b/app/Template/swimlane/table.php
@@ -1,44 +1,76 @@
-<table>
- <tr>
- <?php if (! isset($hide_position)): ?>
- <th><?= t('Position') ?></th>
- <?php endif ?>
- <th class="column-60"><?= t('Name') ?></th>
- <th class="column-35"><?= t('Actions') ?></th>
- </tr>
- <?php foreach ($swimlanes as $swimlane): ?>
- <tr>
- <?php if (! isset($hide_position)): ?>
- <td>#<?= $swimlane['position'] ?></td>
- <?php endif ?>
- <td><?= $this->e($swimlane['name']) ?></td>
- <td>
- <ul>
- <?php if ($swimlane['position'] != 0 && $swimlane['position'] != 1): ?>
+<table
+ class="swimlanes-table table-stripped"
+ data-save-position-url="<?= $this->url->href('Swimlane', 'move', array('project_id' => $project['id'])) ?>">
+ <thead>
+ <tr>
+ <th><?= t('Name') ?></th>
+ <th class="column-8"><?= t('Actions') ?></th>
+ </tr>
+
+ <?php if (! empty($default_swimlane)): ?>
+ <tr>
+ <td>
+ <?= $this->e($default_swimlane['default_swimlane']) ?>
+ <?php if ($default_swimlane['default_swimlane'] !== t('Default swimlane')): ?>
+ &nbsp;(<?= t('Default swimlane') ?>)
+ <?php endif ?>
+ </td>
+ <td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
<li>
- <?= $this->url->link(t('Move Up'), 'swimlane', 'moveup', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
+ <?= $this->url->link(t('Edit'), 'Swimlane', 'editDefault', array('project_id' => $project['id']), false, 'popover') ?>
</li>
- <?php endif ?>
- <?php if ($swimlane['position'] != 0 && $swimlane['position'] != count($swimlanes)): ?>
<li>
- <?= $this->url->link(t('Move Down'), 'swimlane', 'movedown', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
+ <?php if ($default_swimlane['show_default_swimlane'] == 1): ?>
+ <?= $this->url->link(t('Disable'), 'Swimlane', 'disableDefault', array('project_id' => $project['id']), true) ?>
+ <?php else: ?>
+ <?= $this->url->link(t('Enable'), 'Swimlane', 'enableDefault', array('project_id' => $project['id']), true) ?>
+ <?php endif ?>
</li>
+ </ul>
+ </td>
+ </tr>
+ <?php endif ?>
+ </thead>
+ <tbody>
+ <?php foreach ($swimlanes as $swimlane): ?>
+ <tr data-swimlane-id="<?= $swimlane['id'] ?>">
+ <td>
+ <?php if (! isset($disable_handler)): ?>
+ <i class="fa fa-arrows-alt draggable-row-handle" title="<?= t('Change column position') ?>"></i>
+ <?php endif ?>
+
+ <?= $this->e($swimlane['name']) ?>
+
+ <?php if (! empty($swimlane['description'])): ?>
+ <span class="tooltip" title='<?= $this->e($this->text->markdown($swimlane['description'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
<?php endif ?>
- <li>
- <?= $this->url->link(t('Edit'), 'swimlane', 'edit', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id'])) ?>
- </li>
- <li>
- <?php if ($swimlane['is_active']): ?>
- <?= $this->url->link(t('Disable'), 'swimlane', 'disable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
- <?php else: ?>
- <?= $this->url->link(t('Enable'), 'swimlane', 'enable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
- <?php endif ?>
- </li>
- <li>
- <?= $this->url->link(t('Remove'), 'swimlane', 'confirm', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id'])) ?>
- </li>
- </ul>
- </td>
- </tr>
- <?php endforeach ?>
-</table> \ No newline at end of file
+ </td>
+ <td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Edit'), 'swimlane', 'edit', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?>
+ </li>
+ <li>
+ <?php if ($swimlane['is_active']): ?>
+ <?= $this->url->link(t('Disable'), 'swimlane', 'disable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
+ <?php else: ?>
+ <?= $this->url->link(t('Enable'), 'swimlane', 'enable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
+ <?php endif ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Remove'), 'swimlane', 'confirm', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?>
+ </li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+</table>
diff --git a/app/Template/task/changes.php b/app/Template/task/changes.php
index f288a8f4..db844857 100644
--- a/app/Template/task/changes.php
+++ b/app/Template/task/changes.php
@@ -31,7 +31,7 @@
if (empty($task['date_due'])) {
echo '<li>'.t('The due date have been removed').'</li>';
} else {
- echo '<li>'.dt('New due date: %B %e, %Y', $task['date_due']).'</li>';
+ echo '<li>'.t('New due date: ').$this->dt->date($task['date_due']).'</li>';
}
break;
case 'description':
@@ -56,7 +56,7 @@
break;
case 'date_started':
if ($value != 0) {
- echo '<li>'.dt('Start date changed: %B %e, %Y', $task['date_started']).'</li>';
+ echo '<li>'.t('Start date changed: ').$this->dt->datetime($task['date_started']).'</li>';
}
break;
default:
diff --git a/app/Template/task/comments.php b/app/Template/task/comments.php
index 070de320..c22e39ec 100644
--- a/app/Template/task/comments.php
+++ b/app/Template/task/comments.php
@@ -15,12 +15,12 @@
'comment' => $comment,
'task' => $task,
'project' => $project,
- 'not_editable' => isset($not_editable) && $not_editable,
+ 'editable' => $editable,
'is_public' => isset($is_public) && $is_public,
)) ?>
<?php endforeach ?>
- <?php if (! isset($not_editable)): ?>
+ <?php if ($editable): ?>
<?= $this->render('comment/create', array(
'skip_cancel' => true,
'values' => array(
@@ -28,7 +28,7 @@
'task_id' => $task['id'],
),
'errors' => array(),
- 'task' => $task
+ 'task' => $task,
)) ?>
<?php endif ?>
</div>
diff --git a/app/Template/task/details.php b/app/Template/task/details.php
index 9cd10dda..5c2e3cff 100644
--- a/app/Template/task/details.php
+++ b/app/Template/task/details.php
@@ -1,101 +1,130 @@
-<div class="color-<?= $task['color_id'] ?> task-show-details">
- <h2><?= $this->e('#'.$task['id'].' '.$task['title']) ?></h2>
- <?php if ($task['score']): ?>
- <span class="task-score"><?= $this->e($task['score']) ?></span>
- <?php endif ?>
- <ul>
- <?php if ($task['reference']): ?>
- <li>
- <strong><?= t('Reference: %s', $task['reference']) ?></strong>
- </li>
- <?php endif ?>
- <?php if (! empty($task['swimlane_name'])): ?>
- <li>
- <?= t('Swimlane: %s', $task['swimlane_name']) ?>
- </li>
- <?php endif ?>
- <li>
- <?= dt('Created on %B %e, %Y at %k:%M %p', $task['date_creation']) ?>
- </li>
- <?php if ($task['date_modification']): ?>
- <li>
- <?= dt('Last modified on %B %e, %Y at %k:%M %p', $task['date_modification']) ?>
- </li>
- <?php endif ?>
- <?php if ($task['date_completed']): ?>
- <li>
- <?= dt('Completed on %B %e, %Y at %k:%M %p', $task['date_completed']) ?>
- </li>
- <?php endif ?>
- <?php if ($task['date_started']): ?>
- <li>
- <?= dt('Started on %B %e, %Y', $task['date_started']) ?>
- </li>
- <?php endif ?>
- <?php if ($task['date_due']): ?>
- <li>
- <strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong>
- </li>
- <?php endif ?>
- <?php if ($task['time_estimated']): ?>
- <li>
- <?= t('Estimated time: %s hours', $task['time_estimated']) ?>
- </li>
- <?php endif ?>
- <?php if ($task['time_spent']): ?>
- <li>
- <?= t('Time spent: %s hours', $task['time_spent']) ?>
- </li>
- <?php endif ?>
- <?php if ($task['creator_username']): ?>
- <li>
- <?= t('Created by %s', $task['creator_name'] ?: $task['creator_username']) ?>
- </li>
- <?php endif ?>
- <li>
- <strong>
- <?php if ($task['assignee_username']): ?>
- <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?>
- <?php else: ?>
- <?= t('There is nobody assigned') ?>
- <?php endif ?>
- </strong>
- </li>
- <li>
- <?= t('Column on the board:') ?>
- <strong><?= $this->e($task['column_title']) ?></strong>
- (<?= $this->e($task['project_name']) ?>)
- <?= dt('since %B %e, %Y at %k:%M %p', $task['date_moved']) ?>
- </li>
- <li><?= t('Task position:').' '.$this->e($task['position']) ?></li>
- <?php if ($task['category_name']): ?>
- <li>
- <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong>
- </li>
- <?php endif ?>
- <li>
- <?php if ($task['is_active'] == 1): ?>
- <?= t('Status is open') ?>
- <?php else: ?>
- <?= t('Status is closed') ?>
- <?php endif ?>
- </li>
- <?php if ($project['is_public']): ?>
- <li>
- <?= $this->url->link(t('Public link'), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?>
- </li>
- <?php endif ?>
-
- <?php if (! isset($not_editable) && $task['recurrence_status'] != \Kanboard\Model\Task::RECURRING_STATUS_NONE): ?>
- <li>
- <strong><?= t('Recurring information') ?></strong>
- <?= $this->render('task/recurring_info', array(
- 'task' => $task,
- 'recurrence_trigger_list' => $recurrence_trigger_list,
- 'recurrence_timeframe_list' => $recurrence_timeframe_list,
- 'recurrence_basedate_list' => $recurrence_basedate_list,
- )) ?>
- </li>
- <?php endif ?>
- </ul>
-</div>
+<section id="task-summary">
+ <h2><?= $this->e($task['title']) ?></h2>
+ <div class="task-summary-container color-<?= $task['color_id'] ?>">
+ <div class="task-summary-column">
+ <ul class="no-bullet">
+ <li>
+ <strong><?= t('Status:') ?></strong>
+ <span>
+ <?php if ($task['is_active'] == 1): ?>
+ <?= t('open') ?>
+ <?php else: ?>
+ <?= t('closed') ?>
+ <?php endif ?>
+ </span>
+ </li>
+ <li>
+ <strong><?= t('Priority:') ?></strong> <span><?= $task['priority'] ?></span>
+ </li>
+ <?php if (! empty($task['reference'])): ?>
+ <li>
+ <strong><?= t('Reference:') ?></strong> <span><?= $this->e($task['reference']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if (! empty($task['score'])): ?>
+ <li>
+ <strong><?= t('Complexity:') ?></strong> <span><?= $this->e($task['score']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if ($project['is_public']): ?>
+ <li class="smaller">
+ <i class="fa fa-external-link fa-fw"></i>
+ <?= $this->url->link(t('Public link'), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ <div class="task-summary-column">
+ <ul class="no-bullet">
+ <?php if (! empty($task['category_name'])): ?>
+ <li>
+ <strong><?= t('Category:') ?></strong>
+ <span><?= $this->e($task['category_name']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if (! empty($task['swimlane_name'])): ?>
+ <li>
+ <strong><?= t('Swimlane:') ?></strong>
+ <span><?= $this->e($task['swimlane_name']) ?></span>
+ </li>
+ <?php endif ?>
+ <li>
+ <strong><?= t('Column:') ?></strong>
+ <span><?= $this->e($task['column_title']) ?></span>
+ </li>
+ <li>
+ <strong><?= t('Position:') ?></strong>
+ <span><?= $task['position'] ?></span>
+ </li>
+ </ul>
+ </div>
+ <div class="task-summary-column">
+ <ul class="no-bullet">
+ <li>
+ <strong><?= t('Assignee:') ?></strong>
+ <span>
+ <?php if ($task['assignee_username']): ?>
+ <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>
+ <?php else: ?>
+ <?= t('not assigned') ?>
+ <?php endif ?>
+ </span>
+ </li>
+ <?php if ($task['creator_username']): ?>
+ <li>
+ <strong><?= t('Creator:') ?></strong>
+ <span><?= $this->e($task['creator_name'] ?: $task['creator_username']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if ($task['date_due']): ?>
+ <li>
+ <strong><?= t('Due date:') ?></strong>
+ <span><?= $this->dt->date($task['date_due']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if ($task['time_estimated']): ?>
+ <li>
+ <strong><?= t('Time estimated:') ?></strong>
+ <span><?= t('%s hours', $task['time_estimated']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if ($task['time_spent']): ?>
+ <li>
+ <strong><?= t('Time spent:') ?></strong>
+ <span><?= t('%s hours', $task['time_spent']) ?></span>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ <div class="task-summary-column">
+ <ul class="no-bullet">
+ <li>
+ <strong><?= t('Created:') ?></strong>
+ <span><?= $this->dt->datetime($task['date_creation']) ?></span>
+ </li>
+ <li>
+ <strong><?= t('Modified:') ?></strong>
+ <span><?= $this->dt->datetime($task['date_modification']) ?></span>
+ </li>
+ <?php if ($task['date_completed']): ?>
+ <li>
+ <strong><?= t('Completed:') ?></strong>
+ <span><?= $this->dt->datetime($task['date_completed']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if ($task['date_started']): ?>
+ <li>
+ <strong><?= t('Started:') ?></strong>
+ <span><?= $this->dt->datetime($task['date_started']) ?></span>
+ </li>
+ <?php endif ?>
+ <?php if ($task['date_moved']): ?>
+ <li>
+ <strong><?= t('Moved:') ?></strong>
+ <span><?= $this->dt->datetime($task['date_moved']) ?></span>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ </div>
+</section>
diff --git a/app/Template/task/dropdown.php b/app/Template/task/dropdown.php
new file mode 100644
index 00000000..3300ccf0
--- /dev/null
+++ b/app/Template/task/dropdown.php
@@ -0,0 +1,60 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu">#<?= $task['id'] ?></a>
+ <ul>
+ <?php if (isset($task['date_started']) && 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-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'), 'tasklink', '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-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>
+ <?php if (isset($task['is_active'])): ?>
+ <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 endif ?>
+
+ <?= $this->hook->render('template:task:dropdown') ?>
+ </ul>
+</div>
diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php
index 6b6e827a..9cbbfec9 100644
--- a/app/Template/task/layout.php
+++ b/app/Template/task/layout.php
@@ -2,6 +2,9 @@
<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>
@@ -9,7 +12,7 @@
<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->isProjectManagementAllowed($task['project_id'])): ?>
+ <?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'])) ?>
@@ -17,12 +20,12 @@
<?php endif ?>
</ul>
</div>
- <section class="sidebar-container" id="task-section">
+ <section class="sidebar-container">
- <?= $this->render('task/sidebar', array('task' => $task)) ?>
+ <?= $this->render($sidebar_template, array('task' => $task)) ?>
<div class="sidebar-content">
- <?= $task_content_for_layout ?>
+ <?= $content_for_sublayout ?>
</div>
</section>
</section> \ No newline at end of file
diff --git a/app/Template/task/menu.php b/app/Template/task/menu.php
new file mode 100644
index 00000000..cddd930a
--- /dev/null
+++ b/app/Template/task/menu.php
@@ -0,0 +1,78 @@
+<?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'), 'tasklink', '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/public.php b/app/Template/task/public.php
index e3105488..7edf097c 100644
--- a/app/Template/task/public.php
+++ b/app/Template/task/public.php
@@ -1,33 +1,34 @@
<section id="main" class="public-task">
- <?= $this->render('task/details', array('task' => $task, 'project' => $project, 'not_editable' => true)) ?>
+ <?= $this->render('task/details', array('task' => $task, 'project' => $project, 'editable' => false)) ?>
<p class="pull-right"><?= $this->url->link(t('Back to the board'), 'board', 'readonly', array('token' => $project['token'])) ?></p>
<?= $this->render('task/description', array(
'task' => $task,
'project' => $project,
- 'is_public' => true
+ 'is_public' => true,
)) ?>
<?= $this->render('tasklink/show', array(
'task' => $task,
'links' => $links,
'project' => $project,
- 'not_editable' => true
+ 'editable' => false,
+ 'is_public' => true,
)) ?>
<?= $this->render('subtask/show', array(
'task' => $task,
'subtasks' => $subtasks,
- 'not_editable' => true
+ 'editable' => false
)) ?>
<?= $this->render('task/comments', array(
'task' => $task,
'comments' => $comments,
'project' => $project,
- 'not_editable' => true,
+ 'editable' => false,
'is_public' => true,
)) ?>
diff --git a/app/Template/task/remove.php b/app/Template/task/remove.php
index 2f6edc22..e0d655fe 100644
--- a/app/Template/task/remove.php
+++ b/app/Template/task/remove.php
@@ -10,6 +10,6 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/task/show.php b/app/Template/task/show.php
index 68d63c58..0c77f576 100644
--- a/app/Template/task/show.php
+++ b/app/Template/task/show.php
@@ -1,15 +1,38 @@
<?= $this->render('task/details', array(
'task' => $task,
'project' => $project,
- 'recurrence_trigger_list' => $this->task->recurrenceTriggers(),
- 'recurrence_timeframe_list' => $this->task->recurrenceTimeframes(),
- 'recurrence_basedate_list' => $this->task->recurrenceBasedates(),
+ 'editable' => $this->user->hasProjectAccess('taskmodification', 'edit', $project['id']),
)) ?>
-<?= $this->render('task_modification/edit_time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?>
<?= $this->render('task/description', array('task' => $task)) ?>
-<?= $this->render('tasklink/show', array('task' => $task, 'links' => $links, 'link_label_list' => $link_label_list)) ?>
-<?= $this->render('subtask/show', array('task' => $task, 'subtasks' => $subtasks, 'project' => $project, 'users_list' => isset($users_list) ? $users_list : array())) ?>
+
+<?= $this->render('subtask/show', array(
+ 'task' => $task,
+ 'subtasks' => $subtasks,
+ 'project' => $project,
+ 'users_list' => isset($users_list) ? $users_list : array(),
+ 'editable' => true,
+)) ?>
+
+<?= $this->render('tasklink/show', array(
+ 'task' => $task,
+ 'links' => $links,
+ 'link_label_list' => $link_label_list,
+ 'editable' => true,
+ 'is_public' => false,
+)) ?>
+
<?= $this->render('task/time_tracking_summary', array('task' => $task)) ?>
-<?= $this->render('file/show', array('task' => $task, 'files' => $files, 'images' => $images)) ?>
-<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?>
+
+<?= $this->render('task_file/show', array(
+ 'task' => $task,
+ 'files' => $files,
+ 'images' => $images
+)) ?>
+
+<?= $this->render('task/comments', array(
+ 'task' => $task,
+ 'comments' => $comments,
+ 'project' => $project,
+ 'editable' => $this->user->hasProjectAccess('comment', 'edit', $project['id']),
+)) ?>
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index 9ee1e7df..951c5095 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -1,76 +1,33 @@
<div class="sidebar">
- <h2><?= t('Information') ?></h2>
+ <h2><?= t('Task #%d', $task['id']) ?></h2>
<ul>
- <li <?= $this->app->getRouterController() === 'task' && $this->app->getRouterAction() === 'show' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('task', 'show') ?>>
<?= $this->url->link(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'activity' && $this->app->getRouterAction() === 'task' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('activity', 'task') ?>>
<?= $this->url->link(t('Activity stream'), 'activity', 'task', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'task' && $this->app->getRouterAction() === 'transitions' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('task', 'transitions') ?>>
<?= $this->url->link(t('Transitions'), 'task', 'transitions', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'task' && $this->app->getRouterAction() === 'analytics' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('task', 'analytics') ?>>
<?= $this->url->link(t('Analytics'), 'task', 'analytics', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?>
- <li <?= $this->app->getRouterController() === 'task' && $this->app->getRouterAction() === 'timetracking' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('task', 'timetracking') ?>>
<?= $this->url->link(t('Time tracking'), 'task', 'timetracking', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<?php endif ?>
-
- <?= $this->hook->render('template:task:sidebar:information') ?>
- </ul>
- <h2><?= t('Actions') ?></h2>
- <ul>
- <li <?= $this->app->getRouterController() === 'taskmodification' && $this->app->getRouterAction() === 'edit' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Edit the task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'taskmodification' && $this->app->getRouterAction() === 'description' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Edit the description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'taskmodification' && $this->app->getRouterAction() === 'recurrence' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Edit recurrence'), 'taskmodification', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'subtask' && $this->app->getRouterAction() === 'create' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'tasklink' && $this->app->getRouterAction() === 'create' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <li <?= $this->app->checkMenuSelection('subtask', 'show') ?>>
+ <?= $this->url->link(t('Sub-tasks'), 'subtask', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'comment' && $this->app->getRouterAction() === 'create' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <li <?= $this->app->checkMenuSelection('tasklink', 'show') ?>>
+ <?= $this->url->link(t('Internal links'), 'tasklink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'file' && $this->app->getRouterAction() === 'create' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Attach a document'), 'file', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <li <?= $this->app->checkMenuSelection('TaskExternalLink', 'show') ?>>
+ <?= $this->url->link(t('External links'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'file' && $this->app->getRouterAction() === 'screenshot' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Add a screenshot'), 'file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'taskduplication' && $this->app->getRouterAction() === 'duplicate' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'taskduplication' && $this->app->getRouterAction() === 'copy' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'taskduplication' && $this->app->getRouterAction() === 'move' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <li <?= $this->app->getRouterController() === 'taskstatus' ? 'class="active"' : '' ?>>
- <?php if ($task['is_active'] == 1): ?>
- <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- <?php else: ?>
- <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- <?php endif ?>
- </li>
- <?php if ($this->task->canRemove($task)): ?>
- <li <?= $this->app->getRouterController() === 'task' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <?php endif ?>
- <?= $this->hook->render('template:task:sidebar:actions') ?>
+ <?= $this->hook->render('template:task:sidebar', array('task' => $task)) ?>
</ul>
- <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
- <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div>
diff --git a/app/Template/task/time_tracking_details.php b/app/Template/task/time_tracking_details.php
index faa07cb8..147c5109 100644
--- a/app/Template/task/time_tracking_details.php
+++ b/app/Template/task/time_tracking_details.php
@@ -10,14 +10,14 @@
<th><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th>
<th class="column-20"><?= $subtask_paginator->order(t('Start'), 'start') ?></th>
<th class="column-20"><?= $subtask_paginator->order(t('End'), 'end') ?></th>
- <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), 'time_spent') ?></th>
+ <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), \Kanboard\Model\SubtaskTimeTracking::TABLE.'.time_spent') ?></th>
</tr>
<?php foreach ($subtask_paginator->getCollection() as $record): ?>
<tr>
<td><?= $this->url->link($this->e($record['user_fullname'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?></td>
<td><?= t($record['subtask_title']) ?></td>
- <td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td>
- <td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td>
+ <td><?= $this->dt->datetime($record['start']) ?></td>
+ <td><?= $this->dt->datetime($record['end']) ?></td>
<td><?= n($record['time_spent']).' '.t('hours') ?></td>
</tr>
<?php endforeach ?>
diff --git a/app/Template/task/transitions.php b/app/Template/task/transitions.php
index 2ca2387f..d79c2fd9 100644
--- a/app/Template/task/transitions.php
+++ b/app/Template/task/transitions.php
@@ -15,7 +15,7 @@
</tr>
<?php foreach ($transitions as $transition): ?>
<tr>
- <td><?= dt('%B %e, %Y at %k:%M %p', $transition['date']) ?></td>
+ <td><?= $this->dt->datetime($transition['date']) ?></td>
<td><?= $this->e($transition['src_column']) ?></td>
<td><?= $this->e($transition['dst_column']) ?></td>
<td><?= $this->url->link($this->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td>
diff --git a/app/Template/task_creation/form.php b/app/Template/task_creation/form.php
index 325ca1c8..84f74c36 100644
--- a/app/Template/task_creation/form.php
+++ b/app/Template/task_creation/form.php
@@ -1,28 +1,29 @@
-<?php if (! $ajax): ?>
-<div class="page-header">
- <ul>
- <li><i class="fa fa-th fa-fw"></i><?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $values['project_id'])) ?></li>
- </ul>
-</div>
-<?php else: ?>
<div class="page-header">
<h2><?= t('New task') ?></h2>
</div>
-<?php endif ?>
-<form id="task-form" method="post" action="<?= $this->url->href('taskcreation', 'save', array('project_id' => $values['project_id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('taskcreation', 'save', array('project_id' => $values['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<div class="form-column">
<?= $this->form->label(t('Title'), 'title') ?>
- <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?><br/>
+ <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?>
<?= $this->form->label(t('Description'), 'description') ?>
<div class="form-tabs">
<div class="write-area">
- <?= $this->form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"')) ?>
+ <?= $this->form->textarea(
+ 'description',
+ $values,
+ $errors,
+ array(
+ 'placeholder="'.t('Leave a description').'"',
+ 'tabindex="2"',
+ 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $values['project_id'])).'"'
+ )
+ ) ?>
</div>
<div class="preview-area">
<div class="markdown"></div>
@@ -46,34 +47,20 @@
<div class="form-column">
<?= $this->form->hidden('project_id', $values) ?>
-
- <?= $this->form->label(t('Assignee'), 'owner_id') ?>
- <?= $this->form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?><br/>
-
- <?= $this->form->label(t('Category'), 'category_id') ?>
- <?= $this->form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?><br/>
-
- <?php if (! (count($swimlanes_list) === 1 && key($swimlanes_list) === 0)): ?>
- <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?>
- <?= $this->form->select('swimlane_id', $swimlanes_list, $values, $errors, array('tabindex="5"')) ?><br/>
- <?php endif ?>
-
- <?= $this->form->label(t('Column'), 'column_id') ?>
- <?= $this->form->select('column_id', $columns_list, $values, $errors, array('tabindex="6"')) ?><br/>
-
- <?= $this->form->label(t('Complexity'), 'score') ?>
- <?= $this->form->number('score', $values, $errors, array('tabindex="8"')) ?><br/>
-
- <?= $this->form->label(t('Original estimate'), 'time_estimated') ?>
- <?= $this->form->numeric('time_estimated', $values, $errors, array('tabindex="9"')) ?> <?= t('hours') ?><br/>
-
- <?= $this->form->label(t('Due Date'), 'date_due') ?>
- <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="10"'), 'form-date') ?><br/>
- <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
+ <?= $this->task->selectAssignee($users_list, $values, $errors) ?>
+ <?= $this->task->selectCategory($categories_list, $values, $errors) ?>
+ <?= $this->task->selectSwimlane($swimlanes_list, $values, $errors) ?>
+ <?= $this->task->selectColumn($columns_list, $values, $errors) ?>
+ <?= $this->task->selectPriority($project, $values) ?>
+ <?= $this->task->selectScore($values, $errors) ?>
+ <?= $this->task->selectTimeEstimated($values, $errors) ?>
+ <?= $this->task->selectDueDate($values, $errors) ?>
+
+ <?= $this->hook->render('template:task:form:right-column', array('values'=>$values, 'errors'=>$errors)) ?>
</div>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" tabindex="11"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" tabindex="12"/>
<?= 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
diff --git a/app/Template/task_duplication/copy.php b/app/Template/task_duplication/copy.php
index 415b8610..fe2c599a 100644
--- a/app/Template/task_duplication/copy.php
+++ b/app/Template/task_duplication/copy.php
@@ -6,7 +6,7 @@
<p class="alert"><?= t('There is no destination project available.') ?></p>
<?php else: ?>
- <form method="post" action="<?= $this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+ <form class="popover-form" method="post" action="<?= $this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
@@ -39,9 +39,9 @@
<p class="form-help"><?= t('Current assignee: %s', ($task['assignee_name'] ?: $task['assignee_username']) ?: e('not assigned')) ?></p>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
diff --git a/app/Template/task_duplication/duplicate.php b/app/Template/task_duplication/duplicate.php
index 4b50d9ca..376f6b3b 100644
--- a/app/Template/task_duplication/duplicate.php
+++ b/app/Template/task_duplication/duplicate.php
@@ -10,6 +10,6 @@
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/task_duplication/move.php b/app/Template/task_duplication/move.php
index d8d1ba05..8ab81f5b 100644
--- a/app/Template/task_duplication/move.php
+++ b/app/Template/task_duplication/move.php
@@ -6,7 +6,7 @@
<p class="alert"><?= t('There is no destination project available.') ?></p>
<?php else: ?>
- <form method="post" action="<?= $this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+ <form class="popover-form" method="post" action="<?= $this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
@@ -39,9 +39,9 @@
<p class="form-help"><?= t('Current assignee: %s', ($task['assignee_name'] ?: $task['assignee_username']) ?: e('not assigned')) ?></p>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
diff --git a/app/Template/task_external_link/create.php b/app/Template/task_external_link/create.php
new file mode 100644
index 00000000..b62abdb2
--- /dev/null
+++ b/app/Template/task_external_link/create.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Add a new external link') ?></h2>
+</div>
+
+<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off">
+ <?= $this->render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/task_external_link/edit.php b/app/Template/task_external_link/edit.php
new file mode 100644
index 00000000..8caaaebe
--- /dev/null
+++ b/app/Template/task_external_link/edit.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Edit external link') ?></h2>
+</div>
+
+<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off">
+ <?= $this->render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/task_external_link/find.php b/app/Template/task_external_link/find.php
new file mode 100644
index 00000000..36a031d3
--- /dev/null
+++ b/app/Template/task_external_link/find.php
@@ -0,0 +1,28 @@
+<div class="page-header">
+ <h2><?= t('Add a new external link') ?></h2>
+</div>
+
+<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
+
+ <?= $this->form->label(t('External link'), 'text') ?>
+ <?= $this->form->text(
+ 'text',
+ $values,
+ $errors,
+ array(
+ 'required',
+ 'autofocus',
+ 'placeholder="'.t('Copy and paste your link here...').'"',
+ )) ?>
+
+ <?= $this->form->label(t('Link type'), 'type') ?>
+ <?= $this->form->select('type', $types, $values) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Next') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/task_external_link/form.php b/app/Template/task_external_link/form.php
new file mode 100644
index 00000000..932ca521
--- /dev/null
+++ b/app/Template/task_external_link/form.php
@@ -0,0 +1,13 @@
+<?= $this->form->csrf() ?>
+<?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
+<?= $this->form->hidden('id', $values) ?>
+<?= $this->form->hidden('link_type', $values) ?>
+
+<?= $this->form->label(t('URL'), 'url') ?>
+<?= $this->form->text('url', $values, $errors, array('required')) ?>
+
+<?= $this->form->label(t('Title'), 'title') ?>
+<?= $this->form->text('title', $values, $errors, array('required')) ?>
+
+<?= $this->form->label(t('Dependency'), 'dependency') ?>
+<?= $this->form->select('dependency', $dependencies, $values, $errors) ?>
diff --git a/app/Template/task_external_link/remove.php b/app/Template/task_external_link/remove.php
new file mode 100644
index 00000000..01535255
--- /dev/null
+++ b/app/Template/task_external_link/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove a link') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this link: "%s"?', $link['title']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'TaskExternalLink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/task_external_link/show.php b/app/Template/task_external_link/show.php
new file mode 100644
index 00000000..12d8e4f6
--- /dev/null
+++ b/app/Template/task_external_link/show.php
@@ -0,0 +1,50 @@
+<div class="page-header">
+ <h2><?= t('External links') ?></h2>
+</div>
+
+<?php if (empty($links)): ?>
+ <p class="alert"><?= t('There is no external link for the moment.') ?></p>
+<?php else: ?>
+ <table class="table-stripped table-small">
+ <tr>
+ <th class="column-10"><?= t('Type') ?></th>
+ <th><?= t('Title') ?></th>
+ <th class="column-10"><?= t('Dependency') ?></th>
+ <th class="column-15"><?= t('Creator') ?></th>
+ <th class="column-15"><?= t('Date') ?></th>
+ <?php if ($this->user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?>
+ <th class="column-5"><?= t('Action') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($links as $link): ?>
+ <tr>
+ <td>
+ <?= $link['type'] ?>
+ </td>
+ <td>
+ <a href="<?= $link['url'] ?>" target="_blank"><?= $this->e($link['title']) ?></a>
+ </td>
+ <td>
+ <?= $this->e($link['dependency_label']) ?>
+ </td>
+ <td>
+ <?= $this->e($link['creator_name'] ?: $link['creator_username']) ?>
+ </td>
+ <td>
+ <?= $this->dt->date($link['date_creation']) ?>
+ </td>
+ <?php if ($this->user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?>
+ <td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><?= $this->url->link(t('Edit'), 'TaskExternalLink', 'edit', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><?= $this->url->link(t('Remove'), 'TaskExternalLink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ </ul>
+ </div>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </table>
+<?php endif ?>
diff --git a/app/Template/task_file/create.php b/app/Template/task_file/create.php
new file mode 100644
index 00000000..f03ce8dc
--- /dev/null
+++ b/app/Template/task_file/create.php
@@ -0,0 +1,33 @@
+<div class="page-header">
+ <h2><?= t('Attach a document') ?></h2>
+</div>
+<div id="file-done" style="display:none">
+ <p class="alert alert-success">
+ <?= t('All files have been uploaded successfully.') ?>
+ <?= $this->url->link(t('View uploaded files'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </p>
+</div>
+
+<div id="file-error-max-size" style="display:none">
+ <p class="alert alert-error">
+ <?= t('The maximum allowed file size is %sB.', $this->text->bytes($max_size)) ?>
+ <a href="#" id="file-browser"><?= t('Choose files again') ?></a>
+ </p>
+</div>
+
+<div
+ id="file-dropzone"
+ data-max-size="<?= $max_size ?>"
+ data-url="<?= $this->url->href('TaskFile', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+ <div id="file-dropzone-inner">
+ <?= t('Drag and drop your files here') ?> <?= t('or') ?> <a href="#" id="file-browser"><?= t('choose files') ?></a>
+ </div>
+</div>
+
+<input type="file" name="files[]" multiple style="display:none" id="file-form-element">
+
+<div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" id="file-upload-button" disabled>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
+</div>
diff --git a/app/Template/file/remove.php b/app/Template/task_file/remove.php
index 37f648eb..5e6c83f2 100644
--- a/app/Template/file/remove.php
+++ b/app/Template/task_file/remove.php
@@ -8,8 +8,8 @@
</p>
<div class="form-actions">
- <?= $this->url->link(t('Yes'), 'file', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), true, 'btn btn-red') ?>
+ <?= $this->url->link(t('Yes'), 'TaskFile', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/file/screenshot.php b/app/Template/task_file/screenshot.php
index 73b72eae..72214362 100644
--- a/app/Template/file/screenshot.php
+++ b/app/Template/task_file/screenshot.php
@@ -6,7 +6,7 @@
<p id="screenshot-inner"><?= t('Take a screenshot and press CTRL+V or ⌘+V to paste here.') ?></p>
</div>
-<form action="<?= $this->url->href('file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => $redirect)) ?>" method="post">
+<form class="popover-form" action="<?= $this->url->href('TaskFile', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post">
<input type="hidden" name="screenshot"/>
<?= $this->form->csrf() ?>
<div class="form-actions">
diff --git a/app/Template/task_file/show.php b/app/Template/task_file/show.php
new file mode 100644
index 00000000..c3f2bb98
--- /dev/null
+++ b/app/Template/task_file/show.php
@@ -0,0 +1,90 @@
+<?php if (! empty($files) || ! empty($images)): ?>
+<div id="attachments" class="task-show-section">
+
+ <div class="page-header">
+ <h2><?= t('Attachments') ?></h2>
+ </div>
+ <?php if (! empty($images)): ?>
+ <div class="file-thumbnails">
+ <?php foreach ($images as $file): ?>
+ <div class="file-thumbnail">
+ <a href="<?= $this->url->href('FileViewer', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>" class="popover"><img src="<?= $this->url->href('FileViewer', 'thumbnail', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" title="<?= $this->e($file['name']) ?>" alt="<?= $this->e($file['name']) ?>"></a>
+ <div class="file-thumbnail-content">
+ <div class="file-thumbnail-title">
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->e($file['name']) ?> <i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li>
+ <i class="fa fa-download fa-fw"></i>
+ <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
+ </li>
+ <?php if ($this->user->hasProjectAccess('TaskFile', 'remove', $task['project_id'])): ?>
+ <li>
+ <i class="fa fa-trash fa-fw"></i>
+ <?= $this->url->link(t('Remove'), 'TaskFile', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ </div>
+ <div class="file-thumbnail-description">
+ <span class="tooltip" title='<?= t('Uploaded: %s', $this->dt->datetime($file['date'])).'<br>'.t('Size: %s', $this->text->bytes($file['size'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?= t('Uploaded by %s', $file['user_name'] ?: $file['username']) ?>
+ </div>
+ </div>
+ </div>
+ <?php endforeach ?>
+ </div>
+ <?php endif ?>
+
+ <?php if (! empty($files)): ?>
+ <table class="table-stripped">
+ <tr>
+ <th><?= t('Filename') ?></th>
+ <th><?= t('Creator') ?></th>
+ <th><?= t('Date') ?></th>
+ <th><?= t('Size') ?></th>
+ </tr>
+ <?php foreach ($files as $file): ?>
+ <tr>
+ <td>
+ <i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->e($file['name']) ?> <i class="fa fa-caret-down"></i></a>
+ <ul>
+ <?php if ($this->file->getPreviewType($file['name']) !== null): ?>
+ <li>
+ <i class="fa fa-eye fa-fw"></i>
+ <?= $this->url->link(t('View file'), 'FileViewer', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ <li>
+ <i class="fa fa-download fa-fw"></i>
+ <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
+ </li>
+ <?php if ($this->user->hasProjectAccess('TaskFile', 'remove', $task['project_id'])): ?>
+ <li>
+ <i class="fa fa-trash fa-fw"></i>
+ <?= $this->url->link(t('Remove'), 'TaskFile', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ </td>
+ <td>
+ <?= $this->e($file['user_name'] ?: $file['username']) ?>
+ </td>
+ <td>
+ <?= $this->dt->date($file['date']) ?>
+ </td>
+ <td>
+ <?= $this->text->bytes($file['size']) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+</div>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/task_modification/edit_description.php b/app/Template/task_modification/edit_description.php
index 4cae939c..f5a9b0e1 100644
--- a/app/Template/task_modification/edit_description.php
+++ b/app/Template/task_modification/edit_description.php
@@ -2,7 +2,7 @@
<h2><?= t('Edit the description') ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('taskmodification', 'updateDescription', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
@@ -17,7 +17,17 @@
</li>
</ul>
<div class="write-area">
- <?= $this->form->textarea('description', $values, $errors, array('autofocus', 'placeholder="'.t('Leave a description').'"'), 'task-show-description-textarea') ?>
+ <?= $this->form->textarea(
+ 'description',
+ $values,
+ $errors,
+ array(
+ 'autofocus',
+ 'placeholder="'.t('Leave a description').'"',
+ 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $task['project_id'])).'"'
+ ),
+ 'task-show-description-textarea'
+ ) ?>
</div>
<div class="preview-area">
<div class="markdown"></div>
@@ -29,10 +39,6 @@
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
- <?php if ($ajax): ?>
- <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?>
- <?php else: ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- <?php endif ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
diff --git a/app/Template/task_modification/edit_task.php b/app/Template/task_modification/edit_task.php
index f4d7449a..9e98ad5e 100644
--- a/app/Template/task_modification/edit_task.php
+++ b/app/Template/task_modification/edit_task.php
@@ -1,61 +1,35 @@
<div class="page-header">
<h2><?= t('Edit a task') ?></h2>
</div>
-<form id="task-form" method="post" action="<?= $this->url->href('taskmodification', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+<form class="popover-form" method="post" action="<?= $this->url->href('taskmodification', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('project_id', $values) ?>
<div class="form-column">
-
<?= $this->form->label(t('Title'), 'title') ?>
- <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"')) ?><br/>
-
- <?= $this->form->label(t('Description'), 'description') ?>
- <div class="form-tabs">
- <div class="write-area">
- <?= $this->form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"')) ?>
- </div>
- <div class="preview-area">
- <div class="markdown"></div>
- </div>
- <ul class="form-tabs-nav">
- <li class="form-tab form-tab-selected">
- <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a>
- </li>
- <li class="form-tab">
- <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a>
- </li>
- </ul>
- </div>
-
- <?= $this->render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?>
+ <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"')) ?>
+ <?= $this->task->selectAssignee($users_list, $values, $errors) ?>
+ <?= $this->task->selectCategory($categories_list, $values, $errors) ?>
+ <?= $this->task->selectPriority($project, $values) ?>
+ <?= $this->task->selectScore($values, $errors) ?>
</div>
<div class="form-column">
- <?= $this->form->hidden('id', $values) ?>
- <?= $this->form->hidden('project_id', $values) ?>
-
- <?= $this->form->label(t('Assignee'), 'owner_id') ?>
- <?= $this->form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?><br/>
-
- <?= $this->form->label(t('Category'), 'category_id') ?>
- <?= $this->form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?><br/>
-
- <?= $this->form->label(t('Complexity'), 'score') ?>
- <?= $this->form->number('score', $values, $errors, array('tabindex="6"')) ?><br/>
+ <?= $this->task->selectTimeEstimated($values, $errors) ?>
+ <?= $this->task->selectTimeSpent($values, $errors) ?>
+ <?= $this->task->selectStartDate($values, $errors) ?>
+ <?= $this->task->selectDueDate($values, $errors) ?>
+ </div>
- <?= $this->form->label(t('Due Date'), 'date_due') ?>
- <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="7"'), 'form-date') ?><br/>
- <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
+ <div class="form-clear">
+ <?= $this->render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?>
</div>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" tabindex="10">
<?= t('or') ?>
- <?php if ($ajax): ?>
- <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?>
- <?php else: ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- <?php endif ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form> \ No newline at end of file
diff --git a/app/Template/task_modification/edit_time.php b/app/Template/task_modification/edit_time.php
deleted file mode 100644
index 8e7f9b42..00000000
--- a/app/Template/task_modification/edit_time.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<form method="post" action="<?= $this->url->href('taskmodification', 'time', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" class="form-inline task-time-form" autocomplete="off">
-
- <?php if (empty($values['date_started'])): ?>
- <?= $this->url->link('<i class="fa fa-play"></i>', 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-show-start-link', t('Set automatically the start date')) ?>
- <?php endif ?>
-
- <?= $this->form->csrf() ?>
- <?= $this->form->hidden('id', $values) ?>
-
- <?= $this->form->label(t('Start date'), 'date_started') ?>
- <?= $this->form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-datetime') ?>
-
- <?= $this->form->label(t('Time estimated'), 'time_estimated') ?>
- <?= $this->form->numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?>
-
- <?= $this->form->label(t('Time spent'), 'time_spent') ?>
- <?= $this->form->numeric('time_spent', $values, array(), array('placeholder="'.t('hours').'"')) ?>
-
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
-</form> \ No newline at end of file
diff --git a/app/Template/task_modification/edit_recurrence.php b/app/Template/task_recurrence/edit.php
index dc4faa7a..ce69aade 100644
--- a/app/Template/task_modification/edit_recurrence.php
+++ b/app/Template/task_recurrence/edit.php
@@ -4,7 +4,7 @@
<?php if ($task['recurrence_status'] != \Kanboard\Model\Task::RECURRING_STATUS_NONE): ?>
<div class="listing">
- <?= $this->render('task/recurring_info', array(
+ <?= $this->render('task_recurrence/info', array(
'task' => $task,
'recurrence_trigger_list' => $recurrence_trigger_list,
'recurrence_timeframe_list' => $recurrence_timeframe_list,
@@ -15,7 +15,7 @@
<?php if ($task['recurrence_status'] != \Kanboard\Model\Task::RECURRING_STATUS_PROCESSED): ?>
- <form method="post" action="<?= $this->url->href('taskmodification', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+ <form class="popover-form" method="post" action="<?= $this->url->href('TaskRecurrence', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
@@ -38,9 +38,9 @@
<?= $this->form->select('recurrence_basedate', $recurrence_basedate_list, $values, $errors) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
diff --git a/app/Template/task/recurring_info.php b/app/Template/task_recurrence/info.php
index 83ca0960..83ca0960 100644
--- a/app/Template/task/recurring_info.php
+++ b/app/Template/task_recurrence/info.php
diff --git a/app/Template/task_status/close.php b/app/Template/task_status/close.php
index d32863bd..7d200544 100644
--- a/app/Template/task_status/close.php
+++ b/app/Template/task_status/close.php
@@ -8,7 +8,7 @@
</p>
<div class="form-actions">
- <?= $this->url->link(t('Yes'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?>
+ <?= $this->url->link(t('Yes'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
diff --git a/app/Template/task_status/open.php b/app/Template/task_status/open.php
index 615b2464..5d19bfbe 100644
--- a/app/Template/task_status/open.php
+++ b/app/Template/task_status/open.php
@@ -8,7 +8,7 @@
</p>
<div class="form-actions">
- <?= $this->url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?>
+ <?= $this->url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php
index 749f2968..c0d49191 100644
--- a/app/Template/tasklink/create.php
+++ b/app/Template/tasklink/create.php
@@ -2,7 +2,7 @@
<h2><?= t('Add a new link') ?></h2>
</div>
-<form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" method="post" autocomplete="off">
+<form class="popover-form" action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
@@ -21,17 +21,13 @@
'placeholder="'.t('Start to type task title...').'"',
'title="'.t('Start to type task title...').'"',
'data-dst-field="opposite_task_id"',
- 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
+ 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
),
- 'task-autocomplete') ?>
+ 'autocomplete') ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
- <?php if (isset($ajax)): ?>
- <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?>
- <?php else: ?>
- <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- <?php endif ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form> \ No newline at end of file
diff --git a/app/Template/tasklink/edit.php b/app/Template/tasklink/edit.php
index 73b43277..896f84c0 100644
--- a/app/Template/tasklink/edit.php
+++ b/app/Template/tasklink/edit.php
@@ -22,9 +22,9 @@
'placeholder="'.t('Start to type task title...').'"',
'title="'.t('Start to type task title...').'"',
'data-dst-field="opposite_task_id"',
- 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
+ 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
),
- 'task-autocomplete') ?>
+ 'autocomplete') ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php
index 97a3a767..fd8b37a6 100644
--- a/app/Template/tasklink/show.php
+++ b/app/Template/tasklink/show.php
@@ -1,16 +1,18 @@
-<?php if (! empty($links)): ?>
<div class="page-header">
- <h2><?= t('Links') ?></h2>
+ <h2><?= t('Internal links') ?></h2>
</div>
-<table id="links">
+<?php if (empty($links)): ?>
+ <p class="alert"><?= t('There is no internal link for the moment.') ?></p>
+<?php else: ?>
+<table id="links" class="table-small table-stripped">
<tr>
<th class="column-20"><?= t('Label') ?></th>
<th class="column-30"><?= t('Task') ?></th>
<th class="column-20"><?= t('Project') ?></th>
<th><?= t('Column') ?></th>
<th><?= t('Assignee') ?></th>
- <?php if (! isset($not_editable)): ?>
- <th><?= t('Action') ?></th>
+ <?php if ($editable && $this->user->hasProjectAccess('Tasklink', 'edit', $task['project_id'])): ?>
+ <th class="column-5"><?= t('Action') ?></th>
<?php endif ?>
</tr>
<?php foreach ($links as $label => $grouped_links): ?>
@@ -23,12 +25,12 @@
<?php endif ?>
<td>
- <?php if (! isset($not_editable)): ?>
+ <?php if ($is_public): ?>
<?= $this->url->link(
$this->e('#'.$link['task_id'].' '.$link['title']),
'task',
- 'show',
- array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
+ 'readonly',
+ array('task_id' => $link['task_id'], 'token' => $project['token']),
false,
$link['is_active'] ? '' : 'task-link-closed'
) ?>
@@ -36,14 +38,14 @@
<?= $this->url->link(
$this->e('#'.$link['task_id'].' '.$link['title']),
'task',
- 'readonly',
- array('task_id' => $link['task_id'], 'token' => $project['token']),
+ 'show',
+ array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
false,
$link['is_active'] ? '' : 'task-link-closed'
) ?>
<?php endif ?>
- <br/>
+ <br>
<?php if (! empty($link['task_time_spent'])): ?>
<strong><?= $this->e($link['task_time_spent']).'h' ?></strong> <?= t('spent') ?>
@@ -57,19 +59,22 @@
<td><?= $this->e($link['column_title']) ?></td>
<td>
<?php if (! empty($link['task_assignee_username'])): ?>
- <?php if (! isset($not_editable)): ?>
+ <?php if ($editable): ?>
<?= $this->url->link($this->e($link['task_assignee_name'] ?: $link['task_assignee_username']), 'user', 'show', array('user_id' => $link['task_assignee_id'])) ?>
<?php else: ?>
<?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>
<?php endif ?>
<?php endif ?>
</td>
- <?php if (! isset($not_editable)): ?>
+ <?php if ($editable && $this->user->hasProjectAccess('Tasklink', 'edit', $task['project_id'])): ?>
<td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
<ul>
<li><?= $this->url->link(t('Edit'), 'tasklink', 'edit', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li>
<li><?= $this->url->link(t('Remove'), 'tasklink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li>
</ul>
+ </div>
</td>
<?php endif ?>
</tr>
@@ -77,7 +82,7 @@
<?php endforeach ?>
</table>
-<?php if (! isset($not_editable) && isset($link_label_list)): ?>
+<?php if ($editable && isset($link_label_list)): ?>
<form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off">
<?= $this->form->csrf() ?>
@@ -95,9 +100,9 @@
'placeholder="'.t('Start to type task title...').'"',
'title="'.t('Start to type task title...').'"',
'data-dst-field="opposite_task_id"',
- 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
+ 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
),
- 'task-autocomplete') ?>
+ 'autocomplete') ?>
<input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
</form>
diff --git a/app/Template/twofactor/index.php b/app/Template/twofactor/index.php
index 36b92653..b9ee4b49 100644
--- a/app/Template/twofactor/index.php
+++ b/app/Template/twofactor/index.php
@@ -2,36 +2,14 @@
<h2><?= t('Two factor authentication') ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('twofactor', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
-
+<form method="post" action="<?= $this->url->href('twofactor', $user['twofactor_activated'] == 1 ? 'deactivate' : 'show', array('user_id' => $user['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
- <?= $this->form->checkbox('twofactor_activated', t('Enable/disable two factor authentication'), 1, isset($user['twofactor_activated']) && $user['twofactor_activated'] == 1) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
-
-<?php if ($user['twofactor_activated'] == 1): ?>
-<div class="listing">
- <p><?= t('Secret key: ') ?><strong><?= $this->e($user['twofactor_secret']) ?></strong> (base32)</p>
- <p><br/><img src="<?= $qrcode_url ?>"/><br/><br/></p>
- <p>
- <?= t('This QR code contains the key URI: ') ?><strong><?= $this->e($key_url) ?></strong>
- <br/><br/>
- <?= t('Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).') ?>
- </p>
-</div>
-
-<h3><?= t('Test your device') ?></h3>
-<form method="post" action="<?= $this->url->href('twofactor', 'test', array('user_id' => $user['id'])) ?>" autocomplete="off">
-
- <?= $this->form->csrf() ?>
- <?= $this->form->label(t('Code'), 'code') ?>
- <?= $this->form->text('code', array(), array(), array('placeholder="123456"'), 'form-numeric') ?>
-
+ <p><?= t('Two-Factor Provider: ') ?><strong><?= $this->e($provider) ?></strong></p>
<div class="form-actions">
- <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/>
+ <?php if ($user['twofactor_activated'] == 1): ?>
+ <input type="submit" value="<?= t('Disable two-factor authentication') ?>" class="btn btn-red"/>
+ <?php else: ?>
+ <input type="submit" value="<?= t('Enable two-factor authentication') ?>" class="btn btn-blue"/>
+ <?php endif ?>
</div>
</form>
-<?php endif ?>
diff --git a/app/Template/twofactor/show.php b/app/Template/twofactor/show.php
new file mode 100644
index 00000000..d6d8f405
--- /dev/null
+++ b/app/Template/twofactor/show.php
@@ -0,0 +1,31 @@
+<div class="page-header">
+ <h2><?= t('Two factor authentication') ?></h2>
+</div>
+
+<?php if (! empty($secret) || ! empty($qrcode_url) || ! empty($key_url)): ?>
+<div class="listing">
+ <?php if (! empty($secret)): ?>
+ <p><?= t('Secret key: ') ?><strong><?= $this->e($secret) ?></strong></p>
+ <?php endif ?>
+
+ <?php if (! empty($qrcode_url)): ?>
+ <p><br><img src="<?= $qrcode_url ?>"/><br><br></p>
+ <?php endif ?>
+
+ <?php if (! empty($key_url)): ?>
+ <p><?= t('This QR code contains the key URI: ') ?><a href="<?= $this->e($key_url) ?>"><?= $this->e($key_url) ?></a></p>
+ <?php endif ?>
+</div>
+<?php endif ?>
+
+<h3><?= t('Test your device') ?></h3>
+<form method="post" action="<?= $this->url->href('twofactor', 'test', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->label(t('Code'), 'code') ?>
+ <?= $this->form->text('code', array(), array(), array('placeholder="123456"', 'autofocus'), 'form-numeric') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue">
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/user/authentication.php b/app/Template/user/authentication.php
index 20c3d372..0c08f3fb 100644
--- a/app/Template/user/authentication.php
+++ b/app/Template/user/authentication.php
@@ -8,14 +8,7 @@
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('username', $values) ?>
- <?= $this->form->label(t('Google Id'), 'google_id') ?>
- <?= $this->form->text('google_id', $values, $errors) ?>
-
- <?= $this->form->label(t('Github Id'), 'github_id') ?>
- <?= $this->form->text('github_id', $values, $errors) ?>
-
- <?= $this->form->label(t('Gitlab Id'), 'gitlab_id') ?>
- <?= $this->form->text('gitlab_id', $values, $errors) ?>
+ <?= $this->hook->render('template:user:authentication:form', array('values' => $values, 'errors' => $errors, 'user' => $user)) ?>
<?= $this->form->checkbox('is_ldap_user', t('Remote user'), 1, isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) ?>
<?= $this->form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?>
diff --git a/app/Template/user/create_local.php b/app/Template/user/create_local.php
index 98c38f0d..38bd7836 100644
--- a/app/Template/user/create_local.php
+++ b/app/Template/user/create_local.php
@@ -12,34 +12,35 @@
<div class="form-column">
<?= $this->form->label(t('Username'), 'username') ?>
- <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?><br/>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors) ?><br/>
+ <?= $this->form->text('name', $values, $errors) ?>
<?= $this->form->label(t('Email'), 'email') ?>
- <?= $this->form->email('email', $values, $errors) ?><br/>
+ <?= $this->form->email('email', $values, $errors) ?>
<?= $this->form->label(t('Password'), 'password') ?>
- <?= $this->form->password('password', $values, $errors, array('required')) ?><br/>
+ <?= $this->form->password('password', $values, $errors, array('required')) ?>
<?= $this->form->label(t('Confirmation'), 'confirmation') ?>
- <?= $this->form->password('confirmation', $values, $errors, array('required')) ?><br/>
+ <?= $this->form->password('confirmation', $values, $errors, array('required')) ?>
</div>
<div class="form-column">
<?= $this->form->label(t('Add project member'), 'project_id') ?>
- <?= $this->form->select('project_id', $projects, $values, $errors) ?><br/>
+ <?= $this->form->select('project_id', $projects, $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'timezone') ?>
- <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Language'), 'language') ?>
- <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('language', $languages, $values, $errors) ?>
+
+ <?= $this->form->label(t('Role'), 'role') ?>
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
<?= $this->form->checkbox('notifications_enabled', t('Enable email notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?>
</div>
<div class="form-actions">
@@ -49,4 +50,4 @@
</div>
</form>
</section>
-</section> \ No newline at end of file
+</section>
diff --git a/app/Template/user/create_remote.php b/app/Template/user/create_remote.php
index 49d1548c..7399a010 100644
--- a/app/Template/user/create_remote.php
+++ b/app/Template/user/create_remote.php
@@ -12,37 +12,31 @@
<div class="form-column">
<?= $this->form->label(t('Username'), 'username') ?>
- <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?><br/>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors) ?><br/>
+ <?= $this->form->text('name', $values, $errors) ?>
<?= $this->form->label(t('Email'), 'email') ?>
- <?= $this->form->email('email', $values, $errors) ?><br/>
+ <?= $this->form->email('email', $values, $errors) ?>
- <?= $this->form->label(t('Google Id'), 'google_id') ?>
- <?= $this->form->text('google_id', $values, $errors) ?><br/>
-
- <?= $this->form->label(t('Github Id'), 'github_id') ?>
- <?= $this->form->text('github_id', $values, $errors) ?><br/>
-
- <?= $this->form->label(t('Gitlab Id'), 'gitlab_id') ?>
- <?= $this->form->text('gitlab_id', $values, $errors) ?><br/>
+ <?= $this->hook->render('template:user:create-remote:form', array('values' => $values, 'errors' => $errors)) ?>
</div>
<div class="form-column">
<?= $this->form->label(t('Add project member'), 'project_id') ?>
- <?= $this->form->select('project_id', $projects, $values, $errors) ?><br/>
+ <?= $this->form->select('project_id', $projects, $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'timezone') ?>
- <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Language'), 'language') ?>
- <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('language', $languages, $values, $errors) ?>
+
+ <?= $this->form->label(t('Role'), 'role') ?>
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
<?= $this->form->checkbox('notifications_enabled', t('Enable email notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?>
<?= $this->form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?>
</div>
diff --git a/app/Template/user/dropdown.php b/app/Template/user/dropdown.php
new file mode 100644
index 00000000..b74ed6e0
--- /dev/null
+++ b/app/Template/user/dropdown.php
@@ -0,0 +1,27 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li>
+ <i class="fa fa-user fa-fw"></i>
+ <?= $this->url->link(t('View profile'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php if ($user['is_active'] == 1 && $this->user->hasAccess('UserStatus', 'disable') && ! $this->user->isCurrentUser($user['id'])): ?>
+ <li>
+ <i class="fa fa-times fa-fw"></i>
+ <?= $this->url->link(t('Disable'), 'UserStatus', 'confirmDisable', array('user_id' => $user['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ <?php if ($user['is_active'] == 0 && $this->user->hasAccess('UserStatus', 'enable') && ! $this->user->isCurrentUser($user['id'])): ?>
+ <li>
+ <i class="fa fa-check-square-o fa-fw"></i>
+ <?= $this->url->link(t('Enable'), 'UserStatus', 'confirmEnable', array('user_id' => $user['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ <?php if ($this->user->hasAccess('UserStatus', 'remove') && ! $this->user->isCurrentUser($user['id'])): ?>
+ <li>
+ <i class="fa fa-trash-o fa-fw"></i>
+ <?= $this->url->link(t('Remove'), 'UserStatus', 'confirmRemove', array('user_id' => $user['id']), false, 'popover') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/user/edit.php b/app/Template/user/edit.php
index cd10b2ab..f7f67fb7 100644
--- a/app/Template/user/edit.php
+++ b/app/Template/user/edit.php
@@ -8,23 +8,23 @@
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->label(t('Username'), 'username') ?>
- <?= $this->form->text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?><br/>
+ <?= $this->form->text('username', $values, $errors, array('required', isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors) ?><br/>
+ <?= $this->form->text('name', $values, $errors) ?>
<?= $this->form->label(t('Email'), 'email') ?>
- <?= $this->form->email('email', $values, $errors) ?><br/>
+ <?= $this->form->email('email', $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'timezone') ?>
- <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Language'), 'language') ?>
- <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('language', $languages, $values, $errors) ?>
<?php if ($this->user->isAdmin()): ?>
- <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1) ?>
- <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1) ?>
+ <?= $this->form->label(t('Role'), 'role') ?>
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
<?php endif ?>
<div class="form-actions">
diff --git a/app/Template/user/external.php b/app/Template/user/external.php
index 7a42f38e..22c25af2 100644
--- a/app/Template/user/external.php
+++ b/app/Template/user/external.php
@@ -2,54 +2,10 @@
<h2><?= t('External authentications') ?></h2>
</div>
-<?php if (GOOGLE_AUTH): ?>
- <h3><i class="fa fa-google"></i> <?= t('Google Account') ?></h3>
+<?php $html = $this->hook->render('template:user:external', array('user' => $user)) ?>
- <p class="listing">
- <?php if ($this->user->isCurrentUser($user['id'])): ?>
- <?php if (empty($user['google_id'])): ?>
- <?= $this->url->link(t('Link my Google Account'), 'oauth', 'google', array(), true) ?>
- <?php else: ?>
- <?= $this->url->link(t('Unlink my Google Account'), 'oauth', 'unlink', array('backend' => 'google'), true) ?>
- <?php endif ?>
- <?php else: ?>
- <?= empty($user['google_id']) ? t('No account linked.') : t('Account linked.') ?>
- <?php endif ?>
- </p>
-<?php endif ?>
-
-<?php if (GITHUB_AUTH): ?>
- <h3><i class="fa fa-github"></i> <?= t('Github Account') ?></h3>
-
- <p class="listing">
- <?php if ($this->user->isCurrentUser($user['id'])): ?>
- <?php if (empty($user['github_id'])): ?>
- <?= $this->url->link(t('Link my Github Account'), 'oauth', 'github', array(), true) ?>
- <?php else: ?>
- <?= $this->url->link(t('Unlink my Github Account'), 'oauth', 'unlink', array('backend' => 'github'), true) ?>
- <?php endif ?>
- <?php else: ?>
- <?= empty($user['github_id']) ? t('No account linked.') : t('Account linked.') ?>
- <?php endif ?>
- </p>
-<?php endif ?>
-
-<?php if (GITLAB_AUTH): ?>
- <h3><img src="<?= $this->url->dir() ?>assets/img/gitlab-icon.png"/>&nbsp;<?= t('Gitlab Account') ?></h3>
-
- <p class="listing">
- <?php if ($this->user->isCurrentUser($user['id'])): ?>
- <?php if (empty($user['gitlab_id'])): ?>
- <?= $this->url->link(t('Link my Gitlab Account'), 'oauth', 'gitlab', array(), true) ?>
- <?php else: ?>
- <?= $this->url->link(t('Unlink my Gitlab Account'), 'oauth', 'unlink', array('backend' => 'gitlab'), true) ?>
- <?php endif ?>
- <?php else: ?>
- <?= empty($user['gitlab_id']) ? t('No account linked.') : t('Account linked.') ?>
- <?php endif ?>
- </p>
-<?php endif ?>
-
-<?php if (! GOOGLE_AUTH && ! GITHUB_AUTH && ! GITLAB_AUTH): ?>
+<?php if (empty($html)): ?>
<p class="alert"><?= t('No external authentication enabled.') ?></p>
+<?php else: ?>
+ <?= $html ?>
<?php endif ?>
diff --git a/app/Template/user/index.php b/app/Template/user/index.php
index 4008b920..494c1465 100644
--- a/app/Template/user/index.php
+++ b/app/Template/user/index.php
@@ -1,34 +1,32 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'create')): ?>
<ul>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New remote user'), 'user', 'create', array('remote' => 1)) ?></li>
<li><i class="fa fa-upload fa-fw"></i><?= $this->url->link(t('Import'), 'userImport', 'step1') ?></li>
+ <li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
</ul>
<?php endif ?>
</div>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('No user') ?></p>
<?php else: ?>
- <table>
+ <table class="table-stripped">
<tr>
- <th><?= $paginator->order(t('Id'), 'id') ?></th>
- <th><?= $paginator->order(t('Username'), 'username') ?></th>
- <th><?= $paginator->order(t('Name'), 'name') ?></th>
- <th><?= $paginator->order(t('Email'), 'email') ?></th>
- <th><?= $paginator->order(t('Administrator'), 'is_admin') ?></th>
- <th><?= $paginator->order(t('Project Administrator'), 'is_project_admin') ?></th>
- <th><?= $paginator->order(t('Two factor authentication'), 'twofactor_activated') ?></th>
- <th><?= $paginator->order(t('Notifications'), 'notifications_enabled') ?></th>
- <th><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></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>
+ <th class="column-15"><?= $paginator->order(t('Role'), 'role') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Two Factor'), 'twofactor_activated') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Status'), 'is_active') ?></th>
+ <th class="column-5"><?= t('Actions') ?></th>
</tr>
<?php foreach ($paginator->getCollection() as $user): ?>
<tr>
<td>
- <?= $this->url->link('#'.$user['id'], 'user', 'show', array('user_id' => $user['id'])) ?>
- </td>
- <td>
+ <?= '#'.$user['id'] ?>&nbsp;
<?= $this->url->link($this->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?>
</td>
<td>
@@ -38,23 +36,23 @@
<a href="mailto:<?= $this->e($user['email']) ?>"><?= $this->e($user['email']) ?></a>
</td>
<td>
- <?= $user['is_admin'] ? t('Yes') : t('No') ?>
+ <?= $this->user->getRoleName($user['role']) ?>
</td>
<td>
- <?= $user['is_project_admin'] ? t('Yes') : t('No') ?>
+ <?= $user['twofactor_activated'] ? t('Yes') : t('No') ?>
</td>
<td>
- <?= $user['twofactor_activated'] ? t('Yes') : t('No') ?>
+ <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?>
</td>
<td>
- <?php if ($user['notifications_enabled'] == 1): ?>
- <?= t('Enabled') ?>
+ <?php if ($user['is_active'] == 1): ?>
+ <?= t('Active') ?>
<?php else: ?>
- <?= t('Disabled') ?>
+ <?= t('Inactive') ?>
<?php endif ?>
</td>
<td>
- <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?>
+ <?= $this->render('user/dropdown', array('user' => $user)) ?>
</td>
</tr>
<?php endforeach ?>
diff --git a/app/Template/user/last.php b/app/Template/user/last.php
index 8879466e..d6c86391 100644
--- a/app/Template/user/last.php
+++ b/app/Template/user/last.php
@@ -14,7 +14,7 @@
</tr>
<?php foreach ($last_logins as $login): ?>
<tr>
- <td><?= dt('%B %e, %Y at %k:%M %p', $login['date_creation']) ?></td>
+ <td><?= $this->dt->datetime($login['date_creation']) ?></td>
<td><?= $this->e($login['auth_type']) ?></td>
<td><?= $this->e($login['ip']) ?></td>
<td><?= $this->e($login['user_agent']) ?></td>
diff --git a/app/Template/user/layout.php b/app/Template/user/layout.php
index a27f359b..3a0a5ba6 100644
--- a/app/Template/user/layout.php
+++ b/app/Template/user/layout.php
@@ -1,6 +1,6 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'create')): ?>
<ul>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
@@ -13,7 +13,7 @@
<?= $this->render('user/sidebar', array('user' => $user)) ?>
<div class="sidebar-content">
- <?= $user_content_for_layout ?>
+ <?= $content_for_sublayout ?>
</div>
</section>
</section> \ No newline at end of file
diff --git a/app/Template/user/password.php b/app/Template/user/password.php
index 3ef28d33..a24a4ee4 100644
--- a/app/Template/user/password.php
+++ b/app/Template/user/password.php
@@ -9,17 +9,17 @@
<div class="alert alert-error">
<?= $this->form->label(t('Current password for the user "%s"', $this->user->getFullname()), 'current_password') ?>
- <?= $this->form->password('current_password', $values, $errors) ?><br/>
+ <?= $this->form->password('current_password', $values, $errors) ?>
</div>
<?= $this->form->label(t('New password for the user "%s"', $this->user->getFullname($user)), 'password') ?>
- <?= $this->form->password('password', $values, $errors) ?><br/>
+ <?= $this->form->password('password', $values, $errors) ?>
<?= $this->form->label(t('Confirmation'), 'confirmation') ?>
- <?= $this->form->password('confirmation', $values, $errors) ?><br/>
+ <?= $this->form->password('confirmation', $values, $errors) ?>
<div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?>
</div>
diff --git a/app/Template/user/password_reset.php b/app/Template/user/password_reset.php
new file mode 100644
index 00000000..4e9063ef
--- /dev/null
+++ b/app/Template/user/password_reset.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <h2><?= t('Last Password Reset') ?></h2>
+</div>
+
+<?php if (empty($tokens)): ?>
+ <p class="alert"><?= t('The password has never been reinitialized.') ?></p>
+<?php else: ?>
+ <table class="table-small table-fixed">
+ <tr>
+ <th class="column-20"><?= t('Creation') ?></th>
+ <th class="column-20"><?= t('Expiration') ?></th>
+ <th class="column-5"><?= t('Active') ?></th>
+ <th class="column-15"><?= t('IP address') ?></th>
+ <th><?= t('User agent') ?></th>
+ </tr>
+ <?php foreach ($tokens as $token): ?>
+ <tr>
+ <td><?= $this->dt->datetime($token['date_creation']) ?></td>
+ <td><?= $this->dt->datetime($token['date_expiration']) ?></td>
+ <td><?= $token['is_active'] == 0 ? t('No') : t('Yes') ?></td>
+ <td><?= $this->e($token['ip']) ?></td>
+ <td><?= $this->e($token['user_agent']) ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/user/profile.php b/app/Template/user/profile.php
new file mode 100644
index 00000000..176a1491
--- /dev/null
+++ b/app/Template/user/profile.php
@@ -0,0 +1,8 @@
+<section id="main">
+ <br>
+ <ul class="listing">
+ <li><?= t('Username:') ?> <strong><?= $this->e($user['username']) ?></strong></li>
+ <li><?= t('Name:') ?> <strong><?= $this->e($user['name']) ?: t('None') ?></strong></li>
+ <li><?= t('Email:') ?> <strong><?= $this->e($user['email']) ?: t('None') ?></strong></li>
+ </ul>
+</section> \ No newline at end of file
diff --git a/app/Template/user/sessions.php b/app/Template/user/sessions.php
index eabf3672..8db02430 100644
--- a/app/Template/user/sessions.php
+++ b/app/Template/user/sessions.php
@@ -15,11 +15,11 @@
</tr>
<?php foreach ($sessions as $session): ?>
<tr>
- <td><?= dt('%B %e, %Y at %k:%M %p', $session['date_creation']) ?></td>
- <td><?= dt('%B %e, %Y at %k:%M %p', $session['expiration']) ?></td>
+ <td><?= $this->dt->datetime($session['date_creation']) ?></td>
+ <td><?= $this->dt->datetime($session['expiration']) ?></td>
<td><?= $this->e($session['ip']) ?></td>
<td><?= $this->e($session['user_agent']) ?></td>
- <td><?= $this->url->link(t('Remove'), 'user', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?></td>
+ <td><?= $this->url->link(t('Remove'), 'User', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?></td>
</tr>
<?php endforeach ?>
</table>
diff --git a/app/Template/user/show.php b/app/Template/user/show.php
index 220ad87e..9da56666 100644
--- a/app/Template/user/show.php
+++ b/app/Template/user/show.php
@@ -5,13 +5,14 @@
<li><?= t('Username:') ?> <strong><?= $this->e($user['username']) ?></strong></li>
<li><?= t('Name:') ?> <strong><?= $this->e($user['name']) ?: t('None') ?></strong></li>
<li><?= t('Email:') ?> <strong><?= $this->e($user['email']) ?: t('None') ?></strong></li>
+ <li><?= t('Status:') ?> <strong><?= $user['is_active'] ? t('Active') : t('Inactive') ?></strong></li>
</ul>
<div class="page-header">
<h2><?= t('Security') ?></h2>
</div>
<ul class="listing">
- <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : ($user['is_project_admin'] ? t('Project Administrator') : t('Regular user')) ?></strong></li>
+ <li><?= t('Role:') ?> <strong><?= $this->user->getRoleName($user['role']) ?></strong></li>
<li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li>
<li><?= $user['twofactor_activated'] == 1 ? t('Two factor authentication enabled') : t('Two factor authentication disabled') ?></li>
</ul>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index 167c8054..20fd2ad2 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -1,80 +1,80 @@
<div class="sidebar">
<h2><?= t('Information') ?></h2>
<ul>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'show' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?>
- </li>
+ <?php if ($this->user->hasAccess('user', 'show')): ?>
+ <li <?= $this->app->checkMenuSelection('user', 'show') ?>>
+ <?= $this->url->link(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
<?php if ($this->user->isAdmin()): ?>
<li>
<?= $this->url->link(t('User dashboard'), 'app', 'index', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
<?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'timesheet' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'timesheet') ?>>
<?= $this->url->link(t('Time tracking'), 'user', 'timesheet', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'last' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'last') ?>>
<?= $this->url->link(t('Last logins'), 'user', 'last', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'sessions' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'sessions') ?>>
<?= $this->url->link(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
</li>
+ <li <?= $this->app->checkMenuSelection('user', 'passwordReset') ?>>
+ <?= $this->url->link(t('Password reset history'), 'user', 'passwordReset', array('user_id' => $user['id'])) ?>
+ </li>
<?php endif ?>
- <?= $this->hook->render('template:user:sidebar:information') ?>
+ <?= $this->hook->render('template:user:sidebar:information', array('user' => $user)) ?>
</ul>
<h2><?= t('Actions') ?></h2>
<ul>
<?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'edit' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
- </li>
+
+ <?php if ($this->user->hasAccess('user', 'edit')): ?>
+ <li <?= $this->app->checkMenuSelection('user', 'edit') ?>>
+ <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
<?php if ($user['is_ldap_user'] == 0): ?>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'password' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'password') ?>>
<?= $this->url->link(t('Change password'), 'user', 'password', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
<?php if ($this->user->isCurrentUser($user['id'])): ?>
- <li <?= $this->app->getRouterController() === 'twofactor' && $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('twofactor', 'index') ?>>
<?= $this->url->link(t('Two factor authentication'), 'twofactor', 'index', array('user_id' => $user['id'])) ?>
</li>
- <?php elseif ($this->user->isAdmin() && $user['twofactor_activated'] == 1): ?>
- <li <?= $this->app->getRouterController() === 'twofactor' && $this->app->getRouterAction() === 'disable' ? 'class="active"' : '' ?>>
+ <?php elseif ($this->user->hasAccess('twofactor', 'disable') && $user['twofactor_activated'] == 1): ?>
+ <li <?= $this->app->checkMenuSelection('twofactor', 'disable') ?>>
<?= $this->url->link(t('Two factor authentication'), 'twofactor', 'disable', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'share' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'share') ?>>
<?= $this->url->link(t('Public access'), 'user', 'share', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'notifications' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'notifications') ?>>
<?= $this->url->link(t('Notifications'), 'user', 'notifications', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'external' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'external') ?>>
<?= $this->url->link(t('External accounts'), 'user', 'external', array('user_id' => $user['id'])) ?>
</li>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'integrations' ? 'class="active"' : '' ?>>
+ <li <?= $this->app->checkMenuSelection('user', 'integrations') ?>>
<?= $this->url->link(t('Integrations'), 'user', 'integrations', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
- <?php if ($this->user->isAdmin()): ?>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'authentication' ? 'class="active"' : '' ?>>
+ <?php if ($this->user->hasAccess('user', 'authentication')): ?>
+ <li <?= $this->app->checkMenuSelection('user', 'authentication') ?>>
<?= $this->url->link(t('Edit Authentication'), 'user', 'authentication', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
<?= $this->hook->render('template:user:sidebar:actions', array('user' => $user)) ?>
-
- <?php if ($this->user->isAdmin() && ! $this->user->isCurrentUser($user['id'])): ?>
- <li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Remove'), 'user', 'remove', array('user_id' => $user['id'])) ?>
- </li>
- <?php endif ?>
</ul>
- <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
- <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div> \ No newline at end of file
diff --git a/app/Template/user/timesheet.php b/app/Template/user/timesheet.php
index 5c0d3af8..4a6e42c5 100644
--- a/app/Template/user/timesheet.php
+++ b/app/Template/user/timesheet.php
@@ -18,8 +18,8 @@
<tr>
<td><?= $this->url->link($this->e($record['task_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td>
<td><?= $this->url->link($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td>
- <td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td>
- <td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td>
+ <td><?= $this->dt->datetime($record['start']) ?></td>
+ <td><?= $this->dt->datetime($record['end']) ?></td>
<td><?= n($record['time_spent']).' '.t('hours') ?></td>
</tr>
<?php endforeach ?>
diff --git a/app/Template/user_import/step1.php b/app/Template/user_import/step1.php
index 7256bfa6..69643d6d 100644
--- a/app/Template/user_import/step1.php
+++ b/app/Template/user_import/step1.php
@@ -1,6 +1,6 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'create')): ?>
<ul>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
diff --git a/app/Template/user_status/disable.php b/app/Template/user_status/disable.php
new file mode 100644
index 00000000..90d8c757
--- /dev/null
+++ b/app/Template/user_status/disable.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Disable user') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to disable this user: "%s"?', $user['name'] ?: $user['username']) ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'UserStatus', 'disable', array('user_id' => $user['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?>
+ </div>
+</div>
diff --git a/app/Template/user_status/enable.php b/app/Template/user_status/enable.php
new file mode 100644
index 00000000..cd3d4947
--- /dev/null
+++ b/app/Template/user_status/enable.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Enable user') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to enable this user: "%s"?', $user['name'] ?: $user['username']) ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'UserStatus', 'enable', array('user_id' => $user['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?>
+ </div>
+</div>
diff --git a/app/Template/user/remove.php b/app/Template/user_status/remove.php
index 810a3a3f..cd5c09a6 100644
--- a/app/Template/user/remove.php
+++ b/app/Template/user_status/remove.php
@@ -6,8 +6,8 @@
<p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', $user['name'] ?: $user['username']) ?></p>
<div class="form-actions">
- <?= $this->url->link(t('Yes'), 'user', 'remove', array('user_id' => $user['id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?>
+ <?= $this->url->link(t('Yes'), 'UserStatus', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ <?= $this->url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?>
</div>
-</div> \ No newline at end of file
+</div>
diff --git a/app/User/DatabaseUserProvider.php b/app/User/DatabaseUserProvider.php
new file mode 100644
index 00000000..fc626610
--- /dev/null
+++ b/app/User/DatabaseUserProvider.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+
+/**
+ * Database User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class DatabaseUserProvider implements UserProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access protected
+ * @var array
+ */
+ protected $user = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $user
+ */
+ public function __construct(array $user)
+ {
+ $this->user = $user;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return false;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return $this->user['id'];
+ }
+
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return '';
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return '';
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return '';
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return '';
+ }
+
+ /**
+ * Get external group ids
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return array();
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array();
+ }
+}
diff --git a/app/User/LdapUserProvider.php b/app/User/LdapUserProvider.php
new file mode 100644
index 00000000..3a84bfea
--- /dev/null
+++ b/app/User/LdapUserProvider.php
@@ -0,0 +1,206 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+
+/**
+ * LDAP User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class LdapUserProvider implements UserProviderInterface
+{
+ /**
+ * LDAP DN
+ *
+ * @access protected
+ * @var string
+ */
+ protected $dn;
+
+ /**
+ * LDAP username
+ *
+ * @access protected
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * User name
+ *
+ * @access protected
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Email
+ *
+ * @access protected
+ * @var string
+ */
+ protected $email;
+
+ /**
+ * User role
+ *
+ * @access protected
+ * @var string
+ */
+ protected $role;
+
+ /**
+ * Group LDAP DNs
+ *
+ * @access protected
+ * @var string[]
+ */
+ protected $groupIds;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $dn
+ * @param string $username
+ * @param string $name
+ * @param string $email
+ * @param string $role
+ * @param string[]
+ */
+ public function __construct($dn, $username, $name, $email, $role, array $groupIds)
+ {
+ $this->dn = $dn;
+ $this->username = $username;
+ $this->name = $name;
+ $this->email = $email;
+ $this->role = $role;
+ $this->groupIds = $groupIds;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return LDAP_USER_CREATION;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return 'username';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->getUsername();
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return $this->role;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return LDAP_USERNAME_CASE_SENSITIVE ? $this->username : strtolower($this->username);
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return $this->email;
+ }
+
+ /**
+ * Get groups
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return $this->groupIds;
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array(
+ 'is_ldap_user' => 1,
+ );
+ }
+
+ /**
+ * Get User DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getDn()
+ {
+ return $this->dn;
+ }
+}
diff --git a/app/User/OAuthUserProvider.php b/app/User/OAuthUserProvider.php
new file mode 100644
index 00000000..dec26250
--- /dev/null
+++ b/app/User/OAuthUserProvider.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+
+/**
+ * OAuth User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+abstract class OAuthUserProvider implements UserProviderInterface
+{
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ abstract public function getExternalIdColumn();
+
+ /**
+ * User properties
+ *
+ * @access protected
+ * @var array
+ */
+ protected $user = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $user
+ */
+ public function __construct(array $user)
+ {
+ $this->user = $user;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return false;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->user['id'];
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return '';
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return '';
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->user['name'];
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return $this->user['email'];
+ }
+
+ /**
+ * Get external group ids
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return array();
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array();
+ }
+}
diff --git a/app/User/ReverseProxyUserProvider.php b/app/User/ReverseProxyUserProvider.php
new file mode 100644
index 00000000..723b8155
--- /dev/null
+++ b/app/User/ReverseProxyUserProvider.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+use Kanboard\Core\Security\Role;
+
+/**
+ * Reverse Proxy User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class ReverseProxyUserProvider implements UserProviderInterface
+{
+ /**
+ * Username
+ *
+ * @access protected
+ * @var string
+ */
+ protected $username = '';
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $username
+ */
+ public function __construct($username)
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return true;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return 'username';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return REVERSE_PROXY_DEFAULT_ADMIN === $this->username ? Role::APP_ADMIN : Role::APP_USER;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return '';
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return REVERSE_PROXY_DEFAULT_DOMAIN !== '' ? $this->username.'@'.REVERSE_PROXY_DEFAULT_DOMAIN : '';
+ }
+
+ /**
+ * Get external group ids
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return array();
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array(
+ 'is_ldap_user' => 1,
+ 'disable_login_form' => 1,
+ );
+ }
+}
diff --git a/app/Validator/ActionValidator.php b/app/Validator/ActionValidator.php
new file mode 100644
index 00000000..95ee7d21
--- /dev/null
+++ b/app/Validator/ActionValidator.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Action Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class ActionValidator extends Base
+{
+ /**
+ * Validate action creation
+ *
+ * @access public
+ * @param array $values Required parameters to save an action
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('project_id', t('The project id is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('event_name', t('This value is required')),
+ new Validators\Required('action_name', t('This value is required')),
+ new Validators\Required('params', t('This value is required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Validator/AuthValidator.php b/app/Validator/AuthValidator.php
new file mode 100644
index 00000000..36ccdff0
--- /dev/null
+++ b/app/Validator/AuthValidator.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Gregwar\Captcha\CaptchaBuilder;
+
+/**
+ * Authentication Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class AuthValidator extends Base
+{
+ /**
+ * Validate user login form
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateForm(array $values)
+ {
+ return $this->executeValidators(array('validateFields', 'validateLocking', 'validateCaptcha', 'validateCredentials'), $values);
+ }
+
+ /**
+ * Validate credentials syntax
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ protected function validateFields(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('username', t('The username is required')),
+ new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
+ new Validators\Required('password', t('The password is required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors(),
+ );
+ }
+
+ /**
+ * Validate user locking
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ protected function validateLocking(array $values)
+ {
+ $result = true;
+ $errors = array();
+
+ if ($this->userLocking->isLocked($values['username'])) {
+ $result = false;
+ $errors['login'] = t('Your account is locked for %d minutes', BRUTEFORCE_LOCKDOWN_DURATION);
+ $this->logger->error('Account locked: '.$values['username']);
+ }
+
+ return array($result, $errors);
+ }
+
+ /**
+ * Validate password syntax
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ protected function validateCredentials(array $values)
+ {
+ $result = true;
+ $errors = array();
+
+ if (! $this->authenticationManager->passwordAuthentication($values['username'], $values['password'])) {
+ $result = false;
+ $errors['login'] = t('Bad username or password');
+ }
+
+ return array($result, $errors);
+ }
+
+ /**
+ * Validate captcha
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return boolean
+ */
+ protected function validateCaptcha(array $values)
+ {
+ $result = true;
+ $errors = array();
+
+ if ($this->userLocking->hasCaptcha($values['username'])) {
+ if (! isset($this->sessionStorage->captcha)) {
+ $result = false;
+ } else {
+ $builder = new CaptchaBuilder;
+ $builder->setPhrase($this->sessionStorage->captcha);
+ $result = $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
+
+ if (! $result) {
+ $errors['login'] = t('Invalid captcha');
+ }
+ }
+ }
+
+ return array($result, $errors);;
+ }
+}
diff --git a/app/Validator/Base.php b/app/Validator/Base.php
new file mode 100644
index 00000000..ba32a503
--- /dev/null
+++ b/app/Validator/Base.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validators;
+
+/**
+ * Base Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class Base extends \Kanboard\Core\Base
+{
+ /**
+ * Execute multiple validators
+ *
+ * @access public
+ * @param array $validators List of validators
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function executeValidators(array $validators, array $values)
+ {
+ $result = false;
+ $errors = array();
+
+ foreach ($validators as $method) {
+ list($result, $errors) = $this->$method($values);
+
+ if (! $result) {
+ break;
+ }
+ }
+
+ return array($result, $errors);
+ }
+
+ /**
+ * Common password validation rules
+ *
+ * @access protected
+ * @return array
+ */
+ protected function commonPasswordValidationRules()
+ {
+ return array(
+ new Validators\Required('password', t('The password is required')),
+ new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
+ new Validators\Required('confirmation', t('The confirmation is required')),
+ new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
+ );
+ }
+}
diff --git a/app/Validator/CategoryValidator.php b/app/Validator/CategoryValidator.php
new file mode 100644
index 00000000..715aed66
--- /dev/null
+++ b/app/Validator/CategoryValidator.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Category Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class CategoryValidator extends Base
+{
+ /**
+ * Validate category creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $rules = array(
+ new Validators\Required('project_id', t('The project id is required')),
+ new Validators\Required('name', t('The name is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate category modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The id is required')),
+ new Validators\Required('name', t('The name is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Integer('id', t('The id must be an integer')),
+ new Validators\Integer('project_id', t('The project id must be an integer')),
+ new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
+ );
+ }
+}
diff --git a/app/Validator/ColumnValidator.php b/app/Validator/ColumnValidator.php
new file mode 100644
index 00000000..f0f1659b
--- /dev/null
+++ b/app/Validator/ColumnValidator.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Column Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class ColumnValidator extends Base
+{
+ /**
+ * Validate column modification
+ *
+ * @access public
+ * @param array $values Required parameters to update a column
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('This value is required')),
+ new Validators\Integer('id', t('This value must be an integer')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate column creation
+ *
+ * @access public
+ * @param array $values Required parameters to save an action
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $rules = array(
+ new Validators\Required('project_id', t('The project id is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Integer('task_limit', t('This value must be an integer')),
+ new Validators\GreaterThan('task_limit', t('This value must be greater than %d', -1), -1),
+ new Validators\Required('title', t('The title is required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50),
+ );
+ }
+}
diff --git a/app/Validator/CommentValidator.php b/app/Validator/CommentValidator.php
new file mode 100644
index 00000000..4eb54206
--- /dev/null
+++ b/app/Validator/CommentValidator.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Comment Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class CommentValidator extends Base
+{
+ /**
+ * Validate comment creation
+ *
+ * @access public
+ * @param array $values Required parameters to save an action
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $rules = array(
+ new Validators\Required('task_id', t('This value is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate comment modification
+ *
+ * @access public
+ * @param array $values Required parameters to save an action
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('This value is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Integer('task_id', t('This value must be an integer')),
+ new Validators\Integer('user_id', t('This value must be an integer')),
+ new Validators\MaxLength('reference', t('The maximum length is %d characters', 50), 50),
+ new Validators\Required('comment', t('Comment is required'))
+ );
+ }
+}
diff --git a/app/Validator/CurrencyValidator.php b/app/Validator/CurrencyValidator.php
new file mode 100644
index 00000000..ee191523
--- /dev/null
+++ b/app/Validator/CurrencyValidator.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Currency Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class CurrencyValidator extends Base
+{
+ /**
+ * Validate
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('currency', t('Field required')),
+ new Validators\Required('rate', t('Field required')),
+ new Validators\Numeric('rate', t('This value must be numeric')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Validator/CustomFilterValidator.php b/app/Validator/CustomFilterValidator.php
new file mode 100644
index 00000000..07f2a1eb
--- /dev/null
+++ b/app/Validator/CustomFilterValidator.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Custom Filter Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class CustomFilterValidator extends Base
+{
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Required('project_id', t('Field required')),
+ new Validators\Required('user_id', t('Field required')),
+ new Validators\Required('name', t('Field required')),
+ new Validators\Required('filter', t('Field required')),
+ new Validators\Integer('user_id', t('This value must be an integer')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100),
+ new Validators\MaxLength('filter', t('The maximum length is %d characters', 100), 100)
+ );
+ }
+
+ /**
+ * Validate filter creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, $this->commonValidationRules());
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate filter modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('Field required')),
+ new Validators\Integer('id', t('This value must be an integer')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Validator/ExternalLinkValidator.php b/app/Validator/ExternalLinkValidator.php
new file mode 100644
index 00000000..fff4133b
--- /dev/null
+++ b/app/Validator/ExternalLinkValidator.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * External Link Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class ExternalLinkValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, $this->commonValidationRules());
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The id is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Required('url', t('Field required')),
+ new Validators\MaxLength('url', t('The maximum length is %d characters', 255), 255),
+ new Validators\Required('title', t('Field required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255),
+ new Validators\Required('link_type', t('Field required')),
+ new Validators\MaxLength('link_type', t('The maximum length is %d characters', 100), 100),
+ new Validators\Required('dependency', t('Field required')),
+ new Validators\MaxLength('dependency', t('The maximum length is %d characters', 100), 100),
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Required('task_id', t('Field required')),
+ new Validators\Integer('task_id', t('This value must be an integer')),
+ );
+ }
+}
diff --git a/app/Validator/GroupValidator.php b/app/Validator/GroupValidator.php
new file mode 100644
index 00000000..2226abd3
--- /dev/null
+++ b/app/Validator/GroupValidator.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Kanboard\Model\Group;
+
+/**
+ * Group Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class GroupValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, $this->commonValidationRules());
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The id is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Required('name', t('The name is required')),
+ new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100),
+ new Validators\Unique('name', t('The name must be unique'), $this->db->getConnection(), Group::TABLE, 'id'),
+ new Validators\MaxLength('external_id', t('The maximum length is %d characters', 255), 255),
+ new Validators\Integer('id', t('This value must be an integer')),
+ );
+ }
+}
diff --git a/app/Validator/LinkValidator.php b/app/Validator/LinkValidator.php
new file mode 100644
index 00000000..10a826da
--- /dev/null
+++ b/app/Validator/LinkValidator.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Kanboard\Model\Link;
+
+/**
+ * Link Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class LinkValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('label', t('Field required')),
+ new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), Link::TABLE),
+ new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('Field required')),
+ new Validators\Required('opposite_id', t('Field required')),
+ new Validators\Required('label', t('Field required')),
+ new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), Link::TABLE),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Validator/PasswordResetValidator.php b/app/Validator/PasswordResetValidator.php
new file mode 100644
index 00000000..baf2d8d7
--- /dev/null
+++ b/app/Validator/PasswordResetValidator.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Gregwar\Captcha\CaptchaBuilder;
+
+/**
+ * Password Reset Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class PasswordResetValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ return $this->executeValidators(array('validateFields', 'validateCaptcha'), $values);
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, $this->commonPasswordValidationRules());
+
+ return array(
+ $v->execute(),
+ $v->getErrors(),
+ );
+ }
+
+ /**
+ * Validate fields
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ protected function validateFields(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('captcha', t('This value is required')),
+ new Validators\Required('username', t('The username is required')),
+ new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors(),
+ );
+ }
+
+ /**
+ * Validate captcha
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return boolean
+ */
+ protected function validateCaptcha(array $values)
+ {
+ $errors = array();
+
+ if (! isset($this->sessionStorage->captcha)) {
+ $result = false;
+ } else {
+ $builder = new CaptchaBuilder;
+ $builder->setPhrase($this->sessionStorage->captcha);
+ $result = $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
+
+ if (! $result) {
+ $errors['captcha'] = array(t('Invalid captcha'));
+ }
+ }
+
+ return array($result, $errors);;
+ }
+}
diff --git a/app/Validator/ProjectValidator.php b/app/Validator/ProjectValidator.php
new file mode 100644
index 00000000..1c6c90f8
--- /dev/null
+++ b/app/Validator/ProjectValidator.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Kanboard\Model\Project;
+
+/**
+ * Project Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class ProjectValidator extends Base
+{
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Integer('priority_default', t('This value must be an integer')),
+ new Validators\Integer('priority_start', t('This value must be an integer')),
+ new Validators\Integer('priority_end', t('This value must be an integer')),
+ new Validators\Integer('is_active', t('This value must be an integer')),
+ new Validators\Required('name', t('The project name is required')),
+ new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
+ new Validators\MaxLength('identifier', t('The maximum length is %d characters', 50), 50),
+ new Validators\MaxLength('start_date', t('The maximum length is %d characters', 10), 10),
+ new Validators\MaxLength('end_date', t('The maximum length is %d characters', 10), 10),
+ new Validators\AlphaNumeric('identifier', t('This value must be alphanumeric')) ,
+ new Validators\Unique('identifier', t('The identifier must be unique'), $this->db->getConnection(), Project::TABLE),
+ );
+ }
+
+ /**
+ * Validate project creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ if (! empty($values['identifier'])) {
+ $values['identifier'] = strtoupper($values['identifier']);
+ }
+
+ $v = new Validator($values, $this->commonValidationRules());
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate project modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ if (! empty($values['identifier'])) {
+ $values['identifier'] = strtoupper($values['identifier']);
+ }
+
+ $rules = array(
+ new Validators\Required('id', t('This value is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Validator/SubtaskValidator.php b/app/Validator/SubtaskValidator.php
new file mode 100644
index 00000000..1989b7f4
--- /dev/null
+++ b/app/Validator/SubtaskValidator.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Subtask Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class SubtaskValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $rules = array(
+ new Validators\Required('task_id', t('The task id is required')),
+ new Validators\Required('title', t('The title is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The subtask id is required')),
+ new Validators\Required('task_id', t('The task id is required')),
+ new Validators\Required('title', t('The title is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate API modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateApiModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The subtask id is required')),
+ new Validators\Required('task_id', t('The task id is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Integer('id', t('The subtask id must be an integer')),
+ new Validators\Integer('task_id', t('The task id must be an integer')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255),
+ new Validators\Integer('user_id', t('The user id must be an integer')),
+ new Validators\Integer('status', t('The status must be an integer')),
+ new Validators\Numeric('time_estimated', t('The time must be a numeric value')),
+ new Validators\Numeric('time_spent', t('The time must be a numeric value')),
+ );
+ }
+}
diff --git a/app/Validator/SwimlaneValidator.php b/app/Validator/SwimlaneValidator.php
new file mode 100644
index 00000000..4cc780f9
--- /dev/null
+++ b/app/Validator/SwimlaneValidator.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Swimlane Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class SwimlaneValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $rules = array(
+ new Validators\Required('project_id', t('The project id is required')),
+ new Validators\Required('name', t('The name is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The id is required')),
+ new Validators\Required('name', t('The name is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate default swimlane modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateDefaultModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The id is required')),
+ new Validators\Required('default_swimlane', t('The name is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Integer('id', t('The id must be an integer')),
+ new Validators\Integer('project_id', t('The project id must be an integer')),
+ new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
+ );
+ }
+}
diff --git a/app/Validator/TaskLinkValidator.php b/app/Validator/TaskLinkValidator.php
new file mode 100644
index 00000000..c88c2b16
--- /dev/null
+++ b/app/Validator/TaskLinkValidator.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Kanboard\Model\Task;
+
+/**
+ * Task Link Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class TaskLinkValidator extends Base
+{
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Required('task_id', t('Field required')),
+ new Validators\Required('opposite_task_id', t('Field required')),
+ new Validators\Required('link_id', t('Field required')),
+ new Validators\NotEquals('opposite_task_id', 'task_id', t('A task cannot be linked to itself')),
+ new Validators\Exists('opposite_task_id', t('This linked task id doesn\'t exists'), $this->db->getConnection(), Task::TABLE, 'id')
+ );
+ }
+
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, $this->commonValidationRules());
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('Field required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/TaskValidator.php b/app/Validator/TaskValidator.php
index 683cb0b1..1a77dd32 100644
--- a/app/Model/TaskValidator.php
+++ b/app/Validator/TaskValidator.php
@@ -1,14 +1,14 @@
<?php
-namespace Kanboard\Model;
+namespace Kanboard\Validator;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
- * Task validator model
+ * Task Validator
*
- * @package model
+ * @package validator
* @author Frederic Guillot
*/
class TaskValidator extends Base
@@ -37,10 +37,11 @@ class TaskValidator extends Base
new Validators\Integer('recurrence_basedate', t('This value must be an integer')),
new Validators\Integer('recurrence_trigger', t('This value must be an integer')),
new Validators\Integer('recurrence_status', t('This value must be an integer')),
+ new Validators\Integer('priority', t('This value must be an integer')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
new Validators\MaxLength('reference', t('The maximum length is %d characters', 50), 50),
- new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()),
- new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getAllFormats()),
+ new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats(true)),
+ new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateTimeFormats(true)),
new Validators\Numeric('time_spent', t('This value must be numeric')),
new Validators\Numeric('time_estimated', t('This value must be numeric')),
);
diff --git a/app/Validator/UserValidator.php b/app/Validator/UserValidator.php
new file mode 100644
index 00000000..d85d335f
--- /dev/null
+++ b/app/Validator/UserValidator.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Kanboard\Model\User;
+
+/**
+ * User Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class UserValidator extends Base
+{
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\MaxLength('role', t('The maximum length is %d characters', 25), 25),
+ new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
+ new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), User::TABLE, 'id'),
+ new Validators\Email('email', t('Email address invalid')),
+ new Validators\Integer('is_ldap_user', t('This value must be an integer')),
+ );
+ }
+
+ /**
+ * Validate user creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $rules = array(
+ new Validators\Required('username', t('The username is required')),
+ );
+
+ if (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) {
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+ } else {
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
+ }
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate user modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The user id is required')),
+ new Validators\Required('username', t('The username is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate user API modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateApiModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The user id is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate password modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validatePasswordModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The user id is required')),
+ new Validators\Required('current_password', t('The current password is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules()));
+
+ if ($v->execute()) {
+ if ($this->authenticationManager->passwordAuthentication($this->userSession->getUsername(), $values['current_password'], false)) {
+ return array(true, array());
+ } else {
+ return array(false, array('current_password' => array(t('Wrong password'))));
+ }
+ }
+
+ return array(false, $v->getErrors());
+ }
+}
diff --git a/app/check_setup.php b/app/check_setup.php
index be52a718..eec63ed8 100644
--- a/app/check_setup.php
+++ b/app/check_setup.php
@@ -19,14 +19,29 @@ if (version_compare(PHP_VERSION, '5.4.0', '<')) {
}
}
-// Check extension: PDO
-if (! extension_loaded('pdo_sqlite') && ! extension_loaded('pdo_mysql') && ! extension_loaded('pdo_pgsql')) {
- throw new Exception('PHP extension required: pdo_sqlite or pdo_mysql or pdo_pgsql');
+// Check data folder if sqlite
+if (DB_DRIVER === 'sqlite' && ! is_writable('data')) {
+ throw new Exception('The directory "data" must be writeable by your web server user');
}
-// Check extension: mbstring
-if (! extension_loaded('mbstring')) {
- throw new Exception('PHP extension required: mbstring');
+// Check PDO extensions
+if (DB_DRIVER === 'sqlite' && ! extension_loaded('pdo_sqlite')) {
+ throw new Exception('PHP extension required: "pdo_sqlite"');
+}
+
+if (DB_DRIVER === 'mysql' && ! extension_loaded('pdo_mysql')) {
+ throw new Exception('PHP extension required: "pdo_mysql"');
+}
+
+if (DB_DRIVER === 'postgres' && ! extension_loaded('pdo_pgsql')) {
+ throw new Exception('PHP extension required: "pdo_pgsql"');
+}
+
+// Check other extensions
+foreach (array('gd', 'mbstring', 'hash', 'openssl', 'json', 'hash', 'ctype', 'filter', 'session') as $ext) {
+ if (! extension_loaded($ext)) {
+ throw new Exception('PHP extension required: "'.$ext.'"');
+ }
}
// Fix wrong value for arg_separator.output, used by the function http_build_query()
diff --git a/app/common.php b/app/common.php
index 85a2b7d2..e2fe6aff 100644
--- a/app/common.php
+++ b/app/common.php
@@ -1,6 +1,6 @@
<?php
-require dirname(__DIR__) . '/vendor/autoload.php';
+require __DIR__.'/../vendor/autoload.php';
// Automatically parse environment configuration (Heroku)
if (getenv('DATABASE_URL')) {
@@ -14,22 +14,27 @@ if (getenv('DATABASE_URL')) {
define('DB_NAME', ltrim($dbopts["path"], '/'));
}
-// Include custom config file
if (file_exists('config.php')) {
require 'config.php';
}
+if (file_exists('data'.DIRECTORY_SEPARATOR.'config.php')) {
+ require 'data'.DIRECTORY_SEPARATOR.'config.php';
+}
+
require __DIR__.'/constants.php';
require __DIR__.'/check_setup.php';
$container = new Pimple\Container;
+$container->register(new Kanboard\ServiceProvider\SessionProvider);
$container->register(new Kanboard\ServiceProvider\LoggingProvider);
$container->register(new Kanboard\ServiceProvider\DatabaseProvider);
+$container->register(new Kanboard\ServiceProvider\AuthenticationProvider);
+$container->register(new Kanboard\ServiceProvider\NotificationProvider);
$container->register(new Kanboard\ServiceProvider\ClassProvider);
$container->register(new Kanboard\ServiceProvider\EventDispatcherProvider);
-
-if (ENABLE_URL_REWRITE) {
- require __DIR__.'/routes.php';
-}
-
-$container['pluginLoader']->scan();
+$container->register(new Kanboard\ServiceProvider\GroupProvider);
+$container->register(new Kanboard\ServiceProvider\RouteProvider);
+$container->register(new Kanboard\ServiceProvider\ActionProvider);
+$container->register(new Kanboard\ServiceProvider\ExternalLinkProvider);
+$container->register(new Kanboard\ServiceProvider\PluginProvider);
diff --git a/app/constants.php b/app/constants.php
index 4c22f760..011fa784 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -23,46 +23,36 @@ defined('DB_HOSTNAME') or define('DB_HOSTNAME', 'localhost');
defined('DB_NAME') or define('DB_NAME', 'kanboard');
defined('DB_PORT') or define('DB_PORT', null);
+// Database backend group provider
+defined('DB_GROUP_PROVIDER') or define('DB_GROUP_PROVIDER', true);
+
// LDAP configuration
defined('LDAP_AUTH') or define('LDAP_AUTH', false);
defined('LDAP_SERVER') or define('LDAP_SERVER', '');
defined('LDAP_PORT') or define('LDAP_PORT', 389);
-defined('LDAP_START_TLS') or define('LDAP_START_TLS', false);
defined('LDAP_SSL_VERIFY') or define('LDAP_SSL_VERIFY', true);
+defined('LDAP_START_TLS') or define('LDAP_START_TLS', false);
+defined('LDAP_USERNAME_CASE_SENSITIVE') or define('LDAP_USERNAME_CASE_SENSITIVE', false);
+
defined('LDAP_BIND_TYPE') or define('LDAP_BIND_TYPE', 'anonymous');
defined('LDAP_USERNAME') or define('LDAP_USERNAME', null);
defined('LDAP_PASSWORD') or define('LDAP_PASSWORD', null);
-defined('LDAP_ACCOUNT_BASE') or define('LDAP_ACCOUNT_BASE', '');
-defined('LDAP_USER_PATTERN') or define('LDAP_USER_PATTERN', '');
-defined('LDAP_ACCOUNT_FULLNAME') or define('LDAP_ACCOUNT_FULLNAME', 'displayname');
-defined('LDAP_ACCOUNT_EMAIL') or define('LDAP_ACCOUNT_EMAIL', 'mail');
-defined('LDAP_ACCOUNT_ID') or define('LDAP_ACCOUNT_ID', '');
-defined('LDAP_ACCOUNT_MEMBEROF') or define('LDAP_ACCOUNT_MEMBEROF', 'memberof');
-defined('LDAP_ACCOUNT_CREATION') or define('LDAP_ACCOUNT_CREATION', true);
+
+defined('LDAP_USER_BASE_DN') or define('LDAP_USER_BASE_DN', '');
+defined('LDAP_USER_FILTER') or define('LDAP_USER_FILTER', '');
+defined('LDAP_USER_ATTRIBUTE_USERNAME') or define('LDAP_USER_ATTRIBUTE_USERNAME', 'uid');
+defined('LDAP_USER_ATTRIBUTE_FULLNAME') or define('LDAP_USER_ATTRIBUTE_FULLNAME', 'cn');
+defined('LDAP_USER_ATTRIBUTE_EMAIL') or define('LDAP_USER_ATTRIBUTE_EMAIL', 'mail');
+defined('LDAP_USER_ATTRIBUTE_GROUPS') or define('LDAP_USER_ATTRIBUTE_GROUPS', 'memberof');
+defined('LDAP_USER_CREATION') or define('LDAP_USER_CREATION', true);
+
defined('LDAP_GROUP_ADMIN_DN') or define('LDAP_GROUP_ADMIN_DN', '');
-defined('LDAP_GROUP_PROJECT_ADMIN_DN') or define('LDAP_GROUP_PROJECT_ADMIN_DN', '');
-defined('LDAP_USERNAME_CASE_SENSITIVE') or define('LDAP_USERNAME_CASE_SENSITIVE', false);
+defined('LDAP_GROUP_MANAGER_DN') or define('LDAP_GROUP_MANAGER_DN', '');
-// Google authentication
-defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false);
-defined('GOOGLE_CLIENT_ID') or define('GOOGLE_CLIENT_ID', '');
-defined('GOOGLE_CLIENT_SECRET') or define('GOOGLE_CLIENT_SECRET', '');
-
-// Github authentication
-defined('GITHUB_AUTH') or define('GITHUB_AUTH', false);
-defined('GITHUB_CLIENT_ID') or define('GITHUB_CLIENT_ID', '');
-defined('GITHUB_CLIENT_SECRET') or define('GITHUB_CLIENT_SECRET', '');
-defined('GITHUB_OAUTH_AUTHORIZE_URL') or define('GITHUB_OAUTH_AUTHORIZE_URL', 'https://github.com/login/oauth/authorize');
-defined('GITHUB_OAUTH_TOKEN_URL') or define('GITHUB_OAUTH_TOKEN_URL', 'https://github.com/login/oauth/access_token');
-defined('GITHUB_API_URL') or define('GITHUB_API_URL', 'https://api.github.com/');
-
-// Gitlab authentication
-defined('GITLAB_AUTH') or define('GITLAB_AUTH', false);
-defined('GITLAB_CLIENT_ID') or define('GITLAB_CLIENT_ID', '');
-defined('GITLAB_CLIENT_SECRET') or define('GITLAB_CLIENT_SECRET', '');
-defined('GITLAB_OAUTH_AUTHORIZE_URL') or define('GITLAB_OAUTH_AUTHORIZE_URL', 'https://gitlab.com/oauth/authorize');
-defined('GITLAB_OAUTH_TOKEN_URL') or define('GITLAB_OAUTH_TOKEN_URL', 'https://gitlab.com/oauth/token');
-defined('GITLAB_API_URL') or define('GITLAB_API_URL', 'https://gitlab.com/api/v3/');
+defined('LDAP_GROUP_PROVIDER') or define('LDAP_GROUP_PROVIDER', false);
+defined('LDAP_GROUP_BASE_DN') or define('LDAP_GROUP_BASE_DN', '');
+defined('LDAP_GROUP_FILTER') or define('LDAP_GROUP_FILTER', '');
+defined('LDAP_GROUP_ATTRIBUTE_NAME') or define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn');
// Proxy authentication
defined('REVERSE_PROXY_AUTH') or define('REVERSE_PROXY_AUTH', false);
@@ -107,6 +97,9 @@ defined('ENABLE_URL_REWRITE') or define('ENABLE_URL_REWRITE', isset($_SERVER['HT
// Hide login form
defined('HIDE_LOGIN_FORM') or define('HIDE_LOGIN_FORM', false);
+// Disabling logout (for external SSO authentication)
+defined('DISABLE_LOGOUT') or define('DISABLE_LOGOUT', false);
+
// Bruteforce protection
defined('BRUTEFORCE_CAPTCHA') or define('BRUTEFORCE_CAPTCHA', 3);
defined('BRUTEFORCE_LOCKDOWN') or define('BRUTEFORCE_LOCKDOWN', 6);
diff --git a/app/functions.php b/app/functions.php
index fe6e6757..b9b30834 100644
--- a/app/functions.php
+++ b/app/functions.php
@@ -31,13 +31,3 @@ function n($value)
{
return Translator::getInstance()->number($value);
}
-
-/**
- * Translate a date
- *
- * @return string
- */
-function dt($format, $timestamp)
-{
- return Translator::getInstance()->datetime($format, $timestamp);
-}
diff --git a/app/routes.php b/app/routes.php
deleted file mode 100644
index 159e8f6e..00000000
--- a/app/routes.php
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-// Dashboard
-$container['router']->addRoute('dashboard', 'app', 'index');
-$container['router']->addRoute('dashboard/:user_id', 'app', 'index', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/projects', 'app', 'projects', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/activity', 'app', 'activity', array('user_id'));
-
-// Search routes
-$container['router']->addRoute('search', 'search', 'index');
-$container['router']->addRoute('search/:search', 'search', 'index', array('search'));
-
-// Project routes
-$container['router']->addRoute('projects', 'project', 'index');
-$container['router']->addRoute('project/create', 'project', 'create');
-$container['router']->addRoute('project/create/:private', 'project', 'create', array('private'));
-$container['router']->addRoute('project/:project_id', 'project', 'show', array('project_id'));
-$container['router']->addRoute('p/:project_id', 'project', 'show', array('project_id'));
-$container['router']->addRoute('project/:project_id/share', 'project', 'share', array('project_id'));
-$container['router']->addRoute('project/:project_id/edit', 'project', 'edit', array('project_id'));
-$container['router']->addRoute('project/:project_id/integration', 'project', 'integration', array('project_id'));
-$container['router']->addRoute('project/:project_id/users', 'project', 'users', array('project_id'));
-$container['router']->addRoute('project/:project_id/duplicate', 'project', 'duplicate', array('project_id'));
-$container['router']->addRoute('project/:project_id/remove', 'project', 'remove', array('project_id'));
-$container['router']->addRoute('project/:project_id/disable', 'project', 'disable', array('project_id'));
-$container['router']->addRoute('project/:project_id/enable', 'project', 'enable', array('project_id'));
-
-// Action routes
-$container['router']->addRoute('project/:project_id/actions', 'action', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm', array('project_id', 'action_id'));
-
-// Column routes
-$container['router']->addRoute('project/:project_id/columns', 'column', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit', array('project_id', 'column_id'));
-$container['router']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm', array('project_id', 'column_id'));
-$container['router']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move', array('project_id', 'column_id', 'direction'));
-
-// Swimlane routes
-$container['router']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown', array('project_id', 'swimlane_id'));
-
-// Category routes
-$container['router']->addRoute('project/:project_id/categories', 'category', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit', array('project_id', 'category_id'));
-$container['router']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm', array('project_id', 'category_id'));
-
-// Task routes
-$container['router']->addRoute('project/:project_id/task/:task_id', 'task', 'show', array('project_id', 'task_id'));
-$container['router']->addRoute('t/:task_id', 'task', 'show', array('task_id'));
-$container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove', array('project_id', 'task_id'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence', array('project_id', 'task_id'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open', array('task_id', 'project_id'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id'));
-
-// Board routes
-$container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id'));
-$container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id'));
-$container['router']->addRoute('public/board/:token', 'board', 'readonly', array('token'));
-
-// Calendar routes
-$container['router']->addRoute('calendar/:project_id', 'calendar', 'show', array('project_id'));
-$container['router']->addRoute('c/:project_id', 'calendar', 'show', array('project_id'));
-
-// Listing routes
-$container['router']->addRoute('list/:project_id', 'listing', 'show', array('project_id'));
-$container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id'));
-
-// Gantt routes
-$container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id'));
-$container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting'));
-
-// Subtask routes
-$container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit', array('project_id', 'task_id', 'subtask_id'));
-
-// Feed routes
-$container['router']->addRoute('feed/project/:token', 'feed', 'project', array('token'));
-$container['router']->addRoute('feed/user/:token', 'feed', 'user', array('token'));
-
-// Ical routes
-$container['router']->addRoute('ical/project/:token', 'ical', 'project', array('token'));
-$container['router']->addRoute('ical/user/:token', 'ical', 'user', array('token'));
-
-// Auth routes
-$container['router']->addRoute('oauth/google', 'oauth', 'google');
-$container['router']->addRoute('oauth/github', 'oauth', 'github');
-$container['router']->addRoute('oauth/gitlab', 'oauth', 'gitlab');
-$container['router']->addRoute('login', 'auth', 'login');
-$container['router']->addRoute('logout', 'auth', 'logout');